hyyou

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

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


  • Forum Statistics

    • Total Topics
      628277
    • Total Posts
      2981770
  • Similar Content

    • By ForgedInteractive


      Who We Are
      We are Forged Interactive, a small team of like-minded game developers with the sole purpose of making games we love! We're a team of artists, animators, programmers, level designers, writers, composers, producers, and other creative minds. We want to make games that you, the modern gamer want to play! We hope to build a community that enjoys our games as much as we love creating them. With your feedback and support we will be able to achieve that.

      About the Game
      GAME NAME is a fun, action-packed army builder with unique characters, challenges and engaging levels. Set forth on an adventure to protect friends, family and countrymen from new adversaries. Once defeated your enemies turn coat and join you in your adventures. Players can enjoy a range of troops and abilities based on their gameplay style which become more important as maps introduce more challenging terrain, enemies and bosses. Strong orc knights, dangerous shamans, and even a dragon are out on the prowl. Knowing when to fight and when to run, and how to manage your army is essential. Your actions alone decide the fate of this world.

      Previous Work by Team
      Although we are working towards our first game as a team, our team members themselves have past experience in the industry.
      This includes members who have worked on titles including:
      Final Fantasy Kingsglaive, FIFA, Xcom 2 and Civilization.

      Who are we looking for? 3D Modellers Concept Artists Marketing Specialists Level Designer

      What do we expect? Reference work or portfolio. Examples what have you already done and what projects you have worked on academic or otherwise. The ability to commit to the project on a regular basis. If you are going on a two-week trip, we don't mind, but it would be good if you could commit 10+ hours to the project each week. Willingness to work with a royalty based compensation model, you will be paid when the game launches. Openness to learning new tools and techniques
      What can we offer? Continuous support and availability from our side. You have the ability to give design input, and creative say in the development of the game. Shown in credits on websites, in-game and more. Insight and contacts from within the Industry.
      Contact
      If you are interested in knowing more or joining. Please email or PM us on Skype. Myself or Colin will reply to you within 48 hours.

      E-mail: Recruitment@ForgedInteractive.com
      Skype: ForgedInteractive

      Regards,
      David and Colin

      Follow us on:

      Facebook: https://www.facebook.com/ForgedInteractive/
      Twitter: @ForgedInteract
      Youtube: https://www.youtube.com/channel/UCpK..._as=subscriber
      Reddit: https://www.reddit.com/user/Forged_Interactive/
    • By deltaKshatriya
      Hey all, 
      This isn't really a post that fits into any particular forum, so I'm posting it here, but feel free to move it mods, if you feel that it should move. I figured it isn't really specifically about game dev related careers.
      I'm a recent college grad, currently working as a software engineer as part of a rotational program, so I'll be spending some time in my current role then rotating to a new location and new software engineering related position. I did my undergrad in Computer Science, and while Computer Science had been my main career interest for quite some time before college, while working my way through college my main focus really was just to get done with the degree, get a job, and be done with the extreme stress/too much work during college. Now that I'm out, I'm not as sure about my career direction as I was before. While I do still do like Computer Science, software engineering, etc., my current position, although well paying, doesn't really involve me doing much on a day to day basis (for now at least though that's subject to change). The good news is that I've got a lot of control over where I rotate to next. Interestingly enough, initially I got interested in Computer Science because of game dev (as a teenager at least). Then that morphed into AI and machine learning. Now it's....unknown really.
      Now the thing is I've kind of been bouncing around in all sorts of directions. I absolutely love 3d art and have been actively trying to get better at it. I've also taken up writing and considered trying to write a novel in my spare time. Then I'm finding graphics programming very interesting as well (although that's not what my day job is), and I still have quite an interest in machine learning, data science, text mining, etc.
      In short, I have absolutely no clue which direction to move towards. My parents believe I need to get a graduate degree, either an MBA or an MS in Computer Science. I, honestly, have no clue. And so I'm here, wondering what I should do with very little actual idea of what I should do.
      So I'd like to here your thoughts, fellow people of this particular section of the Internet. 
      Thanks in advance!
       
    • By Levi Lohman
      First off, I have some experience in coding, and I've been told I am talented in the ways of mathematics, but I never learned an entire programming language well enough to make an actual game. But I'm not looking for a game engine where there is no coding or scripting at all, I would prefer something where you can set up the game world or levels by dragging and dropping objects in. But I could control the behavior of the objects through simple logic parameters that you set up by selecting things from lists and inputting data. 
      One example is that if you were dropping in the area the player would walk on you could select the object that the player would walk on and from a list that would come up you would select something like "Lable" or "Property" that would bring up a text box where you could input something like "solidSurface" and then you would select the level which would bring up a list where you could select an if/then choice and you would be guided through a thing called "Object Define" where it would say, "If object has lable/property, " and you would select from a list of lables or properties you already made like the "solidSurface" thing you entered in earlier, then you would select some things from a list saying "Player" and you would select an action like "Collide" and finally you would select an action that would happen on collision like "Stop" and you would end up with a surface the player can walk on top of.
      Or if you were making an RPG and you wanted to define how a certain attack worked and had already set up variables for the stats of the player, enemies, and equipment you could type in some things like "preDamage = (weaponAtk x 1.25) x ((playerStrgth / 100) + 1)" and "enemyDefence = enemyArmor x ((enemyEnd / 100) + 1)" and "actualDamage = preDamage - enemyDefence" then you would select an if/then/else template saying something like "if actualDamage < 0, actualDamage = 0, else enemyHP = enemyHP - actualDamage"

      If you know of a game engine that is like or similar to what I'm looking for or if you need more information to know for sure, please leave a reply.
    • By Rannion
      Hi,
      I'm trying to fill a win64 Console with ASCII char.
      At the moment I have 2 solutions: one using std::cout for each line, let's say 30 lines at once using std::endl at the end of each one.
      The second solution is using FillConsoleOutputCharacter. This method seems a lot more robust and with less flickering. But I'm guessing, internally it's using a different table than the one used by std::cout. I'm trying to fill the console with the unsigned char 0xB0 which is a sort of grey square when I use std::cout but when using FillConsoleOutputCharacter it is outputted as the UTF8 char '°'.
      I tried using SetConsoleOutputCP before but could not find a proper way to force it to only use the non-extended ASCII code page...
      Has anyone a hint on this one?
      Cheers!
  • Popular Now