Game Engine API, smart pointers or not?

Started by
35 comments, last by frob 4 years, 11 months ago
1 minute ago, Tim Leijten said:

No no. I wasn't trying to be mean. It's just something I notice in general, both here and on reddit, people expect that if something changes it must be another thread. Which I find interesting.

Here's the thing ..... It wasn't clear you were going to actually save the pointer outside of the API in some object.  I was thinking you would get the pointer, use it for something and be done with it.  In the later scenario the only way it would change is because of another thread.  If you are going to save it, yet also want it to be able to be invalidated, then yes, you need something like a weak pointer.

Advertisement
Just now, Gnollrunner said:

Here's the thing ..... It wasn't clear you were going to actually save the pointer outside of the API in some object.  I was thinking you would get the pointer, use it for something and be done with it.  In the later scenario the only way it would change is because of another thread.  If you are going to save it, yet also want it to be able to be invalidated, then yes, you need something like a weak pointer.

Okay, sorry. I didn't realize.

5 minutes ago, Tim Leijten said:

Okay, sorry. I didn't realize.

No problem.  Good luck with your project!!!!


 I'm not a fan of handles as it doesn't play well with oop.

 

I would prefer going the pointer route. So you are saying shared and weak ptr's are ok when needed. And I should use raw ptr when possible? 

I've worked on projects that use handles and others that use smart pointers. Both could work, I just prefer handles, it's more explicit and safe to me.

Otherwise, in the pointer-case, I'd use raw pointer for situation where I just need to read/write from it, where I do not need to remember which specific object I'm working with (which is 99% of that cases). and this is done to avoid needless ref count increments.

 

With handle is more or less the same thing, you remember the handle only when you want to keep track of specific object otherwise you obtain a pointer to it for read/write and that's all.

3 hours ago, Tim Leijten said:
5 hours ago, SyncViews said:

Sounds more like a `create`. I think the problem here is the "deleted at any point". What is able to delete it, why is it?

E.g. something a player does causes the object to delete itself. Why does everyone think it has something to do with threading?

Because of how you wrote "no way to check if the returned pointer is valid" and "could be deleted at any point".

 

In a single threaded environment, "deleted at any point", should not be true, and you should be able to guarantee a returned pointer is safe.


Entity *target = map->get_nearest_enemy(this->faction(), this->position());
// target should be valid, and there should be no way for it to become unexpected invalid, within this code block
if (target->type()->valid_assassination_target())
{
    target->kill(); // Now, maybe the pointer is invalid
}

The "something a player does causes the object to delete itself" can't happen in the middle of that block if it is only single threaded, and so the raw pointer can be used safely, keeping it over a longer duration of course has other considerations.

The advantage of using a unique ID / "handle" there, is because of memory reuse you can't safely validator a pointer at a later time. But being able to store an ID between frames you can, and then you can retrieve it again later safely.


Entity *target = map->get_entity_by_id(target_id)
if (target != nullptr)
{
    // target still valid, raw pointer is safe as before
}
else
{
    // target is gone
    target_id = INVALID_ENTITY_ID; // fairly easy to have some sentinel value that acts like a null pointer
}

In a more complex environment, you could turn that into your own custom smart pointer, I did that previous as an optimisation on "get_entity_by_id" in a design where I stored things in std::vector like arrays and compacted them (meaning objects could move memory address to keep an efficient memory layout, although that was part of an ECS design, so "Entity*" like OOP didn't exist at all)

Well instead of killing the object directly you could do this:


// Game main loop.
while (true) { 
	std::set<Actor*> actorsThatNeedToBeKilled;
	for(Actor* a : playingActors) {
		// Code.....
		if(...) {
			Actor* enemy = findEnemyToKill();
			if(enemy && ...) {
      			actorsThatNeedToBeKilled.insert(enemy);
      		}
      	}
    }
      
    // Now kill the actors that no longer will participate in the game. and continue with the next frame.
    for(Actor* actorToKill : actorsThatNeedToBeKilled) {
    	game.killAndDeleteActor(actorToKill)
    }
}

 

I sugest against the use of those C++ structs smart_ptr, ref_ptr and whatever is created in the standard because of one single but very important reason, memory management. Most game engines are frequently creating and releasing memory blocks for different things and so they built on top of some management structure. Either garbadge collection or memory buckets and this is simply impossible or at least hard to achieve with pure standard containers because anything belongs to the memory manager, no one else wether API or system owns a memory pointer otherwise the manager couldn't handle cleanup properly.

The reason you do own memory management is memory fragmentation when using pure malloc/new free/delete operations and so a heap overflow gets more probable the longer the game runs

Oh hey it's the reddit guy. Handles are the way to go. They should play with OOP just fine. If you don't like them I recommend to try discussing with us why you don't like them. Try bringing up a specific example where you have trouble of find them unappealing.

29 minutes ago, Randy Gaul said:

Oh hey it's the reddit guy. Handles are the way to go. They should play with OOP just fine. If you don't like them I recommend to try discussing with us why you don't like them. Try bringing up a specific example where you have trouble of find them unappealing.

The reason I don't like them is because I have to do something like getObjectPointer(handle); when I want to communicate with the class. I think that looks pretty ugly. Also, I don't like that with handles, it becomes harder for the user to create GameObjects themself and add them to the engine.

21 minutes ago, Shaarigan said:

The reason you do own memory management is memory fragmentation when using pure malloc/new free/delete operations and so a heap overflow gets more probable the longer the game runs

I believe he should be able to do a custom allocator. I think nobody really uses standard new and delete for this kind of stuff.  It's really rare that I do. Slab allocation is almost instantaneous after initial start up. You can easy make it use inlined free-list push and pops, especially if you aren't worried about threading.  Even if you are, there are some cheats that you can do depending on the usage.

Also in my experience the fragmentation issue is often overstated.  In many applications you have a lot of smallish objects, and yes they may get created and deleted often. However with size specific heaps, or as I like to use, a heap with a hash table of size specific free-lists, the holes get filled before the heap grows.

The main issues I've found with std::shared_ptr and std::weak_ptr is the double sized pointers and the fact that you can't easy pass around raw pointers and then assign them to shared_ptr or week_ptr on the fly since you'll end up with multiple control blocks. However this is kind of a design thing, so for his API it might be fine as long as he's consistent with using smart pointers.

This topic is closed to new replies.

Advertisement