1
\$\begingroup\$

In most programs, if we want to do something to, say, Bob, then myFun(bob) is enough. We know that the function deals with Bob and won't touch Alice.

However, in a game, everything wants to freely interact with everything else:

Option 1

killTime(bob){
  cat=bob.getNeighbor().getPet();
  bob.talkTo(cat);
}

We are forced to have every piece of data hold reference to each other. Web-shaped dependency is hard to maintain, hard to serialize. Chained dots don't look nice, either.

Option 2

killTime(bob,neighborList, petList){
  alice = neighborList.findNeighbor(bob);
  cat = petList.findPet(alice);
  bob.talkTo(cat);
}

The argument list can get long, and it is not clear what should be included. What if we want Bob to read his poem written in his notebook hidden in his drawer?

Option 3

killTime(world, bob){
  cat = findTheCatYouLike(world, bob);
  talkTo(world, bob, cat);
}

Bob could do anything to world in his small talk, not necessarily a bad thing in a game. But it doesn't feel right passing the same world to every function. Also, if we use C, then we are forced to have pointer members instead of data in world, just to reduce the compilation time of #include "world.h"

I don't think any approach I found is good. What would be the least bad solution in this situation?

================== edit ================

To elaborate my question, suppose that we have a RPG system, with world map, characters with HP, MP, so on. Now we want a simple player.walk function. position+=velocity straightforward? Not really. What if we want footprints on map? What if we deal damage to units we pass by? We now need to touch everything in game to in order to walk.

In most programming scenarios, it is assumed that function and data are coupled to each other. Only these functions work on these data, and only these data are used for these functions. Put the functions and the data into a class, and it's high cohesion.

I am looking for an architecture where the opposite is true. I have a bunch of data (hp, mp , pos, vel), a bunch of actions (walk, attack, wait). That is all I can be certain of. I want the flexibility to change my mind everyday how the actions affect the data. In my code, I actually started with option 1 and gradually slid into option 3.

\$\endgroup\$
  • \$\begingroup\$ Do you have a specific snippet of code? Abstract code like this without any context is hard to reason about. There is no right or wrong way. There are ‘efficient’ and ‘less efficient’ solutions, and the only faultless way to test between them is to profile your actual code. \$\endgroup\$ – Rish May 14 at 8:27
0
\$\begingroup\$

There are many architectural patterns you see in game development. So there is no one-size-fits-all solution.

But I would like to introduce you to a common architectural pattern in game development which solves many of the problems the question asks about: The Entity - Component - System architecture.

In a nutshell, each entity is a combination of components (which are just dumb structures without methods) and each game feature is implemented as a system which operates on a set of components.

One problem with the method killTime(bob) is that you can't tell what it actually does just from the name. What does it mean for bob to "kill time"? Wander around the game world? Regenerate health? Pet his cat? All of that? But these are all very different game features which require very different data. So instead of just creating a killTime method, we create a separate system with a separate name for each of these "kill time" features. This doesn't just allow us to cleanly separate game features, it also allows us to cleanly separate their data by only passing the components these systems need.

So we would have code which works more like that:

updateWanderSystem(positionComponent, routeFindingComponent, map);
updateHealthRegenerationSystem(healthComponent);
updatePetSystem(positionComponent, petOwnerComponent);
  • Names which actually say what the function does gameplay-wise
  • Injection of the dependencies the function needs and only those it needs
  • No need to follow long reference chains within these functions
  • Most of these functions will even be unit-testable in isolation without unintended side-effects.
  • (C-specific issue) the system implementations only require the headers for the components they operate on, keeping recompilation times short.

Note that none of these systems operates on "bob" as an object. It operates on a tuple of components which might belong to "bob". The advantage of this pattern is that you can use a system on any entity in the game which has the necessary set of components. Which is why ECS architectures usually don't operate on a per-entity basis but rather run each system one after another passing all components of all entities currently in the game to it as one array each.

This also has performance advantages. When a for-loop operates on an array which contains only the data it needs (as opposed to an array of objects which contain a lot of data it doesn't need) there is a good chance that that array completely fits into the CPU cache. This allows the CPU to copy the array into the cache at the start of the loop and then process the loop lightning fast without doing any memory access.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Just one question. I can see ECS makes actions flexible by partitioning into atomic actions. But how to make variables flexible? For example, I want player's speed to depend on various factors, how should I write updateSpeedSystem without restricting myself on what factor to use? \$\endgroup\$ – user3785097 May 16 at 13:20
  • \$\begingroup\$ @user3785097 You are still thinking in systems as handling a certain kind of object, not by the mechanic they represent. You would usually write a separate system for any game mechanic which can affect speed (GravitySystem, FrictionSystem...) and alter the Velocity component. These systems would receive the Velocity component and whetever else they need. Then, after processing all those systems, you would run the VelocitySystem to apply the resulting velocity to the position. That system would no longer know or care how that velocity was calculated and just apply it to the position. \$\endgroup\$ – Philipp May 16 at 18:53
  • \$\begingroup\$ So whenever I have a relation z=f(x,y) , instead of z=computeZ(x,y) , I use XSystem(x,z) YSystem(y,z) ? \$\endgroup\$ – user3785097 May 17 at 21:24
  • \$\begingroup\$ @user3785097 I am careful with making absolute statements, but in the general case this would be a pattern to consider. \$\endgroup\$ – Philipp May 17 at 21:26
0
\$\begingroup\$

Great that you asked, Why? about such a fundamental aspect of programming. This is how better thinking and better tools emerge. However, classic OO-style programming languages are unlikely to change any time soon, so for you, the key thing is to classify your problem and then evaluate it according to your needs.

"Good" vs "bad" is based on what you see as most important - concision (static structure), orderly organisation of code (static structure), reduced indirection (a dynamic or runtime concern), lack of stateful side-effects (at runtime), etc. Each of these factors plays off against the others.

You'll find that static, architectural concerns are often at odds with runtime performance concerns. A more human-readable codebase (OO being the prime example) tends to be less performant, whilst more performant code, i.e. code better suited to the machine's architecture, is often a lot harder for us to follow.

This answer is long, but I want to put things into perspective for you in terms of the programming paradigms used.

Option 1

This is what OO purists advocate, as it reads like a natural language sentence.

However, it also contains potentially costly double indirections such as bob.getNeighbor().getPet();. Certainly .getNeighbor()'s result can be cached and used multiple times in the same function, but what if you only need it once? If the object is temporarily fixed (for more than a single frame), such as a cat having a new neighbour who will stick around, then you can say cat.neighbour = bob, to avoid making method calls to retrieve the owner every single frame (although profiling the impact thereof, is up to you).

Over time these indirections can be eliminated in critical sections of the code to address performance, with these sections clearly marked as optimised for x and y concerns, to avoid anyone re-refactoring them for readability in future. As performance increases, the code looks less and less canonically OO.

OO incurs costs also through its vtables, which procedural languages do not suffer from.

Option 2

This is sort of a hybrid between pure OO (1) and procedural (3) approaches.

Long argument lists are a common feature of C which is the modern reference point for procedural programming; often these are packed into a single context argument (struct or object) which is similar to this in OO languages (only, it is explicitly given unlike this which is implicit).

However, you are still using .findNeighbor() and .findPet(), which are methods, not global functions, and as such exist only in OO languages.

Option 3

This is pure procedural programming, around long before OO arrived. Functional programming is closely related, albeit that it has additional constraints around inputs and outputs.

Here you will find no this and no somebody.doSomething() methods; only global functions like doSomething(somebody). That is, where (i.e. on what object) each function is located, is not important here: functions exist in global space. This is closer to the way they are represented in the machine's instruction cache, which makes no distinctions.

Procedural programming, for games at least, is commonly used for small, inherently fast programs written in the C language (or C subset of C++). C is commonly used where you need a special purpose component that to be lightning fast - faster than primary code written in e.g. C# or Java.

Functional programming is similar to procedural, but mandates that we inject all of the necessary inputs as function or method arguments, and returns all new output(s) based purely on those inputs and nothing else, i.e. neither inputs nor outputs are based persistent state as found in this members, or global members found at module level. This aims to reduce errors caused by state / side effects, assuring a higher degree of program correctness and debuggability.

Cost of method calls and indirections

Dot-syntax drilldown, e.g. owner.cat.bowl, incurs costs, this is known as double indirection.

In Assembly / machine code, these cause what are called JMP ops, which require the machine to "jump" to different location in either instruction (functions, methods) or data (members) cache, possibly causing cache invalidation, which is a major cause of reduced performance in code.

Conclusion

I'd aim for OO, i.e. 1 or 2, though between these, there is no "right way". I would go with option 1 until I saw a need for option 2, although 1 is more work as you have to write extra getter functions for each little thing (with a decent IDE, this is not so bad). Uncle Bob favours clean architecture over performance, suggesting we aim for the most concisely readable code first, and optimise only in critical sections. His background is in business programming, where maintainability comes first. In games, however, this is only where we start.

If you aim for program correctness right off the bat, and tend to have a mathematical mindset, functional programming may be preferred.

If you like C (or Basic, Pascal or Delphi) for whatever reasons, procedural may be preferred.

| improve this answer | |
\$\endgroup\$

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.