3mcd/javelin

Proposal: default component initializer

Closed this issue · 5 comments

Defining component initializers can be a little tedious. It seems that with the schema information, it should be possible to provide a default component initializer with full typing support:

const defaultComponentInitializer<S extends Schema> = (component: ComponentType<S, any>, props: PropsOfSchema<S>) => {
  Object.assign(component, props);
};

... or similar, such that initializing a component without a user-defined initializer is simply

const Position = createComponentType({
  type: 1,
  schema: {
    x: number,
    y: number,
    z: number,
  },
});

world.spawn(world.component(Position, { x: 0, y: 3, z: 12 }));

What do you think? I'd happily contribute the code if you agree with the concept.

3mcd commented

Hey @a-type, thanks so much for all of these great suggestions. This is a good idea, and would definitely improve the DX quite a bit. I originally held off on implementing this exact feature because I wanted to design an API that encouraged good memory usage practices. I shied away from creating a way to initialize a component using an secondary object that would be thrown away immediately, particularly in games where you might spawn hundreds (or thousands) of entities at once, increasing the chance for major GC events and subsequent frame drops.

However, I think with an opt-in approach we could get the ergonomic benefits, while continuing to encourage the current initializer pattern. I'd love for you to take a stab at this if you have the time.

Do you have any ideas about the API for opt-in behavior? A flag on the component initialization config object, maybe? Anything too fiddly would probably feel just as laborious as writing

initialize(comp, args: { x: number; y: number; z: number }) {
  Object.assign(comp, args);
}

yourself, which is not so bad anyways now that I've done it a few times. It can even be extracted to a wrapper component factory quite easily (which I'm thinking of doing, along with automatic type assignment).

My original idea was to make this a default, rather than explicit opt-in, but along that reasoning, it would encourage this pattern rather than the more efficient use of direct parameters. I agree with your reasoning about the preferred pattern, so I do feel that any change here which is convenient enough would also be too convenient and encourage bad scaling patterns.

So it seems like maybe there is a conflict of interest here. A change in the default behavior would tilt the balance more toward convenience over correctness, but leaving as-is encourages correctness without preventing an initializer like I have above. It might be best to leave it.

3mcd commented

The more I think about this, the more I agree that the current default behavior is ideal, and it shouldn't be too difficult for a library user to write their own reusable function like:

function initializeWithProps<S extends Schema>(component: ComponentOf<S>, props: PropsOfSchema<S>) {
  Object.assign(component, props);
}

What are your thoughts about the library providing this function?

I can certainly see the merits of keeping the core library very straightforward but extensible by users with such helpers. That's often a winning strategy for tools which are positioned to underpin more customized / user-friendly frameworks (thinking about all the tooling which emerged around Redux in the React world, for instance).

The only downside is that users may not think to extend things like that, or different users may extend in different ways which makes it hard to develop an ecosystem of compatible approaches. Again, thinking back to Redux, they basically addressed this by providing very solid and thorough documentation on suggested practices and patterns. IMO it's a good approach, possibly better than being explicitly user-friendly in the core lib, but at least definitely better than trying to support more usages than you really care about.

So, yeah, sounds good. Feel free to close.

3mcd commented

Awesome, I appreciate the thought you put into these suggestions. And good call about writing best practices into the documentation. Javelin docs need a lot of love right now.