diff --git a/packages/core/src/Networking/Entities/Player.ts b/packages/core/src/Networking/Entities/Player.ts index aa6edc2..1bc176e 100644 --- a/packages/core/src/Networking/Entities/Player.ts +++ b/packages/core/src/Networking/Entities/Player.ts @@ -8,7 +8,7 @@ export default abstract class Player extends NetworkedLivingActor { trackedEntities = new Set() - public serialize(): object { + public serialize() { return { ...super.serialize(), $typeName: this.$typeName, diff --git a/packages/core/src/Networking/NetworkedEntityFactory.ts b/packages/core/src/Networking/NetworkedEntityFactory.ts new file mode 100644 index 0000000..5e3d0dd --- /dev/null +++ b/packages/core/src/Networking/NetworkedEntityFactory.ts @@ -0,0 +1,26 @@ +import type NetworkedActor from './NetworkedActor' + +type EntityFactory = (data: Record) => NetworkedActor +const registry = new Map() + +export default class NetworkedEntityFactory { + private static _instance: NetworkedEntityFactory + + static get instance(): NetworkedEntityFactory { + if (!NetworkedEntityFactory._instance) { + NetworkedEntityFactory._instance = new NetworkedEntityFactory() + } + + return NetworkedEntityFactory._instance + } + + register(typeName: string, factory: EntityFactory) { + registry.set(typeName, factory) + } + + create(typeName: string, data: Record): NetworkedActor | null { + const factory = registry.get(typeName) + + return factory ? factory(data) : null + } +} diff --git a/packages/core/src/Networking/NetworkedLivingActor.ts b/packages/core/src/Networking/NetworkedLivingActor.ts index 437ec92..4e52c16 100644 --- a/packages/core/src/Networking/NetworkedLivingActor.ts +++ b/packages/core/src/Networking/NetworkedLivingActor.ts @@ -35,11 +35,18 @@ export default abstract class NetworkedLivingActor extends NetworkedGameObjectMi } } - public serialize(): object { + public serialize(): { + id: string + position: Vector3 + rotation: Vector3 + scale: Vector3 + health: number + state: NetworkedEntityState[] + } { return { ...super.serialize(), state: this.state, - } + } as ReturnType } isDead(): boolean { diff --git a/packages/core/src/World/Chunk.ts b/packages/core/src/World/Chunk.ts index 1da7bf8..522b7ec 100644 --- a/packages/core/src/World/Chunk.ts +++ b/packages/core/src/World/Chunk.ts @@ -1,10 +1,6 @@ import GameObject from './GameObject' export default class Chunk extends GameObject { - public serialize(): object { - throw new Error('Method not implemented.') - } - static CHUNK_SIZE: number constructor(x: number, y: number) { diff --git a/packages/core/src/World/GameObject.ts b/packages/core/src/World/GameObject.ts index dd15ab9..cb8c80b 100644 --- a/packages/core/src/World/GameObject.ts +++ b/packages/core/src/World/GameObject.ts @@ -28,7 +28,7 @@ export default abstract class GameObject extends Entity implements GameObjectInt abstract update(delta: number): void - public serialize(): object { + public serialize() { return { id: this.id, position: this.position, diff --git a/packages/core/src/World/LivingActor.ts b/packages/core/src/World/LivingActor.ts index 52337cc..3e25095 100644 --- a/packages/core/src/World/LivingActor.ts +++ b/packages/core/src/World/LivingActor.ts @@ -9,7 +9,7 @@ export default abstract class LivingActor extends Actor implements LivingEntity abstract takeDamage(amount: number): void abstract heal(amount: number): void - public serialize(): object { + public serialize() { return { ...super.serialize(), health: this.health, diff --git a/packages/core/tests/Networking/NetworkedEntityFactory.test.ts b/packages/core/tests/Networking/NetworkedEntityFactory.test.ts new file mode 100644 index 0000000..12829fe --- /dev/null +++ b/packages/core/tests/Networking/NetworkedEntityFactory.test.ts @@ -0,0 +1,78 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import NetworkedActor from '../../src/Networking/NetworkedActor' +import NetworkedEntityFactory from '../../src/Networking/NetworkedEntityFactory' + +class TestActor extends NetworkedActor { + $typeName = 'testActor' + updateFromNetwork = (_data: object) => { } + update(_delta: number): void { } +} + +describe('networkedEntityFactory', () => { + let factory: NetworkedEntityFactory + + beforeEach(() => { + factory = new NetworkedEntityFactory() + }) + + it('returns null for an unregistered type', () => { + expect(factory.create('unknown', { id: 'id-1' })).toBeNull() + }) + + it('creates a registered entity', () => { + factory.register('testActor', (data) => { + const actor = new TestActor() + actor.id = data.id as string + return actor + }) + + const actor = factory.create('testActor', { id: 'id-1' }) + expect(actor).toBeInstanceOf(TestActor) + }) + + it('passes id and data to the factory function', () => { + const data = { x: 10, y: 20, id: 'id-42' } + let capturedId: string | undefined + let capturedData: Record | undefined + + factory.register('testActor', (data) => { + capturedId = data.id as string + capturedData = data + return new TestActor() + }) + + factory.create('testActor', data) + expect(capturedId).toBe('id-42') + expect(capturedData).toEqual(data) + }) + + it('overwrites a previously registered factory', () => { + factory.register('testActor', () => { + const a = new TestActor() + a.$typeName = 'first' + return a + }) + factory.register('testActor', () => { + const a = new TestActor() + a.$typeName = 'second' + return a + }) + + const actor = factory.create('testActor', 'id-1', {}) as TestActor + expect(actor.$typeName).toBe('second') + }) + + it('handles multiple registered types independently', () => { + class OtherActor extends NetworkedActor { + $typeName = 'otherActor' + updateFromNetwork = (_data: object) => { } + update(_delta: number): void { } + } + + factory.register('testActor', () => new TestActor()) + factory.register('otherActor', () => new OtherActor()) + + expect(factory.create('testActor', 'id-1', {})).toBeInstanceOf(TestActor) + expect(factory.create('otherActor', 'id-2', {})).toBeInstanceOf(OtherActor) + }) +}) diff --git a/packages/multiplayer-template/client/src/Entities/Player.ts b/packages/multiplayer-template/client/src/Entities/Player.ts index f5df611..15746f6 100644 --- a/packages/multiplayer-template/client/src/Entities/Player.ts +++ b/packages/multiplayer-template/client/src/Entities/Player.ts @@ -1,8 +1,10 @@ +import NetworkedEntityFactory from '@mavonengine/core/Networking/NetworkedEntityFactory' import { syncStateStack } from '@mavonengine/core/Networking/syncState' import BasePlayer from '@template/server/Base/Player' import { Vector3 } from 'three' import IdleState from '../Player/IdleState' import WalkingState from '../Player/WalkingState' +import useNetworkState from '../UI/composables/useNetworkState' import PlayerLabel from '../ui/PlayerLabel' import PlayerGraphicsComponent from './Player/PlayerGraphicsComponent' @@ -20,6 +22,16 @@ export default class Character extends BasePlayer { this.graphicalComponent.init() this.label = new PlayerLabel(this, isLocalPlayer ? 'You' : 'Player', isLocalPlayer) + + if (!isLocalPlayer) { + const { networkState } = useNetworkState() + networkState.value.players++ + } + + NetworkedEntityFactory.instance.register(this.$typeName, (data) => { + const d = data as ReturnType + return new Character(d.id, false, new Vector3(d.position.x, d.position.y, d.position.z)) + }) } /** Called by GameSyncManager when this player sends a chat. */ @@ -61,6 +73,11 @@ export default class Character extends BasePlayer { } destroy(): void { + if (!this.isLocalPlayer) { + const { networkState } = useNetworkState() + networkState.value.players-- + } + this.graphicalComponent.destroy() this.label.destroy() super.destroy() diff --git a/packages/multiplayer-template/client/src/GameSyncManager.ts b/packages/multiplayer-template/client/src/GameSyncManager.ts index 25adf45..f2d8270 100644 --- a/packages/multiplayer-template/client/src/GameSyncManager.ts +++ b/packages/multiplayer-template/client/src/GameSyncManager.ts @@ -4,11 +4,10 @@ import type NetworkManager from './NetworkManager' import type PlayerController from './PlayerController' import Game from '@mavonengine/core/Game' import NetworkedActor from '@mavonengine/core/Networking/NetworkedActor' +import NetworkedEntityFactory from '@mavonengine/core/Networking/NetworkedEntityFactory' import NetworkedGameObject from '@mavonengine/core/Networking/NetworkedGameObject' import { ServerCommand } from '@template/server/Commands' -import { Vector3 } from 'three' import Character from './Entities/Player' -import useNetworkState from './UI/composables/useNetworkState' import Trees from './World/Trees' export default class GameSyncManager implements GameObjectInterface { @@ -60,7 +59,7 @@ export default class GameSyncManager implements GameObjectInterface { this.networkManager.destroy() } - update(_delta: number) {} + update(_delta: number) { } private handleStateUpdate(data: any) { const updatedEntities: any[] = data.entities @@ -75,18 +74,9 @@ export default class GameSyncManager implements GameObjectInterface { (existing as NetworkedActor).updateFromNetwork(inEntity) } else if (this.networkManager.socket.id !== inEntity.id) { - // Remote player we haven't seen yet - if (inEntity.$typeName === 'player') { - const { networkState } = useNetworkState() - networkState.value.players++ - - const newChar = new Character( - inEntity.id, - false, - new Vector3(inEntity.position.x, inEntity.position.y, inEntity.position.z), - ) - Game.instance().world.entities.items.set(inEntity.id, newChar) - } + const instance = NetworkedEntityFactory.instance.create(inEntity.$typeName, inEntity) + if (instance) + Game.instance().world.entities.items.set(inEntity.id, instance) } else { // This is our own player coming back from the server @@ -113,10 +103,5 @@ export default class GameSyncManager implements GameObjectInterface { entity.destroy() Game.instance().world.entities.items.delete(entityId) - - if (entity instanceof Character) { - const { networkState } = useNetworkState() - networkState.value.players = Math.max(0, networkState.value.players - 1) - } } } diff --git a/packages/multiplayer-template/server/src/Base/Player.ts b/packages/multiplayer-template/server/src/Base/Player.ts index 4fddcbf..d0d9e88 100644 --- a/packages/multiplayer-template/server/src/Base/Player.ts +++ b/packages/multiplayer-template/server/src/Base/Player.ts @@ -98,7 +98,7 @@ export default class Player extends BasePlayer { super.destroy() } - public serialize(): object { + public serialize() { return { ...super.serialize(), name: this.name,