Decentraland scenes are built around entities, components and systems. This is a common pattern used in the architecture of several game engines, that allows for easy composability and scalability.
Entities are the basic unit for building everything in Decentraland scenes. All visible and invisible 3D objects and audio players in your scene will each be an entity. An entity is nothing more than a container that holds components. The entity itself has no properties or methods of its own, it simply groups several components together.
Components define the traits of an entity. For example, a transform
component stores the entity's coordinates, rotation and scale. A BoxShape
component gives the entity a box shape when rendered in the scene, a Material
component gives the entity a color or texture. You could also create a custom health
component to store an entity's remaining health value, and add it to entities that represent non-player enemies in a game.
If you're familiar with web development, think of entities as the equivalent of Elements in a DOM tree, and of components as attributes of those elements.
Note: In previous versions of the SDK, the scene state was stored in an object that was separate from the entities themselves. As of version 5.0, the scene state is directly embodied by the components that are used by the entities in the scene.
Components like Transform
, Material
or any of the shape components are closely tied in with the rendering of the scene. If the values in these components change, that alone is enough to change how the scene is rendered in the next frame.
Components are meant to store data about their parent entity. They only store this data, they shouldn't modify it themselves. All changes to the values in the components are carried out by Systems. Systems are completely decoupled from the components and entities themselves. Entities and components are agnostic to what systems are acting upon them.
See Component Reference for a reference of all the available constructors for predefined components.
Entities and components are declared as TypeScript objects. The example below shows some basic operations of declaring, configuring and assigning these.
// Create an entityconst box = new Entity()// Create and add a `Transform` component to that entitybox.addComponent(new Transform())// Set the fields in the componentbox.getComponent(Transform).position.set(3, 1, 3)// Create and apply a `BoxShape` component to give the entity a visible formbox.addComponent(new BoxShape())// Add the entity to the engineengine.addEntity(box)COPY CODE
When you create a new entity, you're instancing an object and storing it in memory. A newly created entity isn't rendered and it won't be possible for a player to interact with it until it's added to the engine.
The engine is the part of the scene that sits in the middle and manages all of the other parts. It determines what entities are rendered and how players interact with them. It also coordinates what functions from systems are executed and when.
// Create an entityconst box = new Entity()// Give the entity a shapebox.addComponent(new BoxShape())// Add the entity to the engineengine.addEntity(box)COPY CODE
In the example above, the newly created entity isn't viewable by players on your scene until it's added to the engine.
Note: Entities aren't added to Component groups either until they are added to the engine.
It’s sometimes useful to preemptively create entities and not add them to the engine until they are needed. This is especially true for entities that have elaborate geometries that might otherwise take long to load.
When an entity is added to the engine, its alive
property is implicitly set to true. You can check if an entity is currently added to the engine via this property.
if (myEntity.alive) {log("the entity is added to the engine")}COPY CODE
Note: It's always recommended to add a
Transform
component to an entity before adding it to the engine. Entities that don't have a Transform component are rendered in the (0, 0, 0) position of the scene, so if the entity is added before it has aTransform
, it will be momentarily rendered in that position, and with its original size and rotation.
Entities that have been added to the engine can also be removed from it. When an entity is removed, it stops being rendered by the scene and players can no longer interact with it.
// Remove an entity from the engineengine.removeEntity(box)COPY CODE
Note: Removed entities are also removed from all Component groups.
If your scene has a pointer referencing a removed entity, it will remain in memory, allowing you to still access and change its component's values and add it back.
If a removed entity has child entities, all children of that entity are removed too.
An entity can have other entities as children. Thanks to this, we can arrange entities into trees, just like the HTML of a webpage.
To set an entity as the parent of another, simply use .setParent()
:
// Create entitiesconst parentEntity = new Entity()engine.addEntity(parentEntity)const childEntity = new Entity()// Set parentchildEntity.setParent(parentEntity)COPY CODE
Note: Child entities should not be explicitly added to the engine, as they are already added via their parent entity.
Once a parent is assigned, it can be read off the child entity with .getParent()
.
// Get parent from an entityconst parent = childEntity.getParent()COPY CODE
If a parent entity has a transform
component that affects its position, scale or rotation, its children entities are also affected.
Entities with no shape component are invisible in the scene. These can be used as wrappers to handle and position multiple entities as a group.
To remove an entity's parent, you can assign the entity's parent to null
.
childEntity.setParent(null)COPY CODE
If you set a new parent to an entity that already had a parent, the new parent will overwrite the old one.
Every entity in your scene has a unique autogenrated id property. You can retrieve a specific entity from the engine based on this ID, by referring to the engine.entities[]
array.
engine.entities[entityId]COPY CODE
For example, if a player's click or a raycast hits an entity, this will return the id of the hit entity, and you can use the command above to fetch the entity that matches that id.
When a component is added to an entity, the component's values affect the entity.
One way of doing this is to first create the component instance, and then add it to an entity in a separate expression:
// Create entityconst box = new Entity()engine.addEntity(box)// Create componentconst myMaterial = new Material()// Configure componentmyMaterial.albedoColor = Color3.Red()// Add componentbox.addComponent(myMaterial)COPY CODE
You can otherwise use a single expression to both create a new instance of a component and add it to an entity:
// Create entityconst box = new Entity()engine.addEntity(box)// Create and add componentbox.addComponent(new Material())// Configure componentbox.getComponent(Material).albedoColor = Color3.Red()COPY CODE
Note: In the example above, as you never define a pointer to the entity's material component, you need to refer to it through its parent entity using
.getComponent()
.
By using .addComponentOrReplace()
instead of .addComponent()
you overwrite any existing components of the same kind on a specific entity.
To remove a component from an entity, simply use the entity's removeComponent()
method.
myEntity.removeComponent(Material)COPY CODE
If you attempt to remove a component that doesn't exist in the entity, this action won't raise any errors.
A removed component might still remain in memory even after removed. If your scene adds new components and removes them regularly, these removed components will add up and cause memory problems. It's advisable to instead use an object pool when possible to handle these components.
You can reach components through their parent entity by using the entity's .getComponent()
function.
// Create entity and componentconst box = new Entity()// Create and add componentbox.addComponent(new Transform())// Using getlet transform = box.getComponent(Transform)// Edit values in the componenttransform.position = new Vector3(5, 0, 5)COPY CODE
The getComponent()
function fetches a reference to the component object. If you change the values of what's returned by this function, you're changing the component itself. For example, in the example above, we're setting the position
stored in the component to (5, 0, 5).
box.getComponent(Transform).scale.x = Math.random() * 10COPY CODE
The example above directly modifies the value of the x scale on the Transform component.
If you're not entirely sure if the entity does have the component you're trying to retrieve, use getComponentOrNull()
or getComponentOrCreate()
// getComponentOrNulllet scale = box.getComponentOrNull(Transform)// getComponentOrCreatelet scale = box.getComponentOrCreate(Transform)COPY CODE
If the component you're trying to retrieve doesn't exist in the entity:
getComponent()
returns an error.getComponentOrNull()
returns Null
.getComponentOrCreate()
instances a new component in its place and retrieves it.When you're dealing with Interchangeable component, you can also get a component by space name instead of by type. For example, both BoxShape
and SphereShape
occupy the shape
space of an entity. If you don't know which of these an entity has, you can fetch the shape
of the entity, and it will return whichever component is occupying the shape
space.
let entityShape = myEntity.getComponent(shape)COPY CODE
If you plan to spawn and despawn similar entities from your scene, it's often a good practice to keep a fixed set of entities in memory. Instead of creating new entities and deleting them, you could add and remove existing entities from the engine. This is an efficient way to deal with the player's memory.
Entities that are not added to the engine aren't rendered as part of the scene, but they are kept in memory, making them quick to load if needed. Their geometry doesn't add up to the maximum triangle count for your scene while they aren't being rendered.
// Define spawner singleton objectconst spawner = {MAX_POOL_SIZE: 20,pool: [] as Entity[],spawnEntity() {// Get an entity from the poolconst ent = spawner.getEntityFromPool()if (!ent) return// Add a transform component to the entitylet t = ent.getComponentOrCreate(Transform)t.scale.setAll(0.5)t.position.set(5, 0, 5)//add entity to engineengine.addEntity(ent)},getEntityFromPool(): Entity | null {// Check if an existing entity can be usedfor (let i = 0; i < spawner.pool.length; i++) {if (!spawner.pool[i].alive) {return spawner.pool[i]}}// If none of the existing are available, create a new one, unless the maximum pool size is reachedif (spawner.pool.length < spawner.MAX_POOL_SIZE) {const instance = new Entity()spawner.pool.push(instance)return instance}return null},}spawner.spawnEntity()COPY CODE
When adding an entity to the engine, its alive
field is implicitly set to true
, when removing it, this field is set to false
.
Using an object pool has the following benefits: