Game.getEntityById('player').applyDamage(20);
The bullet class then has access to all entities in the game and can mutate them. Global Mutable State, no? Barring the performance issues of doing this lookup dynamically, is that so bad? Game.getEntityById('player').applyDamage(20);
The bullet class then has access to all entities in the game and can mutate them. Global Mutable State, no? Barring the performance issues of doing this lookup dynamically, is that so bad?In your example - in the 'reduce' step - the implication is that the attack and debuff are handled in a deterministic way, regardless of their order? I feel like then that would require the logic for reducing all game commands needs to reside in the engine loop, which doesn't seem right to me.
The naive way is to give every object in your game an Update() then on each discreet step you iterate through the list of objects in your game and call Update(). This way lies madness because every object can interact with every other object from any Update() and you can never know where the changes are coming from.
The concept above is that you move up a layer of abstraction and you create a GameState object that contains all objects. You instead give each object a GenerateUpdates() method that returns a list of GameStateChanges. Thus to update the game, you take the GameState, iterate through each of the objects in GameState and get a list of GameStateChanges. You then pass both to either GenerateNewGameState (if game states are cheap) or ApplyChangesToGameState (if they are expensive). This reduces all of your mutations to one method and is much easier to update/debug/fix/understand.
Does that help?
This is actually a reliable way to construct most programs; it can aid significantly with reasoning, debugging, and performance. I try to build software this way whenever I can.
The gist of the idea is you would write _pure_ functions like this with an imperative style (example in Haskell)
bullet = do
player <- getEntityByID playerID
applyDamage 20 player
This will require some syntactic sugar (do notation in Haskell, for comprehension in Scala) to make it look procedure / imperative. However, the whole expression remains pure, and does not read or write to global state, it's just a data structure capable of expressing steps of mutations (and/or side effects).A separate part of the system (interpreter) will actually collect all such data structures and apply actual updates to global states in one place and in a principled way.
Some additional benefits includes easily implementing different interpreters (eg. for unit testing), adding "middlewares" to support things like logging and type safety on mutations.
I have no idea whether game programmers uses this pattern or not. However, it's very common in games to add a scripting language (LUA is a popular one) to express things like unit AI, dialog trees on top the underlying engine; which seems rather similar architecturally: adding a layer of indirection, use a expressive (just enough) DSL to encode the logic for mutations with only limited exposure to underlying global states.
A better resource that explores this idea can be found here: http://gameprogrammingpatterns.com/bytecode.html
An object shouldn't have access to independent objects, like a wall or another player.
I'd implement an observer pattern (or some other pattern) so you can safely decouple your bullet class say from the wall object.
UserAction -> bulletFiredMessage -> bullet fires and travels
-> wall receives message -> wall starts to poll if there was an impact
-> boss entity does the same
It's probably not a really good example, but you see state remains in objects.So for games - like for most non-trivial applications - some global mutable state is unavoidable. The task IMHO is not to eliminate it but to reduce it or at least restrict its "degree of globality". For example - even when you can't avoid global mutable state shared by threads within one client process, probably you can reduce the global mutable state shared by different client machines in a multiplayer match.