DOD and memory layout

Started by
31 comments, last by Yuukan 12 years, 2 months ago
This is a very basic example. Is this data oriented? It is already faster than a traditionnal oo approach but I have to call the processing function for each set of entities.[/quote]
It's the basic idea. But instead of clumping everything, just use what you need. Position is useful for many things, but it might not be needed for rendering. So be prepared to change what belongs together.


Orcs allOrcs = Orcs();[/quote]

Think beyond. There is no need to have just orcs. Anything that is subject to movement or physics goes into here. Or perhaps everything that has a position in the world. Only thing all these have in common is position. Or vice versa - if it has position, it's in that array.

There might be more if it's convenient, but position of an orc is no different than that of an elf or a cannon ball. Even when combined with speed, physics are the same for all. What it then comes down to is speed changes, perhaps the acceleration values. Then above might be extended as something like this, in simplest form:
Vector2D * positions;
Vector2D * speeds;
Vector2D * acceleration;
float * mass;

To make orcs move slower, they might have their mass value higher than something like an elf.

And later, things can be clumped together:struct PhysicsProperties {
float mass, width, strength;
}
...
PhysicsProperties * props;
Here, instead of having individual properties per item, one links to a shared template.

Traditional design would instead be something like this:class Orc : .... {
Vector2D position;
float mass, weight, strength;
Vector2D acceleration;
Vector2D ....
...
}
More familiar, but it forces redundant data to be passed all over - renderer does not need physics properties, for example.
Advertisement

Like those bitsquid slides say, the method behind DOD is "isolate tasks, find data objects". In this case, "update position" is the task and position and speed are the data that the task needs. Hence they should be together - they are one data object.


Thanks for the answers. So I must do something like this :

struct Position {
int posx;
int posy;
};
struct Orcs {
Position pos[MAX_ORCS];
int speed[MAX_ORCS];
};

Or maybe this way:

template <unsigned int COUNT>
struct Positions {
int posx[COUNT];
int posy[COUNT];
};
struct Orcs {
Positions<MAX_ORCS> pos;
...
};

?

And my task become:

void ComputePosition(Positions* pos, int* speed, unsigned int count);


And then try to merge all properties needed by the same tasks? The Entity will be just an unsigned int (index in the array for speed, positions and so on) or am I wrong?
Yuukan, I think you have it close.
Your method matches a valid example http://stackoverflow.com/questions/1641580/what-is-data-oriented-design

How you structure the data will depend on what you're doing, and turch does have a point since X and Y are apart of the Position it would be good to keep them together.

You may also want to consider an additional layer of abstraction because if you approach it with what you have you'd end up with a struct for orcs, elves and anything else that would be nearly identical.
How you structure the data will depend on what you're doing[/quote]

Exactly. For the orc example, if all that orcs have to do is update their position with by their speed, the data structure that would make most sense is

struct orc
{
int posx;
int posy;
int speed;
};

struct all_orcs
{
orc[MAX_ORCS]
};


when compiled, the memory acces pattern for the update will look something like this

for (int* ptr; ptr < MAX_ORCS * sizeof(orc); ptr += sizeof(orc)
{
&ptr = &ptr + &(ptr + 2 * sizeof(int)); // first int (posx) = first int + 3rd int (speed)
&(ptr + sizeof(int)) = &(ptr + sizeof(int)) + &(ptr + 2 * sizeof(int)); // second int (posy) = second int + 3rd int
}


which is accessing memory in a very straightforward manner.

However, it is very unlikely that that is all that you want the orcs to be able to do, so you'll have to take into account other operations you want to do on the data.

And as medv4380 said, you probably want to abstract that away behind some sort of component / entity model.
So I was right when I said that my "ComputePosition" should be called for every sets of entities?

About the abstraction layer, a PositionProperties struct might be a good idea and all set of entities that need a position will have an array of that struct. Like this:

[attachment=6878:TestDODES.png]

I don't know how I can merge all Positions into one array and if it's really a great idea, may be the picture above is sufficient.
So I was right when I said that my "ComputePosition" should be called for every sets of entities?[/quote]

Yes, the whole point of DOD is that if you have an operation that you have to perform on all of a certain set of data, you should make the data cache friendly.

I don't know how I can merge all Positions into one array and if it's really a great idea, may be the picture above is sufficient.[/quote]

That goes back to what I was saying on the first page:

Think of "Position" as a component. An entity (orc, elf, etc) is composed of components (but each one may need different components). For each type of component, you have a <component>Manager. At init, each manager allocates a block of memory big enough for, say 2000 components, as well as some sort of hashed data structure with an ID as the key and the pointer to the component as the value. When you need to create a new entity, go to the manager of each component that the entity needs, and call AllocateNewComponent(unsigned int entityID).

The manager creates a new component in the next free slot and adds the id to the hash table. If there isnt a free block available, the manager allocates a new large block. When something needs a component from a certain entity, it calls <component>Manager.getComponent(ID), which looks it up in the hash table and returns the pointer to the component.

Again, the point of this is making uniform access cache friendly and thus really fast, at the cost of a level of indirection when getting data for any single entity (by having to go through a hash table).
Ok so then, how would you write the ComputePosition function? What kind of loop?
I'd look at it as a velocity / position update.


struct PhysicsComponent
{
Vector2 position;
Vector2 velocity
};

class PhysicsComponentManager
{
PhysicsComponent* createComponent(unsigned int id);
PhysicsComponent* getComponent(unsigned int id);

void updateComponents(float deltaTime)
{
for each componentBlock in allBlocks
{
for each component in componentBlock
{
component.position += deltaTime * component.velocity;
}
}

List<componentBlock> allBlocks;
};
What do you mean by componentBlock?
Sorry, that goes back to what I was saying on the first page.

A componentBlock is a chunk of preallocated memory that holds components. When a componentBlock runs out of room for components, a new one is allocated. So a component manager will have one or more componentBlocks, each of which will have many components.

This topic is closed to new replies.

Advertisement