Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hubs Client add-on API #6099

Open
takahirox opened this issue May 25, 2023 · 2 comments
Open

Hubs Client add-on API #6099

takahirox opened this issue May 25, 2023 · 2 comments
Assignees
Labels
enhancement work that enhances an existing feature new-loader

Comments

@takahirox
Copy link
Contributor

takahirox commented May 25, 2023

An add-on in Hubs Client is small programs to add one or set of features in Hubs Client. An add-on consists of Components, Systems, Inflators, Prefabs, Network schemas.

Refer to https://hubs.mozilla.com/docs/dev-client-gameplay.html and https://hubs.mozilla.com/docs/dev-client-networking.html for the details.

Ideally Hubs core (built-in) and user custom features should be implemented as add-ons.

Unfortunately right now we don't have clear APIs to register add-ons and we require hand-edit some Hubs core files.

This issue is to propose new APIs to register add-ons.

Purpose of the new APIs

Allow custom client developers to add their own custom add-ons without editing the Hubs Client core files.

Process of add-on creation

If you want to add a new add-on, you need to do

  • Define components
  • Write and register Systems
  • Write and register Inflators

If you want networked (synced with remote clients) components, you need additional two tasks

  • Write and register Prefabs
  • Write and register Network schemas

Then, the APIs we need are to register Systems, Inflators, Prefabs, and Network schemas.

APIs proposal

Add register APIs to App.

Register System

type System = (world: HubsWorld) => void;

export const SystemOrder = Object.freeze({
  Setup: 0,
  BeforeMatricesUpdate: 100,
  MatricesUpdate: 200,
  BeforeRender: 300,
  Render: 400,
  AfterRender: 500,
  PostProcessing: 600,
  TearDown: 700
});

App.registerSystem = (system: System, order: number = SystemOrder.BeforeMatricesUpdate) => void

Example:

app.registerSystem(fooSystem, SystemOrder.BeforeMatricesUpdate);
app.registerSystem(barSystem, SystemOrder.BeforeRender - 1);

One of the good things of systems is the order of systems execution is controllable and managable. For example, we can ensure that a system that requires updated world matrices can run after the world matrices are updated (by the Hubs Client Core). The second unsigned integer argument is to specify the order, the systems with small numbers run earlier. (Systems with the same order number may be out of order, or first-registered first-run in them.)

Potential advanced APIs

We may add am API to allow to know the order number of a registered system for, for example, to ensure a system runs before or after another certain system, like

App.getSystemOrder = (system: System) => number;

Example: Ensure fooSystem runs before the built-in textSystem

app.registerSystem(fooSystem, app.getSystemOrder(textSystem) - 1);

And we may add an API to deregister system for, for example, replacing a built-in system with a custom system.

App.deregisterSystem = (system: System): void;

Example: Replace a built-in foo system with a custom bar system

app.deregisterSystem(fooSystem);
app.registerSystem(barSystem);

Register Inflator

type Inflator = (world: HubsWorld, eig: number, params?: object) => void;

App.registerInflator = (keys: {jsx?: string, gltf?: string}, inflator: Inflator) => void;

Example:

app.registerInflator({jsx: 'foo', gltf: 'foo'}, fooInflator);

jsx and gltf in the first argument object are keys for JSX and glTF inflators map respectively.

Register Prefab

type Prefab = (params?: object) => EntityDef;

App.registerPrefab = (key: string, prefab: Prefab);

Example:

app.registerPrefab('foo', FooPrefab);

Register Network schema

App.registerNetworkSchema = (key: string, schema: NetworkSchema);

Example:

app.registerNetworkSchema('foo', NetworkedFooSchema);

Custom add-on Example

// Define Component
// src/components/foo.ts
import { defineComponent, Types } from "bitecs";

export const Foo = defineComponent({
  val: Types.f32
});

// Write Inflator
// src/inflators/foo.ts

export type FooParams = {
  val?: number
};

const DEFAULTS: Required<FooParams> = {
  val: 0
};

export const fooInflator = (world: HubsWorld, eid: number, params?: FooParams): void => {
  params = Object.assign({}, params, DEFAULTS);
  addComponent(world, Foo, eid);
  Foo.val[eid] = params.val;
};

// Write System
// src/bit-systems/foo.ts
import { defineQuery } from "bitecs";
import { HubsWorld } from "../app";
import { Foo } from "../components/foo";

const fooQuery = defineQuery([Foo]);

export const fooSystem = (world: Hubsworld): void => {
  fooQuery(world).forEach(eid => {
    const val = Foo.val[eid];
    ...
  });
};

// Write Prefab
// src/prefabs/foo.ts
/** @jsx createElementEntity */
import { createElementEntity, EntityDef } from "../utils/jsx-entity";

export type FooPrefabParams = {
  val: number;
};

export const FooPrefab = (FooPrefabParams: params): EntityDef => {
  return (
    <entity
      networked
      foo={{val: params.val}}
    />
  );
};

// Write Network schema
// src/network-schemas/foo.ts
import { NetworkSchema } from "../utils/network-schemas";
import { defineNetworkSchema } from "../utils/define-network-schema";
import { Foo } from "../components/foo";

const runtimeSerde = defineNetworkSchema(Foo);
export const NetworkedFooSchema: NetworkSchema = {
  componentName: "foo",
  serialize: runtimeSerde.serialize,
  deserialize: runtimeSerde.deserialize
};

// register add-on
// src/hub.ts
...

import { SystemOrder } from "./src/system-order";
import { fooSystem } from "./src/bit-systems/foo";
import { fooInflator } from "./src/inflators/foo";
import { FooPrefab } from "./src/prefabs/foo";
import { NetworkedFooSchema } from "./src/network-schemas/foo";

...

// Built-in add-ons are set up in the constructor
const app = new App();

// TODO: Where should these custom add-on registration code be written?
// Still will we require hand-edit some core files like src/hubs.ts?

// register systems
app.registerSystem(fooSystem, SystemOrder.BeforeMatricesUpdate);

// register inflators
app.registerInflator({jsx: "foo", gltf: "foo"}, fooInflator);

// register prefabs
app.registerPrefab("foo", FooPrefab);

// register network schemas
app.registerNetworkSchema("foo", NetworkedFooSchema);

// kick-off
app.start();

...

Expected Hubs Client Core (built-in add-ons) setup implementation

type SystemEntry = {
  system: System;
  orderPriority: number;
};

export class App {
  private systems: SystemEntry[];
  private renderer: WebGLRenderer;
  private world: HubsWorld;
  // These maps are used by other functions (eg: renderAsEntity())
  // TODO: Think how to expose to them
  private jsxInflators: Map<string, Inflator>;
  private gltfInflators: Map<string, Inflator>;
  private networkSchemas: Map<string, NetworkSchema>;
  private prefabs: Map<string, Prefab>;

  ...

  constructor() {
    ...
    this.world = new HubsWorld();
    ...
    this.init();
    ...
  }

  private init() {
    // register the built-in systems
    this.registerSystem(..., ...);
    this.registerSystem(..., ...);
    this.registerSystem(..., ...);
    this.registerSystem(..., ...);

    // register the built-in inflators
    this.registerInflator(..., ...);
    this.registerInflator(..., ...);
    this.registerInflator(..., ...);
    this.registerInflator(..., ...);

    // register the built-in prefabs
    this.registerPrefab(..., ...);
    this.registerPrefab(..., ...);
    this.registerPrefab(..., ...);
    this.registerPrefab(..., ...);

    // register the built-in network schemas
    this.registerNetworkSchema(..., ...);
    this.registerNetworkSchema(..., ...);
    this.registerNetworkSchema(..., ...);
    this.registerNetworkSchema(..., ...);
  }

  registerSystem(system: System, order: number = SystemOrder.BeforeRender): void {
    this.systems.push({system, order); 
	this.systems.sort((a: SystemEntry, b: SystemEntry) => {
      return a.order - b.order;
    });
  }

  registerInflator(keys: { jsx?: string, gltf?: string }, inflator: Inflator ): void {
    if (keys.jsx !== undefined) {
      this.jsxInflators(keys.jsx, inflator);
    }
    if (keys.gltf !== undefined) {
      this.gltfInflators(keys.gltf, inflator);
    }
  }

  registerPrefab(key: string, prefab: Prefab): void {
    this.prefabs.set(key, prefab);
  }

  registerNetworkSchema(key: string, schema: NetworkSchema): void {
    this.schemas.set(key, schema);
  }

  private mainTick(): void {
    updateElapsedTime();
    for (const entry of this.systems) {
      entry.system(this.world);
    }
  }

  start(): void {
    this.renderer.setAnimationLoop(() => {
      this.mainTick();
    });
  }

  ...
}
@takahirox takahirox added the enhancement work that enhances an existing feature label May 25, 2023
@keianhzo
Copy link
Contributor

I really like this proposal, is simple and really clear. Great job.

@jywarren
Copy link

This is so exciting! Might this be a place for custom shaders like depth of field, or #5575, perhaps!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement work that enhances an existing feature new-loader
Projects
None yet
Development

No branches or pull requests

3 participants