@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in
325 lines • 14.4 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Object3D } from "three";
import { AssetReference } from "../../engine/engine_addressables.js";
import { RoomEvents } from "../../engine/engine_networking.js";
import { syncField } from "../../engine/engine_networking_auto.js";
import { syncDestroy } from "../../engine/engine_networking_instantiate.js";
import { serializable } from "../../engine/engine_serialization_decorator.js";
import { delay, getParam } from "../../engine/engine_utils.js";
import { Behaviour, Component, GameObject } from "../../engine-components/Component.js";
import { EventList } from "../../engine-components/EventList.js";
const debug = getParam("debugplayersync");
/**
* This component instantiates an asset for each player that joins a networked room. The asset will be destroyed when the player leaves the room.
* The asset should have a PlayerState component, and can have other components like SyncedTransform, custom components, etc.
* @category Networking
*/
export class PlayerSync extends Behaviour {
/**
* This API is experimental and may change or be removed in the future.
* Create a PlayerSync instance at runtime from a given URL
* @example
* ```typescript
* const res = await PlayerSync.setupFrom("/assets/demo.glb");
* addComponent(res.asset?.asset, DragControls);
* addComponent(res.asset?.asset, SyncedTransform);
* scene.add(res.gameObject);
* ```
*/
static async setupFrom(url, init) {
const assetReference = AssetReference.getOrCreateFromUrl(url);
if (!assetReference.asset) {
const i = await assetReference.loadAssetAsync();
GameObject.getOrAddComponent(i, PlayerState);
}
const ps = new PlayerSync();
ps._internalInit(init);
ps.asset = assetReference;
const obj = new Object3D();
obj["guid"] = url;
GameObject.addComponent(obj, ps);
return ps;
}
/** when enabled PlayerSync will automatically load and instantiate the assigned asset when joining a networked room */
autoSync = true;
/** This asset will be loaded and instantiated when PlayerSync becomes active and joins a networked room */
asset;
/** Event called when an instance is spawned */
onPlayerSpawned;
_localInstance;
awake() {
this.watchTabVisible();
if (!this.onPlayerSpawned)
this.onPlayerSpawned = new EventList();
}
onEnable() {
this.context.connection.beginListen(RoomEvents.RoomStateSent, this.onJoinedRoom);
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
this.context.connection.beginListen(RoomEvents.LeftRoom, this.destroyInstance);
if (this.context.connection.isInRoom) {
this.onJoinedRoom();
}
}
onDisable() {
this.context.connection.stopListen(RoomEvents.RoomStateSent, this.onJoinedRoom);
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.onJoinedRoom);
this.context.connection.stopListen(RoomEvents.LeftRoom, this.destroyInstance);
}
onJoinedRoom = () => {
if (debug)
console.log("PlayerSync.joinedRoom. autoSync is set to " + this.autoSync);
if (this.autoSync)
this.getInstance();
};
async getInstance() {
if (this._localInstance)
return this._localInstance;
if (debug)
console.log("PlayerSync.createInstance", this.asset?.url);
if (!this.asset?.asset && !this.asset?.url) {
console.error("PlayerSync: can not create an instance because \"asset\" is not set and or has no URL!");
return null;
}
if (!this.gameObject.guid) {
console.warn("PlayerSync: gameObject has no guid! This might cause issues with syncing the player state.");
}
this._localInstance = this.asset?.instantiateSynced({ parent: this.gameObject, deleteOnDisconnect: true }, true);
const instance = await this._localInstance;
if (instance) {
const pl = GameObject.getComponentsInChildren(instance, PlayerState);
if (pl?.length) {
for (const state of pl)
state.owner = this.context.connection.connectionId;
this.onPlayerSpawned?.invoke(instance);
}
else {
this._localInstance = undefined;
console.error("<strong>Failed finding PlayerState on " + this.asset?.url + "</strong>: please make sure the asset has a PlayerState component!");
GameObject.destroySynced(instance);
}
}
else {
this._localInstance = undefined;
console.warn("PlayerSync: failed instantiating asset!");
}
return this._localInstance;
}
destroyInstance = () => {
this._localInstance?.then(go => {
if (debug)
console.log("PlayerSync.destroyInstance", go);
syncDestroy(go, this.context.connection, true, { saveInRoom: false });
});
this._localInstance = undefined;
};
watchTabVisible() {
window.addEventListener("visibilitychange", _ => {
if (document.visibilityState === "visible") {
for (let i = PlayerState.all.length - 1; i >= 0; i--) {
const pl = PlayerState.all[i];
if (!pl.owner || !this.context.connection.userIsInRoom(pl.owner)) {
pl.doDestroy();
}
}
}
});
}
}
__decorate([
serializable()
], PlayerSync.prototype, "autoSync", void 0);
__decorate([
serializable(AssetReference)
], PlayerSync.prototype, "asset", void 0);
__decorate([
serializable(EventList)
], PlayerSync.prototype, "onPlayerSpawned", void 0);
export var PlayerStateEvent;
(function (PlayerStateEvent) {
PlayerStateEvent["OwnerChanged"] = "ownerChanged";
})(PlayerStateEvent || (PlayerStateEvent = {}));
export class PlayerState extends Behaviour {
static _all = [];
/** all instances for all players */
static get all() {
return PlayerState._all;
}
;
static _local = [];
/** all instances for the local player */
static get local() {
return PlayerState._local;
}
static getFor(obj) {
if (obj instanceof Object3D) {
return GameObject.getComponentInParent(obj, PlayerState);
}
else if (obj instanceof Component) {
return GameObject.getComponentInParent(obj.gameObject, PlayerState);
}
return undefined;
}
//** use to check if a component or gameobject is part of a instance owned by the local player */
static isLocalPlayer(obj) {
const state = PlayerState.getFor(obj);
return state?.isLocalPlayer ?? false;
}
// static Callback
static _callbacks = {};
/**
* Add a callback for a PlayerStateEvent
*/
static addEventListener(event, cb) {
if (!this._callbacks[event])
this._callbacks[event] = [];
this._callbacks[event].push(cb);
return cb;
}
static removeEventListener(event, cb) {
if (!this._callbacks[event])
return;
const index = this._callbacks[event].indexOf(cb);
if (index >= 0)
this._callbacks[event].splice(index, 1);
}
static dispatchEvent(event, args) {
if (!this._callbacks[event])
return;
for (const cb of this._callbacks[event]) {
cb(args);
}
}
onOwnerChangeEvent = new EventList();
onFirstOwnerChangeEvent = new EventList();
hasOwner = false;
owner;
/** when enabled PlayerSync will not destroy itself when not connected anymore */
dontDestroy = false;
get isLocalPlayer() {
return this.owner === this.context.connection.connectionId;
}
onOwnerChange(newOwner, oldOwner) {
if (debug)
console.log("PlayerSync.onOwnerChange", this, "newOwner", newOwner, "oldOwner", oldOwner);
// Remove from local owner array if it was local before
const index = PlayerState._local.indexOf(this);
if (index >= 0)
PlayerState._local.splice(index, 1);
// Args to use for dispatching events
const detail = {
playerState: this,
oldValue: oldOwner,
newValue: newOwner
};
// call local events
if (!this.hasOwner) {
this.hasOwner = true;
this.onFirstOwnerChangeEvent?.invoke(detail);
}
this.onOwnerChangeEvent?.invoke(detail);
// call remote events
if (this.owner === this.context.connection.connectionId) {
PlayerState._local.push(this);
// console.warn(this.gameObject.guid, this.guid, this.owner, this.isLocalPlayer, PlayerState.isLocalPlayer(this));
const evt = new CustomEvent("local-owner-changed", { detail: detail });
this.dispatchEvent(evt);
}
const evt = new CustomEvent("owner-changed", { detail: detail });
this.dispatchEvent(evt);
PlayerState.dispatchEvent(PlayerStateEvent.OwnerChanged, evt);
}
awake() {
PlayerState.all.push(this);
if (debug)
console.log("Registered new PlayerState", this.guid, PlayerState.all.length - 1, PlayerState.all);
this.context.connection.beginListen(RoomEvents.UserLeftRoom, this.onUserLeftRoom);
}
onUserLeftRoom = (model) => {
if (model.userId === this.owner) {
if (debug)
console.log("PLAYERSYNC LEFT", this.owner);
this.doDestroy();
return;
}
};
async start() {
if (debug)
console.log("PLAYERSTATE.START, owner: " + this.owner, this.context.connection.usersInRoom([]));
// generate number from owner
// if (this.owner) {
// // string to number
// let num = 0;
// for (let i = 0; i < this.owner.length; i++) {
// num += this.owner.charCodeAt(i);
// }
// console.log(num)
// num = num / 1000
// this.gameObject.position.y = num;
// }
// If a player is spawned but not in the room anymore we want to destroy it
// this might happen in a case where all users get disconnected at once and the server
// still has the syncInstantiate messages that are sent to all clients
if (this.owner) {
// a slight delay is necessary right now because the syncInstantiate call might has created this object already with the owner assigned but the user has not yet joined the room
if (!this.context.connection.isInRoom)
await delay(300);
if (this.context.connection.userIsInRoom(this.owner) == false) {
if (debug)
console.log(`PlayerSync.start → doDestroy \"${this.name}\" because user \"${this.owner}\" is not in room anymore...`, "Currently in room:", ...this.context.connection.usersInRoom());
this.doDestroy();
}
}
else if (!this.owner) {
if (debug)
console.warn("PlayerState.start → owner is undefined!", this.name);
// we can delete it here immediately because it is not synced anymore or the owner has left the room
// we could also do this in a timeout and check if the owner is still not assigned after a second (but that would be a hack)
setTimeout(() => {
if (!this.destroyed && !this.owner) {
if (!this.dontDestroy) {
if (debug)
console.warn(`PlayerState.start → owner is still undefined: destroying \"${this.name}\" instance now`);
this.doDestroy();
}
else if (debug)
console.warn("PlayerState.start → owner is still undefined but dontDestroy is set to true", this.name);
}
else if (debug)
console.log("PlayerState.start → owner is assigned", this.owner);
}, 2000);
}
}
// onEnable() {
// if (debug) this.startCoroutine(this.debugRoutine());
// }
// *debugRoutine() {
// while (!this.destroyed && this.activeAndEnabled) {
// Gizmos.DrawLabel(this.gameObject.worldPosition, this.owner ?? "no owner");
// yield;
// }
// }
/** this tells the server that this client has been destroyed and the networking message for the instantiate will be removed */
doDestroy() {
if (debug)
console.log("PlayerSync.doDestroy → syncDestroy", this.name);
syncDestroy(this.gameObject, this.context.connection, true, { saveInRoom: false });
}
onDestroy() {
this.context.connection.stopListen(RoomEvents.UserLeftRoom, this.onUserLeftRoom);
PlayerState.all.splice(PlayerState.all.indexOf(this), 1);
if (this.isLocalPlayer) {
const index = PlayerState._local.indexOf(this);
if (index >= 0)
PlayerState._local.splice(index, 1);
}
}
}
__decorate([
syncField(PlayerState.prototype.onOwnerChange)
], PlayerState.prototype, "owner", void 0);
//# sourceMappingURL=PlayerSync.js.map