Contributors
Component Creation

Component Creation

How to create new components for ECS7 #

To create new components, we need to do a couple of things before starting coding them in the renderer. There is a step-by-step guide that you need to follow. I will list all the steps, and then you can follow each step with a longer explanation.

  1. Create the proto definition in @dcl/protocol
  2. Generate the new proto TypeScript code in the js-sdk-toolchain
  3. Create a test for js-sdk-toolchain
  4. Generate the new proto C# code in the project
  5. Code the new component
  6. Ensure that the component follows the convention

Create the proto definition in @dcl/protocol #

To create a definition, you must go to this repo: https://github.com/decentraland/protocol

  1. Create the proto definition in this folder: https://github.com/decentraland/protocol/tree/main/ecs/components
  2. Create a PR with the news changes

NOTE: After creating the PR, a GitHub Bot will comment with the package link to test the PR. You can use that link for testing in the following steps

Things to take into account

  • We are using proto 3, so all the definition of the proto must compile with their syntax
  • We have some common types that cannot be recreated
  • The proto should have the basic definition
  • You must add the following code to enumerate the component
import "common/id.proto";
option (ecs_component_id) = 1100;

Example of .proto

syntax = "proto3";

import "common/id.proto";
option (ecs_component_id) = 1020;

message PBAudioSource {
  optional bool playing = 1;
  optional float volume = 2; // default=1.0f
  optional bool loop = 3;
  optional float pitch = 4; // default=1.0f
  string audio_clip_url = 5;
}

Generate the new proto TypeScript code in the js-sdk-toolchain #

Download the following repo: https://github.com/decentraland/js-sdk-toolchain

Inside it, navigate to packages/dcl/ecs (https://github.com/decentraland/js-sdk-toolchain/tree/main/packages/%40dcl/ecs)

And there, you can execute

npm install @dcl/protocol@next

Or the command generated by the GitHub Bot in your @dcl/protocol PR (this must be temporal for testing the PR’s).

Then run the following commands at the root of the js-sdk-toolchain project:

make install
make build

And push the generated code.

Generate the new proto C# code in the project #

To generate the C# code, we need to go to the protocol-gen path in the root of the @decentraland/unity-renderer repository. And execute the following commands:

npm install
npm run build-components

To upgrade to the latest version of the @dcl/protocol (main branch), we should update using

npm install @dcl/protocol@next
npm run build-components

To test a PR, we can use a URL generated by the GitHub Bot in the @dcl/protocol PR: Example:

npm install "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-3143233696.commit-45f1290.tgz"
npm run build-components

After merging @dcl/protocol PR, we must use the @dcl/protocol@next and generate the code.

Code the new component #

Now it is time to implement the functionality of the new component.

The components have five essential parts.

  • ComponentID The ID that the component will use. It must be unique and generated from the proto definition

  • Model This is the model of the component. This is the data that we will use to handle the component. It has been auto-generated with the proto generation and it has the same name of the proto file with a PB in front. For example, if you have a BoxShape.proto definition the generated class of the model will be PBBoxShape

  • Component Handler The component handler will manage all the functionality of the component. In this class you must implement the IECSComponentHandler<ModelClass> (ModelClass is the model. It is a generated class from the proto, the name will be PB + name of the file .proto). This interface has 3 method that are important to implement in order to create a component

        void OnComponentCreated(IParcelScene scene, IDCLEntity entity);
        void OnComponentRemoved(IParcelScene scene, IDCLEntity entity);
        void OnComponentModelUpdated(IParcelScene scene, IDCLEntity entity, ModelType model);
  • Serializer Each component is responsible to implement his serialization and deserialization of the component. This serializer must be able to serialize/deserialize to byte array
  • Register This will register the component into the system, connecting it to the system. This register will register the component in the factory and the component writer

The design of the components is to avoid inheritance so we encourage to use pure functions as much as possible

In order to create them, We must follow the next steps

  1. Create the component folder and assembly. We have all the components unders the follow folder DCLPlugins/ECS7/ECSComponents. You need to create a folder and a new assembly that will hold the component
  2. In the new assembly, you must reference the following one DCL.ECSComponents.Data. This will reference the new model of the component that you just updated
  3. You must create the component handler with all the logic (Take a look at ECSBoxShapeComponentHandler.cs as an example)
  4. You must create the serializer class (probably you can copy it from another class and adapt to your)
  5. You must create the register class
   public static class AudioSourceSerializer
    {
        public static byte[] Serialize(PBAudioSource model)
        {
            int size = model.CalculateSize();
            byte[] buffer = new byte[size];
            CodedOutputStream output = new CodedOutputStream(buffer);
            model.WriteTo(output);
            return buffer;
        }

        public static PBAudioSource Deserialize(object data)
        {
            return PBAudioSource.Parser.ParseFrom((byte[])data);
        }
    }
  1. Add the new register to the ECS7ComponentsComposer class with his corresponding ID
   public class ECS7ComponentsComposer : IDisposable
    {
        private readonly TransformRegister transformRegister;
        private readonly SphereShapeRegister sphereShapeRegister;
        private readonly BoxShapeRegister boxShapeRegister;
        private readonly PlaneShapeRegister planeShapeRegister;
        private readonly CylinderShapeRegister cylinderShapeRegister;
        private readonly AudioStreamRegister audioStreamRegister;
        private readonly AudioSourceRegister audioSourceRegister;

        public ECS7ComponentsComposer(ECSComponentsFactory componentsFactory, IECSComponentWriter componentsWriter)
        {
            transformRegister = new TransformRegister(ComponentID.TRANSFORM, componentsFactory, componentsWriter);
            sphereShapeRegister = new SphereShapeRegister(ComponentID.SPHERE_SHAPE, componentsFactory, componentsWriter);
            boxShapeRegister = new BoxShapeRegister(ComponentID.BOX_SHAPE, componentsFactory, componentsWriter);
            planeShapeRegister = new PlaneShapeRegister(ComponentID.PLANE_SHAPE, componentsFactory, componentsWriter);
            cylinderShapeRegister = new CylinderShapeRegister(ComponentID.CYLINDER_SHAPE, componentsFactory, componentsWriter);
            audioStreamRegister = new AudioStreamRegister(ComponentID.AUDIO_STREAM, componentsFactory, componentsWriter);
            audioSourceRegister = new AudioSourceRegister(ComponentID.AUDIO_SOURCE, componentsFactory, componentsWriter);
        }

        public void Dispose()
        {
            transformRegister.Dispose();
            sphereShapeRegister.Dispose();
            boxShapeRegister.Dispose();
            planeShapeRegister.Dispose();
            cylinderShapeRegister.Dispose();
            audioStreamRegister.Dispose();
            audioSourceRegister.Dispose();
        }
    }

And now you have your component added and working!

Ensure that the component follows the convention #

There is some checklist that we need to have into account while developing new components, this part tries to summarize them.

  • Unit test All the components must include unit test that cover its functionality, dispose and the serialization/deserialization at least to ensure that the component will work
  • Take into account what happens when the component is not inside the scene (Check SceneBoundariesChecker class for more info)
  • If the component renders something into the world, It must add the rendereable info to the data store, this way we add the information of the renderer to the scene so it can counts toward the limit
  • If the component renders something into the world, It must add the MeshesInfo to the entity
  • It must be as perfomant as possible. This code will be executed lots of time so we need to ensure that everything work as smooth as possible
  • It must work with Hot reload in the preview mode. If you has coded the OnComponentRemoved correctly, this will work out of the box, but the hot reload it a way to test that everything work fine with the dispose of the component
  • If the component uses a resource, you must implement the resource management with an AssetPromiseKeeper. The component should notify the AssetPromiseKeeper when the resource is used and when it is not longer used