@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
278 lines • 12.9 kB
JavaScript
import * as flatbuffers from "flatbuffers";
import { Euler, Quaternion, Vector3 } from "three";
import { InstancingUtil } from "../engine/engine_instancing.js";
import { onUpdate } from '../engine/engine_lifecycle_api.js';
import { OwnershipModel, RoomEvents } from "../engine/engine_networking.js";
import { sendDestroyed } from '../engine/engine_networking_instantiate.js';
import { setWorldEuler } from '../engine/engine_three_utils.js';
import * as utils from "../engine/engine_utils.js";
import { registerBinaryType } from '../engine-schemes/schemes.js';
import { SyncedTransformModel } from '../engine-schemes/synced-transform-model.js';
import { Transform } from '../engine-schemes/transform.js';
import { Behaviour, GameObject } from "./Component.js";
import { Rigidbody } from "./RigidBody.js";
const debug = utils.getParam("debugsync");
export const SyncedTransformIdentifier = "STRS";
registerBinaryType(SyncedTransformIdentifier, SyncedTransformModel.getRootAsSyncedTransformModel);
const builder = new flatbuffers.Builder();
/** Creates a flatbuffer model containing the transform data of a game object. Used by {@link SyncedTransform}
*/
export function createTransformModel(guid, b, fast = true) {
builder.clear();
const guidObj = builder.createString(guid);
SyncedTransformModel.startSyncedTransformModel(builder);
SyncedTransformModel.addGuid(builder, guidObj);
SyncedTransformModel.addFast(builder, fast);
const p = b.worldPosition;
const r = b.worldEuler;
const s = b.gameObject.scale; // todo: world scale
// console.log(p, r, s);
SyncedTransformModel.addTransform(builder, Transform.createTransform(builder, p.x, p.y, p.z, r.x, r.y, r.z, s.x, s.y, s.z));
const res = SyncedTransformModel.endSyncedTransformModel(builder);
// SyncedTransformModel.finishSyncedTransformModelBuffer(builder, res);
builder.finish(res, SyncedTransformIdentifier);
return builder.asUint8Array();
}
let FAST_ACTIVE_SYNCTRANSFORMS = 0;
let FAST_INTERVAL = 0;
onUpdate((ctx) => {
const isRunningOnGlitch = ctx.connection.currentServerUrl?.includes("glitch");
const threshold = isRunningOnGlitch ? 10 : 40;
FAST_INTERVAL = Math.floor(FAST_ACTIVE_SYNCTRANSFORMS / threshold);
FAST_ACTIVE_SYNCTRANSFORMS = 0;
if (debug && FAST_INTERVAL > 0)
console.log("Sync Transform Fast Interval", FAST_INTERVAL);
});
/**
* SyncedTransform is a behaviour that syncs the transform of a game object over the network.
* @category Networking
* @group Components
*/
export class SyncedTransform extends Behaviour {
// public autoOwnership: boolean = true;
overridePhysics = true;
interpolatePosition = true;
interpolateRotation = true;
fastMode = false;
syncDestroy = false;
// private _state!: SyncedTransformModel;
_model = null;
_needsUpdate = true;
rb = null;
_wasKinematic = false;
_receivedDataBefore = false;
_targetPosition;
_targetRotation;
_receivedFastUpdate = false;
_shouldRequestOwnership = false;
/** Request ownership of an object - you need to be connected to a room */
requestOwnership() {
if (debug)
console.log("Request ownership");
if (!this._model) {
this._shouldRequestOwnership = true;
this._needsUpdate = true;
}
else
this._model.requestOwnership();
}
hasOwnership() {
return this._model?.hasOwnership ?? undefined;
}
isOwned() {
return this._model?.isOwned;
}
joinedRoomCallback = null;
receivedDataCallback = null;
/** @internal */
awake() {
if (debug)
console.log("new instance", this.guid, this);
this._receivedDataBefore = false;
this._targetPosition = new Vector3();
this._targetRotation = new Quaternion();
// sync instantiate issue was because they shared the same last pos vector!
this.lastWorldPos = new Vector3();
this.lastWorldRotation = new Quaternion();
this.rb = GameObject.getComponentInChildren(this.gameObject, Rigidbody);
if (this.rb) {
this._wasKinematic = this.rb.isKinematic;
}
this.receivedUpdate = true;
// this._state = new TransformModel(this.guid, this);
this._model = new OwnershipModel(this.context.connection, this.guid);
if (this.context.connection.isConnected) {
this.tryGetLastState();
}
this.joinedRoomCallback = this.tryGetLastState.bind(this);
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.joinedRoomCallback);
this.receivedDataCallback = this.onReceivedData.bind(this);
this.context.connection.beginListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
}
/** @internal */
onDestroy() {
// TODO: can we add a new component for this?! do we really need this?!
if (this.syncDestroy)
sendDestroyed(this.guid, this.context.connection);
this._model = null;
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.joinedRoomCallback);
this.context.connection.stopListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
}
tryGetLastState() {
const model = this.context.connection.tryGetState(this.guid);
if (model)
this.onReceivedData(model);
}
tempEuler = new Euler();
onReceivedData(data) {
if (this.destroyed)
return;
if (typeof data.guid === "function" && data.guid() === this.guid) {
if (debug)
console.log("new data", this.context.connection.connectionId, this.context.time.frameCount, this.guid, data);
this.receivedUpdate = true;
this._receivedFastUpdate = data.fast();
const transform = data.transform();
if (transform) {
InstancingUtil.markDirty(this.gameObject, true);
const position = transform.position();
if (position) {
if (this.interpolatePosition)
this._targetPosition?.set(position.x(), position.y(), position.z());
if (!this.interpolatePosition || !this._receivedDataBefore)
this.setWorldPosition(position.x(), position.y(), position.z());
}
const rotation = transform.rotation();
if (rotation) {
this.tempEuler.set(rotation.x(), rotation.y(), rotation.z());
if (this.interpolateRotation) {
this._targetRotation.setFromEuler(this.tempEuler);
}
if (!this.interpolateRotation || !this._receivedDataBefore)
setWorldEuler(this.gameObject, this.tempEuler);
}
}
this._receivedDataBefore = true;
// if (this.rb && !this._model?.hasOwnership) {
// this.rb.setBodyFromGameObject(data.velocity)
// }
}
}
/** @internal */
onEnable() {
this.lastWorldPos.copy(this.worldPosition);
this.lastWorldRotation.copy(this.worldQuaternion);
this._needsUpdate = true;
// console.log("ENABLE", this.guid, this.gameObject.guid, this.lastWorldPos);
if (this._model) {
this._model.updateIsOwned();
}
}
/** @internal */
onDisable() {
if (this._model)
this._model.freeOwnership();
}
receivedUpdate = false;
lastWorldPos;
lastWorldRotation;
/** @internal */
onBeforeRender() {
if (!this.activeAndEnabled || !this.context.connection.isConnected)
return;
// console.log("BEFORE RENDER", this.destroyed, this.guid, this._model?.isOwned, this.name, this.gameObject);
if (!this.context.connection.isInRoom || !this._model) {
if (debug)
console.log("no model or room", this.name, this.guid, this.context.connection.isInRoom);
return;
}
if (this._shouldRequestOwnership) {
this._shouldRequestOwnership = false;
this._model.requestOwnership();
}
const wp = this.worldPosition;
const wr = this.worldQuaternion;
if (this._model.isOwned && !this.receivedUpdate) {
const worlddiff = wp.distanceTo(this.lastWorldPos);
const worldRot = wr.angleTo(this.lastWorldRotation);
const threshold = this._model.hasOwnership || this.fastMode ? .0001 : .001;
if (worlddiff > threshold || worldRot > threshold) {
// console.log(worlddiff, worldRot);
if (!this._model.hasOwnership) {
if (debug)
console.log(this.guid, "reset because not owned but", this.gameObject.name, this.lastWorldPos);
this.worldPosition = this.lastWorldPos;
wp.copy(this.lastWorldPos);
this.worldQuaternion = this.lastWorldRotation;
wr.copy(this.lastWorldRotation);
InstancingUtil.markDirty(this.gameObject, true);
this._needsUpdate = false;
}
else {
this._needsUpdate = true;
}
}
}
// else if (this._model.isOwned === false) {
// if (!this._didRequestOwnershipOnce && this.autoOwnership) {
// this._didRequestOwnershipOnce = true;
// this._model.requestOwnershipIfNotOwned();
// }
// }
if (this._model && !this._model.hasOwnership && this._model.isOwned) {
if (this._receivedDataBefore) {
const factor = this._receivedFastUpdate || this.fastMode ? .5 : .3;
const t = factor; //Mathf.clamp01(this.context.time.deltaTime * factor);
let requireMarkDirty = false;
if (this.interpolatePosition && this._targetPosition) {
const pos = this.worldPosition;
pos.lerp(this._targetPosition, t);
this.worldPosition = pos;
requireMarkDirty = true;
}
if (this.interpolateRotation && this._targetRotation) {
const rot = this.worldQuaternion;
rot.slerp(this._targetRotation, t);
this.worldQuaternion = rot;
requireMarkDirty = true;
}
if (requireMarkDirty)
InstancingUtil.markDirty(this.gameObject, true);
}
}
this.receivedUpdate = false;
this.lastWorldPos.copy(wp);
this.lastWorldRotation.copy(wr);
if (!this._model)
return;
if (!this._model || this._model.hasOwnership === undefined || !this._model.hasOwnership) {
// if we're not the owner of this synced transform then don't send any data
return;
}
// local user is owner:
if (this.rb && this.overridePhysics) {
if (this._wasKinematic !== undefined) {
if (debug)
console.log("reset kinematic", this.rb.name, this._wasKinematic);
this.rb.isKinematic = this._wasKinematic;
}
// TODO: if the SyncedTransform has a dynamic rigidbody we should probably synchronize the Rigidbody's properties (velocity etc)
// Or the Rigidbody should be synchronized separately in which case the SyncedTransform should be passive
}
const updateInterval = 10;
const fastUpdate = this.rb || this.fastMode;
if (this._needsUpdate && (updateInterval <= 0 || updateInterval > 0 && this.context.time.frameCount % updateInterval === 0 || fastUpdate)) {
FAST_ACTIVE_SYNCTRANSFORMS++;
if (fastUpdate && FAST_INTERVAL > 0 && this.context.time.frameCount % FAST_INTERVAL !== 0)
return;
if (debug)
console.log("send update", this.context.connection.connectionId, this.guid, this.gameObject.name, this.gameObject.guid);
this._needsUpdate = false;
const st = createTransformModel(this.guid, this, fastUpdate ? true : false);
// this._state.update(this, this.rb);
// this._state.fast = fastUpdate ? true : false;
this.context.connection.sendBinary(st);
}
}
}
//# sourceMappingURL=SyncedTransform.js.map