1

I'm building a game with the LibGDX game engine. The engine works by registering an implementation of an interface that receives callbacks on the game loop. The interface looks like this:

public interface ApplicationListener {
    /** Called when the {@link Application} is first created. */
    public void create ();

    /** Called when the {@link Application} should render itself. */
    public void render ();
}

My current implementation is littered with mutating code that has resulted in NullPointerExceptions and an overall sense of instability from all the mutability. An example:

class MyApplication extends ApplicationListener {
  private var texture: Texture = null

  def create() = {
    texture = ... // load texture from filesystem
  }

  def render() = {
    // draw texture
  }
}

Is there a design I could apply here to reduce the mutability in my game? Ideally all mutations will be captured in a monad and applied each iteration of the game loop in a single spot in the code.

  • 2
    Your question is probably too broad to be answerable here. If the code otherwise works and isn't enormous, consider posting it at codereview.stackexchange.com, and ask for stability suggestions, not immutability. Note that there are other practices besides immutability that might improve your overall situation. – Robert Harvey Jul 22 '17 at 17:34
  • What language is this? Scala? Groovy? – candied_orange Jul 22 '17 at 17:45
  • 1
    @CandiedOrange The first block is Java code. The second block is Scala code. – Derek Elkins Jul 22 '17 at 21:17
  • Since you are using or at least familiar with Scala, you may want to look at Deprecating the Observer Pattern coauthored by the creator of Scala. – Derek Elkins Jul 23 '17 at 22:00
  • @DerekElkins Thanks. That looks promising. I hadn't considered using reactives. I'll read up on it – Samuel Jul 24 '17 at 2:01
2

I'm going to assume this is a real-time game and that it is not particularly taxing computationally. The former assumption isn't much of a restriction. Things are only simpler for interactive-rate or turn-based games.

Basically, the body of your game loop should be an externally pure function with type (List[InputEvents], GameState) -> (List[OutputEvents], GameState). Any event handlers you have should just add events to a queue that will be emptied (atomically) by the game loop. (You can have multiple input queues, particularly if there is no important ordering constraint between events in different queues. You end up with the body of the game loop accepting a list per queue, or you can merge them together if you want.) You can use double-buffering-like techniques for the queues to minimize contention (which shouldn't be a big issue) and avoid copying the events to a buffer (which probably also won't be a big issue). (There are still plenty of tough details for even a pure GameState update function. Collision detection and response is hard no matter what.)

Once this pure function completes, you render the GameState and fire off the output events (you can actually fire off the events during the body if you want). Output events might include things like triggering sound effects. Critically, none of these output events have any effect on the GameState except via causing new InputEvents to occur in the future. Input events aren't limited to user-input. Let's say loading a texture is an asynchronous operation. Then, when you need to load a texture, you'll produce a LoadTexture output event. Once the texture is loaded, the textureReady event handler will queue a TextureReady event and the next turn through the game loop will incorporate that new texture. This is only necessary if you need to load textures on the fly, otherwise you can just load everything "upfront" in a loading screen.

This reduces the entire core of the program to just a state machine. The body of the game loop is completely sequential so there are no issues of race conditions etc. Indeed, it has the structure of an actor, albeit it only receives one type of message which is the clock tick the contents being the contents of the input queues as of that time. (You can leverage pure parallelism if you like though. Indeed, flat data parallelism fits rather nicely here.) In many cases, this will be completely adequate. Normally using state machines is awkward because it requires turning implicit (usually control) state into explicit state. In this case, there's basically no control flow outside of the game loop. The only implicit control state is due to asynchronous operations which should be relatively rare (code-wise, they may occur quite often at run-time) and it's probably best to just reify that state. For example, you'd have a TextureManager as part of the GameState that will keep track of loaded and pending textures (and probably reference counts...) You'll need to make sure e.g. enemies check whether their texture is ready before they spawn, but this logic can be easily abstracted away. There can be some subtleties. You don't want the player to walk past the boss because it's texture wasn't loaded.

  • I like the idea of using a pure function, and I think I understand and agree with everything you've written, but regarding the simple case of loading a texture like I've outlined in my example, in your game loop would the texture be a component of GameState? E.g. case class GameState(texture: Option[Texture]) that in one iteration would be None and then (once loaded) would transition to Some(...)? Or would assets be represented outside of GameState? – Samuel Jul 24 '17 at 2:14
  • Since you probably have more than one texture which will be shared among more than one game object, you'd probably have something like Map[TextureId, TextureState] where TextureState would keep track of the status and reference count if you're doing that. You can use case classes or Option for this. If you don't need anything more than the texture's ID, e.g. with OpenGL, and you're not reference counting then it may just be an enum of Pending/Loaded. In something like the OpenGL case, you'd produce a RemoveTexture event when you were done with a texture. – Derek Elkins Jul 24 '17 at 2:30
  • Since the game state is being used linearly (unless you are doing something unusual like Braid), there's not much gain in not leveraging mutable state where it simultaneously simplifies code and increases performance. So, I would probably have the TextureManager be a mutable object, and I'm saying this as a Haskell programmer. It would be updated just before the game loop body as all the input events were processed, and it would be used in a read-only manner by the game loop body. If speed, memory usage, and GC pauses aren't a concern, you can have it all be immutable. – Derek Elkins Jul 24 '17 at 2:35
2

You simply can't go seeking a design feature in a vacuum like this.

Want to maximize immutability without considering anything else? Fine, nothing can change state. The game start is the end and the score never changes. :P

Event driven systems trigger events when something changes state! Eliminating mutability is flat out silly.

Does that mean immutability is useless? No. I love my immutable shareable strings even in an event driven game.

What you should aim for is NOT sharing that which is mutable. Any time state info is being shared around that sharing should happen with something immutable. We call these defensive copies.

As for your null problem I'd solve it by keeping Texture private and in a good state. This way it can change atomically and never be caught in a bad state.

I was going to show you an example but honestly I'm not sure what language you're using.

  • Technically, you can write a recursive function that contains no immutable state (the state is stored on the stack during each recursion). But you're right; pursuing immutability just for the sake of immutability is probably a specious endeavor. The OP did say he was trying to improve stability. – Robert Harvey Jul 22 '17 at 17:31
  • I'm already not sharing what is mutable. And just because game state changes doesn't mean each component and asset of my system should be independently mutable. I'm looking for a way I could capture the side-effects and then apply them once in a single spot in the code. The game is implemented in Scala. – Samuel Jul 23 '17 at 20:00
0

This is kind of a roundabout and softcore alternative answer to the excellent ones provided, but the problem I find with event-handling combined with side effects is that it makes it difficult to reason about exactly what happens and relatively when (in relation to something else of note).

Event-handling is about the most unpredictable kind of control flow I've encountered, because it's difficult to know exactly when some piece of code will be executed without understanding the lowest level details of the system (which may include the operating system or third party libraries). The ideal is that it's not supposed to matter when such code is invoked, but when it's combined with tricky side effects that need to occur at a precise order and time (at least relatively with something else), it can be very difficult to reason about the correctness of the system and the changes you want to make to it.

Homogeneous Side Effects In Simple Loops

Meanwhile consider this:

for each texture in textures_to_unload:
     unload(texture)

That's pretty easy to reason about, yes? So that's the goal as I see it when mutability is required. We want to get the control flow as dead-simple as possible with a very homogeneous side effect occurring so that we can easily see what's going on and when in relation to something else (not necessarily in an absolute context since this might be occurring in different threads, but at least in relation to some other states and side effects).

I've found this strategy very helpful in making side effects easier to reason about, and I utilize it by gathering a deferred list of things to uniformly affect out of the complex control flows (cascading events, graph data structure traversal, a combo of both, etc), like a concurrent list of textures to remove instead of removing them right away. Then in a place where it's easy to reason about the correctness of removing textures, I loop through the list and remove them.

Also while it carries the overhead of gathering these lists/sets of things to uniformly process in a deferred fashion, it often opens up more opportunities to parallelize some hefty work.

"More Homogeneous" Event Queues

An alternative is to gather a concurrent queue of side-effect-causing events to perform related to textures, e.g., which isn't quite as easy to reason about since it's not so homogeneous in terms of the side effects it causes, though it might still be easier to reason about than causing the side effects directly in the original events, since it's at least "more homogeneous".

The events in that texture-related queue might at least relate only to side effects associated with textures and nothing else, and the events triggered would only relate to textures and nothing else. So it's at least a little more predictable than doing things to textures directly as a result of a user input event triggered by the OS which triggers a widget event which causes the scene graph to be traversed which causes scene graph events to be called of which one of them causes a texture to be removed only to find that the display context is not valid at the time.

You might use this strategy if there are relative order dependencies among the operations being deferred.

Is there a design I could apply here to reduce the mutability in my game? Ideally all mutations will be captured in a monad and applied each iteration of the game loop in a single spot in the code.

I would suggest if there are mutable designs involved that the goal isn't so much to make everything executed in a single spot but to make things executed in a more homogeneous/uniform and predictable fashion if you need side effects to occur.

For example, with OGL and DX at least, you cannot perform texture operations without a valid display context. That's a relative kind of order dependency: one thing must be in a certain state before the other thing can occur. So deferring processing related to textures can allow you to do all texture-related operations in a spot in code where you can more easily ensure and reason that a valid display context is available.

As another example an indexed mesh might require that polygons are removed or rebuilt to exclude those vertices before the vertices it indexes are removed. Again we have an order dependency, and deferring the removal of vertices to a place where we can more easily ensure that the polygons are no longer indexing those vertices can make the code much easier to reason about.

But you don't necessarily have to make all side effects in the entire system happen in one place to achieve that, and it's not necessarily so helpful to seek that, since then you have a boatload of non-homogeneous side effects interleaved in one place, and that too may not be so easy to reason about. This is assuming you're still dealing with a lot of mutable designs instead of going towards like a hardcore pure functional route revolving around immutable data types and persistent data structures. If you don't go all the way there, an easier goal that often helps things a whole lot is just to seek more homogeneous side effects being applied uniformly with very simple control flows in places where you can easily reason about the order in which things relatively occur.

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.