Where to store container of all objects/actors/collision models/etc

Started by
14 comments, last by crancran 9 years, 9 months ago

Ok, that makes sense. I actually had my objects as separate classes initially, but then I realized that they all had a position, and all had a sprite that needed to be drawn to the screen, so I made the superclass Actor to basically be "thing that exists somewhere and should be drawn." What's the disadvantage to using inheritance in this case? If all I have in Actor is position and sprite, and my other objects that need to do their own thing inherit from that and add on? Why is having Tree and Missile be completely separate classes (that both have the common functionality of being in a location and needing to be drawn to the screen) a better solution?

Sorry if it sounds like I'm contradicting you, I just want to make sure I fully grasp the logic behind what you're saying so I don't run into similar issues later.

Advertisement

Why is having Tree and Missile be completely separate classes (that both have the common functionality of being in a location and needing to be drawn to the screen) a better solution?


Because you start coming up with crazy inheritance structures just to add some extra functionality.

Lets say you start with something which needs to be drawn; Actor { Vector2 position; Sprite* graphic; }
But now you need one to be able to have collisions in some cases; CollidableActor : public Actor { BoundingBox box; }
But not you need one to also be moveable in some cases; MoveableCollidableActor : public CollidableActor { void Move(); }
But now you need one to also be able to fire; ShootableCollidableActor : public MoveableCollidableActor { void Fire(); }
Oh, but now you need a tank; Tank : public ShootableCollidableActor { void Fire(); void Move(); /* because tanks move differently */}
Oh, but now you need a tank which also has rockets; RocketTank : public Tank { void FireRocket(); }
And now I want a Jeep with no gun; Jeep : public MoveableCollidableActor { void Move(); /* because Jeeps move differently */}
And now I want a Jeep with a gun; GunJeep : ShootableCollidableActor : public MoveableCollidableActor { void Fire(); }
And now I want a turret which doesn't move and ooops.. all my shootables are moveables...

At which point you have a ridge hierarchy and things in the wrong place.
If you try to resolve it by moving 'shoot' down and 'move' up then you end up with anything which can move must shoot, but our Jeep doesn't so that's wrong.

This is why the advice is that you should prefer composition over inheritance by default.

In this case;
Actor { Vector2 position; Sprite * graphic }
Tank { Weapon * primaryWeapon; Actor *; CollisionObject * collision; MoveLogic * movelogic; }
RocketTank : Tank { RocketWeapon * secondaryWeapon; }
Jeep { Actor *; CollisionObject * collision; MoveLogic * movelogic; }
GunJeep : Jeep { Weapon * primaryWeapon }
Turret { Actor *; Weapon *; CollisionObject *}

There is still inheritance there but they are short chains (Jeep -> GunJeep, Tank -> RocketTank, MoveLogic -> {TrackedMove, WheelMove}) and are not bound so if you wanted to make a new object like a helicopter it is easy to plug it in and give it more weapons.

Chopper { Weapon* weaponArray[4]; Actor *; CollisionObject * collision; MoveLogic * movelogic; }

So we could build a Chopper with 2 tank guns and 2 rocket guns without having to worry about their firing logic.

(There is also an added bonus that these entities don't have to do their own processing; a container somewhere could hold all active CollisionObjects and process them at once, this means nice cache bonuses for code and data).

Because you start coming up with crazy inheritance structures just to add some extra functionality.

Lets say you start with something which needs to be drawn; Actor { Vector2 position; Sprite* graphic; }
But now you need one to be able to have collisions in some cases; CollidableActor : public Actor { BoundingBox box; }
But not you need one to also be moveable in some cases; MoveableCollidableActor : public CollidableActor { void Move(); }
But now you need one to also be able to fire; ShootableCollidableActor : public MoveableCollidableActor { void Fire(); }
Oh, but now you need a tank; Tank : public ShootableCollidableActor { void Fire(); void Move(); /* because tanks move differently */}
Oh, but now you need a tank which also has rockets; RocketTank : public Tank { void FireRocket(); }
And now I want a Jeep with no gun; Jeep : public MoveableCollidableActor { void Move(); /* because Jeeps move differently */}
And now I want a Jeep with a gun; GunJeep : ShootableCollidableActor : public MoveableCollidableActor { void Fire(); }
And now I want a turret which doesn't move and ooops.. all my shootables are moveables...

At which point you have a ridge hierarchy and things in the wrong place.
If you try to resolve it by moving 'shoot' down and 'move' up then you end up with anything which can move must shoot, but our Jeep doesn't so that's wrong.

This is why the advice is that you should prefer composition over inheritance by default.

In this case;
Actor { Vector2 position; Sprite * graphic }
Tank { Weapon * primaryWeapon; Actor *; CollisionObject * collision; MoveLogic * movelogic; }
RocketTank : Tank { RocketWeapon * secondaryWeapon; }
Jeep { Actor *; CollisionObject * collision; MoveLogic * movelogic; }
GunJeep : Jeep { Weapon * primaryWeapon }
Turret { Actor *; Weapon *; CollisionObject *}

There is still inheritance there but they are short chains (Jeep -> GunJeep, Tank -> RocketTank, MoveLogic -> {TrackedMove, WheelMove}) and are not bound so if you wanted to make a new object like a helicopter it is easy to plug it in and give it more weapons.

Chopper { Weapon* weaponArray[4]; Actor *; CollisionObject * collision; MoveLogic * movelogic; }

So we could build a Chopper with 2 tank guns and 2 rocket guns without having to worry about their firing logic.

(There is also an added bonus that these entities don't have to do their own processing; a container somewhere could hold all active CollisionObjects and process them at once, this means nice cache bonuses for code and data).

Okay, yeah doing everything with inheritance is really messy. However, I'm still not sure how to solve some issues with composition:

1) Who gets what data? My actor needs a position to display on the screen, but so do my CollisionObject and MoveLogic. Do I just give them each a position member and then update all of them whenever any of them change? If I modify them through the unit class that's holding all of them, it seems easy enough to pass the data along, but what happens when my CollisionObject detects a collision and wants to move? I now have to get that information back to my Unit object or? I'm not really sure.

EDIT: My most important question: How does my position get from my MoveLogic to my CollisionObject? My gameloop at the moment is something like this:

  1. Process input from user. Pass commands to actors and let them delegate the responsibility.
  2. Loop through all logic objects (MoveLogic, Collision), and let them do their logic.
  3. Loop through Actor objects and let them update themselves based on their logic objects (grab new position from MoveLogic, etc.)
  4. Get render data from Actors, draw to window
  5. repeat.

2) How do I store them in containers? Without a common superclass, do I need a container for each type of object? With the actor superclass, it was easy:


for( auto actor: actorContainer )
{
    doSomethingToActor( actor );
}

Do I just never actually talk to the Tank, Jeep, Chopper, etc classes directly? So I have a container of Actors, and container of MoveLogics, a container of CollisionObjects, etc. and I just iterate through those regardless of who actually owns them? In that case, again how do I synchronize data between the Actor, MoveLogic, CollisionObjects, and any other members that hold similar data?

EDIT:

As a potential solution to my question about containers, is it ever a good idea to implement some pure virtual classes to function like Java interfaces? Something like:


class MovingObject
{
    public:
    virtual void move( Vec2 move ) = 0;
}

class AttackingObject
{
    public:
    virtual void attack( Actor* target ) = 0;
}


class Jeep: public MovingObject
{
    ...
}

class Tank: public MovingObject, public AttackingObject
{
    ...
}

That way I could refer to the objects themselves in containers, while allowing myself to implement the actual logic behind-the-scenes using composition, which also allows me to keep a container of the CollisionModel, MoveLogic, etc. objects to loop through.

Okay, yeah doing everything with inheritance is really messy. However, I'm still not sure how to solve some issues with composition:

1) Who gets what data? My actor needs a position to display on the screen, but so do my CollisionObject and MoveLogic. Do I just give them each a position member and then update all of them whenever any of them change? If I modify them through the unit class that's holding all of them, it seems easy enough to pass the data along, but what happens when my CollisionObject detects a collision and wants to move? I now have to get that information back to my Unit object or? I'm not really sure.

EDIT: My most important question: How does my position get from my MoveLogic to my CollisionObject? My gameloop at the moment is something like this:


  • Process input from user. Pass commands to actors and let them delegate the responsibility.
  • Loop through all logic objects (MoveLogic, Collision), and let them do their logic.
  • Loop through Actor objects and let them update themselves based on their logic objects (grab new position from MoveLogic, etc.)
  • Get render data from Actors, draw to window
  • repeat.

Have a data structure called PositionInfo, which is given to any object that needs a position in the game world. The MoveLogic modifies this data, the CollisionLogic modifies this data during the collision resolve step, the RenderLogic reads this data to draw the object at the right location.

Have a data structure called CollisionInfo, which is given to any object that can collide with other stuff in the game world. The CollisionLogic reads it and determines if it's colliding with any other CollisionInfo structure.

Have a data structure called RenderInfo, which contains any data required to draw the object (mesh, sprite, etc). The RenderLogic reads from this data, and draw its contents at the location indicated by the PositionInfo data.

Similar with data structures AnimationInfo, LocomotionInfo, InputInfo, HealthInfo, WeaponInfo, etc etc for any other unit data information.

Create a class called Tree, which composes the CollisionInfo, RenderInfo and PositionInfo. The RenderLogic requires PositionInfo and RenderInfo in order to do its job. When a Tree is created, the creation logic will register the Trees' PositionInfo and RenderInfo to the RenderLogic, so on each frame the RenderLogic will loop over all registered data, read from them and do its job. The CollisionInfo and PositionInfo are registered to the CollisionLogic so dynamic objects don't pass through the tree.

Create a class called PlayerCharacter, which composes PositionInfo, CollisionInfo, RenderInfo, AnimationInfo, InputInfo, HealthInfo, LocomotionInfo, ......., and on creation each of these data members are registered to any Logic that you want to operate on your PlayerCharacter. The Logic operations are key here, as they can only operate on objects that have certain combinations of data structures. For instance, you can't register the Tree to the MoveLogic because it doesn't have a LocomotionInfo, even tho it has a PositionInfo.

When you want to create a Tree, ask the TreeFactory to create one (which should also automatically register the Tree data structs to the neccesary Logic). The Tree still owns the data, but the Logic has pointers to this data (and ofcourse, different caching techniques are possible here, but this is a KISS method which should be preferred until you prove it's not good enough from profiling). When you determine the Tree should be destroyed, ask the TreeFactory to destroy it, which will also unregister the data from the Logic.

Your example of finding a collision and wanting to move should be handled by the CollisionLogic entirely. The collision resolve step doesn't modify the position of the object that much, only so it's not in a collision anymore. But your MoveLogic will modify the position with regards to the LocomotionInfo, because the object might have certain acceleration, velocity, forces, friction, dampers etc etc in effect. First find ALL collisions for all pairs of CollisionInfo+PositionInfo, and when you find them all, resolve them one at a time (again, look at Box2D or Bullet).

Your game loop is then modified:

  1. InputLogic()
  2. MoveLogic()
  3. CollisionLogic()
  4. AnimationLogic()
  5. HealthLogic()
  6. WeaponLogic()
  7. ...
  8. RenderLogic()
  9. repeat

2) How do I store them in containers? Without a common superclass, do I need a container for each type of object? With the actor superclass, it was easy:


for( auto actor: actorContainer )
{
    doSomethingToActor( actor );
}
Do I just never actually talk to the Tank, Jeep, Chopper, etc classes directly? So I have a container of Actors, and container of MoveLogics, a container of CollisionObjects, etc. and I just iterate through those regardless of who actually owns them? In that case, again how do I synchronize data between the Actor, MoveLogic, CollisionObjects, and any other members that hold similar data?

EDIT:
As a potential solution to my question about containers, is it ever a good idea to implement some pure virtual classes to function like Java interfaces? Something like:

That way I could refer to the objects themselves in containers, while allowing myself to implement the actual logic behind-the-scenes using composition, which also allows me to keep a container of the CollisionModel, MoveLogic, etc. objects to loop through.


You don't need to sync data since all of the Logic operations have the same pointer the the one Info of a certain object they're all operating on.

Also, if you do a split of data vs logic (the Info vs Logic in my example above), you don't need the virtual interfaces. Logic operates on Info, to which it has a pointer to, and you don't have to cast them in any direction. Simply dereference, read/write and move on.

devstropo.blogspot.com - Random stuff about my gamedev hobby


Do I just never actually talk to the Tank, Jeep, Chopper, etc classes directly? So I have a container of Actors, and container of MoveLogics, a container of CollisionObjects, etc. and I just iterate through those regardless of who actually owns them? In that case, again how do I synchronize data between the Actor, MoveLogic, CollisionObjects, and any other members that hold similar data?

There is no single right way. So the following is just one possibility...

Remember the mention of sub-systems above. That are places where some logic is implemented. It leads away from the object centric view to a task centric view, so to say.

For example: In an object centric view of things, when a collision between objects A and B is given, asking object A for colliders returns B, and asking object B for colliders returns A. That looks like 2 collisions, but is actually only one. In a task centric view of things, a sub-system is asked for collisions, and it returns { (A,B) }.

This could be done with a free function. However, wrapping the task into a class allows to associate management structures that are especially suitable for the task. In the case of collision detection it is known that only dynamic objects can collide. So, if the sub-system has two separate lists, one for dynamic objects and one for static objects, and objects that do not participate on collision (e.g. bird's of a flock) are not represented at all, then processing of collision detection can be done in a more efficient manner.

The fact that a game object is to be considered by a particular sub-system is encoded by components. Components define the how and why a game object participates.

This is the fundamental idea. It need to be fleshed for implementation, of course.

Components like CollisionVolume or Placement are data components. They have a value but no logic. Several sub-systems can refer to the same data component. E.g. the Placement component is used by all sub-systems that need the position and/or orientation of the game object. Other components may define logic, for example as extension to sub-systems, self altering data component values. This way means that for example Placement is needed exactly one for every game object that needs to be placed in the world. Synchronization is done by processing the sub-systems in order, i.e. choosing a suitable order of update() calls inside the game loop.

It is possible to duplicate components in several sub-systems, too (although I do not recommend this in general). There is no principle problem in copying component values as soon as logically previous sub-system runs are completed (which is automatically guaranteed due to the order in the game loop).

2) How do I store them in containers? Without a common superclass, do I need a container for each type of object? With the actor superclass, it was easy:

You are not likely going to be iterating game objects in the loop but rather their components which make up their full behavior. Because of this, you will want to store your component structures in some cache efficient way. As a first pass, a std::vector would suffice.

The next question is where to place these vectors?

I have seen some recommendations where the systems who transform the data own the vectors, but I tend to disagree with this approach. I don't believe any one system owns this information. Systems are designed to read/write data, transforming it along the way as a serial set of inputs & outputs; nothing more. That's not to say these systems won't have their own internal data structures to aid them in their task, but the "input" data from the components feels more like public knowledge information.

I prefer the idea that my game object system owns a series of object pools, each of a specific type of class. These pools are dynamically created either through code when the engine initializes (core components or those extended by user code) or via scripts. The transformation subsystems that operate on my components have a central place they interact with to obtain not only information about a game object but any of the components which make up it's total behavior (aka the game object system).

The beauty is you can accomplish this without any Actor class or a base Component class. That's not to say one shouldn't have them because there are instances where having wrapper classes can simplify an API making it less brittle and easier to alter through code iterations. But none of what I described implies their necessity at all.

This topic is closed to new replies.

Advertisement