#Storing and accessing variables by class (generics and component system)

14 messages · Page 1 of 1 (latest)

marsh pilot
#

I'm writing a component system and trying to achieve the following:

abstract class Component {}

class FooComponent extends Component {
  doFooStuff(): void {
    console.log("foo stuff");
  }
}
class BarComponent extends Component {}

const entity = Entity();
entity.addComponent(FooComponent);
entity.addComponent(BarComponent);

const foo = entity.getComponent(FooComponent);
if(foo !== undefined) {
  foo.doFooStuff();
}

I'm not sure if it's a correct approach but it seems nice.
The problem is that I can't figure out how to do this.

class Entity
{
  protected readonly components = new Map<typeof Component, Component>();

  addComponent(Constructor: typeof Component): void {
    // TypeScript is against potentially creating an instance of an abstract class
    this.components.set(Constructor, new Constructor(this));
  }

  // I'm lost here. I can see that instead of FooComponent instance being returned, it returns (typeof FooComponent), but I don't know how to solve this
  getComponent<T extends typeof Component>(componentClass: T): T | undefined {
    const component = this.components.get(componentClass);
    if (component !== undefined && component instanceof componentClass) {
      return component as unknown as T;
    }

    return undefined;
  }
}
#

I tried to google a bit, would this be a correct solution for the second issue?

getComponent<T extends typeof Component>(componentClass: T): InstanceType<T> | undefined {
  const component = this.components.get(componentClass);
  if (component !== undefined && component instanceof componentClass) {
    return component as InstanceType<T>;
  }

  return undefined;
}
dense meteor
#

I feel like you are implementing the service-locator pattern here. I would not recommend it. See https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/ why

If you need dynamism in the components of your system, i would recommend using dependency injections such as https://github.com/microsoft/tsyringe

or ones provided by heavier framework (I do not recommend grabbing such framework just for the dependency injection stuff ).

#

Now.. about your issue if you really want to implement a service-locator ( please, i ask you to reconsider at it will bring you issues at some point)

You can replace classes by defining a constructor type.

replace

abstract class Component {}

by

interface Class<Interface,ConstructorArgs extends Array<unknown> = never> {
    new (...args:ConstructorArgs):Interface
}

Here, the type Class defines a constructor that will implements some public methods, and requires arguments.

class Hello{
world(){
return "hello world"
}
}

Here, hello is a Class<{world:()=>string},never>

#

Result here .

pliant craneBOT
#
Quadristan#2590

Preview:```ts
interface Component {}

interface Class<
Interface = unknown,
ConstructorArgs extends Array<unknown> = never

{
new (...args: ConstructorArgs): Interface
}

class Entity {
protected readonly components = new Map<
Class,
Component

()

addComponent<TInterface extends Component>(
Constructor: Class<TInterface, [Entity]>
): void {
// TypeScript is against potentially creating a
...```

marsh pilot
#

Thanks for explanation and links but tsyringe and other library solutions are way too complex for me to even begin to understand at the moment. I just want to try to implement Entity Component System because I used it in project made by other people and I liked it.
The whole thing with getComponent returning the same class that I requested is more of a.. syntax sugar (not sure if this is a correct term here) thing for me, so I wouldn't have to do:

let foo = entity.getComponent("Foo");
if(foo !== undefined && foo instance of FooComponent) {

Although maybe I should, considering that instead now I'm doing this which is not that different

let foo = entity.getComponent(FooComponent);
if(foo !== undefined) {
dense meteor
#

let me know if my last playground link needs explanations

marsh pilot
#

Does it mean that for getComponent I would have to use the following?

getComponent<TInterface extends Component>(
  componentClass: Class<TInterface, [Entity]>
): InstanceType<Class<TInterface, [Entity]>> | undefined {
  const component = this.components.get(componentClass);
  if (component !== undefined && component instanceof componentClass) {
    return component;
  }

  return undefined;
}
dense meteor
#

what precisely do you refer to in this example ?

marsh pilot
#

entity.getComponent(FooComponentClass). Retrieves component, check if class matches, returns it.

dense meteor
#

You want to have all classes built at the start so you dont have to do checks ? or what is it that you want to do ?

marsh pilot
#

I have several classes that extend Component.
Depending on what I want I add one or more of them to the Entity.

const entity1 = new Entity([FooComponent, BarComponent]);

Then I can use getComponent to retrieve those components. The function includes checks to make sure that:

  • Component exists in an entity
  • Component is actually of the desired class
    As a result I either get undefined or a component that I wanted.
const foo = entity1.getComponent(FooComponent);
if(foo !== undefined) {
  foo.specificFooMethod();
}
dense meteor
#

That's not easy, if you want to handle components with multiple constructor signatures, you will end up needing a thing really complex, almost like ts-syringe.