Network instantiation of multiple objects
In a multiuser session, typically objects are instantiated using instantiateSynced as such:
import { Behaviour,GameObject,serializable,InstantiateOptions} from "@needle-tools/engine";
import { Vector3, Object3D, } from "three";
export class InstantiateObjectForAll extends Behaviour
{
@serializable(Object3D)
myPrefab?: GameObject;
public makeObject():void{
const options = new InstantiateOptions();
options.context = this.context;
options.position = new Vector3(0,0,0);
GameObject.instantiateSynced(this.myPrefab, options) as GameObject;
}
}
My particular use-case was for generating programmatically a random scene made of cubes, and that scene had to be the same for all users of the same room. I had used the example above but for some unknown reasons sometimes the scenes were partially rendered when instantiating simultaneously >400 objects. @Marcel of Needle suggested to generate a seed (position of all objects in the scene) and send that seed instead using :
this.context.connection.send()
All users using :
this.context.connection.beginListen()
would receive any seed previously sent, upon joining the same room, allowing them to instantiate cubes according to that seed (array of Vector3).
Here is a script illustrating the use of the send method and the beginListen counterpart:
//This is an example of sending the seed of a randomly generated scene made of cubes, for all other instances logging into the same room to create the same scene.
//This script requires a prefab (e.g. a 1x1x1 Cube)
//This script will generate and build randomly positioned cubes (random walk) as a child of the object it is attached to.
//The generateSeed() method is in this script called via a button. The button is deactivated once the seed has been transmitted.
//Any users joining the same room will receive the seed and build the exact same scene
import { Behaviour,GameObject,serializable,InstantiateOptions} from "@needle-tools/engine";
import { Vector3, Object3D } from "three";
export class NetworkedSeed extends Behaviour
{
@serializable(Object3D)
prefab?: GameObject;
@serializable(Object3D)
generateButton?: Object3D;
public seedSize: number = 30;
seed: Vector3[] = [];
onEnable(): void {
this.context.connection.beginListen("mySeed", this.onDataReceived);
if(this.generateButton)
{
this.generateButton.visible=true;
}
}
onDisable(): void {
this.context.connection.stopListen("mySeed", this.onDataReceived);
}
onDataReceived = (data: any) => {
console.log("Received data:", data.mySeed);
if(this.seed.length===0)
{
//prevent other generations of the seed
if(this.generateButton)
{
this.generateButton.visible=false;
}
this.seed=data.mySeed;
//build scene
this.buildScene();
}
};
//generate and send seed to all from the button generateButton
public generateSeed():void{
if(this.seed.length==0) //no seed found => generate one
{
this.seed = [];
const uniquePositions = new Set<string>();
//start at origin
const startPosition = new Vector3(0, 0, 0);
this.seed.push(startPosition.clone());
uniquePositions.add(startPosition.toArray().toString());
//go for a random walk of length : seedSize
while (this.seed.length < this.seedSize) {
const lastPosition = this.seed[this.seed.length - 1];
let newPosition: Vector3;
//walk and add position, making sure they are unique
do {
const direction = this.getRandomDirection();
newPosition = lastPosition.clone().add(direction);
} while (uniquePositions.has(newPosition.toArray().toString()));
this.seed.push(newPosition.clone());
uniquePositions.add(newPosition.toArray().toString());
}
//send the seed to all on the server
this.sendSeed();
//prevent other generations of the seed
if(this.generateButton)
{
this.generateButton.visible=false;
}
}
//build scene locally
this.buildScene();
}
private sendSeed():void{
if(this.seed.length!=0)
{
this.context.connection.send("mySeed",{guid:this.guid, mySeed: this.seed});
console.log("------ SEED SENT -------");
}
}
public buildScene():void{
//check if the seed is not empty
if(this.seed.length==0)
{
console.log("array was empty");
return;
}
//check if the scene has already been built
if(this.gameObject.children.length>0)
{
console.log("Scene already present");
return;
}
// Create cubes at each position of the random walk
for(let i=0; i<this.seed.length; i++)
{
const option = new InstantiateOptions();
option.context = this.context;
option.parent=this.gameObject;
option.position = this.seed[i];
if(this.prefab!=null)
{
const cube = GameObject.instantiate(this.prefab, option) as GameObject;
}
}
console.log("----------- Scene Built ---------");
}
private getRandomDirection(): Vector3 {
const x = Math.random() < 0.5 ? -1 : 1;
const y = Math.random() < 0.5 ? -1 : 1;
const z = Math.random() < 0.5 ? -1 : 1;
return new Vector3(x, y, z);
}
}
The above script is placed on an object (any Transform) and will generate an array of unique Vector3 positions for a specified length (seedSize) after generateSeed() is called (In this case it is called from a button: generateButton).
Once generated it will send the array to the server and build the scene. The building process consist of instantiating the prefab at each Vector3 position of the seed (this.seed) array.
Any user joining the same room after a seed has been generated and sent, will receive the seed from the server and trigger the callback onDataReceived() which will cache the seed array, disable the button, and build the scene with the prefab, according to the seed.
This gives a way to generate a scene and communicate the seed of that scene, for each user to build locally.
This was the solution I chose which worked better than instantiating a complex scene (>400 objects) with instantiateSynced which would occasionally cause bugs.