Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tp3 network rewrite #646

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
15181b3
networking rewrite stage 1:
Jul 5, 2023
250c44c
fixed voip
Jul 5, 2023
546c73e
added clear guards to outbound net system
Jul 5, 2023
8417a42
disable outbound net system when window is not focused
Jul 5, 2023
f9be17b
Revert "disable outbound net system when window is not focused"
Jul 5, 2023
dba623c
renamed inbound/outbound networking files
Jul 6, 2023
bbf2f93
added partial network message test coverage
Jul 6, 2023
ea917a7
fixed spawnable updates, created remaining replicators
Jul 6, 2023
314a1cb
lint
Jul 6, 2023
345d409
non-host spawnables guard & warning
Jul 6, 2023
acdc413
typecheck
Jul 6, 2023
a5e3612
remove node from replicator instances on despawn
Jul 6, 2023
91185f8
net replicator despawn error verbiage fix
Jul 6, 2023
cb05a70
outbound net system cleanup
Jul 6, 2023
737ba10
always remove entity if host on peer leave
Jul 6, 2023
0eb75bb
network/peerId count sync fix
Jul 6, 2023
1b9b784
add bytes written to updates for skipping
Jul 6, 2023
31b2985
interaction held/remove/release fix
Jul 6, 2023
e2eb176
getNetworkReplicator return type fix
Jul 6, 2023
ea60f51
host snapshot usage fix
Jul 6, 2023
c48da8f
added NetworkSpawnPeerAvatarSystem
Jul 6, 2023
0f1310c
only host does out of bounds floor handling
Jul 6, 2023
bada57a
out of bounds floor position fix
Jul 6, 2023
2381ca4
physics body handle mapping bugfix
Jul 7, 2023
9de4c5e
fixed physics body disposal bug
Jul 10, 2023
21a4f59
Merge branch 'physics-body-handle-mapping-bugfix' into tp3_network_re…
Jul 10, 2023
ed20511
fixed peer avatar despawns as host
Jul 10, 2023
af4095e
peerID refactor
Jul 11, 2023
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
60 changes: 60 additions & 0 deletions docs/protocol/avatar-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Avatar Network Schema

```c
struct Spawn {
networkId: uint64 // The avatar's networkId
authorIndex: uint64 // Who controls the avatar
schemaId: uint32 // 1 (Reserved Avatar schema id)
creationDataByteLength: uint32 // 0
creationData: uint8[] // not used
updateData: AvatarUpdate
}

struct AvatarUpdate {
changedBitmask: uint8_t // rigPosition, rigRotation, rigVelocity
rigPosition: float_t[3]
rigRotation: float_t[4]
rigVelocity: float_t[3]
}

struct AvatarXRModeUpdate {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we explain what this struct is used for in this document? Is it an RPC?

xrMode: uint8_t
}
```

```js
const avatarReplicator = createNetworkedReplicator({
spawn(world, { author, creationData, updateData, networked }) {
// Create node in world
// Should be controllable by author
// Return node to spawn
return avatarRoot;
},
encode(node, buffer) {
// Write to buffer, return number bytes written, buffer truncated to that length and written as updateData
},
decode(node, buffer) {
// Read from buffer and apply to node or interp buffer
const interpBuffer = interpMap.get(node);
interpBuffer.add();
},
});

// host
if (host) {
const node = avatarReplicator.spawn(creationData, { destroyOnLeave: true });
// Modify the node as needed and that update data will be sent with the first spawn message
}

avatarReplicator.despawn(node);

for (const { node } of avatarReplicator.spawned()) {
// Spawned nodes should have the latest update data applied to them if it exists,
// not just the original spawn update data
world.environment.addNode(node);
}

for (const node of avatarReplicator.despawned()) {
world.environment.removeNode(node);
}
```
3 changes: 2 additions & 1 deletion docs/protocol/implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ Three components are used to determine how the peer's simulation treats each net
- Enter Query [Networked, Authoring]
- if we are the current host (if Networked.peerIndex is our peerIndex), we can safely send out a spawn across the network for this entity
- if we are a new host being migrated to, the entity has spawned on the network already, therefore do not execute a spawn for this entity
- if we are a new host first joining, wait for HostSnapshot
- Exit Query [Networked, Authoring]
- if we are the current host (if Networked.peerIndex is our peerIndex), we can safely send out a despawn across the network for this entity
- if we are a old host being migrated away from, we should not send a despawn across the network for this entity
- Relaying
- This component indicates that this peer is hosting the simulation for this entity, but is only responsible for relaying the source-of-truth state for this entity which is being received by another peer who is authoring the source-of-truth for the entity
- This component indicates that the current peer is only responsible for relaying the source-of-truth which is being received by the authoring peer.
- Holds the peerIndex of the peer who we are relaying the source-of-truth for

### Networked Object Lifecycle
12 changes: 6 additions & 6 deletions docs/protocol/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Client uses [host election algorithm](#host-election) to determine host among me

Client waits to connect to host's `WebRTCPeerConnection` and ensures that the `RTCDataChannel` is open.

Wait for a `HostSnapshot` message from the host. Note you may receive messages from clients that are not the determined host. This could either be malicious or the result of the Matrix Room's state events not being up to date yet. If the message is from the host, set the local peer index to the `localPeerIndex` returned in the `HostSnapshot` message and the host peer index to the `hostPeerIndex`. If it is not from the host, store the message in case the current host changes. If the connection timeout happens before a `HostSnapshot` message is received stop waiting and show an error to the user and a button allowing them to try to reconnect.
If our client is not determined as host, wait for a `HostSnapshot` message from the host. Note you may receive messages from clients that are not the determined host. This could either be malicious or the result of the Matrix Room's state events not being up to date yet. If the message is from the host, set the local peer index to the `localPeerIndex` returned in the `HostSnapshot` message and the host peer index to the `hostPeerIndex`. If it is not from the host, store the message in case the current host changes. If the connection timeout happens before a `HostSnapshot` message is received stop waiting and show an error to the user and a button allowing them to try to reconnect.

### Disconnect

Expand Down Expand Up @@ -81,6 +81,7 @@ struct PeerInfo {

struct Spawn {
networkId: uint64
authorIndex: uint64
schemaId: uint32
creationDataByteLength: uint32
creationData: uint8[]
Expand All @@ -93,7 +94,6 @@ struct Despawn {

struct Update {
networkId: uint64 // The network id of the spawned node
bitmask: uint8/16/32 // a bitmask indicating which properties of the schema are included in this update
data: uint8[] // The update data
}

Expand All @@ -118,11 +118,11 @@ message HostSnapshot {
hostTime: uint64
localPeerIndex: uint64
hostPeerIndex: uint64
peerCount: uint32
peers: PeerInfo[peerCount]
entityCount: uint32
entityCount: uint16
entitySpawns: Spawn[entityCount]
hostStateByteLength: uint32
peerCount: uint16
peers: PeerInfo[peerCount]
hostStateByteLength: uint16
hostState: uint8[char]
}

Expand Down
16 changes: 8 additions & 8 deletions src/engine/allocator/CursorView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,14 @@ export const writeFloat32 = (v: CursorView, value: number) => {
return v;
};

export const writeUint64 = (v: CursorView, value: number) => {
v.setUint64(v.cursor, value, v.littleEndian);
export const writeUint64 = (v: CursorView, value: bigint) => {
v.setBigInt64(v.cursor, value, v.littleEndian);
v.cursor += BigUint64Array.BYTES_PER_ELEMENT;
return v;
};

export const writeInt64 = (v: CursorView, value: number) => {
v.setInt64(v.cursor, value, v.littleEndian);
export const writeInt64 = (v: CursorView, value: bigint) => {
v.setBigInt64(v.cursor, value, v.littleEndian);
v.cursor += BigInt64Array.BYTES_PER_ELEMENT;
return v;
};
Expand Down Expand Up @@ -209,17 +209,17 @@ export const spaceFloat32 = (v: CursorView) => {
export const spaceUint64 = (v: CursorView) => {
const savePoint = v.cursor;
v.cursor += BigUint64Array.BYTES_PER_ELEMENT;
return (value: number) => {
v.setUint64(savePoint, value, v.littleEndian);
return (value: bigint) => {
v.setBigUint64(savePoint, value, v.littleEndian);
return v;
};
};

export const spaceInt64 = (v: CursorView) => {
const savePoint = v.cursor;
v.cursor += BigInt64Array.BYTES_PER_ELEMENT;
return (value: number) => {
v.setInt64(savePoint, value, v.littleEndian);
return (value: bigint) => {
v.setBigInt64(savePoint, value, v.littleEndian);
return v;
};
};
Expand Down
20 changes: 13 additions & 7 deletions src/engine/config.game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { AudioModule } from "./audio/audio.game";
import { InputModule } from "./input/input.game";
import { UpdateRawInputSystem, ResetRawInputSystem } from "./input/RawInputSystems";
import { PhysicsModule, PhysicsSystem } from "./physics/physics.game";
import { NetworkExitWorldQueueSystem, NetworkModule } from "./network/network.game";
import { NetworkThreadedMessageQueueSystem, NetworkModule } from "./network/network.game";
import { ActionMappingSystem } from "./input/ActionMappingSystem";
import {
KinematicCharacterControllerModule,
Expand All @@ -17,7 +17,7 @@ import {
} from "./editor/editor.game";
import { GameContext } from "./GameTypes";
import { RendererModule } from "./renderer/renderer.game";
import { SpawnablesModule } from "../plugins/spawnables/spawnables.game";
import { SpawnablesModule, SpawnablesSystem } from "../plugins/spawnables/spawnables.game";
import {
RecycleResourcesSystem,
ResourceDisposalSystem,
Expand All @@ -28,7 +28,7 @@ import {
import { ThirdRoomModule, WorldLoaderSystem } from "../plugins/thirdroom/thirdroom.game";
import { UpdateMatrixWorldSystem } from "./component/transform";
import { FlyCharacterControllerModule, FlyControllerSystem } from "./player/FlyCharacterController";
import { NetworkInterpolationSystem } from "./network/NetworkInterpolationSystem";
// import { NetworkInterpolationSystem } from "./network/NetworkInterpolationSystem";
import { PrefabDisposalSystem, PrefabModule } from "./prefab/prefab.game";
import { AnimationSystem } from "./animation/animation.game";
import {
Expand All @@ -40,8 +40,8 @@ import { NametagModule, NametagSystem } from "./player/nametags.game";
import { ScriptingSystem } from "./scripting/scripting.game";
import { GameResourceSystem } from "./resource/GameResourceSystem";
import { RemoteCameraSystem } from "./camera/camera.game";
import { InboundNetworkSystem } from "./network/inbound.game";
import { OutboundNetworkSystem } from "./network/outbound.game";
import { InboundNetworkSystem } from "./network/InboundNetworkSystem";
import { OutboundNetworkSystem } from "./network/OutboundNetworkSystem";
import { GLTFResourceDisposalSystem } from "./gltf/gltf.game";
import { IncomingTripleBufferSystem } from "./resource/IncomingTripleBufferSystem";
import { OutgoingTripleBufferSystem } from "./resource/OutgoingTripleBufferSystem";
Expand All @@ -55,6 +55,8 @@ import { PlayerModule } from "./player/Player.game";
import { ActionBarSystem } from "../plugins/thirdroom/action-bar.game";
import { EnableCharacterControllerSystem } from "./player/CharacterController";
import { CameraRigSystem } from "./player/CameraRig";
// import { TransferAuthoritySystem } from "./network/TransferAuthoritySystem";
import { SpawnAvatarSystem } from "./player/PlayerRig";

export default defineConfig<GameContext>({
modules: [
Expand Down Expand Up @@ -87,6 +89,8 @@ export default defineConfig<GameContext>({
ActionMappingSystem,

InboundNetworkSystem,
// TransferAuthoritySystem,
SpawnAvatarSystem,

WorldLoaderSystem,

Expand All @@ -98,13 +102,15 @@ export default defineConfig<GameContext>({
InteractionSystem,
XRInteractionSystem,
ActionBarSystem,
SpawnablesSystem,
EnableCharacterControllerSystem,

// step physics forward and copy rigidbody data to transform component
PhysicsSystem,

// interpolate towards authoritative state
NetworkInterpolationSystem,
// TODO: rewrite
// NetworkInterpolationSystem,

ScriptingSystem,

Expand All @@ -117,7 +123,7 @@ export default defineConfig<GameContext>({
//EditorSelectionSystem,

OutboundNetworkSystem,
NetworkExitWorldQueueSystem,
NetworkThreadedMessageQueueSystem,

RemoteCameraSystem,
PrefabDisposalSystem,
Expand Down
4 changes: 2 additions & 2 deletions src/engine/editor/editor.game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { disableActionMap, enableActionMap } from "../input/ActionMappingSystem"
import { ActionMap, ActionType, BindingType, ButtonActionState } from "../input/ActionMap";
import { InputModule } from "../input/input.game";
import { flyControlsQuery } from "../player/FlyCharacterController";
import { getCamera } from "../player/getCamera";
import { tryGetCamera } from "../player/getCamera";
import { setRenderNodeOptimizationsEnabled } from "../renderer/renderer.game";

/*********
Expand Down Expand Up @@ -304,7 +304,7 @@ export function EditorStateSystem(ctx: GameContext) {
for (let i = 0; i < ents.length; i++) {
const playerRigEid = ents[i];
const playerRig = tryGetRemoteResource<RemoteNode>(ctx, playerRigEid);
getCamera(ctx, playerRig);
tryGetCamera(ctx, playerRig);

const body = playerRig.physicsBody?.body;

Expand Down
40 changes: 23 additions & 17 deletions src/engine/input/WebXRAvatarRigSystem.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { addComponent, defineQuery, exitQuery, hasComponent, Not, removeComponent } from "bitecs";
import { addComponent, defineQuery, enterQuery, exitQuery, hasComponent, Not, removeComponent } from "bitecs";
import { mat4, quat, vec3 } from "gl-matrix";

import { FlyControls } from "../player/FlyCharacterController";
Expand All @@ -22,13 +22,13 @@ import { getRemoteResource, tryGetRemoteResource } from "../resource/resource.ga
import { teleportEntity } from "../utils/teleportEntity";
import { ActionMap, ActionType, BindingType, ButtonActionState } from "./ActionMap";
import { InputModule } from "./input.game";
import { Networked, Owned } from "../network/NetworkComponents";
import { broadcastReliable } from "../network/outbound.game";
import { Networked, Authoring } from "../network/NetworkComponents";
import { createInformXRModeMessage } from "../network/serialization.game";
import { NetworkModule } from "../network/network.game";
import { XRHeadComponent, XRControllerComponent } from "../player/PlayerRig";
import { AvatarRef } from "../player/components";
import { ourPlayerQuery } from "../player/Player";
import { XRControllerComponent, XRHeadComponent } from "../player/XRComponents";
import { enqueueReliableBroadcast } from "../network/NetworkRingBuffer";

export interface XRAvatarRig {
prevLeftAssetPath?: string;
Expand Down Expand Up @@ -60,9 +60,14 @@ export function addXRAvatarRig(world: World, eid: number) {
const xrAvatarRigQuery = defineQuery([XRAvatarRig]);
const xrAvatarRigExitQuery = exitQuery(xrAvatarRigQuery);

const remoteXRControllerQuery = defineQuery([Networked, Not(Owned), XRControllerComponent]);
const remoteXRHeadQuery = defineQuery([Networked, Not(Owned), XRHeadComponent]);
const remoteAvatarQuery = defineQuery([Networked, Not(Owned), AvatarRef]);
const remoteXRControllerQuery = defineQuery([Networked, Not(Authoring), XRControllerComponent]);
const enteredRemoteXRControllerQuery = enterQuery(remoteXRControllerQuery);

const remoteXRHeadQuery = defineQuery([Networked, Not(Authoring), XRHeadComponent]);
const enteredRemoteXRHeadQuery = enterQuery(remoteXRHeadQuery);

const remoteAvatarQuery = defineQuery([Networked, Not(Authoring), AvatarRef]);
const enteredRemoteAvatarQuery = enterQuery(remoteAvatarQuery);

const _v = vec3.create();
const _q = quat.create();
Expand Down Expand Up @@ -107,7 +112,7 @@ export function WebXRAvatarRigSystem(ctx: GameContext) {
rendererModule.prevXRMode = ourXRMode;

// inform other clients of our XRMode
broadcastReliable(ctx, network, createInformXRModeMessage(ctx, ourXRMode));
enqueueReliableBroadcast(network, createInformXRModeMessage(ctx, ourXRMode));
}

for (let i = 0; i < rigs.length; i++) {
Expand Down Expand Up @@ -173,7 +178,7 @@ export function WebXRAvatarRigSystem(ctx: GameContext) {

// determine visibility of XR objects depending on XR modes of the clients who own those objects

const remoteXRControllers = remoteXRControllerQuery(ctx.world);
const remoteXRControllers = enteredRemoteXRControllerQuery(ctx.world);
for (let i = 0; i < remoteXRControllers.length; i++) {
const eid = remoteXRControllers[i];
const node = tryGetRemoteResource<RemoteNode>(ctx, eid);
Expand All @@ -184,7 +189,7 @@ export function WebXRAvatarRigSystem(ctx: GameContext) {
node.visible = !(sceneSupportsAR && ourXRMode === XRMode.ImmersiveAR && theirXRMode === XRMode.ImmersiveAR);
}

const remoteXRHeads = remoteXRHeadQuery(ctx.world);
const remoteXRHeads = enteredRemoteXRHeadQuery(ctx.world);
for (let i = 0; i < remoteXRHeads.length; i++) {
const eid = remoteXRHeads[i];
const node = tryGetRemoteResource<RemoteNode>(ctx, eid);
Expand All @@ -195,18 +200,19 @@ export function WebXRAvatarRigSystem(ctx: GameContext) {
node.visible = !(sceneSupportsAR && ourXRMode === XRMode.ImmersiveAR && theirXRMode === XRMode.ImmersiveAR);
}

const remoteAvatars = remoteAvatarQuery(ctx.world);
const remoteAvatars = enteredRemoteAvatarQuery(ctx.world);
for (let i = 0; i < remoteAvatars.length; i++) {
const eid = remoteAvatars[i];
const node = tryGetRemoteResource<RemoteNode>(ctx, eid);
const peerId = network.entityIdToPeerId.get(eid)!;
const theirXRMode = network.peerIdToXRMode.get(peerId)!;
// const peerId = network.entityIdToPeerId.get(eid)!;
// const theirXRMode = network.peerIdToXRMode.get(peerId)!;

const avatarEid = AvatarRef.eid[node.eid];
const avatar = tryGetRemoteResource<RemoteNode>(ctx, avatarEid);

// regular avatar is hidden for XR participants
avatar.visible = !(theirXRMode === XRMode.ImmersiveAR || theirXRMode === XRMode.ImmersiveVR);
// TODO
// avatar.visible = !(theirXRMode === XRMode.ImmersiveAR || theirXRMode === XRMode.ImmersiveVR);

// re-scale if the remote avatar is visible and we are AR
if (avatar.visible && ourXRMode === XRMode.ImmersiveAR) {
Expand Down Expand Up @@ -322,15 +328,15 @@ function updateXRController(

// networked ray
const networkedRayNode = createPrefabEntity(ctx, "xr-ray");
addComponent(ctx.world, Owned, networkedRayNode.eid);
addComponent(ctx.world, Authoring, networkedRayNode.eid);
addComponent(ctx.world, Networked, networkedRayNode.eid);
addObjectToWorld(ctx, networkedRayNode);

networkedRayNode.visible = false;

// networked hand
const networkedController = createPrefabEntity(ctx, `xr-hand-${hand}`);
addComponent(ctx.world, Owned, networkedController.eid);
addComponent(ctx.world, Authoring, networkedController.eid);
addComponent(ctx.world, Networked, networkedController.eid);

networkedController.visible = false;
Expand Down Expand Up @@ -404,7 +410,7 @@ function updateXRCamera(

if (!cameraNode || !xrRig.cameraEid) {
cameraNode = createPrefabEntity(ctx, "xr-head");
addComponent(ctx.world, Owned, cameraNode.eid);
addComponent(ctx.world, Authoring, cameraNode.eid);
addComponent(ctx.world, Networked, cameraNode.eid);
cameraNode.visible = false;
addObjectToWorld(ctx, cameraNode);
Expand Down
4 changes: 2 additions & 2 deletions src/engine/input/input.game.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ourPlayerQuery } from "../player/Player";
import { GameContext } from "../GameTypes";
import { defineModule, getModule, Thread } from "../module/module.common";
import { getCamera } from "../player/getCamera";
import { tryGetCamera } from "../player/getCamera";
import { XRMode } from "../renderer/renderer.common";
import { getXRMode } from "../renderer/renderer.game";
import { RemoteNode } from "../resource/RemoteResources";
Expand Down Expand Up @@ -58,6 +58,6 @@ export function getPrimaryInputSourceNode(ctx: GameContext) {
return rightRayNode;
} else {
const playerNode = getRemoteResource<RemoteNode>(ctx, ourPlayer) as RemoteNode;
return getCamera(ctx, playerNode).parent as RemoteNode;
return tryGetCamera(ctx, playerNode).parent as RemoteNode;
}
}
Loading