2
\$\begingroup\$

I have started my first game three years ago and learned a lot during that process. Now I would like to rewrite it to incorporate my "lessons learned" or try out new ideas.

My game consists of space ships that can target other space ships either for combating, trading or communicating. When they have no current target, they look for other ships in their area, lock them as target (by keeping a pointer to them as member variable) and do some stuff. After a ship is destroyed, its pointer used by other ships as a target becomes invalid. To avoid any dangling pointers and crashes, I add the to be destroyed Ship's id to a cleaning list. During the next World's update, I go through all of these to be destroyed ids and remove them as potential targets. See this pseudo code to demonstrate this:

class Ship
{
public:
    Ship();
    Ship::destroy(){
        // set to be destroyed flag
        _world.pointersToDelete(_id);
    }

private:
    void        destroy();

    int         _id;
    Ship*       _target;
    World*      _world;
    bool        _to_be_destroyed;

};

// in World::update
World::update()
{
    for (auto ship: all_ships)
    {
        for (auto id_to_delete: ids_to_delete)
        {
            if(ship._target != nullptr && ship._target->_id == id_to_delete)
            {
                ship._target = nullptr;
            }
        }
    }

    // now trigger the actual final destruction of the ships
    // ...
}

Question: Is this a good design in general? I ran into some problems when I used it in practice:

There are more objects that communicate, combat and trade with ships like Space Stations, Planets, etc.. I would need to go through all of them every update loop to check whether they have no dangling pointers. I am concerned about the performance since my game allows space battles of thousands of ships at the same time.

I thought about using an id approach instead: the _target variable is not a pointer but an id that I can look up in an unordered_map of Ship objects. If the id is not found anymore, the target is not valid any longer. But this could lead to performance issues as well if every object has to check this during every update multiple times.

\$\endgroup\$
  • 1
  • 1
    \$\begingroup\$ Do you know anything about smart pointers? \$\endgroup\$ – Vaillancourt Jun 12 '20 at 12:15
  • \$\begingroup\$ @Theraot Great videos, they addressed many of the problems that I encountered using my solution. Do you have more material regarding common mistakes when using OO in game development? \$\endgroup\$ – mrzo Jun 14 '20 at 20:17
  • \$\begingroup\$ @Vaillancourt Yes, I know about smart pointers. Would you recommend to use them here? As far as I understand, they represent an ownership model for the object they point to. But I would not like that a Ship object which was destroyed is still alive just because some _target variables point to them. \$\endgroup\$ – mrzo Jun 14 '20 at 20:24
  • 1
    \$\begingroup\$ I believe the usual suggestion for this problem is an Entiy-Component-System architecture/pattern. I guess another approach would be a form of garbage collector, either reference counting or mark and sweep (which is more like what you describe). Perhaps reading how those work may help you. \$\endgroup\$ – Theraot Jun 14 '20 at 20:34
5
\$\begingroup\$

I believe an option available to you is to use std smart pointers, more specifically the std::shared_ptr and std::weak_ptr.

Typically, those pointers represent ownership. They also offer a ref-counting mechanism, and I think it's this feature that you're after.

std::shared_ptrs hold a reference to a dynamically allocated object, a count of "hard" references (i.e. how many shared_ptrs point to this object) and a count of "weak" references (i.e. how many weak_ptrs point to this object). When the ref count for "hard" references reaches 0, the dynamically allocated object is destroyed. When the "weak" count reaches 0, the ref-counting object is deleted.

Although you don't seek to share the ownership, you could use the pattern that "shared pointers represent ownership", and still have only one shared pointer over your object, and use the std::weak_ptr to represent "links" to those object.

std::weak_ptrs don't keep objects alive, only std::shared_ptrs do.

So you keep std::shared_ptr<Ship> in your World::all_ships collection (e.g. std::vector<std::shared_ptr<Ship>>), but your Ships keep references to std::weak_ptr<Ship> _target;.

Once you need access to it, it could be a simple matter checking if the _target is still there and creating a temporary "hard" reference to it, until you need it:

Ship::attack()
{
  if ( !_target.expired() )
  {
    if ( auto lockedTarget = _target.lock() )
    {
       lockedTarget->_health -= 42;
    }
  }
}

Later, if you decide that the Ship pointed by a Ship::_target needs to be destroyed, then you just delete the std::shared_ptr<Ship> that points to it; whenever you'll try to reference to it through the weak_ptr like shown above, it will not cause any issue, and the ship's health will not be decreased, as it will not be able to lock the pointer.

This is one option, and I believe it's the easiest and fastest to implement in the situation I think you're in. Know that locking often weak_ptrs into shared_ptrs could cause slowdowns as this has been developed with "atomicity" (multi-threading) in mind. I would still suggest to try it, and then profile it. If it works for your situation, then you're good to go and work on the next top item on your long list.

If this doesn't work, then, as it's been suggested in the comments, you could look into a component based architecture (e.g. Entity-Component-System), but that would require you to change a lot of things in your game.

\$\endgroup\$
  • \$\begingroup\$ Excellent answer, thank you very much! I recently started to read Bob Nystrom's book about Game Design Patterns. I see that there is a lot that I can change in my game's architecture to make it better. So actually I am looking into a component-based architecture now. \$\endgroup\$ – mrzo Jun 16 '20 at 9:52
  • 1
    \$\begingroup\$ If you decide to explore that, you might want to take a look at how EnTT does things. It's relying a lot on modern c++ and templates, but it is a nice approach. \$\endgroup\$ – Vaillancourt Jun 16 '20 at 10:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.