0
\$\begingroup\$

I'm developing a top-down game using Javascript and Canvas in an ECS architecture.

I'm wondering, which of these is better from a design / elegance point of view?

Method 1: Combining the ability function and its metadata into one object:

// in ability factory
createBlinkAbility() {
  return {
    cooldown: 5000,
    castTime: 1000,
    hotkey: "q",
    execute(entity: Entity, scene: Scene) {
      let position = entity.get(CT.Position);

      let level = scene.queryComponent(CT.Level);

      position.x = Helpers.randomInt(0, level.width);
      position.y = Helpers.randomInt(0, level.height);
    }
  }
}

function executeCurrentCast(entity: Entity, ability: Ability) {
  ability.execute(entity); // all abilities have an execute function
}

Method 2: Separating ability metadata from its function:

// in ability factory
createBlinkAbility() {
  return {
    type: "blink",
    cooldown: 5000,
    castTime: 1000,
    hotkey: "q"
  }
}

// in ability factory
castBlink = (entity: Entity, scene: Scene) => {
  let position = entity.get(CT.Position);

  let level = scene.queryComponent(CT.Level);

  position.x = Helpers.randomInt(0, level.width);
  position.y = Helpers.randomInt(0, level.height);
}

function executeCurrentCast(entity: Entity, ability: Ability) {
  switch (ability.type) {
    case "bow": this.abilityFactory.castBow(entity); break;
    case "blink": this.abilityFactory.castBlink(entity); break;
    ...
  }
}

I know in general in an ECS architecture it is wise to separate "state" from "actions", but I'm not sure if this would also apply to things like abilities. It seems like it might be wise to maintain that separation, but the code seems like it might be "cleaner", or shorter at least, in the former case.

Lastly, I'm not really concerned with the performance differences between these two approaches, but rather which is better from a design standpoint.

\$\endgroup\$
1
\$\begingroup\$

I'd argue you should keep the implementation alongside the definition in this case.

It gives you a lot of flexibility.

But .. You don't want to be serialising methods, so your players/NPCs should have references to the above via an ID/Key when serialised.

[assuming the hotkey is not user-configurable, it's fine there. If it is, that should live elsewhere as part of a mapping for hotkey -> ability ID]

I'd suggest you want ability-specific properties stored alongside the method too (eg NumberOfSecondaryTargets or BaseHealingRate).

If you make sure your execute() method definition is as broad as possible to accommodate all ability use-cases, you can remain implementation-agnostic but flexible.

As an example, you could add an optional dictionary of parameter overrides to the execute method.

Say {"DetectionRadius": 500} which would allow special NPCs/scripted events to have more complete control over what happens with each invocation whilst remaining clean and easy to debug (it's up to you whether a mis-match dictionary key is ignored or throws a warning, etc).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.