Wednesday, October 17, 2012

Hackenbraten: Native XInput Support for Sauerbraten

Although I've been programming professionally for eighteen years I've very little to no experience with video game development and I always wanted to get  my hands dirty messing with a FPS engine. An open source game is a great place to start so I checked out Sauerbraten from http://sauerbraten.sourceforge.net/.

The rain was back in Seattle this past weekend. An excellent excuse to cozy up indoors and hack Sauerbraten.

I must say that I am quite impressed with the no-fuss, minimalistic coding style of the authors and by how well the code reads. I was able to find my way around it fairly quickly. As a professional developer I have seen way too many over-complicated code bases, polluted with "clever" design patterns and unnecessary C++ - isms that nobody under an IQ of 300 understands. Heck, I wrote my own fair share of inscrutable C++ templates. Saurebraten's style is all about self-explanatory short functions that focus on the algorithms. The scripting engine is very efficient and elegant.

Because I type all day for a living when I play videogames I prefer giving the keyboard a rest and  use a controller.

Although Sauerbraten has no native support for gamepads it can be played with controllers that know how to emulate keyboard and mouse events (such as the Logitech F510, which comes with software for customizing it). This is okay, albeit not the best experience you can get.

In particular you miss on the rumble / vibration feedback, and shooting things is all that Sauer is all about. There are other potentially cool things such as analog triggers with configurable threshold. And how about some finesse and control the speed of your movement with the thumbstick (move faster when it is pushed farther out)?

I was curious if I could improve my gaming experience (and maybe learn a thing or two about the Sauerbraten engine in the process). Since I was doing this on Windows 7 and the game engine works on Linux and Mac as well one goal was to make a minimal impact on the overall portability.

I ended up with a "hybrid" implementation, in the sense that for some gamepad actions I directly interact with the engine (for moving the player I mess with the move, strafe, and velocity data) and for  others (such as mouse motion) I just push SDL_Events into the event queue.

Overall I am pretty pleased with the result. At the high level, the "mod" I came up with consists of:
  • a change to weapons.cpp so that custom actions can be performed upon firing a gun;
  • one C++ file for the gamepad module proper;
  • two function declarations (the "entry points" into the module), one for polling the controller state and the other for writing back the bindings to the config file when the game exits, and
  • changes to main.cpp and console.cpp to call the functions above.
The first item on the list is orthogonal to the rest of the gamepad code and it could be useful in its own right for example to play the sound of an empty cartridge hitting the floor.

The gamepad can be turned on or off with the gamepadon variable, and gamepadbind allows for extra tweaking. One kludge that I am not particularly proud of is that gamepad buttons cannot be bound directly to cubescript, instead they have to be bound to a key, like in this example:


sound_drop = ( registersound "cristiv/dropcartridge" )
bind F8 [ sound $sound_drop ]
gamepadbind right_trigger_release F8

it would be nicer to  simply say:
gamepadbind right_trigger_release [ sound $sound_drop ]

But this is an exercise for another rainy day. By the way, it is so cool being able to map separate actions to analog trigger presses and releases. For example, in my implementation the default mapping of the left trigger is to zoom in when pressed, and zoom out when released. One idea I'd like to try out some day is to make my player character start jumping when the left trigger is pressed, and stop when it is released.

I am right handed. My default bindings are to control movement with the left thumbstick or D-pad, look around with the right stick, and shoot with the right analog and digital triggers. This scheme can be easily reversed by using the gamepadbind command to remap the triggers and thumbsticks.

One thing I did before proceeding with implementing the gamepad input code, was to add some triggers for the guns, so that I can call custom cubescript when the weapons are fired. In weapons.cpp I added this block (right before the shoteffects function):

    VAR(gun_trigger_debug, 0, 0, 1);

    static void doguntrigger(const fpsent* d, int gun)
    {
        defformatstring(aliasname)("gun_fired_%d", gun);
        if(gun_trigger_debug) conoutf(CON_DEBUG, "%s:%s", name, aliasname);
        if(identexists(aliasname)) execute(aliasname);
    }

And then I added a line at the end of the shoteffects function:

    if (d==player1) doguntrigger(d, gun);

The gamepad code exposes a function called vibrate to cubescript which takes three parameters: the speed for left vibrate motor, speed for right motor, and the duration in milliseconds. The above plumbing allows for  it to be called from my configuration script:

// guns feedback
gun_fired_0 = [ vibrate 20000 20000 500 ]
gun_fired_1 = [ vibrate 5000 35000 200 ]
gun_fired_2 = [ vibrate 10000 25000 300 ]
gun_fired_3 = [ vibrate 32000 64000 360 ]
gun_fired_4 = [ vibrate 0 30000 200 ]
gun_fired_5 = [ vibrate 0 25000 200 ]
gun_fired_6 = [ vibrate 0 25000 200 ]
gun_fired_7 = [ vibrate 2000 10000 200 ]

The next thing to do was to add two function declarations to engine.h (towards the end of the file, right before the closing #endif):

namespace gamepad
{
#if _WIN32
    extern void checkinput(dynent*);
    extern void writebinds(stream*);
#else
    inline void checkinput(dynent*) {}
    inline void writebinds(stream*) {}
#endif
}

These two entry points are called right at the beginning of checkinputs (in main.cpp), and at the end of writebinds (in console.cpp), respectively:

void checkinput()
{
    gamepad::checkinput(player);

    SDL_Event event;
    int lasttype = 0, lastbut = 0;
// etc...


void writebinds(stream *f)
{
 // ...snip...
    gamepad::writebinds(f);
}

Finally, I "just" added a file to the Visual Studio project (the most recent development version of Sauerbraten comes with a solution file in src/vcpp which plays nice with Vistual Studio 2010) called xinputpad.cpp, and then edited its properties so that it uses the engine.h / engine.pch files for precompiled headers (rather than cube.h / cube.pch).

I think this approach is minimally invasive as the code sits nicely in its own file, and the changes to engine.h, main.cpp and console.cpp are tiny. I also tried imitating the terse coding style of Sauerbraten rather than using my own (C++ politically correct and at times bombastic) pen.

I am having a blast (rumble rumble) with this game. Boy I love that rocket launcher!


This is the full xinputpad.cpp code. Be careful when copying and pasting, some characters that are "unsafe" for HTML might have been encoded.

// Experimental support for Microsoft XInput-compatible gamepads in Sauerbraten.
// Zlib license. Copyright (c) 2012  cristi.vlasceanu@gmail.com
#include "engine.h"
#include <XInput.h>
#if defined(_MSC_VER)
 #pragma comment(lib, "XInput.lib")
#endif
#ifndef _countof
 #define _countof(a) sizeof(a)/sizeof(a[0])
#endif

#define DECLARE_XINPUTS \
    XINPUT(none, action_none), \
    XINPUT(left_stick, action_move), \
    XINPUT(right_stick, action_mouse), \
    XINPUT(left_trigger, action_key, "z"), \
    XINPUT(right_trigger, action_key, "MOUSE1"), \
    XINPUT(left_trigger_release, action_key, "z"), \
    XINPUT(right_trigger_release, action_none ), \
    XINPUT(dpad_up, action_key, "w"), \
    XINPUT(dpad_down, action_key, "s"), \
    XINPUT(dpad_left, action_key, "a"), \
    XINPUT(dpad_right, action_key, "d"), \
    XINPUT(start, action_none), \
    XINPUT(back, action_none), \
    XINPUT(left_thumb, action_none), \
    XINPUT(right_thumb, action_none), \
    XINPUT(left_shoulder,  action_key, "c"),\
    XINPUT(right_shoulder, action_key, "MOUSE1"), \
    XINPUT(button_a, action_key, "0"),\
    XINPUT(button_b, action_key, "F10"), \
    XINPUT(button_x, action_key, "F9"), \
    XINPUT(button_y, action_key, "SPACE")

#define XINPUT(i,...) x_##i
enum { DECLARE_XINPUTS };

#undef XINPUT
#define STRINGIZE(i) #i
#define XINPUT(i,...) STRINGIZE(i)
static const char* inputs[] = { DECLARE_XINPUTS };

// bind to keyboard, mouse motion or player movement
enum actiontype
{ 
    action_none,
    action_mouse,
    action_move,
    action_key,
};
static const char* actions[] = { "none", "mouse", "move" };

struct keym;  // defined in console.cpp
extern keym* findbind(char* key);
extern void execbind(keym &k, bool isdown);

#undef XINPUT
#define XINPUT(i,...) { __VA_ARGS__ }

static struct action
{
    actiontype type;
    const char* def;
    keym* km;
    Uint8 prevstate;
    string name;
}
const defaultbinds [] = { DECLARE_XINPUTS };

static void bindaction(action& a, actiontype type, const char* key)
{
    a.type = type;
    a.def = NULL;
    a.km = NULL;
    a.prevstate = 0;

    if (type==action_key && key)
    {
        copystring(a.name, key);
        a.km = findbind(const_cast<char*>(key));
        if(!a.km) conoutf(CON_ERROR, "unknown key \"%s\"", key);
    }
}

// used when synthesizing mouse events, affects how fast the player turns
VARP(gamepadspeed, 0, 32, 512);
// invert y axis when emulating mouse motion events
VARP(gamepadinverty, 0, 0, 1);

VARP(triggerthreshold, 0, XINPUT_GAMEPAD_TRIGGER_THRESHOLD, 255);


struct controller
{
    enum thumb { left, right };
    int id, vibratemillis;
    bool bounded;
    action binds[_countof(defaultbinds)];
    static int count;

    controller() : id(count++), vibratemillis(0), bounded(false) { }
    ~controller() { XINPUT_STATE state; if (getstate(state)) vibrate(0, 0); }

    void resetbinds()
    {
        loopi(_countof(binds))
            bindaction(binds[i], defaultbinds[i].type, defaultbinds[i].def);
        bounded = true;
    }

    void vibrate(int left, int right, int duration = 0)
    {
        XINPUT_VIBRATION vibration = { min(left, 65535), min(right, 65535) };
        XInputSetState(id, &vibration);
        vibratemillis = duration + lastmillis;
    }

    bool getstate(XINPUT_STATE& state)
    {
        ZeroMemory(&state, sizeof state);
        bool result = (XInputGetState(id, &state) == ERROR_SUCCESS);
        if (lastmillis > vibratemillis) vibrate(0, 0);
        return result;
    }

    void motionevent(int x, int y, int dx, int dy)
    {
        SDL_Event e = { };
        e.type = SDL_MOUSEMOTION;
        e.motion.x = x;
        e.motion.y = y;
        e.motion.xrel = dx;
        e.motion.yrel = gamepadinverty ? dy : -dy;
        SDL_PushEvent(&e);
    }

    int inline mousedelta(int x, int xmax, int speed)
    {
        if (x >= xmax) return speed;
        if (x < -xmax) return -speed;
        return 0;
    }

    // compute the delta that we're going to use in constructing a fake mouse motion event
    int mousedelta(thumb t, int x)
    {
        static const int xmax = 32767;
        const int speed = gamepadspeed;
        int dx = mousedelta(x, xmax, speed);
        for (int i = 2; i != 32; i *= 2)
            if (dx == 0) dx = mousedelta(x, xmax / i, speed / i);
        return dx;
    }

    void checkthumbstick(thumb t, int x, int y, dynent* player)
    {
        static const int deadzone[] = { XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE };

        int dx, dy;

        switch (binds[t + x_left_stick].type)
        {
        case action_mouse:
            dx = mousedelta(t, x);
            dy = mousedelta(t, y);
            if (dx || dy) motionevent(0, 0, dx, dy);
            break;

        case action_move:
            if (player->k_up || player->k_down || player->k_left || player->k_right) break;
            player->move = y > deadzone[t] ? 1 : (y < -deadzone[t] ? -1 : 0);
            player->strafe = x > deadzone[t] ? -1 : (x < -deadzone[t] ? 1 : 0);
            if (player->move || player->strafe) player->vel.mul(sqrtf(x*x + y*y) / 32767);
            break;
        }
    }

    void checkbindkey(action& a, Uint8 on)
    {
        if (a.type != action_key) return;
        if (on != a.prevstate)
        {
            if (a.km) execbind(*a.km, on != 0);
            a.prevstate = on;
        }
    }

    void checktrigger(thumb t, BYTE level)
    {
        const bool pressed = level > triggerthreshold;
        // check for trigger release
        if (!pressed && binds[t + x_left_trigger].prevstate)
        {
            auto& release = binds[t + x_left_trigger + 2];
            if (release.type==action_key && release.km) execbind(*release.km, true);
        }
        checkbindkey(binds[t + x_left_trigger], pressed);
    }

    void checkbuttons(WORD b)
    {
        checkbindkey(binds[x_dpad_up], (b & XINPUT_GAMEPAD_DPAD_UP) != 0);
        checkbindkey(binds[x_dpad_down], (b & XINPUT_GAMEPAD_DPAD_DOWN) != 0);
        checkbindkey(binds[x_dpad_left], (b & XINPUT_GAMEPAD_DPAD_LEFT) != 0);
        checkbindkey(binds[x_dpad_right], (b & XINPUT_GAMEPAD_DPAD_RIGHT) != 0);

        checkbindkey(binds[x_start], (b & XINPUT_GAMEPAD_START) != 0);
        checkbindkey(binds[x_back], (b & XINPUT_GAMEPAD_BACK) != 0);
        checkbindkey(binds[x_left_thumb], (b & XINPUT_GAMEPAD_LEFT_THUMB) != 0);
        checkbindkey(binds[x_right_thumb], (b & XINPUT_GAMEPAD_RIGHT_THUMB) != 0);
        
        // these are the digital triggers on some Logitech controllers
        checkbindkey(binds[x_left_shoulder], (b & XINPUT_GAMEPAD_LEFT_SHOULDER) != 0);
        checkbindkey(binds[x_right_shoulder], (b & XINPUT_GAMEPAD_RIGHT_SHOULDER) != 0);

        checkbindkey(binds[x_button_a], (b & XINPUT_GAMEPAD_A) != 0);
        checkbindkey(binds[x_button_b], (b & XINPUT_GAMEPAD_B) != 0);
        checkbindkey(binds[x_button_x], (b & XINPUT_GAMEPAD_X) != 0);
        checkbindkey(binds[x_button_y], (b & XINPUT_GAMEPAD_Y) != 0);
    }    
};
int controller::count = 0;

static controller c;

ICOMMAND(vibrate, "iii", (int* left, int* right, int* millisec), {
    c.vibrate(*left, *right, *millisec);
});

void gamepadbind(const char* name, const char* act)
{
    if (!c.bounded) c.resetbinds();
    if (strcasecmp(name, "reset") == 0) { c.resetbinds(); return; }

    int a = 0, n = 1;
    for (; a != _countof(actions) && strcasecmp(actions[a], act); ++a);
    for (; n != _countof(inputs) && strcasecmp(inputs[n], name); ++n);
    if (a == _countof(actions)) a = action_key;
    if (n == _countof(inputs)) conoutf(CON_ERROR, "gamepad input %s is not defined", name); 
    else if (a == action_key && (n == x_left_stick || n == x_right_stick))
        conoutf(CON_ERROR, "Cannot bind thumbsticks to key presses");
    else if ((a == action_move || a == action_mouse) && n != x_left_stick && n != x_right_stick)
        conoutf(CON_ERROR, "Cannot bind mouse or movement to key presses");
    else bindaction(c.binds[n], actiontype(a), act);
}
COMMAND(gamepadbind, "ss");

VARP(gamepadon, 0, 0, 1);

namespace gamepad
{
    void checkinput(dynent* player)
    {
        if (!gamepadon) return;
        if (!c.bounded) c.resetbinds();
    
        XINPUT_STATE state;
        if (!c.getstate(state)) return;

        c.checkthumbstick(controller::left, state.Gamepad.sThumbLX, state.Gamepad.sThumbLY, player);
        c.checkthumbstick(controller::right, state.Gamepad.sThumbRX, state.Gamepad.sThumbRY, player);
        c.checktrigger(controller::left, state.Gamepad.bLeftTrigger);
        c.checktrigger(controller::right, state.Gamepad.bRightTrigger);
        c.checkbuttons(state.Gamepad.wButtons);
    }

    void writebinds(stream* f)
    { 
        for (int i = 1; i != _countof(c.binds); ++i)
        {
            action& a = c.binds[i];
            f->printf("gamepadbind %s %s\n", inputs[i], a.type==action_key ? a.name : actions[a.type]);
        }
    }
}


2 comments:

Anonymous said...

Wow! an impressive amount of work, way over my head. I have been trying to figure out how to bind a key to 'mouse look down' so I can use it in an alias to rifle jump. Any ideas?
Mick.

Cristian Vlasceanu said...

Mick,
Try adding this snippet to one of the engine files (append it to main.cpp for instance):
#ifndef MAX_MOUSE_DELTA
#define MAX_MOUSE_DELTA 1200
#endif

void sendmotionevent(int dx, int dy, bool inverty = false)
{
SDL_Event e = { };
e.type = SDL_MOUSEMOTION;
e.motion.x = 0;
e.motion.y = 0;
e.motion.xrel = clamp(dx, -MAX_MOUSE_DELTA, MAX_MOUSE_DELTA);
e.motion.yrel = clamp(inverty ? dy : -dy, -MAX_MOUSE_DELTA, MAX_MOUSE_DELTA);
SDL_PushEvent(&e);
}

ICOMMAND(lookdown, "i", (int* speed), {
sendmotionevent(0, -(*speed));
});

Then "bind [ lookdown 1000 ]".

Play with it and adjust the numbers until it feels right.

Good luck!