4
\$\begingroup\$

Many many thanks to anyone who chooses to take the time to read through this all and offer any advice.

I'm writing a game in which multiple different kinds of Objects move around a grid-based board structure and interact with each other in various ways. The objects are all derived from a relatively barebones base class

class Object{
    int ID;
    virtual void render() = 0;
    virtual void update() = 0;
    ...
};

I'm also loosely using a component system. So for example there's a MovementComponent,

class MovementComponent{
    direction currentMovementDir;
    int movementTimer;
    int speed;
};

Which Objects that can move contain a pointer to an instance of, which is instantiated upon creation of the Object. The Object base class has a virtual function for returning each kind of Component, as follows:

class Object{
    ...
    virtual MovementComponent * getMovementComponent() {return NULL;}
    virtual PathfindingComponent * getPathfindingComponent() {return NULL;}
    //etc. for every kind of Component I introduce to the game code.
};

And then the derived classes just override these functions to provide pointers to their components. This is great, because I can sort of determine the kind of object I'm dealing with based on whether or not I get NULL when I request to see the object's various components. So as an example,

class Player : public Object{
    MovementComponent * movementC_;
    Player(){
        movementC_ = new MovementComponent;
    }
    ~Player(){
        delete movementC_;
    }
    void update(){
        //Perform input stuff here etc.
    }
    virtual MovementComponent * getMovementComponent(){return movementC_;}
}

and so on so forth.

My board structure is simply an array of pointers to Objects, and the movement of Objects on the board is handled by a whole different system. All of the Objects are in a linked list held by a wrapper class called ObjectManager which handles adding new objects of any kind to the game or removing them etc.

My question is that I'm having an increasingly hard time writing elegant code for the various kinds of object interactions that are meant to take place in the game. The controls are intended to be fairly simple, so for example the player just 'actions' the tile they are facing and the game logic handles how this should play out.

Lets say for example its an enemy. 'Action'ing the enemy should just attack the enemy's health based on our current weapon's strength (or something along those lines, this game isn't really about combat, but just for the sake of the example). Within the player class's update function, the part that processes input might look like

void Player::update(){
    ...
    if(actionKeyPressed){
        //Uses the map's list of pointers to get a pointer to whatever object is in the tile in front of the player.
        Object * target = WorldMap.getObject(pos,facing);
        if(target.getHealthComponent() && target.getIsEnemyComponent()){
            target.getHealthComponent.attack(weaponstrength);
        }
    }
}

But you can see how this turns into very ugly code very fast. I can't not check for a healthComponent because if there isn't one, I'll throw an error when I try to invoke its 'attack()' member function. Is there an elegant way to avoid the massive if-else branch I'm going to be sitting on here? It starts to get even worse when we look at Object interactions that might function differently in different contexts with the same Object types. Maybe there's a FriendlyTroop Object that, when actioned by the player at < full health, heals the FriendlyTroop, but when at full health, instead sets a flag to true in the FriendlyTroop's AI that tells it to head out into battle. I guess in addition to any general advice anyone has to offer, the big questions I have are:

-Am I doing this right at all? Is there a better way to handle this overall type-checking dilemma?

-Does every element of that FriendlyTroop's AI need to be pushed out into a unique FriendlyTroopAIComponent type now so that the Player class can interact with it?

-What if I wanted to add an NPC that could perform the same actions as the Player? Would this 'action' if-else structure be better in a PlayerAction component that this NPC could then also have an instance of? Or should the NPC just be a derived class of the Player? Or vice versa???

\$\endgroup\$
  • \$\begingroup\$ github.com/alecthomas/entityx \$\endgroup\$ – DanielCollier Jan 9 '17 at 20:02
  • \$\begingroup\$ Is possible to filter objects by their interfaces in C++? Is so, you'd isolate the ugly code within the interface, and call it like if (target != NUL) this.methodToInterface(target) \$\endgroup\$ – MVCDS Jan 11 '17 at 21:14
2
\$\begingroup\$

Okay, disclaimer: this has gone completely overkill. But it was a lot of fun to put together!

The end product looks like this:

Object *target = WorldMap.getObject(pos,facing);
target->dispatch([&](components::EnemyTag &, components::Health &h) {
    h.attack(weaponStrength);
});

dispatch takes in any function or function object (here a lambda), and inspects its parameter list. The parameters' types are used to call the correponding getSomethingComponent virtual functions. If all components are found, then the lambda is called with them and dispatch returns true. Otherwise, nothing happens and dispatch returns false.

See it live on Coliru

To make the wiring simpler, I have replaced the getSomethingComponent virtual functions with overloads of a virtual get_(tag<SomethingComponent>) function. These are now an implementation detail, and are generated with a macro.

Full code:

#include <type_traits>
#include <tuple>
#include <iostream>

////////////////////////////////////////////////////////////////////////////////
// Dummy tag to hold one or more types

template <class... T>
struct tag { };

////////////////////////////////////////////////////////////////////////////////
// typename params<F>::type is tag<P1, P2, ... Pn>
// where P's are the parameter types of the function or function-object F.

template <class F>
struct params : params<decltype(&F::operator())> { };

template <class R, class T, class... Params>
struct params<R (T::*)(Params...)> { using type = tag<Params...>; };

template <class R, class T, class... Params>
struct params<R (T::*)(Params...) const> { using type = tag<Params...>; };

template <class R, class... Params>
struct params<R (*)(Params...)> { using type = tag<Params...>; };

template <class R, class... Params>
struct params<R (&)(Params...)> { using type = tag<Params...>; };

////////////////////////////////////////////////////////////////////////////////
// Dereference every pointer in Args and call f with them

template <class F, class Args, std::size_t... Idx>
void derefCall(F &&f, Args &args, std::index_sequence<Idx...>) {
    std::forward<F>(f)(*std::get<Idx>(args)...);
}

////////////////////////////////////////////////////////////////////////////////
// Defining your components

namespace components {
    struct Health {
        int value = 100;
    };

    struct EnemyTag { };
}

////////////////////////////////////////////////////////////////////////////////
// Defining the base class

struct Object {

    // obj.get<Comp>() : retrieves a component by type (or nullptr)
    template <class CompType>
    CompType *get() {
        return get_(tag<CompType>{});
    }

    // dispatch : tries to call f with all of the components it requires
    // Returns whether all components were found and f was called.
    template <class F>
    bool dispatch(F &&f) {
        return dispatch_(std::forward<F>(f), typename params<std::decay_t<F>>::type{});
    }

private:

    // Define one virtual function get_(tag<Comp>) for every component type.
#define OBJECT_DECLARE_COMPONENT(CompType) \
    virtual ::components::CompType *get_(tag<::components::CompType>) { return nullptr; }

    OBJECT_DECLARE_COMPONENT(Health)
    OBJECT_DECLARE_COMPONENT(EnemyTag)

#undef OBJECT_DECLARE_COMPONENT

    // Implementation of dispatch, Params are the parameters needed by f.
    template <class F, class... Params>
    bool dispatch_(F &&f, tag<Params...>) {

        bool allGood = true;

        // Construct a tuple with the component pointers
        // For each component required by f:
        auto ptrs = std::make_tuple([&]() -> std::decay_t<Params>* {

            // One component is already missing, skip the rest
            if(!allGood)
                return nullptr;

            // Get the component of the adequate type
            auto *comp = get<std::decay_t<Params>>();

            // Failure: break the chain
            if(!comp)
                allGood = false;

            return comp;
        }()...);

        // Missing component, bail out
        if(!allGood)
            return false;

        // Call f with the dereferenced pointers
        derefCall(std::forward<F>(f), ptrs, std::index_sequence_for<Params...>{});
        return true;
    }
};

// Helper macro to override the component getters
#define OBJECT_OVERRIDE_COMPONENT(CompType) \
    ::components::CompType *get_(tag<::components::CompType>) override 

// Enemy class
struct Enemy : Object {

    OBJECT_OVERRIDE_COMPONENT(EnemyTag) {
        return &enemyTag;
    }

    OBJECT_OVERRIDE_COMPONENT(Health) {
        return &health;
    }

    components::EnemyTag enemyTag;
    components::Health health;
};

int main() {
    Enemy e;
    Object &o = e;

    // Nice call syntax
    bool dispatched = o.dispatch([](components::EnemyTag &, components::Health &h) {
        h.value -= 10;
    });

    if(dispatched)
        std::cout << "Dispatched successfully!\n";

    std::cout << "Enemy has " << e.get<components::Health>()->value << "hp left.\n";
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.