Data-Oriented Design Patterns: Command

April 3, 2021

I’ve been reading Richard Fabian’s Data-Oriented Design book lately, and wanted to try applying some of its concepts to a common gamedev problem often solved using the Command design pattern. I will readily admit that I’m probably misunderstanding some things and misapplying techniques, and maybe writing what professionals will consider very ugly code, but I’ll risk embarrassing myself to share what I’ve been up to.

The problem I chose to tackle is a (very) simple input handler. The data-oriented approach I chose takes a table of keys mapped to function-pointers, checks those keys against an input buffer, and generates a table of functions to execute. I like this better than the if (button_pressed) { jump() } approach I’ve seen elsewhere because the execution table is generic - it can be passed to the AI handler and the physics engine, and extended with functions to be applied to entities other than the player. Then all functions can be called together in one big loop.

I wanted to focus on the logic of the program, so I haven’t delved into input polling yet, instead using the terminal input to collect a pseudo-input-buffer. I’ll update the code when I’ve worked out how to interrogate keyboard state.

inputhandler.cpp

int main() {
    // Bindings are stored in vec_inputBindings, while vec_actions contains a
    // list of to-be-executed actions and object IDs. AI will also add to
    // vec_actions, so all actions, regardless of target object, can be
    // executed together.
    vector<inputBinding> vec_inputBindings;
    vector<action> vec_actions;

    // Eventually these bindings will be moved to an external file instead of
    // being hard-coded into source.
    vec_inputBindings.push_back(inputBinding(' ', actions::jump));
    vec_inputBindings.push_back(inputBinding('c', actions::crouch));
    vec_inputBindings.push_back(inputBinding('x', actions::attack));

    while(cin) {
        // First, the vec_actions needs to be reset (for now).
        // handleInputs() checks for button presses to determine which player
        // actions need to be executed. executeActions() then calls all 
        // collected functions with their associated ObjectIDs.
        vec_actions.clear();
        handleInputs(vec_inputBindings, vec_actions);
        executeActions(vec_actions);
    }
}

actions.h

// Just a handful of functions for initial testing, but this list will grow
// much larger.
namespace actions {
    void jump(int x) { cout << x << " ^JUMPS^" << endl; }
    void crouch(int x) { cout << x << " _crouches_" << endl; }
    void attack(int x) { cout << x << " a-TTACKS!!!" << endl; }
}

// Function pointers are a pain in the ass to declare repeatedly, and anytime new
// params are added, they need to be changed everywhere. This defines actionFunc
// as an alias for void(*)(int).
typedef void(*actionFunc)(int);

// This struct defines a row in our actions-to-execute table. I think it will
// become bigger over time as I define actions::functions and need to add more
// parameters? Although maybe I'll store those parameters in a related table.
// Dunno yet.
struct action {
    actionFunc execute;
    int ID;
    action(actionFunc f, int i) {
        execute = f;
        ID = i;
    }
};

void executeActions(vector<action> &actions) {
    for (auto action : actions) {
        action.execute(action.ID);
    }
}

inputs.h

// IDs will eventually be in an object info header, but this can stay here for
// now.
int playerID = 0;

struct inputBinding {
    char key;
    actionFunc action;
    inputBinding(char b, actionFunc f){
        key = b;
        action = f;
    }
};

// For now, changing a binding deletes any existing instance of the new key
// AND the new action, which means an action can't be bound to two buttons at
// once. This simplifies input handling a little, since there will never be
// duplicate actions in a frame, making us apply multiple attacks or jumps at
// once. This is an area, like handleInputs() below, that could possibly benefit
// from turning vec_inputBindings into a std::map.
void changeBindKey(char key, actionFunc action, vector<inputBinding> &vec_inputBindings) {
    for (int i = 0; i < vec_inputBindings.size(); i++ ) {
        if (vec_inputBindings[i].key == key) {
            vec_inputBindings.erase(vec_inputBindings.begin()+i);
        }
        if (vec_inputBindings[i].action == action) {
            vec_inputBindings.erase(vec_inputBindings.begin()+i);
        }
    }
    vec_inputBindings.push_back({key, action});
}

// Right now this just checks for button presses - none of the control logic is
// here. You can crouch and jump and attack all at the same time, you can jump
// on consecutive frames, etc.
void handleInputs(vector<inputBinding> &vec_inputBindings, vector<action> &vec_actions) {

    // For now I'm just using STDIN for testing. Actually querying the OS for inputs
    // is a topic for another day. I'm using the string function getline() because
    // cin >> inputBuffer doesn't preserve whitespace keys.
    string inputBuffer;
    getline(cin, inputBuffer);

    // The way I've chosen to do this checks every mapped button to see if it
    // appears in the inputBuffer. I'm uncertain if this is the best approach.
    // Most buttons will usually be unpressed, so this is a lot of wasted
    // conditional checks. I could alternatively store input mappings in a
    // std::map, and loop through inputBuffer chars to key into the map, but
    // the key would need to be checked with map.find() first, and duplicates
    // would need to be prevented, possibly by sorting and unique-ing the
    // inputBuffer. The tradeoffs seem like a wash, and I have no idea what
    // will be appropriate once actual input polling is used, so for now,
    // this is nice and clean and it'll do.
    for (auto inputBinding : vec_inputBindings) {
        if(inputBuffer.find(inputBinding.key) != string::npos) {
            vec_actions.push_back(action(inputBinding.action, playerID));
        }
    }
}