playcanvas / playcanvas/engine

[RFC] Async/Lazy Component Systems

Open
#6,986 15 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
JavaScript
Stars
16.8k
Forks
2k
Avg merge
4h 32m
Merged PRs (30d)
222

Description

Before you can add a component to an entity, you currently need to register the component system with the app.

opts.componentSystems = [RenderComponentSystem]
const app = new AppBase(canvas);
app.init(opts);
app.start();

new Entity('cube').addComponent('render');

This has a couple of friction points:

  1. Creates unnecessary boilerplate. Engine users are required to manage a list of component system. This either means that systems are included when not needed, or worse, components are added without systems, causing errors and friction for developers
  2. In editor projects, every component systems is added by default, regardless of whether they're used or not.
  3. In both cases neither can be tree-shaken, as they're instantiated by the App

An optimal solution would;

  1. Only instantiate components systems when necessary
  2. Not require manually maintaining a list of component systems.
  3. Support tree-shaking
Proposal

Update the addComponent() method to resolve to a lazily loaded component system:

const createComponentSystem = async path => {
    const class = await import(path);
    return new class();
}

addComponent(type, data) {
  let system = this._app.systems[type];
  if (!system) {
    switch(type) {
      case 'camera' : await createComponentSystem('./CameraComponentSystem.js');
      case 'render' : await createComponentSystem('./RenderComponentSystem.js');
    }
  } 
  // ...
}
Benefits of this approach
  1. Maintains similar api addComponent('camera')
  2. Component Systems are lazily loaded.
  3. Simplifies engine only startup by deprecating the need for specifying systems array.
  4. Better supports tree-shaking.
  5. Editor projects only load necessary components.
Cons

Modifies the addComponent() to become async. Strictly speaking this is a breaking change as it requires await addComponent() which means a semantic major version bump.

Other options

An alternative non breaking change would be to use statically imported components which are not async

import CameraComponentSystem from './CameraComponentSystem'
addComponent(type) {
  // ...
  switch(type) {
      case 'camera' : CameraComponentSystem;
      ....
    } 
}

This still has the benefit of a not requiring specifying a component system array, but is harder to tree-shake.


Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading src/framework/app-base.js at the component-system initialization referenced in the issue, then trace addComponent() and the systems list. Compare the asynchronous and static-import proposals, and define how lazy instantiation, tree-shaking, editor behavior, and the breaking API change would be validated before implementation.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
game-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.