Jump to content
  • Advertisement
Sign in to follow this  
MuTeD

Component-Entity game design issue

This topic is 1860 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hello. This was going fine for a very short while, until it made no more sense logically.

 

You cannot acquire an entities input (player, NPC, etc) without hard-coding everything that should apply to that particular entity.

You cannot create a universal form of physics (to a degree), without hard-coding everything that should apply to a particular entity.

If those two statements are incorrect: I obviously cannot see any fathomable way around it (hard-coding).

I am also obviously posting here because I am trying to solve these two major problems (and more), and cannot.

 

This is the source code (WIP) that I have currently. Although I just started to experiment with another idea I had:

#include <vector>
#include <map>
#include <iostream>
 
#include <allegro5/allegro.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_primitives.h>
 
const int FPS = 30;
 
bool paint = true;
 
///////////////////////////////////////////////////
// http://stackoverflow.com/questions/764824/best-way-to-organize-entities-in-a-game
///////////////////////////////////////////////////
typedef unsigned long EntityId;
 
class Component {
    Component(EntityId id) : owner(id) {}
    EntityId owner;
};
 
template <typename C> class System {
    std::map<EntityId, C*> components;
};
///////////////////////////////////////////////////
 
// Helper structure
struct PVector {
    double x;
    double y;
 
    PVector() : x(0), y(0) { }
    PVector(double x_, double y_) : x(x_), y(y_) { }
 
    void add(PVector v) {
        x += v.x;
        y += v.y;
    }
 
    void sub(PVector v) {
        x -= v.x;
        y -= v.y;
    }
};
 
// Helper structure (RECT from Win32 API relies on int; I need type double)
struct RECTANGLE {
    double left;
    double top;
    double right;
    double bottom;
 
    RECTANGLE() : left(0.0), top(0.0), right(0.0), bottom(0.0) { }
};
 
/*
// This is where the logic starts to fall apart
// A "controller" cannot independently exist without knowledge of WHAT it's moving (velocity)
// If every game object (entity) has independent laws for physics (including movement)
// How can one create a universal "CONTROLLER" component without directly knowing of the laws
// That should independently apply to each individual unique game object (entity)?
 
struct CONTROLLER {
    void moveUp() {
    }
};
*/
 
struct isEntity;
 
struct CONTROL {
    typedef enum { NON, UP, DOWN, LEFT, RIGHT } eLastKnown;
    eLastKnown lastKeyPressed;
 
    CONTROL() { lastKeyPressed = NON; }
 
    void update(ALLEGRO_EVENT& ev) {
        if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {
            switch (ev.keyboard.keycode) {
                case ALLEGRO_KEY_UP: {
                    std::cout << "KEY_UP!\n";
                    lastKeyPressed = UP;
                } break;
 
                case ALLEGRO_KEY_DOWN: {
                    std::cout << "KEY_DOWN!\n";
                    lastKeyPressed = DOWN;
                } break;
 
                case ALLEGRO_KEY_LEFT: {
                    std::cout << "KEY_LEFT!\n";
                    lastKeyPressed = LEFT;
                } break;
 
                case ALLEGRO_KEY_RIGHT: {
                    std::cout << "KEY_RIGHT!\n";
                    lastKeyPressed = RIGHT;
                } break;
            }
        }
    }
};
 
struct cPlayerInput : CONTROL {
};
 
struct isEntity {
    isEntity() { ++ID; }
    static int ID;
};
 
int isEntity::ID = 0;
 
struct GRAPHICS {
    ALLEGRO_BITMAP* image;
 
    void render(RECTANGLE* area) {
        al_draw_bitmap(image, area->left, area->top, 0);
    }
 
    void loadImage(std::string path, RECTANGLE& area) {
        image = al_load_bitmap(path.c_str());
 
        // Automatically adjusts the "size" of the game object (ENTITY)'s occupied area in the world
        area.right = al_get_bitmap_width(image);
        area.bottom = al_get_bitmap_height(image);
    }
};
 
// Game object ("ENTITY")
// -- A "platform" contains only one attribute: A point in space (defined as a rectangle)
// A platform can be interacted with (moved, destroyed, 'landed on' with no effect on the platform itself)
struct PLATFORM : isEntity {
    PLATFORM() { }
    PLATFORM(double x, double y, std::string path) {
        area.top = y;
        area.left = x;
        graphics.loadImage(path, area);
    }
 
    GRAPHICS graphics;
    isEntity* entityID;
    PLATFORM* test;
 
    RECTANGLE area;
};
 
// Game object ("ENTITY")
// -- A "ball" contains a point in space (defined as a rectangle)
// A ball can move in any given direction (up/down/left/right keys)
// A ball cannot pass through "solid" objects of type 'PLATFORM'
struct BALL {
    BALL(double x, double y, std::string path) {
        area.top = y;
        area.left = x;
        graphics.loadImage(path, area);
    }
 
    GRAPHICS graphics;      // Ability to display/update/remove graphics of the "BALL" type
 
    cPlayerInput input;     // Hard-coded input component (cannot fathom other options)
 
    RECTANGLE area;         // Location of the ball in the world
    PVector velocity;       // Velocity of the ball in the world
    PVector acceleration;   // Acceleration of the ball in the world
 
    // Forced to use a method within a "game object" as opposed to using a "component" to add ability via magic struct/class
    void applyInput(ALLEGRO_EVENT& ev) {
        input.update(ev);
        std::cout << "Last key pressed: " << input.lastKeyPressed << std::endl;
    }
};
 
// "World" is defined as a "SYSTEM" (entity manager)
// -- The world contains: An infinite number of game objects of type "BALL"
// The world also contains an infinite number of game objects of type "PLATFORM"
struct World {
    std::vector<BALL> ball;
    std::vector<PLATFORM> platform;
 
    // "applyInput()" should 'describe' the direction in which a game object WANTS to move (yielding to the 'laws of physics')
    void applyInput(ALLEGRO_EVENT& ev) {
        for (std::vector<BALL>::iterator it = ball.begin(); it != ball.end(); ++it) {
                it->applyInput(ev);
 
                if (it->input.lastKeyPressed == 1) {
                    --it->area.top;
                }
 
                applyPhysics();
        }
    }
 
    // "applyPhysics()" should ultimately modify (accepting "Input" decisions) the input given previously
    void applyPhysics() {
        for (std::vector<BALL>::iterator it = ball.begin(); it != ball.end(); ++it) {
            if (it->area.top > it->area.top - 10) {
                it->area.top = 0;
            }
        }
    }
 
    // "renderObjects()" should call upon all objects "render()" method
    void renderObjects() {
 
        // Render all game objects of type "BALL"
        for (std::vector<BALL>::iterator it = ball.begin(); it != ball.end(); ++it) {
                it->graphics.render(&it->area);
        }
 
        // Render all game objects of type "PLATFORM"
        for (std::vector<PLATFORM>::iterator it = platform.begin(); it != platform.end(); ++it) {
                it->graphics.render(&it->area);
        }
    }
};
 
// "Physics" is defined as a "SYSTEM" (physics manager for entities)
// -- Physics is responsible for:
// -- * Entity velocity
// -- ** Entity acceleration
// -- *** Entity collision (AABB)
struct Physics {
    World* world; // The world to be dealt with
 
    isEntity* getEntity(int ID) {
        return NULL;
    }
 
    void moveEntityID(int ID, isEntity* ptr) {
        std::cout << "ptr ID: " << ptr->ID << " | ID: " << ID << std::endl;
 
        if (ptr->ID == ID) {
            PLATFORM* temp = static_cast<PLATFORM*>(ptr);
            std::cout << "PLATFORM upper-left vertex is located at: (" << temp->area.left << ", " << temp->area.top << ")\n\n";
        }
    }
 
};
 
// Helper function (to reduce code clutter later in the main function)
void addBallEntity(World* world, double x, double y, std::string path) {
    world->ball.push_back(BALL(x, y, path.c_str()));
}
 
// Helper function (to reduce code clutter later in the main function)
void addPlatformEntity(World* world, double x, double y, std::string path) {
    world->platform.push_back(PLATFORM(x, y, path.c_str()));
}
 
int main(int argc, char **argv) {
 
    ALLEGRO_DISPLAY *display = NULL;
 
    al_init();
    al_install_keyboard();
    al_init_image_addon();
    al_init_primitives_addon();
 
    display = al_create_display(640, 480);
 
    ALLEGRO_EVENT_QUEUE *event_queue = NULL;
    ALLEGRO_TIMER *timer = NULL;
    timer = al_create_timer(1.0 / FPS);
 
    event_queue = al_create_event_queue();
    al_register_event_source(event_queue, al_get_display_event_source(display));
    al_register_event_source(event_queue, al_get_keyboard_event_source());
    al_register_event_source(event_queue, al_get_timer_event_source(timer));
 
    al_clear_to_color(al_map_rgb(0, 0, 0));
    al_flip_display();
 
    al_start_timer(timer);
 
    // This creates an entire game world to play in ("SYSTEM")
    World world;
    Physics physics;
 
    // This adds a game object ("ENTITY") of type BALL to the game world ("SYSTEM")
    addBallEntity(&world, 50, 250, "flag.bmp");
 
    // This adds 15 game objects ("ENTITIES") of type PLATFORM to the game world ("SYSTEM")
    for (int n = 0; n < 15; ++n) {
        addPlatformEntity(&world, 50 + (n * 32), 277, "platform.bmp");
 
        // physics.moveEntity expects an entity ID tag, and a pointer to the entity itself
        // This could be simplified by creating an "ENTITY MANAGER" (game object manager?)
        // Instead of referring to any particular object by it's literal physical address
        // We refer to objects by ID tags given at their creation (updated automatically)
        physics.moveEntityID(1, static_cast<isEntity*>(&world.platform.at(n)));
    }
 
    for (int n = 0; n < 15; ++n) {
        addPlatformEntity(&world, 50 + (n * 32), 309, "dirt.bmp");
    }
 
    bool quit = false;
 
    while (!quit) {
        do {
            ALLEGRO_EVENT ev;
            al_wait_for_event(event_queue, &ev);
 
            if (ev.type == ALLEGRO_EVENT_TIMER) {
                paint = true;
            } else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
                quit = true;
            }
 
            if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {
                world.applyInput(ev);
 
                switch (ev.keyboard.keycode) {
                    case ALLEGRO_KEY_ESCAPE:
                        quit = true;
                        break;
                }
            }
        } while (!al_is_event_queue_empty(event_queue));
 
        if (paint == true) {
            paint = false;
 
            al_clear_to_color(al_map_rgb(0,0,0));
 
            al_draw_line(50, 277, 300, 277, al_map_rgb(255, 255, 255), 0);
            al_draw_line(50, 200, 300, 200, al_map_rgb(255, 255, 255), 0);
 
            // Renders all objects from the game world, to the screen
            world.renderObjects();
 
            al_flip_display();
        }
    }
 
    al_destroy_display(display);
    al_destroy_timer(timer);
    al_destroy_event_queue(event_queue);
 
    return EXIT_SUCCESS;
}

Any help would be extremely appreciated. Thanks. smile.png

Share this post


Link to post
Share on other sites
Advertisement

You cannot acquire an entities input (player, NPC, etc) without hard-coding everything that should apply to that particular entity.
You cannot create a universal form of physics (to a degree), without hard-coding everything that should apply to a particular entity.


These two statements are not true.

What you are building up there isn't really what is classically called an entity or component system. A first indicator of that fact is that you have a vector of BALL and a vector of PLATFORM. In a component system, you would only have a vector of OBJECT. (Or possibly OBJECTID, depending on the structure you use.)

Any time you have hard-coded structures named BALL or PLATFORM you are most likely building something other than an entity system. There are a couple of different ways you can build an entity system, but all of them hinge around the idea that objects are composed of parts in aggregate.

One form uses an integral ID to tie the parts together, holding the actual objects representing the parts in numerous arrays scattered throughout the subsystems. Another form uses an OBJECT structure as a bucket, or container, of components, using the structure itself to tie the parts together. All objects are just objects; what make them act as balls or platforms are the components they contain or encompass.

Share this post


Link to post
Share on other sites
 

 

You cannot acquire an entities input (player, NPC, etc) without hard-coding everything that should apply to that particular entity.
You cannot create a universal form of physics (to a degree), without hard-coding everything that should apply to a particular entity.


These two statements are not true.

What you are building up there isn't really what is classically called an entity or component system. A first indicator of that fact is that you have a vector of BALL and a vector of PLATFORM. In a component system, you would only have a vector of OBJECT. (Or possibly OBJECTID, depending on the structure you use.)

Any time you have hard-coded structures named BALL or PLATFORM you are most likely building something other than an entity system. There are a couple of different ways you can build an entity system, but all of them hinge around the idea that objects are composed of parts in aggregate.

One form uses an integral ID to tie the parts together, holding the actual objects representing the parts in numerous arrays scattered throughout the subsystems. Another form uses an OBJECT structure as a bucket, or container, of components, using the structure itself to tie the parts together. All objects are just objects; what make them act as balls or platforms are the components they contain or encompass.

 

If you refer to the very top of the source code: I was in the process of modifying what I had laid out. I was attempting to use an ID-tag system (ergo: "isEntity" struct), but ran into a problem. I had an idea that seemed near impossible (templates with map containers, based on virtual functions from inheritance). After searching around Google, I stumbled upon that person's solution (which is exactly what I wanted to do, but was unable to express in C++).

 

I understand what you're saying regarding "all objects (entities) are objects (entities)." What you're implying is that everything is dynamically created.

I've read so many differing arguments on the subject (posts dating from 2005 to 2014), this is what I've concluded: There is no universal concept of it yet.

Every person uses a different metaphor with their own conceptual idea of the design to solve their particular problem. The only commonalities between all is the "buzz words."

 

I will happily modify it to use strictly dynamically created entities ("objects" as you said) here shortly, and see what comes to mind afterwards.

I still cannot foresee how you would create an entity that reacts to gravity in an opposite manner than another object would (dynamic or statically created).

 

Unless you're implying to create multiple sets of "physics components" and apply (staple together) it the respective entity?

Which, if that is the case, would mean exactly what I am saying: Everything is going to be hard-coded (to a "component").

 

Which brings me to another major question (assuming all above is true): How would you implement the system(s)?

A "world" system to represent in-game play (for a player), a "demoWorld" system to represent demo-play (by an AI-controlled player), and for networking, another system all together?

 

Thank you, and thank you for the reply. I will start modifying the source code from start to end now to follow what I understand. smile.png

 

Share this post


Link to post
Share on other sites

Hello. Thanks again for the (incredibly unexpected) informative reply. Much appreciated. smile.png

 

I will possibly edit this post tomorrow at some point when I wake up, and continue with more questions, but for now, just to suffice:

#include <vector>
#include <map>
#include <iostream>
 
#include <allegro5/allegro.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_primitives.h>
 
const int FPS = 30;
 
bool paint = true;
 
///////////////////////////////////////////////////
// http://stackoverflow.com/questions/764824/best-way-to-organize-entities-in-a-game
///////////////////////////////////////////////////
typedef unsigned long long int EntityId;
 
class Component {
    Component(EntityId id) : owner(id) {}
    EntityId owner;
};
 
template <typename C> class System {
    std::map<EntityId, C*> components;
};
///////////////////////////////////////////////////
 
struct COMPONENT {
    COMPONENT() { ++ID; }
    static EntityId ID;
};
 
EntityId COMPONENT::ID = -1;
 
struct OBJECT : public COMPONENT {
    std::vector<COMPONENT *> component;
 
    OBJECT() {
        std::cout << "OBJECT object constructor called\n";
    }
 
    void addComponent(COMPONENT* newComponent) {
        component.push_back(newComponent);
 
        std::cout << "Added OBJECT component; ID tag = " << newComponent->ID << std::endl;
    }
};
 
// Helper structure
struct PVector : public COMPONENT {
    double x;
    double y;
 
    PVector() : x(0), y(0) { }
    PVector(double x_, double y_) : x(x_), y(y_) { }
 
    void add(PVector v) {
        x += v.x;
        y += v.y;
    }
 
    void sub(PVector v) {
        x -= v.x;
        y -= v.y;
    }
};
 
// Helper structure (RECT from Win32 API relies on int; I need type double)
struct RECTANGLE : public COMPONENT {
    double left;
    double top;
    double right;
    double bottom;
 
    RECTANGLE() : left(0.0), top(0.0), right(0.0), bottom(0.0) { }
};
 
struct GRAPHICS : public COMPONENT {
    ALLEGRO_BITMAP* image;
    RECTANGLE area;
 
    GRAPHICS(std::string path, double x, double y) {
        image = al_load_bitmap(path.c_str());
 
        area.left = x;
        area.top = y;
 
        // Automatically adjusts the "size" of the game object (ENTITY)'s occupied area in the world
        area.right = al_get_bitmap_width(image);
        area.bottom = al_get_bitmap_height(image);
 
        if (image == NULL) {
            std::cout << "Line 92: image returned NULL\n";
        }
    }
 
    void render() {
        if (image != NULL) {
            al_draw_bitmap(image, area.left, area.top, 0);
        }
    }
};
 
struct playerPhysics : public COMPONENT {
    bool gravityInEffect;
 
    playerPhysics() : gravityInEffect(true) {
    }
 
    void applyGravity(RECTANGLE& area) {
        if (gravityInEffect == true) {
            ++area.top;
            ++area.bottom;
        }
    }
};
 
struct World {
};
 
int main() {
 
    ALLEGRO_DISPLAY *display = NULL;
 
    al_init();
    al_install_keyboard();
    al_init_image_addon();
    al_init_primitives_addon();
 
    display = al_create_display(640, 480);
 
    ALLEGRO_EVENT_QUEUE *event_queue = NULL;
    ALLEGRO_TIMER *timer = NULL;
    timer = al_create_timer(1.0 / FPS);
 
    event_queue = al_create_event_queue();
    al_register_event_source(event_queue, al_get_display_event_source(display));
    al_register_event_source(event_queue, al_get_keyboard_event_source());
    al_register_event_source(event_queue, al_get_timer_event_source(timer));
 
    al_clear_to_color(al_map_rgb(0, 0, 0));
    al_flip_display();
 
    al_start_timer(timer);
 
    // This creates an entire game world to play in ("SYSTEM")
    //World world;
 
    OBJECT* player = new OBJECT;
    OBJECT* platform = new OBJECT;
 
    player->addComponent(new GRAPHICS("flag.bmp", 50.0f, 50.0f));
    player->addComponent(new playerPhysics());
    GRAPHICS *p1 = (GRAPHICS *)player->component.at(0);
 
    platform->addComponent(new GRAPHICS("platform.bmp", 50, 277));
    platform->addComponent(new RECTANGLE());
 
    GRAPHICS *p2 = (GRAPHICS *)platform->component.at(0);
    playerPhysics* pp1 = (playerPhysics*)player->component.at(1);
 
    bool quit = false;
 
    while (!quit) {
        do {
            ALLEGRO_EVENT ev;
            al_wait_for_event(event_queue, &ev);
 
            if (ev.type == ALLEGRO_EVENT_TIMER) {
                pp1->applyGravity(p1->area);
                paint = true;
            } else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
                quit = true;
            }
 
            if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {
 
                switch (ev.keyboard.keycode) {
                    case ALLEGRO_KEY_ESCAPE:
                        quit = true;
                        break;
                }
            }
        } while (!al_is_event_queue_empty(event_queue));
 
        if (paint == true) {
            paint = false;
 
            al_clear_to_color(al_map_rgb(0,0,0));
 
            al_draw_line(50, 200, 300, 200, al_map_rgb(255, 255, 255), 0);
            al_draw_line(50, 277, 300, 277, al_map_rgb(255, 255, 255), 0);
 
            p1->render();
            p2->render();
 
            al_flip_display();
        }
    }
 
    al_destroy_display(display);
    al_destroy_timer(timer);
    al_destroy_event_queue(event_queue);
 
    return EXIT_SUCCESS;
}

Apparently the forums have a bug? I cannot post code tags and have text afterwards, without it omitting all the text below. :unsure:

 

Anyway (to recap) This is my understanding of what you had to do earlier. Based on your (limited) example, I saw no explicit declaration of "player."

I was assuming based on the new allocator, that inheritance was being used (to down-cast). That's why I used it in my attempt.

 

Correct me if I'm wrong.

 

And thanks again. :)

Share this post


Link to post
Share on other sites
Now you are getting into the next tricky part of these types of systems: communicating with objects and making them do their "thing".

In the Object-As-ID system, this is performed by iterating through all the sub-systems (graphics, physics, etc...) and calling some sort of Update() method on the sub-system which will, in turn, update all the various components. This type of system gets tricky in that sub-systems can often be very inter-dependent, and the order of updating can often be very rigid. For example, you might want to update Physics before you update Graphics (since the physics step will update the object's transformation, and Graphics will require up-to-date transformation to draw at the proper transform). As more sub-systems are added, this staging of updates can get complex and even brittle unless handled very carefully. However, we've kind of moved on from that type of entity system anyway, so...

How exactly do you communicate with an object that is composed of parts? Especially if you are not supposed to really know or care exactly what parts it comprises? Object lifetime and management systems typically shouldn't know or care how an object is structured, since that gets right back to the dependency problems introduced by static, hard-coded classes. So when you see lines such as:

playerPhysics* pp1 = (playerPhysics*)player->component.at(1);
in your object handling code or main game loop like this, then you should have some alarm bells going off in your head since that part of your game really shouldn't know or care what components any given object contains, nor should it know exactly where to locate a given component like that. The game loop is responsible for handing off high level instructions to all objects, and the components themselves should handle the particulars.

So how can it be done? One commonly used technique is to implement event passing. (This is the technique Urho3D uses.) Somewhere in your system code is a structure or backbone that allows you to construct an event or message and hand it off, either to a specific object or to all objects in general, as required. Any object or component can be a message receiver as their needs require. In Urho3D, objects must specifically register for certain events, optionally originating from a specified object. (Defaulting to listening to the specified event originating from ANY object, if none specified.)

An event is usually configured as an event Type and some packet of arbitrary data that might be required by objects processing the event. (This is another tricky area, this specifying of arbitrary data packets, especially in statically-typed languages such as C++.) The high level game loop is going to launch off a sequence of high-level events which other objects will listen for and act upon. These high level events are staged in the proper order so that things occur as is best for the systems.

To cite an example, again from Urho3D, the main loop will trigger, in sequence, these events:

BeginFrame
Update
PostUpdate
RenderUpdate
PostRenderUpdate
EndFrame

Some of these events (such as BeginFrame/EndFrame and the post-update ones) do not necessarily convey any data. Others, such as Update, convey a packet of data which can be accessed by any objects receiving the event: namely, a floating-point precision value, TimeStep, indicating how much time to advance the simulation and animation. Any object or any component of an object can listen for Update to do its thing.

Additionally, sub-systems (such as the PhysicsWorld component of the scene manager, if present) might listen for specific events from the main loop and fire off additional events in response. For example, the PhysicsWorld will listen for Update and fire off, in response, the events PhysicsPreStep (right before the physics simulation is advanced) and PhysicsPostStep (right after the simulation is advanced), allowing other objects to listen for these events and act accordingly.

Events are hooked using a subscription model. For example, a component can subscribe to the Update event, providing a callback that is called whenever the Update event is broadcast. It can use this callback to implement things such as movement, advancing animations, etc... A component on a physics-enabled object could listen for PhysicsPreStep event, for example, and use that event to cause a force to be applied to the object that is equal to 2x the negative of the gravity force vector; the result of this force application would counter-act the force of gravity, then cause an additional impulse equal to the force of gravity but in the opposite direction.

By using an event system like this, it is not necessary that the top-level game loop have access to individual objects' component lists in order to apply updates. It just sends a generic update event, and the components handle themselves accordingly in response to the Update event and any other events that are subsequently generated.

Now, it is possible (and probably preferable) to still provide some sort of GetComponent() method to objects. Urho3D does. If you are confident that a given object contains a component of a given type, then it can be faster and much less messy to access that component directly rather than trying to go through the event system. For an example, in Urho3D you can add an AnimatedModel component to an object to implement an animated character. Controlling the animation, however, is done through an AnimationController component, which is used to play animations such as Walk, Run, etc... This controller component will call GetComponent("AnimatedModel") and obtain a direct reference to the animated model component in order to play the animation. Of course, such systems should always fail gracefully; if there is no AnimatedModel present in an object, AnimationController should not crash or segfault when looking for one. And typically, you would only use this direct access internally on a given object, since external objects usually shouldn't know the exact composition of a foreign object. How you handle these dependencies, though, is very much up to your particular game structure.

Events can be used to communicate between game objects (Player object might send a ApplyDamage event to a target combatant object, for example) or to allow a game object to communicate with all the parts of its self. (For example, in Urho3D animations can specify events to fire off at specific times during an animation played by AnimationController; other components owned by the object, such as a user-defined Footsteps component, could listen for a specific event generated by the walk cycle animation to know when to play a footstep sound.) Logging sub-systems can be set up to listen for all events of a given type, regardless of origin, in order to facilitate the tracking of event flow for debugging situations. While the coding for these kinds of event systems gets a tad tricky, the solution itself is fairly elegant once implemented.

Share this post


Link to post
Share on other sites

Hi!,

 

Really good explanation and ideas about the component system model. I have a doubt though. Everyone says that components must not rely on other components.  The problem is that components ussually need data from other components. An example is the animated mesh & animation controller component you exposed before. In this example, both components are really tied. Others examples not so simple are for example the data that must share an animation controller component with a physics component becuase physics are driving the animation, the Brain component with the animation system, etc... One big global dependency is the Frame component or Transfrom. I have seen this one being integrated in the "container" object because it is used a lot, so, it is accesed by almost all components.

 

Would you mind recomending what to do in this cases? Because get the needed components from the current component and check if they exist or not doesn't seem good.

 

Thanks in advance.

Edited by HexDump

Share this post


Link to post
Share on other sites

What JTippetts described for the animation scenario is just one of several ways to approach the problem.  Another is to approach it in a more event-oriented fashion where the AnimationController subscribes to various events that indicate combat, movement, idle, etc.  When those events are fired, the AnimationController merely updates it's internal state machine.  The state transitions trigger a series of additional events that the AnimatedModel has subscribed to and it plays out those animations.

 

But in this case, what you've done is abstracted away what really is an explicit dependency between two components.  Sure, one could argue that you could replace the AnimatedModel component with some other behavior that listens to the same series of events and you could then swap between the implementations on a per entity basis.  This has some merit, but such a design should come from actual need rather than over engineering a solution that you may never utilize and you find is much harder to maintain than had you just kept the dependency explicit.

 

There is no formal approach here and there a literal dozens of ways you can approach this.  Some people advocate duplicating data in cases of transform to avoid cache misses during various update passes while others design some elaborate handle-to-id lookup indirection table so that the cost to get a component of a type on a per-entity basis inside a tight loop of components is minimized.  But in both of these designs, this should be something that comes from profiling to determine whether its really a bottleneck.  

 

You could also have system A that processes component A lookup whether the entity has component B when the system A is told that a component has been added/removed from the entity.  The system maintains a list of entities w/component A & B and a list that has component A but no B.  This way you can do specialized cache friendly operations on the entities with A&B and perhaps other operations cache friendly on those with just A.  This avoids the lookup and branch problems that come up with if-conditions inside loops.

 

In the end, all this boils down to tuning and profiling more so than the design and implementation of a component entity system.  

Share this post


Link to post
Share on other sites

Hi MuTeD,

 

I wanted to share with you an example, from my entity component system, of how you can build entities with components, and how they can be controlled with systems.  Much of the implementation is squirreled away in the engine, but it hopefully gives you an idea .

 

This just shows how I create systems, and then create 2 entities, one is a player controlled entity, the other has an "enemy" component, and is AI controlled

    pWorld = Esc::TWorld::GetInstance();
 
    cpInitChipmunk();
 
    mpSpace = cpSpaceNew();
 
    // create all the systems we'll need
    PhysicsSystem = new TPhysicsSystem(mpSpace);
    RenderingSystem = new TRenderingSystem(app);
    DamageSystem = new TDamageSystem();
    DeathSystem = new TDeathSystem(mpSpace);
    InputSystem = new TInputSystem(app);
    EnemySystem = new TEnemySystem();
 
    // add the systems to the Entity-Component world
    pWorld->AddSystem(InputSystem);
    pWorld->AddSystem(EnemySystem, 1000, true);
    pWorld->AddSystem(PhysicsSystem);
    pWorld->AddSystem(RenderingSystem);
    pWorld->AddSystem(DamageSystem);
    pWorld->AddSystem(DeathSystem);
    pWorld->AddSystem(new TTestSystem());
 
    Esc::TEntityPtr Entity1;
    Esc::TEntityPtr Entity2;
 
    // Create the 1st entity, to be the player, and has the "Input" component
    Entity1 = pWorld->CreateEntity();
 
    // Generate all the components this entity will use (Graphics, Physical, Health, and Input)
    TGraphicsObject* graphicComp(new TGraphicsObject(Utilities::ImageGet("gfx/player.bmp")));
    TPhysicalObject* physicalComp;
    THealth* healthComp(new THealth(100, 100));
    TInput* inputComp(new TInput());
 
    // Create a Physics object
    uint32_t width, height;
    graphicComp->GetSize(width, height);
 
    cpBody *pBody = cpBodyNew(1,
                           cpMomentForCircle(1, (width+height)/4,
                                             0.0f, cpvzero));
    pBody->p = cpv(pApp->GetWidth()/2,pApp->GetHeight()/2);
    //pBody->v_limit = 200.0f;
 
    cpShape *pShape = cpCircleShapeNew(pBody, (width+height)/4, cpvzero);
 
    pShape->collision_type = 1;
    pShape->layers = 1;
 
    pShape->e = 0.0f;  // not elatics, no bouncing
    pShape->u = 0.0f; // 0.0 is frictionless
 
    cpSpaceAddBody(mpSpace, pBody);
    cpSpaceAddShape(mpSpace, pShape);
 
    physicalComp = new TPhysicalObject(mpSpace, pBody, pShape, 200.0f);
 
    // Now add all the components to the entity
    pWorld->AddComponent(Entity1, graphicComp);
    pWorld->AddComponent(Entity1, physicalComp);
    pWorld->AddComponent(Entity1, healthComp);
    pWorld->AddComponent(Entity1, inputComp);
 
    pWorld->SetGroup(Entity1, "Player");
    pWorld->AddEntity(Entity1);
 
    // Add Enemy entity, no Input component, instead an "Enemy" component
    Entity2 = pWorld->CreateEntity();
    TGraphicsObject* graphicComp2(new TGraphicsObject(Utilities::ImageGet("gfx/blueenemy.bmp")));
    TPhysicalObject* physicalComp2;
    TDamage* damageComp2(new TDamage(10));
    TEnemy* enemyComp2(new TEnemy());
 
    // Create a Physics object
    graphicComp->GetSize(width, height);
 
    cpBody *pBody2 = cpBodyNew(1,
                           cpMomentForCircle(1, (width+height)/4,
                                             0.0f, cpvzero));
    pBody2->p = cpv(400,400);
    //pBody2->v_limit = 150.0f;
 
    cpShape *pShape2 = cpCircleShapeNew(pBody2, (width+height)/4, cpvzero);
    pShape2->collision_type = 2;
    pShape2->layers = 1;
 
    pShape2->e = 0.0f;  // not elatics, no bouncing
    pShape2->u = 0.0f; // 0.0 is frictionless
 
    cpSpaceAddBody(mpSpace, pBody2);
    cpSpaceAddShape(mpSpace, pShape2);
 
    physicalComp2 = new TPhysicalObject(mpSpace, pBody2, pShape2, 150.0f);
 
    pWorld->AddComponent(Entity2, graphicComp2);
    pWorld->AddComponent(Entity2, physicalComp2);
    pWorld->AddComponent(Entity2, damageComp2);
    pWorld->AddComponent(Entity2, enemyComp2);
 
    pWorld->AddEntity(Entity2);

Later, I have an "InputSystem" which only runs on entities with the "Input" component

void TInputSystem::Update(Esc::TEntityPtr entity, uint32_t tickDelta)
{
    TPhysicalObject *physicalObject =
      static_cast<TPhysicalObject*>(entity->GetComponent("PhysicalObject"));
    const sf::Input& Input = mpApp->GetInput();
    cpFloat speed = physicalObject->GetMaxSpeed();
 
    // find location of Mouse and point player towards it
    cpFloat MouseX = (cpFloat)Input.GetMouseX();
    cpFloat MouseY = (cpFloat)Input.GetMouseY();
 
    cpFloat Angle = atan2(MouseY - physicalObject->GetBody()->p.y,
                          MouseX - physicalObject->GetBody()->p.x);
    physicalObject->GetBody()->a = Angle;
 
    if (Utilities::KeyIsDown(Input, GAME_KEY_UP)) {
        physicalObject->GetBody()->f.x = cos(Angle)*speed;
        physicalObject->GetBody()->f.y = sin(Angle)*speed;
    }
    else if (Utilities::KeyIsDown(Input, GAME_KEY_FIRE)) {
        physicalObject->GetBody()->f.x = cos(Angle)*2*speed;
        physicalObject->GetBody()->f.y = sin(Angle)*2*speed;
    }
    else {
        physicalObject->GetBody()->f = cpvzero;
    }
    if (Utilities::KeyIsDown(Input, GAME_KEY_DOWN)) {
        // Slow it down
        physicalObject->GetBody()->v.x = physicalObject->GetBody()->v.x + -physicalObject->GetBody()->v.x/16;
        physicalObject->GetBody()->v.y = physicalObject->GetBody()->v.y + -physicalObject->GetBody()->v.y/16;
    }
 
}
 
And, in my DeathSystem, if the entity with the Input dies, I switch the player and the enemy (just to exercise the component/entities)
void TDeathSystem::Update(Esc::TComponentPtr component, uint32_t tickDelta)
{
    THealth *health = static_cast<THealth*>(component);
 
    if (health->IsDead()) {
        printf("Entity Dead! Swap\n");
 
        // Let's try and swap some components, healthEntity is the old player entity, to become the new enemy
        Esc::TEntityPtr healthEntity = component->GetOwnerEntity();
 
        // Find the enemy connected to us via the Collision component
        TCollision *collisionComp = static_cast<TCollision*>(healthEntity->GetComponent("Collision"));
 
        Esc::TEntityPtrs entities;
        collisionComp->GetEntitiesColliding(entities);

        // damageEntity is the old enemy entity, will become the new player entity
        Esc::TEntityPtr damageEntity = entities[0];
 
        health->AddHealth(100);
 
        World->SetGroup(healthEntity, "Player", false);
 
        Esc::TComponentPtr healthComp = component;
        Esc::TComponentPtr inputComp = static_cast<TInput*>(healthEntity->GetComponent("Input"));
        Esc::TComponentPtr damageComp = static_cast<TDamage*>(damageEntity->GetComponent("Damage"));
        Esc::TComponentPtr enemyComp = static_cast<TEnemy*>(damageEntity->GetComponent("Enemy"));
 
        World->RemoveComponent(healthEntity, healthComp, false);
        World->RemoveComponent(healthEntity, inputComp, false);
        World->RemoveComponent(damageEntity, damageComp, false);
        World->RemoveComponent(damageEntity, enemyComp, false);
 
        World->AddComponent(damageEntity, healthComp);
        World->AddComponent(damageEntity, inputComp);
        World->AddComponent(healthEntity, damageComp);
        World->AddComponent(healthEntity, enemyComp);
 
        World->SetGroup(damageEntity, "Player", true);
 
        //World->DeleteEntity(healthEntity);
 
 
    }
}

 

This is just an example, and not really the best, but I'm just trying to show how you can create systems to act on components/entities, and add and remove components from entities.  This example, and more can be seen in my Escape System on github

Edited by BeerNutts

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!