Maros Hluska Maros Hluska - 6 months ago 27
Javascript Question

Simultaneous keyboard input (diagonal game movement)

I'm working on a 2D game in Node where the character needs to move diagonally. This is a top-down, text-based game purely in a Node environment (no browser, so I don't have nice keydown/keyup events at my disposal).

I'm using the keypress library to read user input but I don't know how to capture two keys at once to cause diagonal movement (e.g. down arrow and right arrow). Here's the code I currently have for horizontal and vertical movement:

game = new Game()

process.stdin.on('keypress', (ch, key) ->
return unless key
return process.stdin.pause() if key.ctrl and is 'c'

player = game.player
when 'up'
if key.shift then player.turnUp() else player.moveUp()
when 'right'
if key.shift then player.turnRight() else player.moveRight()
when 'down'
if key.shift then player.turnDown() else player.moveDown()
when 'left'
if key.shift then player.turnLeft() else player.moveLeft()
when 'enter'
when 'b'


This is a turn-based game and currently the run loop of the game is advanced on user input. Instead, I think what I need to do is have an interval-based game update and keep track of the two most recent keypress events and the time between them.

Is there a more robust way?


I ended up building a keyboard module that reports when simultaneous keys are pressed. It's built on top of the keypress library.

Every time a key is pressed the keyboard module checks what keys have been pressed in the past 100ms and reports those as a single key event. It is throttled to emit events at most once every 40ms to give it time to collect key combinations. The downside is that single-key inputs now have a 40ms delay. This is a consequence of how input works in the terminal.

EventEmitter = require('events').EventEmitter
keypress = require('keypress')
_ = require('underscore')

# This module allows for reading simultaneous key events in the terminal.
module.exports = class Keyboard extends EventEmitter
    _keyTimes: {}

    constructor: ->

        @_emitKeyStatus = _.throttle(@_emitKeyStatus, 40, leading: false)
        process.stdin.on('keypress', (ch, key) => @_processKey(key))

    # Batch-emits any keys recently pressed.
    _emitKeyStatus: ->
        currentTime = @_tuple2time(process.hrtime())
        millisecond = 1000000
        keys = {}

        for own keyName, time of @_keyTimes
            if currentTime - time < 100 * millisecond
                keys[keyName] = true

        @emit('keypress', keys)

    _tuple2time: (tuple) ->
        (tuple[0] * 1e9) + tuple[1]

    _processKey: (key) ->
        return unless key
        return @emit('quit') if key.ctrl and is 'c'

        time = @_tuple2time(process.hrtime())

        # Treat ctrl, shift and meta as distinct keys.
        @_keyTimes.shift = time if key.shift 
        @_keyTimes.ctrl = time if key.ctrl
        @_keyTimes.meta = time if key.meta

        @_keyTimes[] = time