5
\$\begingroup\$

My game uses configuration files in JSON format. One of them is used for setting up control bindings, and it looks like this:

{
  "DisplaySettings": {
    "IsFullScreen": false,
    "PreferredBackBufferWidth": 800,
    "PreferredBackBufferHeight": 600
  },
  "GamePadSettings": {
    "Back": "Exit",
    "DPadDown": "MoveDown",
    "DPadLeft": "MoveLeft",
    "DPadRight": "MoveRight",
    "DPadUp": "MoveUp"
  },
  "KeyboardSettings": {
    "Escape": "Exit",
    "S": "MoveDown",
    "A": "MoveLeft",
    "D": "MoveRight",
    "W": "MoveUp"
  }
}

In the Main method of the Program class (my composition root), I read the file and hand it off to the Input class:

private static class Program
{
    private static void Main()
    {
        IFileHandler fileHandler = new JsonHandler();

        var settings = fileHandler.Read<Settings>( "settings.json" );
        var level = fileHandler.Read<Level>( "level.json" );
        var player = fileHandler.Read<Entity>( "player.json" );

        using ( var game = new MyGame( new Graphics( settings.DisplaySettings, level, player),
                                       new Input( settings.GamePadSettings, settings.KeyboardSettings ) ) )
        {
            game.Run();
            fileHandler.Write( "player.json", player );
            fileHandler.Write( "level.json", level );
        }
    }
}

fileHandler.Read<Settings> returns one of these (these are default values):

public class Settings
{
    public DisplaySettings DisplaySettings = new DisplaySettings();
    public GamePadSettings GamePadSettings = new GamePadSettings();
    public KeyboardSettings KeyboardSettings = new KeyboardSettings();
}
public class DisplaySettings
{
    public bool IsFullScreen = false;
    public int PreferredBackBufferWidth = 800;
    public int PreferredBackBufferHeight = 600;
}
public class GamePadSettings
{
    public string Back = "Exit";
    public string DPadDown = "MoveDown";
    public string DPadLeft = "MoveLeft";
    public string DPadRight = "MoveRight";
    public string DPadUp = "MoveUp";
}
public class KeyboardSettings
{
    public string Escape = "Exit";
    public string S = "MoveDown";
    public string A = "MoveLeft";
    public string D = "MoveRight";
    public string W = "MoveUp";
}

Obviously this object is injected into the Input class, which is injected into MyGame:

public class MyGame : Game
{
    private readonly IGraphics graphics;
    private readonly IInput input;

    public MyGame( IGraphics graphics, IInput input )
    {
        if ( graphics == null || input == null )
        {
            throw new ArgumentNullException();
        }

        this.graphics = graphics;
        this.input = input;

        graphics.Setup( this );
        input.Setup( this );

        Content.RootDirectory = "Content";
    }

    public void MoveDown()
    {
        // TODO: Collision detection
        graphics.Scroll( new Vector2( 0, 1 ) );
    }

    public void MoveLeft()
    {
        graphics.Scroll( new Vector2( -1, 0 ) );
    }

    public void MoveRight()
    {
        graphics.Scroll( new Vector2( 1, 0 ) );
    }

    public void MoveUp()
    {
        graphics.Scroll( new Vector2( 0, -1 ) );
    }

    // Other methods...
}

When MyGame is constructed, it calls the Setup method of the Input class, which creates a dictionary using reflection:

public class Input : IInput
{
    private readonly GamePadSettings gamePadSettings;
    private readonly KeyboardSettings keyboardSettings;
    private IDictionary<Buttons, Action> gamePadBindings;
    private IDictionary<Keys, Action> keyboardBindings;

    public Input( GamePadSettings gamePadSettings, KeyboardSettings keyboardSettings )
    {
        if ( gamePadSettings == null || keyboardSettings == null )
        {
            throw new ArgumentNullException();
        }

        this.gamePadSettings = gamePadSettings;
        this.keyboardSettings = keyboardSettings;
    }

    public void Setup( Game game )
    {
        if ( game == null )
        {
            throw new ArgumentNullException();
        }

        SetupGamePad( game );
        SetupKeyboard( game );
    }

    private void SetupGamePad( Game game )
    {
        gamePadBindings = new Dictionary<Buttons, Action>();

        AddButtonBinding( game, Buttons.Back, gamePadSettings.Back );
        AddButtonBinding( game, Buttons.DPadDown, gamePadSettings.DPadDown );
        AddButtonBinding( game, Buttons.DPadLeft, gamePadSettings.DPadLeft );
        AddButtonBinding( game, Buttons.DPadRight, gamePadSettings.DPadRight );
        AddButtonBinding( game, Buttons.DPadUp, gamePadSettings.DPadUp );
    }

    private void SetupKeyboard( Game game )
    {
        keyboardBindings = new Dictionary<Keys, Action>();

        AddKeyBinding( game, Keys.Escape, keyboardSettings.Escape );
        AddKeyBinding( game, Keys.S, keyboardSettings.S );
        AddKeyBinding( game, Keys.A, keyboardSettings.A );
        AddKeyBinding( game, Keys.D, keyboardSettings.D );
        AddKeyBinding( game, Keys.W, keyboardSettings.W );
    }

    private void AddButtonBinding( Game game, Buttons button, string methodName )
    {
        var method = FindMethod( game, methodName );

        if ( method == null )
        {
            return;
        }

        gamePadBindings.Add( button, () => method.Invoke( game, null ) );
    }

    private void AddKeyBinding( Game game, Keys key, string methodName )
    {
        var method = FindMethod( game, methodName );

        if ( method == null )
        {
            return;
        }

        keyboardBindings.Add( key, () => method.Invoke( game, null ) );
    }

    // Other methods...

    private static MethodInfo FindMethod( object obj, string methodName )
    {
        return obj.GetType().GetMethod( methodName,
                                        BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public );
    }
}

If you know how reflection works in .Net, then you know it's looking for the methods by name in the MyGame class. The Exit method is actually defined in the Game base class, which I don't control.

What does everyone think of this? I am considering moving the MoveDown, MoveLeft, etc. methods into another class, like IPhysics or something, where I would also do the collision detection. Since I haven't gotten that far yet, these methods live in MyGame for now.

EDIT: Initially I tried to focus this question as much as possible on the Input class, but I would be grateful for any advice.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I'm not sure about binding directly to methods based on a configuration file like this, because it means you have to be very careful about what methods does your Game type expose (assuming the player has access to the configuration file, which it probably does).

Consider adding a level of abstraction, either something like PlayerActions class, with methods that would just call the real methods, or maybe a Dictionary<string, Action>.

Also, I think player inputs should be handled by some kind of player object, not the game object directly. Otherwise, things like split-screen would be hard to implement.

Some specific notes:

  1. Public fields are usually discouraged. But the syntax for properties with default values is quite verbose. And it would be nice to make the types immutable by using private setters and constructors, but that could cause issues with deserialization. So in the end, I think public fields are okay here.

  2. Think about using dictionaries for the two input settings. That would allow you to avoid repetition in calling AddXxxBinding. Though it would also make setting the defaults less nice, so I'm not sure this would be actually better.

  3. I think it would make sense to save Game into a field of Input. That way, you don't have to pass it as a parameter all the time.

  4. There are several changes I would make to the AddButtonBinding() and AddKeyBinding() methods:

    1. The methods are almost the same. I would combine them into one generic method.

    2. Should bad method name really cause no action? I would at least log it somewhere, if you don't want to crash completely.

    3. You can use Delegate.CreateDelegate() instead of MethodInfo.Invoke and a lambda.

    With those changes, the method would look something like:

    private void AddBinding<TInput>(
        Game game, Dictionary<TInput, Action> bindings, TInput input, string methodName)
    {
        var method = FindMethod(game, methodName);
    
        if (method == null)
            throw new InvalidOperationException();
    
        bindings.Add(input, (Action)Delegate.CreateDelegate(typeof(Action), game, method));
    }
    
  5. Your FindMethod() searches for static methods too, but you're invoking the found method as an instance method. Because of that, I would remove BindingFlags.Static.

    Also, FindMethod() doesn't have much to do with input, it probably belongs to a separate class.

\$\endgroup\$
4
  • \$\begingroup\$ Good advice. I will consider each point carefully. About no. 3: Input is a field in Game - wouldn't adding Game as a field in Input cause a circular reference and/or a memory leak? As for no. 5: FindMethod exists in Input only as a "helper" method. Because it's so small, I don't see a real benefit to moving it. \$\endgroup\$ Feb 20, 2014 at 16:09
  • \$\begingroup\$ Yes, it would be a circular reference, but that doesn't cause memory leaks in .Net (or other environments with decent GC). \$\endgroup\$
    – svick
    Feb 20, 2014 at 16:20
  • \$\begingroup\$ I hate to get off-topic too much, but actually it would not be a circular reference. If Game had a Game property, or Input had an Input property, that would be circular. (I spent the last 37 minutes wracking my brain over this.) Anyways, you're right that it would make more sense to save Game into a field of Input. \$\endgroup\$ Feb 20, 2014 at 17:03
  • \$\begingroup\$ Circular reference means there is a cycle of references, and that cycle can be of any size, including 1 (GameGame) and 2 (GameInputGame). Such cycles are problem for simple reference counting GCs. \$\endgroup\$
    – svick
    Feb 20, 2014 at 17:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.