Callback based user input

When things get above a certain complexity, using callbacks for user input is often useful. For this purpose I wrote an module to deal with user input:


'''Handles all form of user input transparently'''
import json
import os
import logging

import bge


KEY_MAP = {
    'UP': 'WKEY',
    'DOWN': 'SKEY',
    'LEFT': 'AKEY',
    'RIGHT': 'DKEY',
}


LOG = logging.getLogger()
SETTINGS = ['KEYS':KEY_MAP]  # Normally loaded from somewhere else
EVENT_CALLBACKS = dict()
CLICKABLES = dict()


MOUSE_EVENTS = {
    'LEFTMOUSE': bge.events.LEFTMOUSE,
    'RIGHTMOUSE': bge.events.RIGHTMOUSE,
    'MIDDLEMOUSE': bge.events.MIDDLEMOUSE,
    'WHEELUPMOUSE': bge.events.WHEELUPMOUSE,
    'WHEELDOWNMOUSE': bge.events.WHEELDOWNMOUSE,
    }


def init(scheduler):
    '''Initializes the controller'''
    load_settings()
    bge.render.showMouse(True)
    scheduler.register_function(update_control)


def update_control():
    '''Updates the control'''
    update_keys()
    update_mouse()


def update_mouse():
    '''Checks for clickables under the mouse'''
    scene = bge.logic.getCurrentScene()
    cam = scene.active_camera
    pos = list(bge.logic.mouse.position)
    aspect = bge.render.getWindowHeight() / bge.render.getWindowWidth()
    pos[0] = (pos[0] - 0.5) * cam.ortho_scale
    pos[1] = -(pos[1] - 0.5) * cam.ortho_scale * aspect

    raw = cam.rayCast(pos + [-1], pos + [1], 20, "", 0, 0, 2)
    obj = raw[0]
    if obj is not None and obj in CLICKABLES:
        CLICKABLES[obj](generate_mouse_event(raw))


def generate_mouse_event(raw):
    '''Converts a raycast into a mouse event'''
    obj, pos, nor, poly, uv_pos = raw
    event = dict()
    event['obj'] = obj
    event['pos'] = pos
    event['nor'] = nor
    event['poly'] = poly
    event['uv'] = uv_pos
    for eve in MOUSE_EVENTS:
        event[eve] = bge.logic.mouse.events[MOUSE_EVENTS[eve]]

    return event


def update_keys():
    '''Fires all callbacks associated with keys'''
    for event in EVENT_CALLBACKS:
        if event in bge.logic.keyboard.active_events:
            funct = EVENT_CALLBACKS[event][0]
            args = EVENT_CALLBACKS[event][1]
            if args is None:
                funct()
            else:
                funct(*args)


def add_callback(event, funct, args=None):
    '''Registers a callback for a certain action name'''
    assert event in SETTINGS['KEYS']
    event_id = bge.events.__dict__[SETTINGS['KEYS'][event]]
    EVENT_CALLBACKS[event_id] = (funct, args)


def make_clickable(obj, funct):
    '''Registers a function to be called when the mouse is over an
    object'''
    CLICKABLES[obj] = funct

To run it, you have to initialize it with the init() function, then update it every frame. In this case I’ve used a schedular I’ve written, but it is trivial to change it to run directly from logic bricks.

To add a keyboard event you can do things like:


Control.add_callback('UP', move_funct, args=[[1,0]])
Control.add_callback('DOWN', move_funct, args=[[-1,0]])

To make an object clickable, you do something similar:


Control.make_clickable(button_obj, button_click_funct)

When the mouse if over the clickable object, various details are passed to it including:

  • Mouse position (screen space)
  • Status of all mouse buttons
  • UV co-ordinates of the mouse.