Decentraland run scenes inside a WebWorker, in an ES5 context including Fetch + WebSockets + dcl object.
Every generated scene should run by itself. That is, a piece of code that can be evaluated by an eval in a proper context. You can compile scenes using Webpack, esbuild, or the Decentraland CLI.
The scenes will interact with the renderer using the dcl object.
The dcl object is a property of the global context of the workers. It has all the methods required to talk to the renderer and receive messages. It also handles module loading for the scenes (like the @decentraland/Ethereum module).
Here is a permalink with the current dcl object, it is the DecentralandInterface: https://github.com/decentraland/explorer/blob/3000b7c97810d72f80a950f757ecfbc1a1af730a/kernel/packages/decentraland-ecs/src/decentraland/Types.ts#L29-L87
Also, Decentraland promised to maintain working the previous versions of the SDK as long as possible. So far, it has been true. Scenes deployed with the first public version of the SDK continue working today.
That is the main reason why everything is condensed in the dcl object. It is the anti-corruption layer that enabled Decentraland to migrate from THREE.js to Babylon to unity without changing the scene's code. And since the underlying implementation is completely decoupled with the scenes, optimizations are easy to do without compromises, or anyone could run scenes in other clients or even native sandboxes leveraging the deployed scenes.
By design, Decentraland assumes the scenes will follow an entity-component system approach. All the messages to the renderer were designed to suffice with the use cases of an ECS. You can find the semantics of the messages in the old babylon implementation https://github.com/decentraland/kernel/blob/6.0.0/packages/engine/entities/SharedSceneContext.ts#L177-L333.
Here is a brief summary of the semantics of the messages:
/// #ECS.SceneStarted: This message is sent after the scene ends executing the initialization code. Before the render loop.
/// #ECS.UpdateEntityComponent: Updates an ephemeral component C by Name in the entity E
/// 0) Find the entity E by ID
/// 1) If E doesn't exist, finalize.
/// 2) E.UpdateComponent(Name, ClassID, JSON)
/// #ECS.AttachEntityComponent: Attach the disposable component C to the entity E
/// 0) Find the entity E by ID
/// 1) If E doesn't exist, finalize.
/// 2) Find the component C by ID
/// 3) If C doesn't exist, finalize.
/// 4) E.attachDisposableComponent(slotName, C)
/// #ECS.ComponentCreated: Creates a disposable component C by `classID` and stores it by ID
/// 0) Find the disposable component constructor K by `classId`
/// 1) If K doesn't exist, finalize.
/// 2) Create instance C using K
/// 3) Register C in the map of disposable components
/// #ECS.ComponentDisposed: Disposes a disposable component C and releases all of it allocated resources
/// 0) Find the disposable component C by ID
/// 1) If C doesn't exist, finalize
/// 2) Dispose the component C
/// 3) Remove component C from map of disposable components
/// #ECS.ComponentRemoved: Removes a component by Name from an entity E
/// 0) Find the entity E by ID
/// 1) If E doesn't exist, finalize
/// 2) remove the component Name from E
/// #ECS.ComponentUpdated: Updates a disposable component C using a JSON payload
/// 0) Find the disposable component C by ID
/// 1) If C doesn't exist, finalize
/// 2) C.Update(JSON)
/// #ECS.CreateEntity
/// 0) If the created ID will be '0', finalize.
/// 1) If the entity is already created, finalize.
/// 2) Create the entity E
/// 3) Attach entity E to rootEntity
/// 4) Add entity E to the entity map
/// #ECS.RemoveEntity
/// 0) If the ECS tries to remove the root entity, finalize.
/// 1) Get the entity E
/// 2) If E doesn't exist, finalize.
/// 3) For all entity C in P.children
/// 3.0) Set C as child of rootEntity
/// 4) E.Dispose()
/// 5) Remove E from the entity list
/// #ECS.SetEntityParent: Set the entity E as child of entity P
/// 0) If entityId is '0', finalize.
/// 1) Find the entity E
/// 2) If E doesn't exist, finalize.
/// 3) If parentId is '0'.
/// 3.0) If True, Set E as child of rootEntity
/// 3.1) If False, Find entity P
/// 3.2) If P exists
/// 3.2.0) If True, Set E as child of P
/// 3.2.1) If Flase, Set E as child of rootEntity
Again, those messages are triggered by calling by the dcl object: https://github.com/decentraland/kernel/blob/6.0.0/packages/scene-system/scene.system.ts#L180-L361
The WebWorkers (one per scene) send messages to the renderer, and the engine knows how to interpret every message
and convert them into visible and interactive things. It is not specified how the workers talk to the engine. Do what you feel correct to fullfill the use cases. Messages are sent to the renderer once per frame, at the end of the frame. Those are sent by calling the sendBatch method from the @decentraland/EngineAPI module.
The dcl object has two special methods for modules: loadModule and callRpc
loadModule(moduleName): Promise<{rpcHandle: string, methods: string[]}>: Load a remote module by name, returns a promise. That module should be an instance specifically for the scene, loads an instance of the EngineAPI for our scene, meaning we cannot modify nor see another scenes entities.callRpc(rpcHandle: string, methodName: string, args: any[]): Promise<any>: Calls a method of a loaded module.
const engineModule = await dcl.loadModule('@decentraland/EngineAPI')`
dcl.callRpc(engineModule.rpcHandle, 'sendBatch', [...batchedEngineMessages])The tick of the scenes is controlled by the runtime via the dcl.onUpdate(callback) callbacks.
You must register all the callbacks and call them in order every frame, passing the delta time every iteration.