Emit and broadcast user transformations

master
Ian Mancini 4 years ago
parent 16cde00e23
commit 4f8255fac7

@ -0,0 +1,40 @@
// @ts-nocheck
import React, { useEffect, useRef } from 'react'
import api, { Transform } from '../store'
type PhantomProps = {
id: string
}
const Phantom: React.FC<PhantomProps> = ({ id }) => {
const ref = useRef<THREE.Mesh>(null)
useEffect(() => {
if (ref.current) {
api.subscribe(
(state: Transform | null) => {
if (!state) {
return
}
ref.current?.position.fromArray([
state.position[0],
state.position[1] + 1.25,
state.position[2],
])
ref.current?.rotation.fromArray(state.rotation)
},
(state) => state?.userTransforms?.[id],
)
}
}, [ref.current])
return (
<mesh ref={ref}>
<sphereBufferGeometry args={[1, 16, 16]} />
<meshStandardMaterial color="hotpink" />
</mesh>
)
}
export default Phantom

@ -4,6 +4,8 @@ import { SphereProps, useSphere } from '@react-three/cannon'
import { useThree, useFrame } from 'react-three-fiber' import { useThree, useFrame } from 'react-three-fiber'
import { Event } from 'three' import { Event } from 'three'
import useStore from '../store'
const SPEED = 5 const SPEED = 5
const keys: Record<string, string> = { const keys: Record<string, string> = {
KeyW: 'forward', KeyW: 'forward',
@ -45,23 +47,49 @@ const usePlayerControls = () => {
// TODO Improve physics in player // TODO Improve physics in player
const Player = (props: SphereProps) => { const Player = (props: SphereProps) => {
const socket = useStore((state) => state.socket)
const { forward, backward, left, right, running } = usePlayerControls()
const { camera } = useThree()
const [ref, api] = useSphere(() => ({ const [ref, api] = useSphere(() => ({
type: 'Dynamic', type: 'Dynamic',
fixedRotation: true, fixedRotation: true,
position: [0, 4, 0], position: [0, 1.5, 0],
args: 0.2,
mass: 9, mass: 9,
linearDamping: 0, linearDamping: 0,
angularDamping: 0, angularDamping: 0,
material: { friction: 0, restitution: 0 }, material: { friction: 0, restitution: 0 },
...props, ...props,
})) }))
const { forward, backward, left, right, running } = usePlayerControls()
const { camera } = useThree() useEffect(() => {
const socketEmitTransformInterval = setInterval(() => {
if (socket && ref.current && camera) {
socket.emit('transform', {
position: [
ref.current.position.x,
ref.current.position.y,
ref.current.position.z,
],
rotation: [camera.rotation.x, camera.rotation.y, camera.rotation.z],
})
}
}, 16)
return () => {
clearInterval(socketEmitTransformInterval)
}
}, [])
const velocity = useRef([0, 0, 0]) const velocity = useRef([0, 0, 0])
useEffect(() => void api.velocity.subscribe((v) => (velocity.current = v)), []) useEffect(() => void api.velocity.subscribe((v) => (velocity.current = v)), [])
useFrame(() => { useFrame(() => {
if (!ref.current) return if (!ref.current) return
camera.position.copy(ref.current.position) camera.position.set(
ref.current.position.x,
ref.current.position.y + 1,
ref.current.position.z,
)
frontVector.set(0, 0, Number(backward) - Number(forward)) frontVector.set(0, 0, Number(backward) - Number(forward))
sideVector.set(Number(left) - Number(right), 0, 0) sideVector.set(Number(left) - Number(right), 0, 0)
direction direction

@ -0,0 +1,27 @@
import React from 'react'
import shallow from 'zustand/shallow'
import Phantom from './Phantom'
import useStore from '../store'
const Users: React.FC = () => {
const socket = useStore((state) => state.socket)
const users = useStore(
(state) => (state.userTransforms ? Object.keys(state.userTransforms) : null),
shallow,
)
if (users === null || socket === null) {
return null
}
return (
<>
{users.map((user) =>
user === socket.id ? null : <Phantom id={user} key={user} />,
)}
</>
)
}
export default Users

@ -7,6 +7,7 @@ import Player from './Player'
import Lighting from './Lighting' import Lighting from './Lighting'
import Effects from './Effects' import Effects from './Effects'
import Controls from './Controls' import Controls from './Controls'
import Users from './Users'
const Scene: React.FC = () => { const Scene: React.FC = () => {
return ( return (
@ -18,7 +19,7 @@ const Scene: React.FC = () => {
<Physics <Physics
gravity={[0, 0, 0]} gravity={[0, 0, 0]}
iterations={2} iterations={2}
size={10} size={2}
defaultContactMaterial={{ defaultContactMaterial={{
friction: 0, friction: 0,
restitution: 0, restitution: 0,
@ -29,6 +30,7 @@ const Scene: React.FC = () => {
</Physics> </Physics>
<Effects /> <Effects />
</Suspense> </Suspense>
<Users />
<Controls /> <Controls />
</> </>

@ -43,7 +43,7 @@ const MenuOverlay: React.FC = () => {
<ModalContent> <ModalContent>
<ModalHeader>museo.red</ModalHeader> <ModalHeader>museo.red</ModalHeader>
<ModalBody> <ModalBody>
Usa las teclas <Kbd>W</Kbd>, <Kbd>A</Kbd> <Kbd>S</Kbd> y <Kbd>D</Kbd> para Usa las teclas <Kbd>W</Kbd>, <Kbd>A</Kbd>, <Kbd>S</Kbd> y <Kbd>D</Kbd> para
moverte. Usá el mouse para mirar al rededor moverte. Usá el mouse para mirar al rededor
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>

@ -8,18 +8,22 @@ import MenuOverlay from './MenuOverlay'
import Error from './Error' import Error from './Error'
import Scene from '../3d' import Scene from '../3d'
import useStore from '../store' import useStore, { UserTransforms } from '../store'
const Museo: React.FC = () => { const Museo: React.FC = () => {
const setSocket = useStore((state) => state.setSocket) const setSocket = useStore((state) => state.setSocket)
const setError = useStore((state) => state.setError) const setError = useStore((state) => state.setError)
const error = useStore((state) => state.error) const error = useStore((state) => state.error)
const setUserTransforms = useStore((state) => state.setUserTransforms)
useEffect(() => { useEffect(() => {
const socket = io() const socket = io()
setSocket(socket) setSocket(socket)
socket.on('disconnect', () => setError('SOCKET')) socket.on('disconnect', () => setError('SOCKET'))
socket.on('broadcast-transforms', (payload: UserTransforms) => {
setUserTransforms(payload)
})
return () => { return () => {
socket.close() socket.close()
@ -34,7 +38,7 @@ const Museo: React.FC = () => {
) : ( ) : (
<> <>
<MenuOverlay /> <MenuOverlay />
<Canvas colorManagement> <Canvas colorManagement concurrent>
<Scene /> <Scene />
</Canvas> </Canvas>
</> </>

@ -4,15 +4,26 @@ import { Socket } from 'socket.io-client'
type Error = null | 'SOCKET' type Error = null | 'SOCKET'
export type Transform = {
position?: number[]
rotation?: number[]
}
type userId = string
export type UserTransforms = Record<userId, Transform>
type State = { type State = {
pointerLockControls: PointerLockControls | undefined pointerLockControls: PointerLockControls | undefined
setPointerLockControls: (controls: PointerLockControls) => void
pointerLocked: boolean pointerLocked: boolean
setPointerLockStatus: (status: boolean) => void setPointerLockStatus: (status: boolean) => void
setPointerLockControls: (controls: PointerLockControls) => void
socket: null | Socket socket: null | Socket
setSocket: (socket: Socket | null) => void setSocket: (socket: Socket | null) => void
error: Error error: Error
setError: (error: Error) => void setError: (error: Error) => void
userTransforms: UserTransforms | null
setUserTransforms: (userTransforms: UserTransforms) => void
} }
const useStore = create<State>((set) => ({ const useStore = create<State>((set) => ({
@ -24,6 +35,8 @@ const useStore = create<State>((set) => ({
setSocket: (socket) => set(() => ({ socket })), setSocket: (socket) => set(() => ({ socket })),
error: null, error: null,
setError: (error) => set(() => ({ error })), setError: (error) => set(() => ({ error })),
userTransforms: null,
setUserTransforms: (userTransforms) => set(() => ({ userTransforms })),
})) }))
export default useStore export default useStore

@ -23,7 +23,14 @@ import logger, { morganStream } from './logger'
import passport from 'passport' import passport from 'passport'
import mongoose from 'mongoose' import mongoose from 'mongoose'
import redisClient, { asyncHEXISTS, asyncHGET, asyncHSET, asyncHDEL } from './redis' import redisClient, {
asyncHEXISTS,
asyncHGET,
asyncHSET,
asyncHDEL,
asyncHGETALL,
asyncDEL,
} from './redis'
import { authRouter, initPassport } from './auth/passport' import { authRouter, initPassport } from './auth/passport'
@ -47,6 +54,9 @@ mongoose.connect(
const app = express() const app = express()
app.set('trust proxy', 1) app.set('trust proxy', 1)
const server = http.createServer(app) const server = http.createServer(app)
asyncDEL('socket')
asyncDEL('transform')
const io = new SocketIO(server, { serveClient: false }) const io = new SocketIO(server, { serveClient: false })
const pubClient = redisClient const pubClient = redisClient
@ -120,15 +130,34 @@ io.on('connection', async (socket) => {
await asyncHSET('socket', String(socket.request.user._id), String(socket.id)) await asyncHSET('socket', String(socket.request.user._id), String(socket.id))
logger.debug(`Assigned ${socket.id} to ${socket.request.user._id}`) logger.debug(`Assigned ${socket.id} to ${socket.request.user._id}`)
socket.on('kick', () => { socket.on('transform', async (payload) => {
console.log('kicking') await asyncHSET('transform', String(socket.id), JSON.stringify(payload))
}) })
socket.on('disconnect', async () => { socket.on('disconnect', async () => {
logger.debug(`User with id ${socket.id} disconnected`) logger.debug(`User with id ${socket.id} disconnected`)
await asyncHDEL('socket', String(socket.request.user._id), String(socket.id)) await asyncHDEL('transform', String(socket.id))
await asyncHDEL('socket', String(socket.request.user._id))
})
}) })
async function broadcastTransforms() {
const transforms = await asyncHGETALL('transform')
if (transforms) {
let parsed = {}
Object.entries(transforms).forEach(([key, value]) => {
parsed = { ...parsed, [key]: { ...JSON.parse(value) } }
}) })
io.sockets.emit('broadcast-transforms', parsed)
}
setTimeout(broadcastTransforms, 30)
}
broadcastTransforms()
if (process.env.NODE_ENV !== 'PRODUCTION') { if (process.env.NODE_ENV !== 'PRODUCTION') {
app.use('/', proxy('http://localhost:4000/')) app.use('/', proxy('http://localhost:4000/'))
} else { } else {

@ -25,5 +25,7 @@ export const asyncHEXISTS = promisify(client.HEXISTS).bind(client)
export const asyncHGET = promisify(client.HGET).bind(client) export const asyncHGET = promisify(client.HGET).bind(client)
export const asyncHSET = promisify(client.HSET).bind(client) export const asyncHSET = promisify(client.HSET).bind(client)
export const asyncHDEL = promisify(client.HDEL).bind(client) export const asyncHDEL = promisify(client.HDEL).bind(client)
export const asyncHGETALL = promisify(client.HGETALL).bind(client)
export const asyncDEL = promisify(client.DEL).bind(client)
export default client export default client

Loading…
Cancel
Save