Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Eliminating coupling and global state in the "Game" singleton

I'm working on a game (writing my own physics engine), and trying to follow good design in writing it. Recently, I've been running into many hard-to-find bugs, and in order to catch these bugs easier, I've been trying to write unit-tests. This has been difficult to do, due to the fact that a lot of my components are tightly coupled, especially to the game module.

In my game module I export a singleton class instance, which holds the current game state, time, game events, etc. However, after reading this and through researching how to reduce this coupling, I've decided to try and rewrite the class such that it is no longer a singleton.

The idea is to use a GameState class, and pass around these objects everywhere, so that unit-tests can create minimal states for tests. Most functions then just become a function of game state, and return a new game state. However, I've run into some design issues:

My entity objects' position and velocity are python properties that are calculated according to the current time. This means that I can't simply pass in a GameState object without rewriting this as functions (resulting in icky syntax). Code example:

class Entity:
    @property
    def position(self):
        """Interpolate the position from the last known position from that time."""
        # Here, the game class instance is referenced directly, creating coupling.
        dt = game.game_time - self._last_valid_time
        # x_f = x_i + v_i*t + 1/2 at^2
        return self._position + self._velocity * dt + .5 * self._acceleration * dt**2

I've done some research on how to resolve this, and ran across Dependency Injection. Essentially, I could pass a GameState 'getter' object into the initializer of every entity. All GameState 'getter' objects would simply return the current state. Example GameStateGetter class:

class GameStateGetter:
    _CurrentState = None

    def update(self, new_state):
        GameStateGetter._CurrentState = new_state

    def __getattr__(self, name):
        # GameState object properties are immutable, so this can't cause issues
        return getattr(GameStateGetter._CurrentState, name)

Now for my questions.

  • Would using GameState 'getter' objects even be a good idea?

One problem would be the interface for updating the current game state (I have defined an update method, but this seems like a strange interface). Also, given that the problem with global variables is unpredictable program state, this wouldn't really prevent that.

  • Is there another way to "inject" the game dependency to the Entity class's position property?

Ideally, I want to keep things simple. A GameStateGetter class sounds very abstract (even if the implementation is simple). It would be nice if my properties could implicitly pass the current state.

Thanks in advance for any help you can provide!

like image 436
Casey Kuball Avatar asked Nov 05 '22 00:11

Casey Kuball


1 Answers

Injecting a GameStateProvider into the Entity constructor is a pretty simple place to start, and it leaves you with something that can be refactored as your logic becomes more complex.

There's no need to update eachEntity with a new state when the state changes, though. If the provider's an object, it's mutable by default. (Why would you want something that changes all the time, the game state, to be immutable, anyway?) You can change the current_time attribute on the provider and then any time something gets position it will contain the correct value.

The pattern looks like this:

class Entity(object):

   def __init__(self, game_state_provider):
      self.provider = game_state_provider

   @property
   def position(self):
      # lazily evaluate position as a function of the current time
      if self._last_valid_time == self.provider.current_time:
         return self._position
      self._last_valid_time = self.provider.current_time
      self._position = // insert physics here
      return self._position
like image 50
Robert Rossney Avatar answered Nov 09 '22 08:11

Robert Rossney