Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/Networking/Entities/Player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default abstract class Player extends NetworkedLivingActor {

trackedEntities = new Set<string>()

public serialize(): object {
public serialize() {
return {
...super.serialize(),
$typeName: this.$typeName,
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/Networking/NetworkedEntityFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type NetworkedActor from './NetworkedActor'

type EntityFactory = (data: Record<string, unknown>) => NetworkedActor
const registry = new Map<string, EntityFactory>()

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<string, unknown>): NetworkedActor | null {
const factory = registry.get(typeName)

return factory ? factory(data) : null
}
}
11 changes: 9 additions & 2 deletions packages/core/src/Networking/NetworkedLivingActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<NetworkedLivingActor['serialize']>
}

isDead(): boolean {
Expand Down
4 changes: 0 additions & 4 deletions packages/core/src/World/Chunk.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/World/GameObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/World/LivingActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
78 changes: 78 additions & 0 deletions packages/core/tests/Networking/NetworkedEntityFactory.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> | 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)
})
})
17 changes: 17 additions & 0 deletions packages/multiplayer-template/client/src/Entities/Player.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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<BasePlayer['serialize']>
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. */
Expand Down Expand Up @@ -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()
Expand Down
25 changes: 5 additions & 20 deletions packages/multiplayer-template/client/src/GameSyncManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
}
}
}
2 changes: 1 addition & 1 deletion packages/multiplayer-template/server/src/Base/Player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default class Player extends BasePlayer {
super.destroy()
}

public serialize(): object {
public serialize() {
return {
...super.serialize(),
name: this.name,
Expand Down
Loading