Guides
Managing scene Components

Managing scene Components

Introduction

Components are the fundamental building blocks to add content to the game space. Components can be diverse: 3D models, VRM Avatars, Images, Audio Assets, Text, ... In fact, everything that can be added to the scene is represented by a component.

In the scripting API, All these components are represented via classes that share a common ancestor class Component3D.

Managing scene components

The entrypoint to working with game components is the Components variable. This is an instance of the ComponentManager class, and it can be imported from the "@oo/scripting" package.

import { Components } from "@oo/scripting"

The Components instance contains all the components added to the space. It is responsible for querying, creating, destroying and duplicating components.

The class offers various ways to query for the created components. We'll describe typical patterns to access scene components from the script.

Accessing a single component via a Script id

When you add a component in the studio, in the left GUI panel, in the Scripting section you can set Script Identifier for the component. Say for example it was set to enemy, In the script you can get the instance create using the ComponentManager.byId() method:

import { Components } from "@oo/scripting"
 
 
export default class GameManager {
 
    // ...
    enemy = null
 
    onReady() {
 
        this.enemy = Components.byId("enemy");
    }
}

One important detail is that we placed the code inside the onReady lifecycle method. This is how we ensure that the component has been loaded. It would be a mistake to place the code, for example, inside the onPreload lifecycle method. Because it's not guaranteed that the component has been loaded and created at this time.

Script identifiers are supposed to be unique among components.

Accessing a collection of components by via a Script tag

Suppose you have many ennemies places on the scene, and you want to access them as a collection. In the studio you can set Script tag for each one of them. Say for example it was set to enemies, In the script you can get the instance create using the ComponentManager.byTag() method:

import { Components } from "@oo/scripting"
 
 
export default class GameManager {
 
    // ...
    enemies = null
 
    onReady() {
 
        this.enemies = Components.byTag("enemies");
    }
}

Accessing components without setting script id or tag

Some time it might be tedious to set a tag for all components; for example say you added a bunch of kit assets of type pickup, ... You can get all the assets by

import { Components } from "@oo/scripting"
 
 
export default class GameManager {
 
    // ...
    pickups = null
 
    onReady() {
 
        this.pickups = Components.filter(it => it.data.kitType?.startsWith("pickup"))
    }
}

There other helper methods that can be used to find specific components. Checkout the docs of ComponentManager class for more information.

Duplicating Components

In a typical workflow, you'll be adding components to the scene through the Oncyber Studio interface. Alternatively you can add a template object in the studio, and then use it to create many instances and place them via scripting.

Suppose, for instance, you have a enemy entity in your game that's represented as an avatar component. You can either place all the enemies beforehand in the studio. Alternatively you can add just one avatar component to the scene and then use the scripting API to create many duplicates the placed avatar. Then you can configure place those duplicated instances in different places on the scene.

For example, assuming you added an avatar component and set its script identifier in the studio to enemy. The following snippet shows how to create many instances of the avatar and place them in the scene:

import { World, Componens, Player } from "@oo/scripting"
 
 
export default class GameManager {
 
    enemies = []
 
    async onReady() {
 
        const enemyTemplate = Components.byId("enemy")
 
        for (let i = 0; i < 20; i++ ) {
 
            const enemy = await enemyTemplate.duplicate()
 
            this.enemies.push(enemy)
        }
 
        // hides the template
        enemyTemplate.visible = false
    }
 
    onStart() {
 
        this.enemies.forEach(enemy => {
 
            const posX = Math.random() * 100
 
            const posZ = Math.random() * 100
 
            enemy.position.set(posX, 0, posZ)
        })
    }
}

In the snippet above, we get our avatar template using Components.byId. We then call the Component3D.duplicate() method to create 20 copies. The copy will initially have the same state as the original one.

Note we placed the duplicate code inside the onReady lifecycle callback, this is where we're sure that all components that were added via studio has been loaded and instantiated.

Next, we use the onStart() lifecycle callback to spawn the enemies at random position on the scene. Using onStart allows us to vary the spawn positions of the enemies on each game restart. If we wanted to keep the same positions accross restarts, we would place the positionning code in onReady instead.

Destroying Components

Sometimes, you may need to remove components from the game, either as part of gameplay or due to changes in the game environment. The destroy method allows you to remove a component from the game scene.

For example, suppose we have various model components used to represent collectible coins. When a player collects a coin, we can remove the collected coin from the scene.

onCollectCoin = (coinModel) => {
 
    coinModel.destroy()
}

Creating Components

Typically you'll be adding components to the scene through the Oncyber Studio interface. But You can also use the scripting API to add components dynamically at runtime.

To add new components to your game, you use the Components.create method. This method allows you to instantiate a new component directly in your game script.

Each component type has a specific data interface that defines its properties and behaviors when created. When creating a component, you provide this schema to the create method. This schema configures the initial state of the component, setting up its characteristics such as size, position, color, or any other relevant properties. For a detailed info on the data schema for each individual component, see the API reference specific to the component.

For example, to create a new 3D model component, you would specify the model file, its initial position, scale, and rotation in the game world:

const myModel = await Components.create({
    type: "model",
    url: "path/to/model.glb",
    position: { x: 0, y: 0, z: 0 },
    scale: { x: 1, y: 1, z: 1 },
    rotation: { x: 0, y: 0, z: 0 }
});

Some components like background, lighting, ... are singletons. Singleton components can only be added via the studio (Some components like background are required and thus automatically added when you create a new game).

Understanding and utilizing components effectively is key to building rich and interactive game environments in Oncyber Studio. By mastering the creation, duplication, and destruction of components, you can dynamically modify the game world in response to player actions and game events.