The saying goes something like this:
"Program to an interface/abstraction, not to an implementation".
We all know interfaces as a means of decoupling in object oriented programming. Like a contract that some object fulfills.
But something I cant wrap my head around is:
How do I program to an interface/abstraction in data oriented design?
Like in call some "Drawable" but I dont now if its a Rectangle or a Circle, but it implements the interface "Drawable".
Thanks
This is a great question. I believe what you are asking is how do you achieve polymorphism with Data Oriented Design(DOD)?
Short Answer: You don't do it with interfaces. That's an Object Oriented Programming (OOP) way of achieving polymorphism. In DOD, polymophism can be achieved with the Entity Component System (ECS) pattern.
Long Answer (with examples):
Here is an example of polymorphism in OOP:
public interface Drawable
{
void Draw();
}
public class Circle: Drawable
{
public float posX, posY;
public float radius;
public void Draw() { /* Draw Circle */ }
}
public class Rectangle: Drawable
{
public float posX, posY;
public float width, height;
public void Draw() { /* Draw Rectangle */ }
}
And here is how you achieve polymorphism with DOD and ECS (psuedo code):
public struct Position { public float x, y; }
public struct Circle { public float radius; }
public struct Rectangle { public float width, height; }
public class DrawCirlceSystem
{
public void OnUpdate()
{
ComponentQuery
.SelectReadOnly(typeof(Position), typeof(Circle))
.ForEachEntity((Entity entity, Position position, Circle circle) => {
/* Draw Circle */
});
}
}
public class DrawRectangleSystem
{
public void OnUpdate()
{
ComponentQuery
.SelectReadOnly(typeof(Position), typeof(Rectangle))
.ForEachEntity((Entity entity, Position position, Rectangle rectangle) => {
/* Draw Rectangle */
});
}
}
So if you had the following data layout:
Entity 1: [Position, Circle]
Entity 2: [Position, Circle]
Entity 3: [Position, Rectangle]
DrawCircleSystem
will only execute over entities 1 and 2, while DrawRectangleSystem
will only execute over entity 3. Thus, polymorphism is achieved through the queryability of these systems.
Programming in this way is much more performant then OOP. But beyond that, it also makes our code more scalable and optimizable. For example, if you wanted to implement culling so only the entities that are within view are actually rendered, we can easily do that with very little refactor effort. All we need to do is introduce a new system that handles the culling by adding or removing a new component called Visible
to entities we want to draw:
public struct Visible { }
public class CircleCullingSystem
{
public void OnUpdate()
{
// Give me all Circle entities that are NOT Visible
ComponentQuery
.SelectReadOnly(typeof(Position), typeof(Ciricle))
.Exclude(typeof(Visible))
.ForEachEntity((Entity entity, Position position, Circle circle) => {
// Add 'Visible' component to entity if it's within view range
});
// Give me all Circle entities that are Visible
ComponentQuery
.SelectReadOnly(typeof(Position), typeof(Ciricle))
.FilterBy(typeof(Visible))
.ForEachEntity((Entity entity, Position position, Circle circle) => {
// Remove 'Visible' component from entity if it's out of view range
});
}
}
And then we just update our query in the DrawCirlceSystem
so that it filters by the Visible
component:
public class DrawCirlceSystem
{
public void OnUpdate()
{
// Process all visible circle entities
ComponentQuery
.SelectReadOnly(typeof(Position), typeof(Circle))
.FilterBy(typeof(Visible))
.ForEachEntity((Entity entity, Position position, Circle circle) => {
/* Draw Circle */
});
}
}
And of course we would need to create a RectangleCullingSystem
similar to our CircleCullingSystem
since the culling behavior of rectangles is different from circles.