Advertisement

How do you model data and behaviors in your non-standard game loops?

Started by August 13, 2024 06:45 PM
1 comment, last by frob 3 months, 1 week ago

I see ECS thrown around a lot as a general solution, but I don't believe in general solutions.

Let's compare two wildly different games: Generic FPS game (Any) and Nethack:

In FPS, everything happens in “real”time, combat boils down to who clicked on who first, very little window for player to feel like it's unfair because of realtime factor, worst case scenario, both players shoot eachother and die at the same time, this is entirely possible and acceptable as each could've done something different.

In Nethack, you walk up to enemy, they can hit you for free, if you wait for enemy to walk up to you, you can hit them for free. Now there's a problem. If you were already in combat, and hit enemy with fatal blow, it would be sort of odd if enemy hit you back before dying. This presents a problem of sequential actions, including such that prevent further actions of, well, actors. The thing that happens in FPS games rarely, more often with projectiles rather than hitscan, but is acceptable, suddenly turns very bad!

ECS has 3 letters in it, meaning it by itself can be split up into 3 separate things and as a programmer I can do whatever I want with that, so I'd use the idea of Systems from the ECS to model interactions of things damaging other things by using a notion of actions: simply take every complex decision by player input or AI quirkiness and turn it into data. Entity wants to attack an entity? Sure just attack_actions.add(attacker, victim). When the right time comes, and this is heavily dependent on the game… attack_actions.perform(), this calculates every attack request that happened during that frame/tick and maybe turns them into further unique action requests, such as audio_actions.add(choose_one_of({"deflect_armor.wav", “miss.wav”}))if attack misses, yes, audio is modeled as action, it can either be mixed all at once in same thread or… It could even be implemented as a request for a dedicated thread that happens asynchronously, it really depends on latency requirements after all. Action queues as I call them, are pretty cool and can hide a lot of things under simple “please do this for me, when you can”, as long as this sort of partial evaluation is acceptable.

But as per above, this is not acceptable in Nethack, I need “happens before” and even something like “cancels the action” to really make use of this. Maybe you can give me some advice on how to proceed in programming for such a “non-generic” game that doesn't yield itself easily to "just compute everything in batches”?

Because the whole idea of optimizing for icache by batching actions into their own separate loops goes out of the window, I'm leaning heavily towards OOP, after all if I am already going to have thrashing all over the place due to sequenced varied actions that happen in mostly low amounts, virtual calls and polymorphism becomes less and less of a thing that will be responsible for bad performance, while code will become easier to maintain, and if I just use OOP and ignore the “big picture” of data, “happens before” is handled implicitly, and actions that didn't happen, won't happen due to sequential nature of processing each entity to completion as opposed to partially. But maybe I'm missing something here?

You can't really compare across those boundaries, and some of those comparisons are meaningless.

Nethack is an inherently turn based game, as are games like Civilization or X-Com or many older JRPG and games like Chess or Go and other physical games. Actions don't happen continuously. There is an action, and then events are processed, then the game waits for the next player's turn.

FPS games are continuous, the simulation is always running and events progressing regardless of the player.

It isn't really meaningful to compare turn-based events with continuous events.

And as for Nethack specifically, the source is available and straightforward to read. Everything in the world is processed in a well-defined order. All the steps are fully specified. During each turn specific things happen in a specific order. At the end of your turn each creature on the level processes their own turns in the specific order.

thingamajigfabric said:
ECS to model interactions of things damaging other things by using a notion of actions: simply take every complex decision by player input or AI quirkiness and turn it into data.

ECS has nothing to do with either turn based or continuous. Many game systems are based on “HAS-A” mechanics typical of ECS. In practice many physical board games are effectively ECS as well. Each player has their own player sheet or card area for the main objects where they attach markers, attach other cards, attach modifiers, and similar. You may have an entity (player sheet), and you can attach a weapon component, an armor component, and similar. Or you may have a game like MTG where you have permanent cards as entities, and to them you can attach any number of enchantment components, certain artifact components, and counter components. A card or monster or player certainly has base attributes, but they're modified by the HAS-A effect of components attached to them.

Nethack also has a great deal of composition in its implementation. All monster objects in the world (including the player) can have armors and defensive items, weapons and offensive items, and misc objects like money, rings and potions. All of that is composition with HAS-A relationships. Monsters can similarly be affected by attached effects of haste or slow, an alignment counter, and more, again with HAS-A effects. There are also a bunch of fun rules in monster generation; lawful minions don't get cursed, bad, or rusting objects, leaders always get better battle gear than the rest of the group, etc. Monsters get the same effects as wielding objects or keeping items in the inventory, like an unsuspecting nymph picking up a loadstone suddenly getting burdened by the weight. The systems all operate under HAS-A mechanics of composition.

thingamajigfabric said:
Because the whole idea of optimizing for icache by batching actions into their own separate loops goes out of the window, I'm leaning heavily towards OOP

These are also mostly unrelated to what you've written. Cache optimization and OOP are nearly orthogonal. You can develop OOP code that is tremendously optimized for system caches, you can also develop OOP code that is a nightmare for system caches.

One very simple design decision of a structure-of-arrays or an array-of-structures can have a huge difference in cache performance with zero difference in being object oriented. In both cases you have a system that's like “this is a point cloud for a particle system”, still have everything object oriented, but the choice of how you manipulate those objects is either a cache-friendly performance jet engine that rips through the data faster than expected, or a cache-unfriendly performance nightmare, both options can use principles of object oriented programming.

Advertisement

This topic is closed to new replies.

Advertisement