Wednesday, November 23, 2011

Component Oriented Systems

Recently I have been doing some research on component oriented approached as an alternative to object oriented design. The problem with object oriented design is as things grow it gets harder to isolate and reuse individual parts of an object. The solution usually is to refactor your classes to make them more granular, this however takes time and is a constant struggle as you add in more features. As you gain more functionality you get a slew of classes and then when the time comes to debug it can be hard to track down at what place something went wrong. Of course all of this can be avoided by careful planning before hand but it's hardly the case where the requirements are static and there will be no changes to the game down the line.

Component oriented systems resolve this issue by separating out logic and data into different components. This makes adding new functionality and testing much easier. You can now write unit tests for a specific component to make sure they function correctly and since they work in isolation you don't have to worry about another feature coming along and breaking something. You also now have the power to add in new features to an entity (I describe an entity as a game object) without having to refactor existing code.

I've read a few articles on component oriented programming and they gave me a good idea on what one would look like but I didn't find many systems out there in the wild. I decided to create a component library which I call COP (Component Oriented Programming) to give others a framework or at least a basic implementation to mess with or give ideas for their own system. The full source for this library is available here . The library is written in C on linux and contains a makefile for building the library and a sample of how to use the library. This code should be easily portable to windows or mac, the only dependencies are libuuid and glib.

To help give you an idea of what the library is like I will run through the sample code. The sample contains two component systems, one that stores position data and another that does the physics (movement and basic collision detection). So here is our main code:


#include

#include <cop.h>
#include "positionData.h"
#include "physicsSystem.h"
#include "systemIds.h"

int main(int argv, char** argc)
{
int i = 0;
Point* p;
PhysicsInfo* pi;
COP_Entity* entities[5];


/* initialize COP */
COP_init();

/* initialize some custom systems */
positionData_init();
physicsSystem_init();

/* create 5 entities */
for(; i < 5; i++)
{
COP_Entity* ent = COP_entity_new();
entities[i] = ent;

/* add some position data */
p = (Point*)COP_system_addEntity(ent, POSITION_DATA);
p->x = i;
p->y = -i;

/* add it to our physics system */
pi = COP_system_addEntity(ent, PHYSICS_SYSTEM);
pi->radius = 1;
pi->movementVector->x = 1;
pi->movementVector->y = 1;
}

/* print out all the entities information */
for(i = 0; i < 5; i++)
{
COP_entity_print(entities[i]);
}

/* do a tick from the physics system */
physicsSystem_update();

/* print out all the entities information and see what has changed */
for(i = 0; i < 5; i++)
{
COP_entity_print(entities[i]);
}


/* shutdown COP */
COP_shutdown();

return 0;
}



First you initialize the COP library and all the component systems you create/use.


/* initialize COP */
COP_init();

/* initialize some custom systems */
positionData_init();
physicsSystem_init();


Next we add 5 entities and add a position and physics system component to them.


for(; i < i++)
{
COP_Entity* ent = COP_entity_new();
entities[i] = ent;

/* add some position data */
p = (Point*)COP_system_addEntity(ent, POSITION_DATA);
p->x = i;
p->y = -i;

/* add it to our physics system */
pi = COP_system_addEntity(ent, PHYSICS_SYSTEM);
pi->radius = 1;
pi->movementVector->x = 1;
pi->movementVector->y = 1;
}


COP_system_addEntity will add an entity to a system given the entity and the the id of the system and will return the data for that component. POSITION_DATA and PHYSICS_SYSTEM are just integers that are defined in an enum.


Next we print out all the entities. COP_entity_print will print out all the information for a given entity. Example output looks like this:

Entity: 32459875
POSITION_DATA:
Position: (0.0, 0.0)
PHYSICS_SYSTEM:
Radius: 1.0
MovementVector: (1.0, 1.0)



Next we update the "game" with a tick from the physics system with physicsSystem_update. Lastly we shutdown the COP library and exit.


This is very a simple example of how your components could look. The only thing we haven't really looked at is how a component system is implemented. Let's take a look at the PositionData system.



/* positionData.c */

#include <cop.h>
#include <stdio.h>
#include <malloc.h>
#include <glib.h>

#include "positionData.h"
#include "systemIds.h"

/*
from positionData.h for clarity
typedef struct
{
float x;
float y;
}Point;
*/

Point* point_new(float x, float y)
{
Point* p = (Point*)malloc(sizeof(Point));
p->x = x;
p->y = y;

return p;
}

void point_add(Point* a, Point* b)
{
a->x += b->x;
a->y += b->y;
}




void* positionData_addEntity(void* data, COP_Entity* entity)
{
Point* p = point_new(0, 0);
g_hash_table_insert(data, entity, p);

return p;
}


void* positionData_getEntityData(void* data, COP_Entity* entity)
{
return g_hash_table_lookup(data, (void*)entity);
}


void positionData_removeEntity(void* data, COP_Entity* entity)
{
Point* entData = (Point*)positionData_getEntityData(data, entity);
free(entData);

g_hash_table_remove(data, entity);
}


bool positionData_hasEntity(void* data, COP_Entity* entity)
{
return positionData_getEntityData(data, entity) != NULL;
}

void positionData_printEntity(void* data)
{
Point* p;
printf("\tPOSITION_DATA: ");

if(data != NULL)
{
p = (Point*)data;
printf("\n\t\tPosition: (%f, %f)", p->x, p->y);
}

printf("\n");
}



void positionData_init()
{
COP_System* system = COP_system_new(POSITION_DATA,
positionData_addEntity,
positionData_removeEntity,
positionData_hasEntity,
positionData_getEntityData,
positionData_printEntity);

system->data = g_hash_table_new((GHashFunc)COP_entity_hash, (GEqualFunc)COP_entity_equal);

COP_system_add(system);
}



This code is fairly simple and straight forward but there are a few points that are worth discussing. First let's start with positionData_init. When creating a new system it requires a few functions, a way to add an entity to the system, removing an entity, if the system has an entity, getting the data for that entity and lastly a way to print out the data for the system. A system also contains a pointer for all the data it will hold, in this case the positionData system is using a hash table between an Entity and a Point. You will notice that all the required functions use void* data as their first parameter such as in void positionData_addEntity(void* data, COP_Entity* entity), this contains the system data.

The physics system follows the same format as the position data system however it also has an update function. All the update function does is take the movementVector then test for collisions against other objects. In the real world you would want the movement vector to be generated from input or maybe a script, these two items could fit into different component systems. Anyways the basics for the component system are there, have fun with it.

No comments:

Post a Comment