• Advertisement

C++ base class for every game object : bad practice?

Recommended Posts

In my Entity-Component-System, every entity has Com_OwnerShip component.

Com_OwnerShip = can own [0,∞) entity / can be owned by [0,1] entity

For example :-

  • a rocket OWN 1 physic object  + 3 graphic object
  • a rope OWN 10 physic object  + 10 graphic object
  • a physic object OWN 1 Bullet(R) physic internal object
  • a game chunk OWN 16*16*16 blocks

Here is a way to implement it (can be optimized further):-

class Com_OwnerShip{
    std::vector<Com_OwnerShip*> ownees;
    Com_OwnerShip* owners;
};

At the end of every time step, I list every owner that was marked as to be destroyed, then mark all corresponding ownee entities as to be destroyed.   The process is propagative.     Then, I delete all "to be destroyed" entity in a batch-style.

I have used it in a few prototypes and always find that it is very convenient.

However, I feel that it is probably a bad practice. 

  • It probably share the disadvantage of Base_Game_object in OOP. (?)
  • It costs a little more memory, not modular, too wildcard, too "duck-typing".
  • It is a dirty-design (?).
  • Nonetheless, it is better than Strong-Pointer (std::unique_ptr) because I can control entity/component deletion very easily.

Is it a good practice?     What are alternative approaches?   Am I too nervous?

 

Edited by hyyou

Share this post


Link to post
Share on other sites
Advertisement

Unless there's more complexity than what you're describing, I'm not sure why you dont just use smart pointers and let those deal with your object lifetime management (std::shared_ptr, std::weak_ptr).  It would be easier and less bug-prone than what you're doing now.

Also, what's the point of having a component that has to be in every single entity?  Why not just put that functionality into the entity?

Share this post


Link to post
Share on other sites
38 minutes ago, 0r0d said:

Unless there's more complexity than what you're describing, I'm not sure why you dont just use smart pointers and let those deal with your object lifetime management (std::shared_ptr, std::weak_ptr).  It would be easier and less bug-prone than what you're doing now.

Also, what's the point of having a component that has to be in every single entity?  Why not just put that functionality into the entity?

Thank 0r0d.   I agree std::shared_ptr and std::weak_ptr are less error-prone.

I didn't push the functionality to the entity because I am still too scared by this idea.

There are a few cases that delete-flagging is more useful.

First Reason : safer callback

Suppose that :-

  • A rocket E1 owns a physic body E2.
  • The physic body E2 owns a Bullet physic internal entity E3.

In a std::unique_ptr-style : a user delete a rocket -> E1 E2 and E3 will be deleted immediately (in reverse order).  

After E3 is deleted, the Bullet physic engine will still has a dangling pointer to the physic body (btRigidBody).   It is quite hard to workaround this issue.  Collision callback (collision-end) will be a mess.

2nd Reason : more multi-threading friendly

By using flag, every entity is ensured to exist until the end of time-step.   Component iterator is easier to implement and require less mutex lock.

For me, such assumption make various parts of gameplay easier to code.

3rd Reason : cache friendly (not sure)

With flagging, deleting is executed as batch.

 

Edited by hyyou

Share this post


Link to post
Share on other sites
1 hour ago, hyyou said:

In a std::unique_ptr-style : a user delete a rocket -> E1 E2 and E3 will be deleted immediately (in reverse order).  

After E3 is deleted, the Bullet physic engine will still has a dangling pointer to the physic body (btRigidBody).   It is quite hard to workaround this issue.  Collision callback (collision-end) will be a mess.

If you end up with a dangling pointer because two objects share ownership of that pointer, you have maybe chosen the wrong smart pointer semantics to start with.

Short answer to your original question: if it works and lets you finish the game, it's an acceptable design.

Edited by Bregma

Share this post


Link to post
Share on other sites

If you want to use C++, then write C++ code:

struct Rocket
{
  Ptr<RigidBody> physics;
  Ptr<Model> rocketBody;
  Ptr<Model> rocketFlame;
  Ptr<Model> rocketExplosion;
};
struct Rope
{
  Ptr<RigidBody> physics[10];
  Ptr<Model> graphics[10];
};
etc...

If you want fully dynamic duck typing, then write your code in Python, Lua, etc instead, where the language supports that kind of thing naively... Don't try to recreate these language features yourself by abusing C++ and OOP :P 

Share this post


Link to post
Share on other sites
9 hours ago, hyyou said:

Thank 0r0d.   I agree std::shared_ptr and std::weak_ptr are less error-prone.

I didn't push the functionality to the entity because I am still too scared by this idea.

There are a few cases that delete-flagging is more useful.

First Reason : safer callback

Suppose that :-

  • A rocket E1 owns a physic body E2.
  • The physic body E2 owns a Bullet physic internal entity E3.

In a std::unique_ptr-style : a user delete a rocket -> E1 E2 and E3 will be deleted immediately (in reverse order).  

After E3 is deleted, the Bullet physic engine will still has a dangling pointer to the physic body (btRigidBody).   It is quite hard to workaround this issue.  Collision callback (collision-end) will be a mess.

2nd Reason : more multi-threading friendly

By using flag, every entity is ensured to exist until the end of time-step.   Component iterator is easier to implement and require less mutex lock.

For me, such assumption make various parts of gameplay easier to code.

3rd Reason : cache friendly (not sure)

With flagging, deleting is executed as batch.

 

I would be much more afraid of using raw pointers.  If you do end up with dangling smart pointers it's because your ownership was not properly defined.  I would use std::shared_ptr or std::unique_ptr (whichever is suited for your use case) for the ownership and then only store std::weak_ptr for anything that doesnt explicitly own an object/component.  Then for temporary pointers you use std::shared_ptr or raw pointers, again depending on your use cases.  If you're clear about who owns what and dont store std::shared_ptr's to things that you dont own, you should be fine.

As far as the MT requirements, that would certainly qualify as "more complexity" than what you originally talked about.  However, we're still talking about ownership.  If you use std pointers then you have to take ownership seriously.  It's something you have to put thought into, you cant just use std::shared_ptr everywhere (for example), just like you cant just use raw pointers everywhere and expect to be free from problems.   Using raw pointers only makes the situation worse. What if you forget to tag some component somewhere as "to delete"?  This is what really creates the mess, and it's why you need to tag and delete things all at the end... because that's the only way to try to clean up the mess.

Lastly, deleting in batches will probably not help you with cache performance.  Those objects/components will still be scattered all over in memory, so I doubt there will be any cache benefits.

Share this post


Link to post
Share on other sites
13 hours ago, 0r0d said:

However, we're still talking about ownership.  If you use std pointers then you have to take ownership seriously.  It's something you have to put thought into, you cant just use std::shared_ptr everywhere (for example), just like you cant just use raw pointers everywhere and expect to be free from problems.  

It's not like you have the option to not take object ownership seriously. Whatever you do, your objects will need to be allocated before use, kept live with valid pointers while they are in use, and deleted or recycled after use, with the risk of errors resulting in dereferencing null or invalid pointers or prematurely destroying data.

You need to be sure that your smart pointers do what you want, with the same standards and the same complexity as manual memory management. You aren't doing less memory management only because it's hidden away in a class; explicitly deleting pointers only means that your needs aren't a good fit for the available smart pointers, object pools etc.

Share this post


Link to post
Share on other sites

The various types of smart pointers (and raw pointers) not only exist to make your ownership semantics clear and self-documenting, but also to make it difficult to disobey those semantics.

Raw pointers convey a lack of ownership. Using them any other way only acts to obfuscate the true ownership semantics of your code and makes it more error-prone and harder to reason about.

Case in point:

On 10/2/2017 at 1:02 AM, hyyou said:

Here is a way to implement it (can be optimized further):-


class Com_OwnerShip{
    std::vector<Com_OwnerShip*> ownees;
    Com_OwnerShip* owners;
};

There is a very serious inconsistency here. On the one hand, the single 'owners' field conveys unique ownership. On the other hand, what's stopping two entities from having a third shared entity in both of their 'ownees' vectors, i.e. shared ownership? What goes in the 'owners' field of that third entity? What happens if an entity has multiple owners, and one of those owners is destroyed? How do you handle ownership cycles?

What ownership semantics do you want? Start there and then pick the right tool for the job. Working backwards will only set yourself up for major problems later.

Share this post


Link to post
Share on other sites

Thank you everyone for many useful advises. xDxD

On 10/2/2017 at 5:29 PM, Bregma said:

Short answer to your original question: if it works and lets you finish the game, it's an acceptable design.

Thank.  It is my "default" mindset.  xD

On 10/2/2017 at 5:42 PM, Hodgman said:

struct Rocket { Ptr<RigidBody> physics;... };

I had implemented like this.  It is very easy to understand and debug.

However, in some rare cases (10% for me), the deletion is required to be delayed.   

For Bullet physics engine, it is the best to invoke collision callback in a batch manner.  

- update game logic
--- MARK delete a rocket
- propagate the MARK to physic object
- update physic
--- collision callback 
   -> discover that the manifold between ...
       ... physic object p1-vs-p2 is no longer exist
   -> invoke Physic::collisionEnd(p1->userData,p2->userData)
- delete every thing that was MARK-ed

I believe that the correct approach is to delay to deletion of everything.  Otherwise, the p1 can be a dangling pointer.  It is also very handy to know for sure that p1->userData (which is Rocket instance) is not a dangling pointer.

Thus, I adopt the "delay deletion" practice into everything.

An alternative simpler approach is to let a system that handle rocket 1.broadcast and 2.invoke object deletion manually (like your example), but  I prefer a more automatic way.

I am not sure how game engine should support ownership relation that need the delay. I may think too much. 

14 hours ago, Zipster said:

On the other hand, what's stopping two entities from having a third shared entity in both of their 'ownees' vectors, i.e. shared ownership?   ... ... What ownership semantics do you want?

The question makes me reconsider about many things.  Thank.

I am using semantic like this.  It has run-time assertion.  I am OK about it, but not sure if it is good :-

relation_ownership->add(rocket, physicTriangle);
relation_ownership->add(rocket, graphicWing);
relation_ownership->add(rocket, barrier);
relation_ownership->add(barrier, graphicLightSphere);

I try to avoid share_pointer, because it is quite notorious - hard to debug / circle reference / slow.   I also don't need it because I know the clear ownership.

 

 

Share this post


Link to post
Share on other sites
11 hours ago, hyyou said:

I had implemented like this.  It is very easy to understand and debug.

However, in some rare cases (10% for me), the deletion is required to be delayed.   

For Bullet physics engine, it is the best to invoke collision callback in a batch manner.  


- update game logic
--- MARK delete a rocket
- propagate the MARK to physic object
- update physic
--- collision callback 
   -> discover that the manifold between ...
       ... physic object p1-vs-p2 is no longer exist
   -> invoke Physic::collisionEnd(p1->userData,p2->userData)
- delete every thing that was MARK-ed

I believe that the correct approach is to delay to deletion of everything.  Otherwise, the p1 can be a dangling pointer.  It is also very handy to know for sure that p1->userData (which is Rocket instance) is not a dangling pointer.

Thus, I adopt the "delay deletion" practice into everything.

An alternative simpler approach is to let a system that handle rocket 1.broadcast and 2.invoke object deletion manually (like your example), but  I prefer a more automatic way.

I am not sure how game engine should support ownership relation that need the delay. I may think too much. 

You could also have the physics engine own the user data, and allow the game objects to access it. That way the object could be deleted immediately and the 'delete me' flag could be in the user data. I'm sure there's also a way to allow the physics engine to clean up the object immediately, but I'm not familiar with Bullet so I can't comment on any specific limitations or best practices.

But the point is that there's often a way around deferred deletion and other tricks... if you rethink the data ownership a bit :)

11 hours ago, hyyou said:

I am using semantic like this.  It has run-time assertion.  I am OK about it, but not sure if it is good :-


relation_ownership->add(rocket, physicTriangle);
relation_ownership->add(rocket, graphicWing);
relation_ownership->add(rocket, barrier);
relation_ownership->add(barrier, graphicLightSphere);

I try to avoid share_pointer, because it is quite notorious - hard to debug / circle reference / slow.   I also don't need it because I know the clear ownership.

From what you've described, unique ownership should be sufficient. Shared ownership isn't required as often as one might think.

Share this post


Link to post
Share on other sites
On 10/8/2017 at 4:31 AM, Zipster said:

You could also have the physics engine own the user data, and allow the game objects to access it. That way the object could be deleted immediately and the 'delete me' flag could be in the user data.

I never thought of this approach. 

Yes, Bullet doesn't support it, but it is possible and not too hard.  Thank! 

Share this post


Link to post
Share on other sites

Looking at stuff like "design by contract" and Objective-C's old way of garbage collection, ownership and object referencing can (under specific assumptions) also be implemented based on assurance: Objects are instantiated and destroyed only at well defined moments and a guaranteed to exists in-between. E.g. objects of the game scope are instantiated when the gameplay starts and are available until the gameplay ends. Similarly objects of the level scope are handled. Proxying can be used to change underlying aspects without invalidating references. Using scopes this way allows to "garbage collect" parts of the used memory in large blocks.

Perhaps the most dynamic scope is on the entity level. My solution works as follows: The components of entities are owned by the respective sub-systems. The existence of components is guaranteed only during a timespan of the game's runloop. I.e. instantiation and deletion is done at a specific point per run through the loop, corresponding of what is mentioned in the OP. In particular, the EntityServices sub-system manages lifetime of entities. It holds collections of (a) entities that get destroyed during the current timespan, (b) entities that were destroyed during the previous timespan, (c) entities that get created during the current timespan, and (d) entities that were created during the previous timespan. So other sub-systems can investigate collections (b) and (d) and react accordingly by updating their components storage, and they can alter collections (a) and (c) to request the deletion or creation of entities on the next turn. Obviously, the dependency of sub-systems needs to be obeyed, but that is required by the update sequence of sub-systems within the loop anyway.

 

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now


  • Advertisement
  • Advertisement
  • Popular Now

  • Advertisement
  • Similar Content

    • By Gabriel Gonzalez
      Hi everyone, my name is Gabriel and this is my first post here. I graduated from college over three years ago with a degree in Physics and now I want to start a career as a gameplay programmer. Besides a single C++ programming class in college I have not had any prior experience programming. What I have done to learn until now is to use SFML to recreate Arkanoid and Space Invaders. My question is, am I on the right track if I just continue creating games from scratch using libraries such as SFML or would I benefit more if I move on to using an engine such as Unreal or Unity? Also, of how much help (if any) would my degree be when trying to join a team? I live in San Diego, CA if that matters at all.   I do appreciate in advance any guidance anyone could offer me.
    • By Alexander Winter
       
        Jumpaï is a game about creating platformer levels and playing them online with everyone. Will you become the most popular level maker or will you be a speedrunner holding world records on everyone's levels? More into casual play? No problems! You can happily play through the giant level database or chill at people's hub. Meet new people, make new friends, learn to master the game by asking pros or ask for people's favorite tricks on level making.  





      Unlike other games of its genre, Jumpaï is about playing levels with everyone in real time. You have the fun to see how other people are playing and get to realize you are not the only one failing that jump!

      The game is currently into development and still have lots to do. I am looking for people willing to help how they can. Developer? Graphist? Play tester? Sound manager? Game designer? I'm welcoming everyone. The project is so big I have a lot of work to do in all areas. Server backend, UI/UX, Game networking, Gameplay and even the website some day. As you can see from the default buttons, the game has been made with LibGDX. 

      If you plan to take an important role into the development of the game, we will discuss how you will get paid once the game generates money. Note that I'm not working on the game full-time. I'm studying full-time and working on it is a hobby. It's been 14 months since it started.

      So, are you interested? If so join me on my discord https://discord.gg/dwRTNCG and I'll answer all your questions.

      Additionnal screenshots:
       



    • By Ahrakeen
      Hello My team is working on the game neo arcana a logical crisis.
       
      We are presently looking for coders to help with the user interface for it. as well as a working combat system
      the aim is to have a system where cards represent the skills and abillities of a character. 
      each character wille have core cards that is the make up of what sort of character that is.
      then there will be other skills and abillities that can be shared among the party.
       
      the game itselfls is a strategic roleplayed game turn based. set in an alternate earth where magic is real but suppressed from the common people.
      And we want to use a combat system where a character gets one move and one action per turn.
       
      We are presently working with unreal but if you as a coder have a better idea we are at this stage able to switch engine
      So if interested in this project let me know
    • By MidnightFoxGaming
      Hi, I’m starting out in game development as a whole and while I have some experience with programming in the past with small things like mods and a single byond game that I took over for a previous person I’m rather new to the start from the ground up thing.
      im looking to get some advice on where to start for tools/material/resources for such. I already have the core concept for the game hashed out, this has been a relitivly solo thing so far so any help is appreciated thank you.
  • Advertisement