@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.
372 lines • 15.5 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 { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
import { RoomEvents } from "../engine/engine_networking.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import * as utils from "../engine/engine_utils.js";
import { getParam } from "../engine/engine_utils.js";
import { getIconElement } from "../engine/webcomponents/icons.js";
import { Behaviour } from "./Component.js";
const viewParamName = "view";
const debug = utils.getParam("debugsyncedroom");
/**
* SyncedRoom is a behaviour that will attempt to join a networked room based on the URL parameters or a random room.
* It will also create a button in the menu to join or leave the room.
* You can also join a networked room by calling the core methods like `this.context.connection.joinRoom("roomName")`.
*
* @example Join a networked room
* ```typescript
* const myObject = new Object3D();
* myObject.addComponent(SyncedRoom, { roomName: "myRoom" });
* ```
*
* @example Join a random networked room
* ```typescript
* const myObject = new Object3D();
* myObject.addComponent(SyncedRoom, { joinRandomRoom: true });
* ```
*
* @example Join a random networked room with prefix - this ensures that no room name collisions happen when running multiple applications on the same server instance
* ```typescript
* const myObject = new Object3D();
* myObject.addComponent(SyncedRoom, { joinRandomRoom: true, roomPrefix: "myApp_" });
* ```
*
* @category Networking
* @group Components
*/
export class SyncedRoom extends Behaviour {
/**
* The name of the room to join.
* @default ""
*/
roomName = "";
/**
* The URL parameter name to use for the room name. E.g. if set to "room" the URL will look like `?room=roomName`.
* @default "room"
*/
urlParameterName = "room";
/**
* If true, the room will be joined automatically when this component becomes active.
* @default undefined which means it will join a random room if no roomName is set.
*/
joinRandomRoom;
/**
* If true and no room parameter is found in the URL then no room will be joined.
* @default false
*/
requireRoomParameter = false;
/**
* If true, the room will be rejoined automatically when disconnected.
* @default true
*/
autoRejoin = true;
/**
* If true, a join/leave room button will be created in the menu.
* @default true
*/
createJoinButton = true;
/**
* If true, a join/leave room button for the view only URL will be created in the menu.
* @default false
*/
createViewOnlyButton = false;
/**
* Get current room name from the URL parameter or the view parameter.
*/
get currentRoomName() {
const view = utils.getParam(viewParamName);
if (view)
return view;
return utils.getParam(this.urlParameterName);
}
_lastJoinedRoom;
/** The room prefix to use for the room name. E.g. if set to "room_" and the room name is "name" the final room name will be "room_name". */
set roomPrefix(val) {
this._roomPrefix = val;
}
get roomPrefix() {
return this._roomPrefix;
}
_roomPrefix = "";
/** @internal */
awake() {
if (this.joinRandomRoom === undefined && this.roomName?.length <= 0) {
this.joinRandomRoom = true;
}
if (debug)
console.log(`SyncedRoom roomName:${this.roomName}, urlParamName:${this.urlParameterName}, joinRandomRoom:${this.joinRandomRoom}`);
}
/** @internal */
onEnable() {
// if the url contains a view parameter override room and join in view mode
const viewId = utils.getParam(viewParamName);
if (viewId && typeof viewId === "string" && viewId.length > 0) {
console.log("Join as viewer");
this.context.connection.joinRoom(viewId, true);
return;
}
// If setup to join a random room
this.tryJoinRoom();
if (this.createJoinButton) {
const button = this.createRoomButton();
this.context.menu.appendChild(button);
}
if (this.createViewOnlyButton) {
this.onEnableViewOnlyButton();
}
}
/** @internal */
onDisable() {
this._roomButton?.remove();
this.onDisableViewOnlyButton();
if (this.roomName && this.roomName.length > 0)
this.context.connection.leaveRoom(this.roomName);
}
/** @internal */
onDestroy() {
this.destroyRoomButton();
}
/** Will generate a random room name, set it as an URL parameter and attempt to join the room */
tryJoinRandomRoom() {
this.setRandomRoomUrlParameter();
this.tryJoinRoom();
}
/** Try to join the currently set roomName */
tryJoinRoom(call = 0) {
if (call === undefined)
call = 0;
let hasRoomParameter = false;
if (this.urlParameterName?.length > 0) {
const val = utils.getParam(this.urlParameterName);
if (val && (typeof val === "string" || typeof val === "number")) {
hasRoomParameter = true;
const roomNameParam = utils.sanitizeString(val.toString());
this.roomName = roomNameParam;
}
else if (this.joinRandomRoom) {
console.log("No room name found in url, generating random one");
this.setRandomRoomUrlParameter();
if (call < 1)
return this.tryJoinRoom(call + 1);
}
}
else {
if (this.joinRandomRoom && (this.roomName === null || this.roomName === undefined || this.roomName.length <= 0)) {
this.roomName = this.generateRoomName();
}
}
if (this.requireRoomParameter && !hasRoomParameter) {
if (debug || isDevEnvironment())
console.warn("[SyncedRoom] Missing required room parameter \"" + this.urlParameterName + "\" in url - will not connect.\nTo allow joining a room without a query parameter you can set \"requireRoomParameter\" to false.");
return false;
}
if (!this.context.connection.isConnected) {
this.context.connection.connect();
}
this._lastJoinedRoom = this.roomName;
if (this._roomPrefix)
this.roomName = this._roomPrefix + this.roomName;
if (this.roomName.length <= 0) {
console.warn("[SyncedRoom] Room name is not set so we can not join a networked room.\nPlease choose one of the following options to fix this:\nA) Set a room name in the SyncedRoom component\nB) Set a room name in the URL parameter \"?" + this.urlParameterName + "=my_room\"\nC) Set \"joinRandomRoom\" to true");
return false;
}
if (debug)
console.log("Join " + this.roomName);
this._userWantsToBeInARoom = true;
this.context.connection.joinRoom(this.roomName);
return true;
}
_lastPingTime = 0;
_lastRoomTime = -1;
_userWantsToBeInARoom = false;
/** @internal */
update() {
if (this.context.connection.isConnected) {
if (this.context.time.time - this._lastPingTime > 3) {
this._lastPingTime = this.context.time.time;
this.context.connection.sendPing();
}
if (this.context.connection.isInRoom) {
this._lastRoomTime = this.context.time.time;
}
}
if (this._lastRoomTime > 0 && this.context.time.time - this._lastRoomTime > .3) {
this._lastRoomTime = -1;
if (this.autoRejoin) {
if (this._userWantsToBeInARoom) {
console.log("Disconnected from networking backend - attempt reconnecting now");
this.tryJoinRoom();
}
}
else if (isDevEnvironment())
console.warn("You are not connected to a room anymore (possibly because the tab was inactive for too long and the server kicked you?)");
}
}
/**
* Get the URL to view the current room in view only mode.
*/
getViewOnlyUrl() {
if (this.context.connection.isConnected && this.context.connection.currentRoomViewId) {
const url = window.location.search;
const urlParams = new URLSearchParams(url);
if (urlParams.has(this.urlParameterName))
urlParams.delete(this.urlParameterName);
urlParams.set(viewParamName, this.context.connection.currentRoomViewId);
return window.location.origin + window.location.pathname + "?" + urlParams.toString();
}
return null;
}
setRandomRoomUrlParameter() {
const params = utils.getUrlParams();
const room = this.generateRoomName();
// if we already have this parameter
if (utils.getParam(this.urlParameterName)) {
params.set(this.urlParameterName, room);
}
else
params.append(this.urlParameterName, room);
utils.setState(room, params);
}
generateRoomName() {
let roomName = "";
for (let i = 0; i < 6; i++) {
roomName += Math.floor(Math.random() * 10).toFixed(0);
}
return roomName;
}
_roomButton;
_roomButtonIconJoin;
_roomButtonIconLeave;
createRoomButton() {
if (this._roomButton) {
return this._roomButton;
}
const button = document.createElement("button");
this._roomButton = button;
button.classList.add("create-room-button");
button.setAttribute("priority", "90");
button.onclick = () => {
if (this.context.connection.isInRoom) {
if (this.urlParameterName) {
utils.setParamWithoutReload(this.urlParameterName, null);
}
this.context.connection.leaveRoom();
this._userWantsToBeInARoom = false;
}
else {
if (this.urlParameterName) {
const name = getParam(this.urlParameterName);
// true check for ?room= without an actual name
if (!name || name === true) {
if (this._lastJoinedRoom)
utils.setParamWithoutReload(this.urlParameterName, this._lastJoinedRoom);
else
this.setRandomRoomUrlParameter();
}
;
}
this.tryJoinRoom();
}
};
this._roomButtonIconJoin = getIconElement("group");
this._roomButtonIconLeave = getIconElement("group_off");
this.updateRoomButtonState();
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.updateRoomButtonState);
this.context.connection.beginListen(RoomEvents.LeftRoom, this.updateRoomButtonState);
return button;
}
updateRoomButtonState = () => {
if (!this._roomButton)
return;
if (this.context.connection.isInRoom) {
this._roomButton.title = "Leave the networked room";
this._roomButton.textContent = "Leave Room";
this._roomButtonIconJoin?.remove();
this._roomButton.prepend(this._roomButtonIconLeave);
}
else {
this._roomButton.title = "Create or join a networked room";
this._roomButton.textContent = "Join Room";
this._roomButtonIconLeave?.remove();
this._roomButton.prepend(this._roomButtonIconJoin);
}
};
destroyRoomButton() {
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.updateRoomButtonState);
this.context.connection.stopListen(RoomEvents.LeftRoom, this.updateRoomButtonState);
}
_viewOnlyButton;
onEnableViewOnlyButton() {
if (this.context.connection.isConnected) {
this.onCreateViewOnlyButton();
}
else {
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.onCreateViewOnlyButton);
this.context.connection.beginListen(RoomEvents.JoinedRoom, this.onCreateViewOnlyButton);
}
}
onDisableViewOnlyButton() {
this.context.connection.stopListen(RoomEvents.JoinedRoom, this.onCreateViewOnlyButton);
this._viewOnlyButton?.remove();
}
onCreateViewOnlyButton = () => {
if (!this._viewOnlyButton) {
const button = document.createElement("button");
this._viewOnlyButton = button;
button.classList.add("view-only-button");
button.setAttribute("priority", "90");
button.onclick = () => {
const viewUrl = this.getViewOnlyUrl();
if (viewUrl?.length) {
// share
if (navigator.canShare({ url: viewUrl })) {
navigator.share({ url: viewUrl })?.catch(err => {
console.warn(err);
});
}
else {
navigator.clipboard.writeText(viewUrl);
showBalloonMessage("View only URL copied to clipboard");
}
}
else {
showBalloonWarning("Could not create view only URL");
}
};
button.title = "Copy the view only URL: A page accessed by the view only URL can not be modified by visiting users.";
button.textContent = "Share View URL";
button.prepend(getIconElement("visibility"));
}
this.context.menu.appendChild(this._viewOnlyButton);
};
}
__decorate([
serializable()
], SyncedRoom.prototype, "roomName", void 0);
__decorate([
serializable()
], SyncedRoom.prototype, "urlParameterName", void 0);
__decorate([
serializable()
], SyncedRoom.prototype, "joinRandomRoom", void 0);
__decorate([
serializable()
], SyncedRoom.prototype, "requireRoomParameter", void 0);
__decorate([
serializable()
], SyncedRoom.prototype, "autoRejoin", void 0);
__decorate([
serializable()
], SyncedRoom.prototype, "createJoinButton", void 0);
__decorate([
serializable()
], SyncedRoom.prototype, "createViewOnlyButton", void 0);
__decorate([
serializable()
], SyncedRoom.prototype, "roomPrefix", null);
//# sourceMappingURL=SyncedRoom.js.map