Skip to content

Commit

Permalink
correctly sync all bodies to the appropriate clients; correctly init …
Browse files Browse the repository at this point in the history
…WebRTC when a 2nd client connects

improve & simplify the parsing of signaling server URLs. relative urls are not allowed, and the endpoint to connect to for published session codes is relative to the document's origin
thus, this code is now identical in dev & prod, and functions equivalently
  • Loading branch information
Penguin-Spy committed Apr 11, 2024
1 parent 14863f3 commit 864e700
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 90 deletions.
5 changes: 4 additions & 1 deletion client/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export default class Client {
// generate a UUID for the player if one does not exist
this.uuid = localStorage.getItem("player_uuid")
if(this.uuid === null) {
this.uuid = crypto.randomUUID()
// generates a UUID by asking for a blob url (which are always uuids). doesn't use crypto.randomUUID() because it doesn't work in non-secure contexts
const url = URL.createObjectURL(new Blob())
this.uuid = url.substr(-36)
URL.revokeObjectURL(url)
localStorage.setItem("player_uuid", this.uuid)
}
console.log("[client] player uuid", this.uuid)
Expand Down
5 changes: 4 additions & 1 deletion common/World.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Body from "/common/Body.js"

import * as CANNON from 'cannon'
import * as THREE from 'three'
import { DT } from "/common/util.js"
import { CircularQueue, DT } from "/common/util.js"
import CelestialBody from "/common/bodies/CelestialBody.js"
import CharacterBody from "./bodies/CharacterBody.js"
import TestBody from "/common/bodies/TestBody.js"
Expand Down Expand Up @@ -48,6 +48,7 @@ export default class World {
})

this.nextNetID = 0 // unique across everything (even bodies & components won't share one)
this.netSyncQueue = new CircularQueue()

// load bodies
data.bodies.forEach(b => this.loadBody(b))
Expand Down Expand Up @@ -175,9 +176,11 @@ export default class World {
this.physics.addBody(body.rigidBody)
if(body.mesh) this.scene.add(body.mesh)
this.bodies.push(body)

body.netID = this.nextNetID
this.nextNetID++
body.netPriority = 0
this.netSyncQueue.push(body)
}

getBodyByNetID(netID) {
Expand Down
34 changes: 33 additions & 1 deletion common/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,38 @@ class TwoWayMap {
}
}

// circular queue thing
class CircularQueue {
#array; #cursor
constructor() {
this.#array = []
this.#cursor = 0
}

/** The number of elements in the queue */
get size() {
return this.#array.length
}

/** Pushes an element onto the front of the queue with maximum priority.
* @param {any} element */
push(element) {
// insert the new element at the current cursor
this.#array.splice(this.#cursor, 0, element)
}

/** Returns the element with the highest priority and resets its priority.
* @returns {any} */
next() {
const element = this.#array[this.#cursor]
this.#cursor++ // goes to the next highest priority element, looping around to the start of the array when necessary
if(this.#cursor > this.#array.length) {
this.#cursor = 0
}
return element
}
}

/**
* type safety checks during deserialization
* @param {any} variable The value to check the type of
Expand Down Expand Up @@ -43,7 +75,7 @@ function check(variable, type) {
}
}

export { TwoWayMap, check }
export { TwoWayMap, CircularQueue, check }

/**
* The fraction of a second that 1 update takes. \
Expand Down
3 changes: 0 additions & 3 deletions link/Constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const SIGNAL_ENDPOINT = "wss://voxilon.penguinspy.dev/signal"

const PacketType = Object.freeze({
CHAT: 0,
LOAD_WORLD: 1,
Expand All @@ -8,6 +6,5 @@ const PacketType = Object.freeze({
})

export {
SIGNAL_ENDPOINT,
PacketType
}
79 changes: 38 additions & 41 deletions link/DirectLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import PeerConnection from '/link/PeerConnection.js'
import PacketEncoder from '/link/PacketEncoder.js'
import PacketDecoder from '/link/PacketDecoder.js'
import Link from '/link/Link.js'
import { SIGNAL_ENDPOINT, PacketType } from '/link/Constants.js'
import { PacketType } from '/link/Constants.js'
import Client from '/client/Client.js'
import { sessionPublishURL } from '/link/util.js'
const { CHAT, SYNC_BODY } = PacketType

export default class DirectLink extends Link {
/** @type {World} */
world

/**
* @param {Client} client
*/
Expand Down Expand Up @@ -72,17 +76,15 @@ export default class DirectLink extends Link {

/* --- Direct Link methods --- */

async publish(uri) {
async publish() {
try {
console.info("Publishing session to signaling server")

if(!uri) { uri = SIGNAL_ENDPOINT + "/new_session" }

// create session & start listening for WebRTC connections
this.ws = new WebSocket(uri)
this.ws.onmessage = e => {
this.ws = new WebSocket(sessionPublishURL)
this.ws.addEventListener("message", e => {
const data = JSON.parse(e.data)
console.log("[link Receive]", data)
console.log("[link signal receive]", data)
switch(data.type) {
case "hello": // from the signaling server
console.info(`Join code: ${data.join_code}`)
Expand All @@ -102,14 +104,13 @@ export default class DirectLink extends Link {
client.id = data.from
client.uuid = data.uuid

// replaces the websocket onmessage handler with the peer connection one for establishing WebRTC
client.pc = new PeerConnection(this.ws, client.id)

client.dataChannel = client.pc.createDataChannel("link", {
ordered: false,
negotiated: true, id: 0
})
client.dataChannel.onclose = e => { console.info(`[dataChannel:${client.id}] close`) }
client.dataChannel.onclose = e => { console.info(`[dataChannel:${client.id}] close`, e) }
client.dataChannel.onmessage = ({ data }) => {
try {
this._handlePacket(client, data)
Expand Down Expand Up @@ -138,11 +139,11 @@ export default class DirectLink extends Link {
default:
break;
}
}
})

this.ws.onclose = ({ code, reason }) => {
this.ws.addEventListener("close", ({ code, reason }) => {
console.warn(`Websocket closed | ${code}: ${reason}`)
}
})

} catch(err) {
console.error("An error occured while publishing the universe:", err)
Expand All @@ -158,19 +159,19 @@ export default class DirectLink extends Link {
this.emit('chat_message', packet)
this.broadcast(PacketEncoder.CHAT(packet.author, packet.msg))
break;
case SYNC_BODY:
// validate it is the clients own body
if(packet.i !== client.body.netID) {
console.error(`client #${client.id} sent sync packet for incorrect body:`, packet)
client.dataChannel.close()
break
}
client.body.position.set(...packet.p)
client.body.velocity.set(...packet.v)
client.body.quaternion.set(...packet.q)
client.body.angularVelocity.set(...packet.a)
case SYNC_BODY:
// validate it is the clients own body
if(packet.i !== client.body.netID) {
console.error(`client #${client.id} sent sync packet for incorrect body:`, packet)
client.dataChannel.close()
break
}

client.body.position.set(...packet.p)
client.body.velocity.set(...packet.v)
client.body.quaternion.set(...packet.q)
client.body.angularVelocity.set(...packet.a)

break;
default:
throw new TypeError(`Unknown packet type ${packet.$}`)
Expand All @@ -189,24 +190,20 @@ export default class DirectLink extends Link {
}
}

step(deltaTime) {
// update the world (physics & gameplay)
super.step(deltaTime)

// then calculate the priority of objects
// (TODO)
this.client.activeController.body.netPriority++;

// send sync packets
const ourBody = this.client.activeController.body
if(ourBody.netPriority > 60) {
ourBody.netPriority = 0
const ourBodySync = PacketEncoder.SYNC_BODY(this.client.activeController.body)

this.broadcast(ourBodySync)
// ran after each DT world step
postUpdate() {
const body = this.world.netSyncQueue.next()
if(!body) return
const bodySync = PacketEncoder.SYNC_BODY(body)

for(const client of this._clients) {
if(client.dataChannel.readyState === "open" &&
client.body !== body) { // do not send a client a sync packet for their own body, they have the authoritative state of it
client.dataChannel.send(bodySync)
}
}
}

stop() {
this.ws.close(1000, "stopping client")
for(const client of this._clients) {
Expand Down
6 changes: 5 additions & 1 deletion link/Link.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export default class Link {
let maxSteps = 10;

while(this.#accumulator > DT && maxSteps > 0) {
this.world.step(DT)
this.world.step()
this.postUpdate()
this.#accumulator -= DT
maxSteps--
}
Expand All @@ -40,4 +41,7 @@ export default class Link {
}

}

postUpdate() {
}
}
45 changes: 18 additions & 27 deletions link/NetworkLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import PacketEncoder from '/link/PacketEncoder.js'
import PacketDecoder from '/link/PacketDecoder.js'
import Link from '/link/Link.js'
import PlayerController from '/client/PlayerController.js'
import { SIGNAL_ENDPOINT, PacketType } from '/link/Constants.js'
import { PacketType } from '/link/Constants.js'
import { parseSignalTarget } from '/link/util.js'
const { CHAT, LOAD_WORLD, SET_CONTROLLER_STATE, SYNC_BODY } = PacketType

const JOIN_CODE_REGEX = /^([A-HJ-NP-Z0-9]{5})$/

// CONNECTING: waiting for WebSocket connect, join request, and WebRTC connect
// LOADING: WebRTC connected, waiting for world to load
Expand All @@ -29,23 +29,14 @@ export default class NetworkLink extends Link {
this._readyReject = reject
})

// open a WebRTC data channel with the host of the specified game
if(target.match(JOIN_CODE_REGEX)) { // convert join code to full url
console.log(`prefixing ${target}`)
target = `${SIGNAL_ENDPOINT}/${target}`
}
// normalize url (URL constructor is allowed to throw an error)
const targetURL = new URL(target, document.location)
targetURL.protocol = targetURL.hostname === "localhost" ? "ws" : "wss"
targetURL.hash = ""
console.log(targetURL)

// create websocket & add msg handler
const targetURL = parseSignalTarget(target)
console.info("Connecting to ", targetURL.href)
this.ws = new WebSocket(targetURL)

this.ws.onmessage = (e) => {
this.ws.addEventListener("message", e => {
const data = JSON.parse(e.data)
console.log("[link Receive]", data)
console.log("[link signal receive]", data)
switch(data.type) {
case "join":
if(data.approved) { // request to join was approved, continue with WebRTC
Expand Down Expand Up @@ -81,24 +72,24 @@ export default class NetworkLink extends Link {
default:
break;
}
}
})

this.client = client

// finally, request to join
this.ws.onopen = () => {
this.ws.addEventListener("open", () => {
this.ws.send(JSON.stringify({
type: "join",
username: this.username,
uuid: this.client.uuid
}))
}
this.ws.onclose = ({ code, reason }) => {
})
this.ws.addEventListener("close", ({ code, reason }) => {
console.warn(`Websocket closed | ${code}: ${reason}`)
if(this._readyState === CONNECTING) {
this._readyReject(new Error("Websocket closed while connecting"))
}
}
})
}

get ready() { return this._readyPromise }
Expand Down Expand Up @@ -134,17 +125,17 @@ export default class NetworkLink extends Link {
this.client.setController(packet.type, body)

break;

case SYNC_BODY:
const syncedBody = this.world.getBodyByNetID(packet.i)

syncedBody.position.set(...packet.p)
syncedBody.velocity.set(...packet.v)
syncedBody.quaternion.set(...packet.q)
syncedBody.angularVelocity.set(...packet.a)

break;

default:
throw new TypeError(`Unknown packet type ${packet.$}`)
}
Expand All @@ -153,19 +144,19 @@ export default class NetworkLink extends Link {
send(packet) {
this.dataChannel.send(packet)
}

step(deltaTime) {
// update the world (physics & gameplay)
super.step(deltaTime)

// then calculate the priority of syncing our own body
this.bodyNetPriority++
if(this.bodyNetPriority > 30) {
this.bodyNetPriority = 0
this.dataChannel.send(PacketEncoder.SYNC_BODY(this.client.activeController.body))
}
}

stop() {
try {
this.ws.close(1000, "stopping client")
Expand Down
Loading

0 comments on commit 864e700

Please sign in to comment.