danprince / midas

🫅 Traditional roguelike where everything you touch turns to gold.
https://danprince.itch.io/midas
2 stars 0 forks source link

Event / Command Separation #25

Open danprince opened 4 years ago

danprince commented 4 years ago

Want to separate input events from game commands. For example:

class GameScreen {
  handleInputEvent(event) {
    switch (event.key) {
      // keys can be rebound via config
      case config.KEYBOARD_MOVE_LEFT:
        game.dispatchCommand({ type: "move", direction: Directions.WEST });
        break;
    }
  }

  handleCommand(command) {
    switch (command.type) {
      case "move":
        systems.movement.moveInDirection(game.player, command.direction);
        break;
    }
  }
}

// send input events to game so game can pass to screens
onkeydown = event => game.dispatchInputEvent(event);

The UI would generate commands directly when the player clicked on buttons etc.

danprince commented 4 years ago

This is a bit awkward to manage when every screen is handling commands and events. Feels like a lot of duplicated code.

What if the command handling was implemented inside the engine and the UI just did the event -> command mapping?

Would there need to be a domain model internally to contextualise commands? Or could commands be unambiguous enough that the game would always know what to do with them?

danprince commented 4 years ago

Don't really like handling events and commands in the UI so planning on putting command handling inside the game instead, then the UI just has to do event -> command mapping.

danprince commented 4 years ago

Done in dc497f93fb0de9c2b7dea81c6a968d2966843145. Seems good.

Here are some notes on implementing the type safety for these kind of data-driven systems.

Types get tricky when Command is a union of all possible commands. I tried doing that so that the type would be inferred from the commands.js file but its much simpler to have the generic form of the type (e.g. { type: string, payload: any } etc).

However, that means that the type won't really get validated when you create/use commands. It's possible to retain a lot of the type safety by putting a function in between that takes the actual command handler as an argument and infers the parameter type.

// Payload is inferred from Commands.move
game.dispatch(Commands.move, Direction.NORTH)
danprince commented 4 years ago

Need to think a bit about the command result. Currently command handlers return a boolean to show whether the game should advance a turn.

Maybe want a more detailed result object as a return value. That could show whether the command was successful, but also whether to advance time etc.