dynamix::object
- just an empty objectThat's easy
class dragon
{
public:
void fly();
void walk_on_two_legs();
void ai_take_the_wheel();
const model& get_model() const;
void render_with_directx() const;
// ...
}
What if we also have a horse or a human character?
Still easy
class dragon : public flying_creature,
public two_legged_creature, public monster_ai,
public animated_model, public directx_rendering
{};
class horse : public walking_creature,
public four_legged_creature, public neutral_ai,
public animated_model, public direcx_rendering
{};
// ... you get the point
That, in a way, is even worse, because flying_creature
has no way of telling animated_model
which animation to play.
OK. This time using real mixins
template <typename object_type>
class flying_creature
{
public:
void fly()
{
flap_wings();
static_cast<object_type*>(this)
->set_animation("flying");
}
// ...
};
class dragon : public flying_creature<dragon>,
public two_legged_creature<dragon>,
public monster_ai<dragon>,
public animated_model<dragon>,
public directx_rendering<dragon>
{};
template <typename object_type>
class flying_creature : public virtual game_object
{ /* ... */ };
std::vector<game_object*> objects;
This is identical to vector<void*>
.
I have no way of using the objects in this array.
Fine. I'm rolling-up my sleeves
class flying_creature : public virtual game_object
{
public:
virtual void fly() override
{
this->flap_wings();
this->set_animation("flying");
}
// ...
};
class dragon : public flying_creature,
public two_legged_creature, public monster_ai,
public animated_model, public directx_rendering
{
};
So, all, ALL possible methods will exist as pure virtual in game_object
Also, a walking creature cannot fly. You can never instantiate dragon
since it's abstract.
Having separate methods for flying and walking was a bad idea, anyway. There should be a single method: move
. In fact how about this:
class game_object
{
virtual void move() = 0; // flying, walking, vehicles
// enemy/neutral ai, keyboard control
virtual void decide_action() = 0;
//...
};
class dragon : public flying_creature,
public two_legged_creature, public monster_ai,
public animated_model, public directx_rendering
{
// there still might be invalid actions for the object
// list them here
virtual void use(item*) override {
throw bad_call();
}
};
This will work.
Aww, yiss!
But...
game_object
is a coupling focal pointI could add such if-checks to all my methods, but I suppose you won't like this
Object
class game_object
{
control* m_control;
physical_data* m_physical_data;
rendering* m_rendering;
mobility* m_mobility;
...
};
// compose
game_entity dragon;
dragon.set_control(new monster_ai);
dragon.set_physical_data(new animated_model("dragon.x"));
dragon.set_mobility(new flyer);
...
// modify
dragon.set_control(new player_control);
Component
class component
{
game_object* self;
};
class monster_ai
: public control, public component
{
virtual void decide_action() override
{
...
self->get_mobility()->move_to(good_guy);
}
};
This is, actually, a pretty decent solution.
But...
struct mobility
{
virtual void move_to(target t) = 0;
virtual bool can_move_to(target t) const = 0;
};
What if we want to override only can_move_to
?
OK. I give up. In such cases people just don't use C++. You want to embed a scripting language like Python, or Lua, or JavaScript.
That's a decent solution too.
module FlyingCreature
def move_to(target)
puts can_move_to?(target) ?
"flying to #{target}"
: "can't fly to #{target}"
end
def can_move_to?(target)
true # flying creatures don't care
end
end
module AfraidOfEvens
def can_move_to?(target)
return target%2 != 0
end
end
a = Object.new
a.extend(FlyingCreature)
a.move_to(10)
a.extend(AfraidOfEvens)
a.move_to(10)
Finally
DynaMix Dragon
object dragon; // just an empty object
mutate(dragon)
.add<flying_creature>()
.add<two_legged_creature>()
.add<monster_ai>()
.add<animated_model>()
.add<directx_rendering>();
::set_model(dragon, "dragon.x");
//...
::decide_action(dragon); // attack player
mutate(dragon)
.remove<monster_ai>()
.add<player_control>();
::decide_action(dragon); // read keyboard
DynaMix Mixin
class monster_ai
{
void decide_action()
{
...
::move_to(dm_this, good_guy);
}
};
dm_this
is like self
: the object we're a part ofDYNAMIX_DECLARE_MIXIN(monster_ai);
DYNAMIX_DEFINE_MIXIN(monster_ai, decide_actions_msg);
DYNAMIX_MESSAGE_0(void, decide_action);
DYNAMIX_MESSAGE_1(void, move_to, object*, target);
DYNAMIX_MESSAGE_2(rt, foo, arg1_t, arg1, arg2_t, arg2);
DYNAMIX_DEFINE_MESSAGE(decide_action);
DYNAMIX_DEFINE_MESSAGE(move_to);
DYNAMIX_DEFINE_MESSAGE(foo);
And that's how we truly separate the interface from the implementation.
MixQuest
std::function
Borislav Stanimirov / ibob.github.io
DynaMix is here: github.com/ibob/dynamix/
Link to these slides: http://ibob.github.io/slides/dynamix/
Slides license Creative Commons By 3.0