Search Unity

Input abstraction and game state

Discussion in 'Scripting' started by Member123456, Oct 21, 2014.

?

What do you use for your input system?

  1. Delegates/Events

    66.7%
  2. Messages/Notification Center

    4.2%
  3. Hard coded input in the classes that require them

    29.2%
  1. Member123456

    Member123456

    Joined:
    Oct 17, 2012
    Posts:
    237
    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
     
    Magiichan likes this.
  2. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    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?
     
    Magiichan likes this.
  3. GarthSmith

    GarthSmith

    Joined:
    Apr 26, 2012
    Posts:
    1,240
    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.
     
    TonyLi likes this.
  4. Member123456

    Member123456

    Joined:
    Oct 17, 2012
    Posts:
    237
    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?
    Code (CSharp):
    1. if (GameInput.GetButtonDown("Action"))
    2. {
    3.     // Do Something;
    4. }
    Instead of just handling the action press as an event that goes out to all things that act upon that key press?
     
  5. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,702
    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.
     
  6. PolymorphiK Games

    PolymorphiK Games

    Joined:
    Oct 5, 2014
    Posts:
    51
    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.
     
    GarthSmith likes this.
  7. GarthSmith

    GarthSmith

    Joined:
    Apr 26, 2012
    Posts:
    1,240
    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.
    Code (csharp):
    1. if (GameInput.GetButtonDown(ButtonCode.Action))
     
    PolymorphiK Games likes this.
  8. PolymorphiK Games

    PolymorphiK Games

    Joined:
    Oct 5, 2014
    Posts:
    51
    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.
     
  9. Member123456

    Member123456

    Joined:
    Oct 17, 2012
    Posts:
    237
  10. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    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.

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5.  
    6. using com.spacepuppy;
    7. using com.spacepuppy.Ui;
    8. using com.spacepuppy.Utils;
    9.  
    10. namespace com.apoc.Ui
    11. {
    12.  
    13.     public class ApocPlayerInputDevice : PlayerInputDevice
    14.     {
    15.         #region Fields
    16.  
    17.         public const float AXLE_BUTTON_DEADZONE = 0.5f;
    18.         public const float MOVEMENT_AXLE_DEADZONE = 0.2f;
    19.         public const float MOVEMENT_RADIAL_DEADZONE = 0.2f;
    20.         public const float SWORD_AXLE_DEADZONE = 0.2f;
    21.         public const float SWORD_RADIAL_DEADZONE = 0.2f;
    22.  
    23.         //InputManager ids
    24.         public const string HORIZONTAL = "Horizontal";
    25.         public const string VERTICAL = "Vertical";
    26.         public const string MOVEMENT = "Movement";
    27.         public const string JUMP = "Jump";
    28.         public const string GRAB = "Grab";
    29.         public const string KICK = "Kick";
    30.         public const string SWORDX = "SwordX";
    31.         public const string SWORDY = "SwordY";
    32.         public const string SWORDSWING = "SwordSwing";
    33.         public const string VAULTAIM = "VaultAim";
    34.  
    35.         public enum ApocInputs
    36.         {
    37.             Movement = 0,
    38.             Jump = 1,
    39.             Grab = 2,
    40.             Kick = 3,
    41.             SwordDirection = 4,
    42.             SwordSwing = 5,
    43.             VaultAim = 6
    44.         }
    45.  
    46.         public static string GetInputId(ApocInputs ei)
    47.         {
    48.             return ei.ToString();
    49.         }
    50.  
    51.         private ApocPlayerInputInfo _currentState;
    52.         private ApocPlayerInputInfo _lastState;
    53.  
    54.         #endregion
    55.  
    56.         #region CONSTRUCTOR
    57.  
    58.         public ApocPlayerInputDevice(string playerId)
    59.             : base(playerId)
    60.         {
    61.             this.InputSignatures.Add(new DualAxleInputSignature(GetInputId(ApocInputs.Movement), (int)ApocInputs.Movement, HORIZONTAL, VERTICAL)
    62.                                          {
    63.                                             AxleDeadZone = MOVEMENT_AXLE_DEADZONE,
    64.                                             AxleCutoff = DeadZoneCutoff.Scaled,
    65.                                             RadialDeadZone = MOVEMENT_RADIAL_DEADZONE,
    66.                                             RadialCutoff = DeadZoneCutoff.Scaled
    67.                                          });
    68.             this.InputSignatures.Add(new ButtonInputSignature(GetInputId(ApocInputs.Jump), (int)ApocInputs.Jump));
    69.             this.InputSignatures.Add(new AxleButtonInputSignature(GetInputId(ApocInputs.Grab), (int)ApocInputs.Grab) { AxisButtonDeadZone = AXLE_BUTTON_DEADZONE });
    70.             this.InputSignatures.Add(new ButtonInputSignature(GetInputId(ApocInputs.SwordSwing), (int)ApocInputs.SwordSwing));
    71.             this.InputSignatures.Add(new SwordInputSignature(ApocInputs.SwordDirection));
    72.             this.InputSignatures.Add(new AxleButtonInputSignature(GetInputId(ApocInputs.VaultAim), (int)ApocInputs.VaultAim) { AxisButtonDeadZone = AXLE_BUTTON_DEADZONE });
    73.         }
    74.  
    75.         #endregion
    76.  
    77.         #region Properties
    78.  
    79.  
    80.         #endregion
    81.  
    82.         #region IPlayerInputDevice Interface
    83.  
    84.         public override void Update()
    85.         {
    86.             base.Update();
    87.  
    88.             _lastState = _currentState;
    89.  
    90.             var info = new ApocPlayerInputInfo();
    91.  
    92.             var mv = this.GetCurrentDualAxleState((int)ApocInputs.Movement);
    93.             var swrd = this.GetCurrentDualAxleState((int)ApocInputs.SwordDirection);
    94.  
    95.             info.Movement = mv;
    96.             info.SwordPosition = swrd;
    97.             info.Jump = this.GetCurrentButtonState((int)ApocInputs.Jump);
    98.             info.Grab = this.GetCurrentButtonState((int)ApocInputs.Grab);
    99.             info.Kick = this.GetCurrentButtonState((int)ApocInputs.Kick);
    100.             info.SwordSwing = this.GetCurrentButtonState((int)ApocInputs.SwordSwing);
    101.             info.VaultAim = this.GetCurrentButtonState((int)ApocInputs.VaultAim);
    102.  
    103.             //done
    104.             _currentState = info;
    105.         }
    106.  
    107.         public ApocPlayerInputInfo GetCurrentState()
    108.         {
    109.             return _currentState;
    110.         }
    111.  
    112.         public ApocPlayerInputInfo GetLastState()
    113.         {
    114.             return _lastState;
    115.         }
    116.  
    117.         #endregion
    118.  
    119.  
    120.         #region Special Types
    121.  
    122.         private class SwordInputSignature : AbstractInputSignature, IDualAxleInputSignature
    123.         {
    124.  
    125.             #region Fields
    126.  
    127.             private Vector2 _current;
    128.  
    129.             #endregion
    130.  
    131.             #region CONSTRUCTOR
    132.  
    133.             public SwordInputSignature(ApocInputs hash)
    134.                 :base(GetInputId(hash), (int)hash)
    135.             {
    136.             }
    137.  
    138.             #endregion
    139.  
    140.             #region IDualAxleInputSignature Interface
    141.  
    142.             public Vector2 CurrentState
    143.             {
    144.                 get { return _current; }
    145.             }
    146.  
    147.             #endregion
    148.  
    149.             #region IInputSignature Interface
    150.  
    151.             public override void Update(PlayerInputDevice device)
    152.             {
    153.                 var bSwordSwingButton = Input.GetButton(SWORDSWING);
    154.                 var x = (bSwordSwingButton) ? Input.GetAxis(HORIZONTAL) : Input.GetAxis(SWORDX);
    155.                 var y = (bSwordSwingButton) ? Input.GetAxis(VERTICAL) : Input.GetAxis(SWORDY);
    156.                 _current = DeadZoneCutoffUtil.CutoffDualAxis(new Vector2(x, y), SWORD_AXLE_DEADZONE, DeadZoneCutoff.Scaled, SWORD_RADIAL_DEADZONE, DeadZoneCutoff.Scaled).normalized;
    157.             }
    158.  
    159.             #endregion
    160.  
    161.         }
    162.  
    163.         [System.Obsolete()]
    164.         private class GrappleInputSignature : AbstractInputSignature, IButtonInputSignature
    165.         {
    166.            
    167.             #region Fields
    168.  
    169.             private ButtonState _current;
    170.  
    171.             #endregion
    172.  
    173.             #region CONSTRUCTOR
    174.  
    175.             public GrappleInputSignature(ApocInputs hash)
    176.                 :base(GetInputId(hash), (int)hash)
    177.             {
    178.             }
    179.  
    180.             #endregion
    181.  
    182.             #region IButtonInputSignature Interface
    183.  
    184.             public ButtonState CurrentState
    185.             {
    186.                 get { return _current; }
    187.             }
    188.  
    189.             #endregion
    190.  
    191.             #region IInputSignature Interface
    192.  
    193.             public override void Update(PlayerInputDevice device)
    194.             {
    195.                 var s = _current; //last state
    196.                 _current = ButtonState.None;
    197.  
    198.                 var d = Input.GetAxis(GRAB);
    199.                 if (d == 0)
    200.                 {
    201.                     d = Input.GetAxis("GrabAlt");
    202.                     if (d < 0) d = 0;
    203.                 }
    204.  
    205.                 if (d > AXLE_BUTTON_DEADZONE)
    206.                 {
    207.                     switch (s)
    208.                     {
    209.                         case ButtonState.None:
    210.                         case ButtonState.Released:
    211.                             _current = ButtonState.Down;
    212.                             break;
    213.                         case ButtonState.Down:
    214.                         case ButtonState.Held:
    215.                             _current = ButtonState.Held;
    216.                             break;
    217.                     }
    218.                 }
    219.                 else
    220.                 {
    221.                     switch (s)
    222.                     {
    223.                         case ButtonState.None:
    224.                         case ButtonState.Released:
    225.                             _current = ButtonState.None;
    226.                             break;
    227.                         case ButtonState.Down:
    228.                         case ButtonState.Held:
    229.                             _current = ButtonState.Released;
    230.                             break;
    231.                     }
    232.                 }
    233.  
    234.             }
    235.  
    236.             #endregion
    237.  
    238.         }
    239.  
    240.         #endregion
    241.  
    242.     }
    243.  
    244. }
    245.  
    And the struct it feeds out for representing the input state:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3.  
    4. using com.spacepuppy;
    5. using com.spacepuppy.Ui;
    6. using com.spacepuppy.Utils;
    7.  
    8. namespace com.apoc.Ui
    9. {
    10.     public struct ApocPlayerInputInfo
    11.     {
    12.         public Vector2 Movement;
    13.         public Vector2 SwordPosition;
    14.         public ButtonState Jump;
    15.         public ButtonState Grab;
    16.         public ButtonState Kick;
    17.         public ButtonState SwordSwing;
    18.         public ButtonState VaultAim;
    19.        
    20.         public static ApocPlayerInputInfo Empty
    21.         {
    22.             get { return new ApocPlayerInputInfo(); }
    23.         }
    24.  
    25.  
    26.         public ButtonState GetButtonState(string sId)
    27.         {
    28.             switch (sId)
    29.             {
    30.                 case ApocPlayerInputDevice.JUMP:
    31.                     return this.Jump;
    32.                 case ApocPlayerInputDevice.GRAB:
    33.                     return this.Grab;
    34.                 case ApocPlayerInputDevice.KICK:
    35.                     return this.Kick;
    36.                 case ApocPlayerInputDevice.SWORDSWING:
    37.                     return this.SwordSwing;
    38.                 default:
    39.                     return ButtonState.None;
    40.             }
    41.         }
    42.  
    43.         public ButtonState GetButtonState(ApocPlayerInputDevice.ApocInputs input)
    44.         {
    45.             switch(input)
    46.             {
    47.                 case ApocPlayerInputDevice.ApocInputs.Jump:
    48.                     return this.Jump;
    49.                 case ApocPlayerInputDevice.ApocInputs.Grab:
    50.                     return this.Grab;
    51.                 case ApocPlayerInputDevice.ApocInputs.Kick:
    52.                     return this.Kick;
    53.                 case ApocPlayerInputDevice.ApocInputs.SwordSwing:
    54.                     return this.SwordSwing;
    55.                 case ApocPlayerInputDevice.ApocInputs.VaultAim:
    56.                     return this.VaultAim;
    57.                 default:
    58.                     return ButtonState.None;
    59.             }
    60.         }
    61.  
    62.     }
    63.    
    64. }
    65.  
     
  11. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    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/spacepupp...runk/SpacepuppyUnityFramework/Notification.cs

    https://code.google.com/p/spacepupp...uppyUnityFramework/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.
     
  12. Member123456

    Member123456

    Joined:
    Oct 17, 2012
    Posts:
    237
    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.
    Code (CSharp):
    1. using UnityEngine;
    2. using Rewired;
    3.  
    4. public class InputDispatcher : MonoBehaviour
    5. {
    6.     public int playerId;
    7.     private Player _player;
    8.  
    9.     private const string MOVELR = "MoveLR";
    10.     private const string MOVEFB = "MoveFB";
    11.     private const string FIRE = "Fire";
    12.    
    13.     void Awake()
    14.     {
    15.         _player = ReInput.players.GetPlayer(playerId);
    16.     }
    17.  
    18.     void FixedUpdate()
    19.     {
    20.         Movement();
    21.         FireButton();
    22.     }
    23.  
    24.     private void FireButton()
    25.     {
    26.         if (_player.GetButtonDown(FIRE))
    27.         {
    28.             this.DispatchGlobalEvent("OnFireButtonDown", null);
    29.         }
    30.         if (_player.GetButtonUp(FIRE))
    31.         {
    32.             this.DispatchGlobalEvent("OnFireButtonUp", null);
    33.         }
    34.     }
    35.  
    36.     private void Movement()
    37.     {
    38.         var leftStickX = _player.GetAxisRaw(MOVELR);
    39.         var leftStickY = _player.GetAxisRaw(MOVEFB);
    40.         if (leftStickX != 0.0f || leftStickY != 0.0f)
    41.         {
    42.             object[] paramsArray = new object[2] {leftStickX, leftStickY};
    43.             this.DispatchGlobalEvent("OnMovementUpdated", paramsArray);
    44.         }
    45.     }
    46. }
    47.  
    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.
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class PlayerController : MonoBehaviour
    4. {
    5.     void Awake()
    6.     {
    7.         this.AddGlobalEventListener("OnMovementUpdated", OnMovementUpdatedHandler);
    8.         this.AddGlobalEventListener("OnFireButtonDown", OnFireButtonDownHandler);
    9.         this.AddGlobalEventListener("OnFireButtonUp", OnFireButtonUpHandler);
    10.     }
    11.  
    12.     void OnDisable()
    13.     {
    14.         this.RemoveGlobalEventListener("OnMovementUpdated", OnMovementUpdatedHandler);
    15.         this.RemoveGlobalEventListener("OnFireButtonDown", OnFireButtonDownHandler);
    16.         this.RemoveGlobalEventListener("OnFireButtonUp", OnFireButtonUpHandler);
    17.     }
    18.  
    19.     private void OnMovementUpdatedHandler(IEventObject aEvent)
    20.     {
    21.         string inputX = aEvent.GetParam(0).ToString();
    22.         string inputY = aEvent.GetParam(1).ToString();
    23.  
    24.         Debug.Log("Input X: " + inputX);
    25.         Debug.Log("Input Y: " + inputY);
    26.     }
    27.  
    28.     private void OnFireButtonDownHandler(IEventObject aEvent)
    29.     {
    30.         Debug.Log("Fire Pressed!");
    31.     }
    32.  
    33.     private void OnFireButtonUpHandler(IEventObject aEvent)
    34.     {
    35.         Debug.Log("Fire Released!");
    36.     }
    37. }
    38.  
     
  13. guavaman

    guavaman

    Joined:
    Nov 20, 2009
    Posts:
    5,633
    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.