Input abstraction and game state

I am still fairly new to programming and I have now come upon something that is a little perplexing to me. I have my game setup with an InputHandler class, but that class is really almost useless the way I have it now, because it is tightly coupled to classes that require input.

Almost all good tutorials and projects out there capture and use input directly in the classes that require that information. I started doing some digging into how to better abstract out input in a way that its easily expandable, and I came across a bunch of resources, but most of them confuse the heck out of me. It is my understanding that the best way to go about this is some sort of event system, and whether its delegates/events or a messaging system seems to be up in the air.

Ultimately, I think it would be a great resource for this to be a high level discussion for new programmers like myself looking to get into proper coding practices. I know this also has a little something to do with state machines to handle the same inputs for different things.

Here is a sample of what I’m trying to do in my own project.

  • Receive data from axis and send that axis data out

  • Move the player when not in a menu

  • Move to different menu areas when the menu is open, and the player remains still

  • Get a key as an interaction/confirm button

  • Use this button when you can move the player to interact with something

  • Use this button when in menus to confirm a selection

  • Get a key for attack/cancel

  • When you control the player the button causes an attack

  • When you are in the menu, the button cancels back to the previous page

1 Like

I agree this would be a good topic, but it’s a good one for the Scripting forum. You’re asking implementation questions, which relate to code design, but not game design.

Moderators, is it possible to simply move a thread into the right forum? Or do you have to lock it and ask the OP to repost elsewhere?

1 Like

We use a GameInput class that has static functions GetButton, GetButtonDown, GetButtonUp. It’s laid out similarly to Unity’s Input class. The only difference is that GameInput is all in game terms. We have Jump, Action, Run, Ok, Cancel, Pause, etc…

Inside GameInput, we check Unity’s Input class, InControl, and for mobile HUD buttons. Nothing else in our game knows about these things though, so all our input is abstracted in the GameInput class.

This way, everything in our game can check to see if “Jump” was pressed, but we only have to change code in a single class, GameInput, to change the actual controls.

1 Like

So you are not using events or messages at all? This reads to me like you just abstract out the input so it’s easy to change the key bindings. I’m assuming your classes that require input though all still know about the GameInput class. So you use something like this?

if (GameInput.GetButtonDown("Action"))
{
    // Do Something;
}

Instead of just handling the action press as an event that goes out to all things that act upon that key press?

You can also map AI control (instead of physical input devices) to GameInput to let an AI control a unit. Brett Laming wrote a good article on this in AI Wisdom 4. AIGameDev.com has the slides from a presentation he did.

I structure the inputs into 1 static class (InputManager), 1 abstract class (GameController) and X amount of controllers (KeyboardController, XBOXController, MobileController etc…). InputManager is responsible for maintaining the delegates/events subscriptions for any object that requires to know when a key or something has been pressed (mainly the player). Then I have an abstract class called GameController which any class can inherit from. This is great because KeyboardController, XBOXController, MobileController etc… in the most basic sense do the same thing. The only difference is the detection those inputs vary dramatically which is why they are separated into their own scripts.

At runtime, I generate an XML file called OptionData.xml which holds the string of the controller to be used. I have a .asset in unity that holds a string id, and GameObject prefab where I can Instantiate the type of controller required. For instance, in the OptionData.xml the default values for the playerController string field will depend on the system it is detecting. If its in an iOS or Android it will set the playerController to MobileController if its a a standalone it will set it to KeybaordController. The main menu for Standalone only, there is an option to set the playerController to XBOXController if they have the XBOXController setup with their computer. On mobile this option does show up, instead they have the option to tweak setting for the joystick / use screen triggers etc.

A great deal and thought went into going for Inputs and their organizations, this is needed if you want your game to be well engineered. This approach does take a bit longer to setup, but once it is setup you can just add it to any game as it is independent of the game. All it cares about is input, then depending on the input whatever object (mainly player) gets executed will do something like jump or walk or run etc. I have tested this with 3D and 2D and works just fine with minor very minor tweaks, this is usually just commenting out code as some 3D inputs don’t work for 2D games.

1 Like

Yup, that’s exactly how we do it. Actually, we have something very similar to @Polymorphik where we can plug in different types of input into GameInput. We have separate classes that map controller inputs, keyboard inputs, or mobile hud button inputs to handle whatever kind of input device we desire. Only GameInput knows about specific input implementations.

Everything else in the game only knows about GameInput without knowing about the original input source.

EDIT: Instead of strings we use a bitflag enum to describe the different types of inputs. This is similar to KeyCode used with Unity’s Input class.if (GameInput.GetButtonDown(ButtonCode.Action))

1 Like

Same here, KeyCode is the best way we also built a KeyCode listener class so we can remap the codes for the input based on the player’s preference. This includes the mouse and keyboard as well as joysticks.

I found this. It looks like one of the simplest pre-built event systems that I could plug all kinds of events into, including input. What do you think?

I also have a wrapper around the unity Input system. I call it a ‘InputDevice’ and it is updated on every tick where it stores a struct representation of the current state. I always keep a copy of the current and last state in the InputDevice.

There is a method to get either the current or last state.

I also have an InputManager that all input devices get put into. The multiple instances can represent various players, or various modes, or what not.

The InputDevice has a collection attached to it of all the inputs available. There is a special interface for this called ‘IInputSignature’, of which there is the simple forwarding signature that wraps directly around the unity Input class. But there is others such as ‘AnalogButtonSimulator’ that makes an analog trigger or stick act like a on/off dual state button. Others like ComboInputSignature that represent a combo that needs to be pressed to register an input. Such as 2 buttons at the same time, or a sequence of buttons in some order.

Here you can see the specific implementation for the player controls in a game I’m currently working on. Not in the constructor how I set up all the input signatures. And at the bottom I have to custom IInputSignatures specific to this controller.

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

using com.spacepuppy;
using com.spacepuppy.Ui;
using com.spacepuppy.Utils;

namespace com.apoc.Ui
{

    public class ApocPlayerInputDevice : PlayerInputDevice
    {
        #region Fields

        public const float AXLE_BUTTON_DEADZONE = 0.5f;
        public const float MOVEMENT_AXLE_DEADZONE = 0.2f;
        public const float MOVEMENT_RADIAL_DEADZONE = 0.2f;
        public const float SWORD_AXLE_DEADZONE = 0.2f;
        public const float SWORD_RADIAL_DEADZONE = 0.2f;

        //InputManager ids
        public const string HORIZONTAL = "Horizontal";
        public const string VERTICAL = "Vertical";
        public const string MOVEMENT = "Movement";
        public const string JUMP = "Jump";
        public const string GRAB = "Grab";
        public const string KICK = "Kick";
        public const string SWORDX = "SwordX";
        public const string SWORDY = "SwordY";
        public const string SWORDSWING = "SwordSwing";
        public const string VAULTAIM = "VaultAim";

        public enum ApocInputs
        {
            Movement = 0,
            Jump = 1,
            Grab = 2,
            Kick = 3,
            SwordDirection = 4,
            SwordSwing = 5,
            VaultAim = 6
        }

        public static string GetInputId(ApocInputs ei)
        {
            return ei.ToString();
        }

        private ApocPlayerInputInfo _currentState;
        private ApocPlayerInputInfo _lastState;

        #endregion

        #region CONSTRUCTOR

        public ApocPlayerInputDevice(string playerId)
            : base(playerId)
        {
            this.InputSignatures.Add(new DualAxleInputSignature(GetInputId(ApocInputs.Movement), (int)ApocInputs.Movement, HORIZONTAL, VERTICAL)
                                         {
                                            AxleDeadZone = MOVEMENT_AXLE_DEADZONE, 
                                            AxleCutoff = DeadZoneCutoff.Scaled,
                                            RadialDeadZone = MOVEMENT_RADIAL_DEADZONE,
                                            RadialCutoff = DeadZoneCutoff.Scaled
                                         });
            this.InputSignatures.Add(new ButtonInputSignature(GetInputId(ApocInputs.Jump), (int)ApocInputs.Jump));
            this.InputSignatures.Add(new AxleButtonInputSignature(GetInputId(ApocInputs.Grab), (int)ApocInputs.Grab) { AxisButtonDeadZone = AXLE_BUTTON_DEADZONE });
            this.InputSignatures.Add(new ButtonInputSignature(GetInputId(ApocInputs.SwordSwing), (int)ApocInputs.SwordSwing));
            this.InputSignatures.Add(new SwordInputSignature(ApocInputs.SwordDirection));
            this.InputSignatures.Add(new AxleButtonInputSignature(GetInputId(ApocInputs.VaultAim), (int)ApocInputs.VaultAim) { AxisButtonDeadZone = AXLE_BUTTON_DEADZONE });
        }

        #endregion

        #region Properties


        #endregion

        #region IPlayerInputDevice Interface

        public override void Update()
        {
            base.Update();

            _lastState = _currentState;

            var info = new ApocPlayerInputInfo();

            var mv = this.GetCurrentDualAxleState((int)ApocInputs.Movement);
            var swrd = this.GetCurrentDualAxleState((int)ApocInputs.SwordDirection);

            info.Movement = mv;
            info.SwordPosition = swrd;
            info.Jump = this.GetCurrentButtonState((int)ApocInputs.Jump);
            info.Grab = this.GetCurrentButtonState((int)ApocInputs.Grab);
            info.Kick = this.GetCurrentButtonState((int)ApocInputs.Kick);
            info.SwordSwing = this.GetCurrentButtonState((int)ApocInputs.SwordSwing);
            info.VaultAim = this.GetCurrentButtonState((int)ApocInputs.VaultAim);

            //done
            _currentState = info;
        }

        public ApocPlayerInputInfo GetCurrentState()
        {
            return _currentState;
        }

        public ApocPlayerInputInfo GetLastState()
        {
            return _lastState;
        }

        #endregion


        #region Special Types

        private class SwordInputSignature : AbstractInputSignature, IDualAxleInputSignature
        {

            #region Fields

            private Vector2 _current;

            #endregion

            #region CONSTRUCTOR

            public SwordInputSignature(ApocInputs hash)
                :base(GetInputId(hash), (int)hash)
            {
            }

            #endregion

            #region IDualAxleInputSignature Interface

            public Vector2 CurrentState
            {
                get { return _current; }
            }

            #endregion

            #region IInputSignature Interface

            public override void Update(PlayerInputDevice device)
            {
                var bSwordSwingButton = Input.GetButton(SWORDSWING);
                var x = (bSwordSwingButton) ? Input.GetAxis(HORIZONTAL) : Input.GetAxis(SWORDX);
                var y = (bSwordSwingButton) ? Input.GetAxis(VERTICAL) : Input.GetAxis(SWORDY);
                _current = DeadZoneCutoffUtil.CutoffDualAxis(new Vector2(x, y), SWORD_AXLE_DEADZONE, DeadZoneCutoff.Scaled, SWORD_RADIAL_DEADZONE, DeadZoneCutoff.Scaled).normalized;
            }

            #endregion

        }

        [System.Obsolete()]
        private class GrappleInputSignature : AbstractInputSignature, IButtonInputSignature
        {
           
            #region Fields

            private ButtonState _current;

            #endregion

            #region CONSTRUCTOR

            public GrappleInputSignature(ApocInputs hash)
                :base(GetInputId(hash), (int)hash)
            {
            }

            #endregion

            #region IButtonInputSignature Interface

            public ButtonState CurrentState
            {
                get { return _current; }
            }

            #endregion

            #region IInputSignature Interface

            public override void Update(PlayerInputDevice device)
            {
                var s = _current; //last state
                _current = ButtonState.None;

                var d = Input.GetAxis(GRAB);
                if (d == 0)
                {
                    d = Input.GetAxis("GrabAlt");
                    if (d < 0) d = 0;
                }

                if (d > AXLE_BUTTON_DEADZONE)
                {
                    switch (s)
                    {
                        case ButtonState.None:
                        case ButtonState.Released:
                            _current = ButtonState.Down;
                            break;
                        case ButtonState.Down:
                        case ButtonState.Held:
                            _current = ButtonState.Held;
                            break;
                    }
                }
                else
                {
                    switch (s)
                    {
                        case ButtonState.None:
                        case ButtonState.Released:
                            _current = ButtonState.None;
                            break;
                        case ButtonState.Down:
                        case ButtonState.Held:
                            _current = ButtonState.Released;
                            break;
                    }
                }

            }

            #endregion

        }

        #endregion

    }

}

And the struct it feeds out for representing the input state:

using UnityEngine;

using com.spacepuppy;
using com.spacepuppy.Ui;
using com.spacepuppy.Utils;

namespace com.apoc.Ui
{
    public struct ApocPlayerInputInfo
    {
        public Vector2 Movement;
        public Vector2 SwordPosition;
        public ButtonState Jump;
        public ButtonState Grab;
        public ButtonState Kick;
        public ButtonState SwordSwing;
        public ButtonState VaultAim;
       
        public static ApocPlayerInputInfo Empty
        {
            get { return new ApocPlayerInputInfo(); }
        }


        public ButtonState GetButtonState(string sId)
        {
            switch (sId)
            {
                case ApocPlayerInputDevice.JUMP:
                    return this.Jump;
                case ApocPlayerInputDevice.GRAB:
                    return this.Grab;
                case ApocPlayerInputDevice.KICK:
                    return this.Kick;
                case ApocPlayerInputDevice.SWORDSWING:
                    return this.SwordSwing;
                default:
                    return ButtonState.None;
            }
        }

        public ButtonState GetButtonState(ApocPlayerInputDevice.ApocInputs input)
        {
            switch(input)
            {
                case ApocPlayerInputDevice.ApocInputs.Jump:
                    return this.Jump;
                case ApocPlayerInputDevice.ApocInputs.Grab:
                    return this.Grab;
                case ApocPlayerInputDevice.ApocInputs.Kick:
                    return this.Kick;
                case ApocPlayerInputDevice.ApocInputs.SwordSwing:
                    return this.SwordSwing;
                case ApocPlayerInputDevice.ApocInputs.VaultAim:
                    return this.VaultAim;
                default:
                    return ButtonState.None;
            }
        }

    }
   
}

I personally wrote my own Notification system that was semi-inspired by the Actionscript3 event system. But I made it a bit more strongly typed.

I wrote an article about it a while back on my website:
http://jupiterlighthousestudio.com/spacepuppy_unity_framework_and_unity_notification_system/

But I changed around the internal implementation a bit since the article was written (note the ‘edit’ at the end of the article). The interface of the whole thing is identical so it remained compatible through the change, it’s just the implementation was altered to make it more efficient (I got a 10 times increase in speed).

Latest version of the Notification class, and the NotificationDispatcher addition that was added as implementation to speed things up.

https://code.google.com/p/spacepuppy-unity-framework/source/browse/trunk/SpacepuppyUnityFramework/Notification.cs

https://code.google.com/p/spacepuppy-unity-framework/source/browse/trunk/SpacepuppyUnityFramework/INotificationDispatcher.cs

Note it’s designed to also consider what I call my ‘entity model’. Which I describe here:
http://jupiterlighthousestudio.com/organizing-a-group-of-gameobjects-in-unity/

Basically I flag some parent GameObject as the ‘root’ of some entity. And I defined several methods to quickly navigate around the hierarchy of that ‘root’. The way the Notification system integrates is that you can dispatch the Notification not just from the GameObject it originated from, but from its root as well. So then you can just register to listen from the root with out having to listen to the specific object. This is great for things like hitboxes and weapons, where you don’t know where in the hierarchy it is, and it varies from entity to entity, so you just listen to the root for if the hitbox was hit.

Well I ended up with something really nice here. All of the options above still kind of confused me, but thats just because im still really new to programming. Using Epix Events I was easily able to create the rough outline of what input events will look like. Really this package saved my life due to it’s simplicity. I’m also using Rewired to handle input from multiple devices. It looks something like this. Let me know if you would improve it any. Thanks!

InputDispatcher class that gets the input and outputs the events with parameters if needed.

using UnityEngine;
using Rewired;

public class InputDispatcher : MonoBehaviour
{
    public int playerId;
    private Player _player;

    private const string MOVELR = "MoveLR";
    private const string MOVEFB = "MoveFB";
    private const string FIRE = "Fire";
   
    void Awake()
    {
        _player = ReInput.players.GetPlayer(playerId);
    }

    void FixedUpdate()
    {
        Movement();
        FireButton();
    }

    private void FireButton()
    {
        if (_player.GetButtonDown(FIRE))
        {
            this.DispatchGlobalEvent("OnFireButtonDown", null);
        }
        if (_player.GetButtonUp(FIRE))
        {
            this.DispatchGlobalEvent("OnFireButtonUp", null);
        }
    }

    private void Movement()
    {
        var leftStickX = _player.GetAxisRaw(MOVELR);
        var leftStickY = _player.GetAxisRaw(MOVEFB);
        if (leftStickX != 0.0f || leftStickY != 0.0f)
        {
            object[] paramsArray = new object[2] {leftStickX, leftStickY};
            this.DispatchGlobalEvent("OnMovementUpdated", paramsArray);
        }
    }
}

A simple script called PlayerController which listens for the input events. As you can see the parameters I pass in from the left stick in the dispatcher class can be used here in a sample movement method.

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    void Awake()
    {
        this.AddGlobalEventListener("OnMovementUpdated", OnMovementUpdatedHandler);
        this.AddGlobalEventListener("OnFireButtonDown", OnFireButtonDownHandler);
        this.AddGlobalEventListener("OnFireButtonUp", OnFireButtonUpHandler);
    }

    void OnDisable()
    {
        this.RemoveGlobalEventListener("OnMovementUpdated", OnMovementUpdatedHandler);
        this.RemoveGlobalEventListener("OnFireButtonDown", OnFireButtonDownHandler);
        this.RemoveGlobalEventListener("OnFireButtonUp", OnFireButtonUpHandler);
    }

    private void OnMovementUpdatedHandler(IEventObject aEvent)
    {
        string inputX = aEvent.GetParam(0).ToString();
        string inputY = aEvent.GetParam(1).ToString();

        Debug.Log("Input X: " + inputX);
        Debug.Log("Input Y: " + inputY);
    }

    private void OnFireButtonDownHandler(IEventObject aEvent)
    {
        Debug.Log("Fire Pressed!");
    }

    private void OnFireButtonUpHandler(IEventObject aEvent)
    {
        Debug.Log("Fire Released!");
    }
}

Hi Zionmoose,

I know this thread is a bit old, but if you’re not aware, Rewired has an event based system you can use instead having to poll for input like you are doing in your example. See HowTo’s - Getting Input from the docs for an example.