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

Client + Server refactors #34

Merged
merged 3 commits into from
Jul 17, 2023
Merged
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
7,068 changes: 1,324 additions & 5,744 deletions package-lock.json

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions packages/character-network/src/CharacterNetworkClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { CharacterNetworkCodec, CharacterNetworkClientUpdate } from "./CharacterNetworkCodec";

export class CharacterNetworkClient {
public connected: boolean = false;
public clientUpdates: Map<number, CharacterNetworkClientUpdate> = new Map();
public id: number = 0;

public sendUpdate(update: CharacterNetworkClientUpdate): void {
if (!this.connected) {
console.log("Not connected to the server");
return;
}
const encodedUpdate = CharacterNetworkCodec.encodeUpdate(update);
this.connection.ws?.send(encodedUpdate);
}

public connection = {
clientId: null as number | null,
ws: null as WebSocket | null,
connect: (url: string, timeout = 5000) => {
return new Promise<void>((resolve, reject) => {
const wsPromise = new Promise<void>((wsResolve, wsReject) => {
try {
this.connection.ws = new WebSocket(url);
this.connection.ws.onerror = () => {
this.connection.ws = null;
this.connected = false;
wsReject(new Error("WebSocket player server not available"));
};
this.connection.ws.onmessage = async (message: MessageEvent) => {
if (typeof message.data === "string") {
const data = JSON.parse(message.data);
if (data.type === "ping") {
this.connection.ws?.send(
JSON.stringify({ type: "pong", id: this.connection.clientId }),
);
}
if (typeof data.connected !== "undefined" && this.connected === false) {
this.connection.clientId = data.id;
this.id = this.connection.clientId!;
this.connected = true;
console.log(`Client ID: ${data.id} joined`);
wsResolve();
}
if (typeof data.disconnect !== "undefined") {
this.clientUpdates.delete(data.id);
console.log(`Client ID: ${data.id} left`);
}
} else if (message.data instanceof Blob) {
const arrayBuffer = await new Response(message.data).arrayBuffer();
const updates = CharacterNetworkCodec.decodeUpdate(arrayBuffer);
this.clientUpdates.set(updates.id, updates);
} else {
console.log(message.data);
}
};
} catch (error) {
console.log("Connection failed:", error);
wsReject(error);
}
});

const timeoutPromise = new Promise<void>((_, timeoutReject) => {
const id = setTimeout(() => {
clearTimeout(id);
timeoutReject(new Error("WS Connection timeout exceeded"));
}, timeout);
});

Promise.race([wsPromise, timeoutPromise])
.then(() => resolve())
.catch((err) => reject(err));
});
},
};
}
82 changes: 82 additions & 0 deletions packages/character-network/src/CharacterNetworkCodec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
export enum AnimationState {
"idle" = 0,
"walking" = 1,
"running" = 2,
"jumpToAir" = 3,
"air" = 4,
"airToGround" = 5,
}

export type CharacterNetworkClientUpdate = {
id: number;
position: { x: number; y: number; z: number };
rotation: { quaternionY: number; quaternionW: number };
state: AnimationState;
};

export class CharacterNetworkCodec {
static animationStateToByte(state: AnimationState): number {
switch (state) {
case AnimationState.idle:
return 0;
case AnimationState.walking:
return 1;
case AnimationState.running:
return 2;
case AnimationState.jumpToAir:
return 3;
case AnimationState.air:
return 4;
case AnimationState.airToGround:
return 5;
default:
throw new Error("Invalid animation state");
}
}

static byteToAnimationState(byte: number): AnimationState {
switch (byte) {
case 0:
return AnimationState.idle;
case 1:
return AnimationState.walking;
case 2:
return AnimationState.running;
case 3:
return AnimationState.jumpToAir;
case 4:
return AnimationState.air;
case 5:
return AnimationState.airToGround;
default:
throw new Error("Invalid byte for animation state");
}
}

static encodeUpdate(update: CharacterNetworkClientUpdate): Uint8Array {
const buffer = new ArrayBuffer(19);
const dataView = new DataView(buffer);
dataView.setUint16(0, update.id); // id
dataView.setFloat32(2, update.position.x); // position.x
dataView.setFloat32(6, update.position.y); // position.y
dataView.setFloat32(10, update.position.z); // position.z
dataView.setInt16(14, update.rotation.quaternionY * 32767); // quaternion.y
dataView.setInt16(16, update.rotation.quaternionW * 32767); // quaternion.w
dataView.setUint8(18, CharacterNetworkCodec.animationStateToByte(update.state)); // animationState
return new Uint8Array(buffer);
}

static decodeUpdate(buffer: ArrayBuffer): CharacterNetworkClientUpdate {
const dataView = new DataView(buffer);
const id = dataView.getUint16(0); // id
const x = dataView.getFloat32(2); // position.x
const y = dataView.getFloat32(6); // position.y
const z = dataView.getFloat32(10); // position.z
const quaternionY = dataView.getInt16(14) / 32767; // quaternion.y
const quaternionW = dataView.getInt16(16) / 32767; // quaternion.w
const state = CharacterNetworkCodec.byteToAnimationState(dataView.getUint8(18)); // animationState
const position = { x, y, z };
const rotation = { quaternionY, quaternionW };
return { id, position, rotation, state };
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { Vector2, Vector3 } from "three";
import WebSocket from "ws";

import { CharacterNetworkCodec } from "./character-network-codec";
import { heartBeatRate, packetsUpdateRate, pingPongRate } from "./character-network-settings";
import { Client, ClientUpdate } from "./types";
import {
AnimationState,
CharacterNetworkClientUpdate,
CharacterNetworkCodec,
} from "./CharacterNetworkCodec";

class CharacterNetworkServer {
codec: CharacterNetworkCodec = new CharacterNetworkCodec();
export type Client = {
socket: WebSocket;
update: CharacterNetworkClientUpdate;
};

clients: Map<number, Client> = new Map();
clientLastPong: Map<number, number> = new Map();
export class CharacterNetworkServer {
private clients: Map<number, Client> = new Map();
private clientLastPong: Map<number, number> = new Map();

constructor() {
setInterval(this.sendPlayerUpdates.bind(this), packetsUpdateRate);
Expand Down Expand Up @@ -59,20 +64,25 @@ class CharacterNetworkServer {
}

for (const { update } of this.clients.values()) {
socket.send(this.codec.encodeUpdate(update));
socket.send(CharacterNetworkCodec.encodeUpdate(update));
}

this.clients.set(id, {
socket: socket as WebSocket,
update: { id, position: new Vector3(), rotation: new Vector2(), state: "idle" },
update: {
id,
position: { x: 0, y: 0, z: 0 },
rotation: { quaternionY: 0, quaternionW: 0 },
state: AnimationState.idle,
},
});

socket.on("message", (message: WebSocket.Data, _isBinary: boolean) => {
let update;

if (message instanceof Buffer) {
const arrayBuffer = new Uint8Array(message).buffer;
update = this.codec.decodeUpdate(arrayBuffer);
update = CharacterNetworkCodec.decodeUpdate(arrayBuffer);
} else {
try {
const data = JSON.parse(message as string);
Expand Down Expand Up @@ -112,18 +122,16 @@ class CharacterNetworkServer {
}

sendPlayerUpdates(): void {
const updates: ClientUpdate[] = [];
const updates: CharacterNetworkClientUpdate[] = [];
this.clients.forEach((client) => {
updates.push(client.update);
});

for (const update of updates) {
const encodedUpdate = this.codec.encodeUpdate(update);
const encodedUpdate = CharacterNetworkCodec.encodeUpdate(update);
this.clients.forEach((client) => {
client.socket.send(encodedUpdate);
});
}
}
}

export { CharacterNetworkServer };
147 changes: 0 additions & 147 deletions packages/character-network/src/character-network-client.ts

This file was deleted.

Loading
Loading