1
\$\begingroup\$

I've been building a turn-based style RPG in Typescript based upon this ROT-JS Tutorial. For the most part it's fairly clear and I have a handle on everything, except I've begun building out a graphics engine to render the game in 2D sprites rather than terminal style graphics. Inspiration is the game presentation of modern tile-based RPGs like Moonring or Caves of Qud's graphical release.

The roadblock I'm currently having, though, involves input handling and precise control over it. The tutorial's method of input handling is to have the engine standup some listeners on initialization and simply update the game any time an event is detected

window.addEventListener('keydown', (event) => {
  this.update(event);
});

Eventually this settles on a structure where the game is broken down into screens (Main Menu, GameScreen, GameOver, etc) that has an input handler that gets invoked by the window listener, and returns an action from that input handler

window.addEventListener('keydown', (event) => {
  engine.screen.update(event);
});

With each game screen being defined like this

class BaseGameScreen {
inputHandler: BaseInputHandler

constructor()

update(event) {
  const action = this.inputHandler.handleKeyPress(event)
}

This works for the ASCII style presentation of most roguelikes, but felt sluggish when holding down a key, since it reverted to the polling rate after the initial press. To address this, I moved the udpate call into the requestAnimationFrame loop that the renderer uses

class Input {
  keyMap: new Map()

  constructor() {
     window.addEventListener("keydown", this.markPressed)
     window.addEventListenred("keyup", this.markReleased)
  }

  markPressed(event) {
    this.keyMap.set(event.key, "pressed")
  }

  markReleased(event) {
    this.keyMap.set(event.key, "released")
  }
}

const input = new Input()

function renderScreen() {
  requestAnimationFrame(renderScreen)

  // could probably consolidate these into one call but eh
  window.engine.screen.update(input)
  window.engine.screen.render()
}

renderScreen()

This greatly improved the input feel of the game, but way in the opposite way. Now even a casual tap of the keyboard causes the player to fly across the tiles, taking several turns at once. I even followed this tutorial for using the command pattern in an input handler to be able to define a list of what keys are pressed, and what actions correspond to those keys

And it works, but the game design in the tutorial is intended for a real-time multiplayer game, and includes reference a gameState object that I just don't have or need, really. And each screen will have a different set of actions that need to be defined, and this is where I'm getting confused

My goal is to have a system where the input handlers know nothing about what KEY is pressed, but instead respond to which actions are requested, without repeating an action so fast the player can't reasonably respond

Something like

  ... // inside inputHandler
  handleEvent() {
  if (event.wantsMovement) {
    doMovementAction()
  } else if (event.wantsInventory) {
    doInventoryAction()
  } 
}

But, how do I effectively add this? Should I be doing something like const event = InputManger(this.actionMap) where each screen defines its own actions, or should I just do it before the update call

const input = new InputManager(actionMap)

renderScreen() {
   const event = input.getInputs()
   window.engine.screen.update(event)
}

Anyway, I know that was a lot of questions but I hope I was clear on what I'm looking to do

\$\endgroup\$
5
  • \$\begingroup\$ How do you want to handle key holding? Should it move continiously or only one space/ tile? \$\endgroup\$
    – Zibelas
    Commented Apr 19, 2024 at 16:04
  • \$\begingroup\$ For now one space per tile. I've also considered doing it at set intervals where the game processes quick animations (moving from tile to tile, attack animation on attack, etc), though in that situation I'm unsure where the best place to handle throttling the input would be \$\endgroup\$
    – Joel
    Commented Apr 19, 2024 at 16:24
  • \$\begingroup\$ I asked a question a while back about how to abstract an XBox controller. A lot of the concepts in the answer will be relevant (like using "impulses" for input) \$\endgroup\$
    – Basic
    Commented Apr 19, 2024 at 16:32
  • \$\begingroup\$ @basic That is helpful, thank you. Though I do have two questions: Are impulses just an identifier in a data set, like say you hit A to jump, you'd have to array with the value "Jump" in it? And is that where you control whether something is a held action or one that fires once and then doesn't fire again until the player presses the button again? \$\endgroup\$
    – Joel
    Commented Apr 20, 2024 at 14:11
  • \$\begingroup\$ [With the caveat I'm not an expert on this topic]: You could have index-per-impulse and check the contents is present OR just use the first N positions and iterate through them to consume/handle them. The latter approach would require all impulses to match a common format (or it becomes a pain parsing them). You can define the lifetime of an impulse to meet your needs (maybe the control unsets jump after 1 tick, regardless of whether the button's held). Someone with more experience can hopefully give you a better answer. \$\endgroup\$
    – Basic
    Commented Apr 21, 2024 at 14:25

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.