Advertisement

Ways of differentiating behavior using an object pool pattern?

Started by August 06, 2021 04:00 PM
26 comments, last by DevReign 3 years, 3 months ago

What kinds of entities do you have? If it is related to ECS, you should know that Entities are considered to be no more than IDs pointing to their components or vice versa.

If you read the blog post I wrote about in my previous post and decide to implement your own data structures like vector and array, you're in a good position to also implement your own object pools. I did that and everything I have which uses data under the hood inherits from my array class. It doesn't do much, just keep track of a data pointer and size as same as offer some utility functions to operate on elements.

First you should internalize that in C++ unlike other “high programming languages”, everything is just memory. An object is pointless as it is just a blog of memory and a pointer to the vtable. Once you got this, everything will be a lot easier because you could start to convert pointers into everything. Get an array or vector class, allocate a certain amount of bytes and you're ready to go for your very own object pool.

I've written a lockless object pool for our C# tooling code which you could easily adapt to be used in C++. It is simply a block of memory and a collection of pointers which point to the free memory addresses. I'm using platform atomics to swap free and used pointers

You can have a class hierarchy even if the language believes it doesn't exist. The only thing you need is predictable access to the field describing what data you have and a systematic approach to keeping separate classes separate.

struct BaseShip {
    int id; // Class Id, you may want to use an enum. 1=base, 2=shield, 3=wave
    int energy;
};
struct ShieldShip {
    BaseShip bs;
    int shield_power;
}
struct WaveShip {
    BaseShip bs;
    int wave_power;
}

As I use the same order and types for all common data at the start of the structures, C promises that the common fields are at the same offset, and a simple pointer cast is sufficient to switch to the base class.

// Constructors
void baseship_init(BaseShip *bs, int cid) { bs->id = cid; bs->energy = 100; }
void shieldship_init(ShieldShip *ss, int cid) { baseship_init((BaseShip *)ss, cid); ss->shield_power = 1; }

// Class specific code for update.
void baseship_update(BaseShip *bs) { bs->energy++; }
void shieldship_update(ShieldShip *ss) { baseship_update((BaseShip *)ss); ss->shield_power += 100; }
void waveship_update(WaveShip *ws) { /* Dont call Super.update() */  ws->wave_power += 20; }

// Generic update code
void ship_update(BaseShip *bs) {
    switch (bs->id) {
    case 1: baseship_update(bs); return;
    case 2: shieldship_update((ShieldShip *)bs); return;
    case 3: waveship_update((WaveShip *)bs); return;
    default: crash_weird_class_id(bs->id);
    }
}

This is one way to do it. As you can see, the spot where ‘id’ is actually used is only in the dispatch switch. What you can do as optimization is to store the specific ‘update’ function pointer in the base class instead of having the dispatch switch. Personally I prefer the above as it's simpler to code and maintain. Also modern compilers will rewrite ‘ship_update’ heavily, so the costs are probably less then it looks.

DevReign said:
but it sounds like i may have to split up the entities into more as well.

Did you consider seeing them as a base + a composition of properties? You can consider a Ship and an Airplane as completely different things, you can also see them as a single Transport with both “move on water” and “move on air” properties that may or may not be present.

Advertisement

Oops, this reply sent twice. Edited to fix, but can't delete it.

None

@Shaarigan I'm not using ECS, it would be overkill for my needs and i don't want to split up all my objects for it if i don't need the performance.

I'm in between doing the object pool method or trying to do a 1 tree deep hierarchy of branching classes from an abstract class. I did read the blog post, and I tried asking for help about implementing it in the game dev discord, but they kept insisting i'm wasting my time trying to make an allocator and to just let it fragment the memory because they don't think i need the performance? so I didn't end up making any progress

None

Coding functionality (ie the stuff that makes the game run), and handling allocation / pooling / organizing are separate things. The former says what data you need where, the latter is about how you store the former.

I'd suggest first do the former, then see if that works sufficiently (ie happily ignore any possible fragmentation problems until it runs), then optionally rework allocation / pooling / whatever.

@Alberth That is a pretty nice clean approach. each ship type in that example would need to be in it's own array or object pool?

I'm not entirely certain I understand. I think I looked at them as a base + properties, I just meant that If i have different ships as in your example they would all need their own array or object pools to be stored since they are different sizes. That's assuming I don't have an allocator to dynamically allocate them without fragmentation if that matters?

None

Advertisement

@Alberth I would definitely like to focus on the functionality. I guess if i wasn't worried about all that i would just dynamically allocate space for each object of any class as i need it and keep track of them in an array of pointers. if fragmentations bad though, i might as well stick with the object pool approach before i build the entire game around that

None

DevReign said:
I'm not entirely certain I understand. I think I looked at them as a base + properties, I just meant that If i have different ships as in your example they would all need their own array or object pools to be stored since they are different sizes. That's assuming I don't have an allocator to dynamically allocate them without fragmentation if that matters?

You do seem to be stuck in this allocation thing aren't you?

I don't know what your target platform is, but if it's a modern system, then memory runs in the GBs. So even if one object is 1KB, and you have 100% fragmentation, you can still have 4,000,000 instances if you assume a small 4GB memory (modern systems are 8 GB or larger). You'll have a very hard time running out of memory for a game that lasts a couple of hours.

More concretely, if you make 100 instances of 1KB each every minute (about 3 instance every 2 seconds) without ever re-using freed memory, you'll run out in 40,000 minutes or around 11 hours game play.

All real-life memory allocators are much more smart than that. You'll have a next to impossible hard time fragmenting 4GB memory unless you construct a special case for it where you probably also need to know the memory allocator works.

DevReign said:
I would definitely like to focus on the functionality. I guess if i wasn't worried about all that i would just dynamically allocate space for each object of any class as i need it and keep track of them in an array of pointers. if fragmentations bad though, i might as well stick with the object pool approach before i build the entire game around that

The first rule of optimization is not to optimize before you have a problem. Provide evidence to show your problem is real first. Don't fight the fire before you have witnessed the smoke.

All authors of memory allocators are very much aware of fragmentation, and I am sure they pull all kinds tricks to reduce the problem or eliminate it. Their techniques are probably much more advanced than what you and I know about it.

The trivial way to solve fragmentation is to make everything the same size:

union ShipMemory {
   BaseShip bs;
   ShieldShip ss;
   WaveShip ws;
};

BaseShip *new_baseship() {
    ShipMemory m = new ShipMemory;
    baseship_init(&m->bs, 1);
    return &m->bs;
}

and done.

But really, don't do this. Chances are memory allocators do this kind of stuff already so you don't have to. Even if they don't you'll have to make a game that lasts days before you run into trouble. People don't play a game that long non-stop.

DevReign said:
they kept insisting i'm wasting my time trying to make an allocator

The same people told me to not make a game engine. My opinion is if people don't try to make their own tech in order to improve things, there won't be any progress on “just using the existing software”. One could argue the same way against using an object pool rather than new/delete everything on demand, so what ¯\_(ツ)_/¯

I just share my experience but in the end it is your decision ?

@Alberth The target platform is PC, but I may port the game to phones and consoles if it's successful. I'm only trying to future proof things a bit so i don't have to rewrite everything later, but not over optimize.

I'm not stuck on allocation; It's just most similar to what I'm used to from programming in higher level languages. I like keeping all my object code together in one class. It seems like the most flexible option to allow for the richest behavior of the objects without complicating things. I don't want to knowingly implement something inefficient that's considered bad design if it may cause me problems in the future, there's a better way of doing it and more experienced developers think I shouldn't. I'm happy to use any method that works, just seeing what my options are and looking for advice. while it sounds like fragmentation probably isn't a problem from what you said, the way you said it makes me think if you were in my position you still wouldn't dynamically allocate anything and would do something like in your previous example?

None

This topic is closed to new replies.

Advertisement