Tactics RPG User Input Controller

In this lesson we will be writing a component to manage user input. We will work with Unity’s Input Manager so that your game should work across a variety of input devices (keyboard, controller, etc). The component we write will be reusable so that any script requiring input can receive and act on these events.

Custom Event Args

It is common to use an EventHandler when posting an event. Using this delegate pattern you must pass along the sender of the event, and an EventArgs (or subclass) as well. When we post input events, it is handy to pass along information such as what button was pressed, or what direction is the user trying to apply. Most of the time, all I ever need to pass is a single field of data. Rather than creating a custom subclass of EventArgs for each occasion, we can create a generic version. Create a new folder within the Scripts folder called EventArgs. Then create a script within this folder called InfoEventArgs and use the following implementation:

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

public class InfoEventArgs<T> : EventArgs 
{
	public T info;
	
	public InfoEventArgs() 
	{
		info = default(T);
	}
	
	public InfoEventArgs (T info)
	{
		this.info = info;
	}
}

This is a pretty simple class which can hold a single field of any data type named info. I created two constructors, an empty one which inits itself using the default keyword (this keyword handles both reference and value types), and one which allows the user to specify the intial value.

Unity’s Input Manager

Unity provides an Input Manager to help simplify the… well, the management of input – that was obvious. From the menu bar choose Edit->Project Settings->Input. Look in the inspector and you will be able to see the various mappings of input concepts to input mechanisms. Expand the Axes (if it isnt already open) and you should see several entries such as: “Horizontal”, “Fire1”, and “Jump”. There are actually several entries for most. One entry for “Horizontal” monitors keyboard input from the arrow keys or the ‘a’ and ‘d’ keys. Another entry for “Horizontal” monitors keyboard input for Joystick axis input. In your code, you can check if there is “Horizontal” input from any of those sources with a single reference to that name.

Unity has done most of the heavy lifting for us, however, one of my own personal complaints with this manager (and several of their other systems) is a lack of support for events. You must check the status of Input every frame (through an Update method or Coroutine) in order to make sure you dont miss anything. As you may have guessed, this is not terribly efficient, and can be a bit cumbersome to re-implement everywhere you need input. Therefore, I will do this process only once, and then share the results via events with any other interested script.

Create another subfolder of Scripts called Controller. Inside this folder create our script, InputController.cs and open it for editing.

We will be using the “Horizontal” and “Vertical” inputs for a variety of things such as moving the tile selection cursor around the board (to select a move location or attack target) or to change the selected item in a UI menu. As I mentioned before, we will need to check for input on every frame, so let’s go ahead and take advantage of the Update method. Add the following code to your script:

void Update () 
{
	Debug.Log(Input.GetAxis("Horizontal"));
}

Save your script, attach it to any gameobject in a new scene, and press play. Every frame, a new debug log will print to the console (Make sure Collapse is disabled so they appear in the correct time-wise order). Watch what happens to the value when your press the left or right arrow keys, or the ‘a’ and ‘d’ keys.

Pressing right or ‘d’ causes the output to raise toward positive one, and pressing left or ‘a’ causes the output to lower toward negative one. If you aren’t pressing in either direction, the output will ease back to zero. With this function, Unity has smoothed the input for us. If I were making a game where a character could move freely through the world such as an FPS, then that easing would help movement look a little more natural.

For our game, I don’t want any of the smoothing on directional input. Since we are snapping to cells on a board or between menu options, etc. a very obvious on/off tap of a button will be better for us. In this case there is another method we can try:

void Update () 
{
	Debug.Log(Input.GetAxisRaw("Horizontal"));
}

Save the script and run the scene again. Now the keyboard presses result in jumps immediately from zero to one or negative one depending on the direction you press.

You may have noticed that some games allow input both through pressing, and through holding. For example, as soon as I press an arrow key, the tile might move on the board. If I keep holding the arrow, after a short pause, the tile might continue moving at a semi-quick rate.

I want to add this “repeat” functionality to our script, but since I will need it for multiple axis, it makes sense to track each one as a separate object so that we can reuse our code. I will add another class inside this script – normally I dont like to do that, but this second class is private and will only be used by our input controller, so it is an exception.

class Repeater
{
	const float threshold = 0.5f;
	const float rate = 0.25f;
	float _next;
	bool _hold;
	string _axis;

	public Repeater (string axisName)
	{
		_axis = axisName;
	}

	public int Update ()
	{
		int retValue = 0;
		int value = Mathf.RoundToInt( Input.GetAxisRaw(_axis) );

		if (value != 0)
		{
			if (Time.time > _next)
			{
				retValue = value;
				_next = Time.time + (_hold ? rate : threshold);
				_hold = true;
			}
		}
		else
		{
			_hold = false;
			_next = 0;
		}

		return retValue;
	}
}

At the top of the Repeater class I defined two const values. The threshold value determines the amount of pause to wait between an intial press of the button, and the point at which the input will begin repeating. The rate value determines the speed that the input will repeat.

Next, I added a few private fields. I use _next to mark a target point in time which must be passed before new events will be registered – it defaults and resets to zero, so that the first press is always immediately registered. I use _hold to indicate whether or not the user has continued pressing the same button since the last time an event fired. Finally, I use _axis to store the axis that will be monitored through Unity’s Input Manager. This value is assigned via the class constructor.

After the constructor, I have an Update method. Note that this class is not a MonoBehaviour, so the Update method wont be triggered by Unity – we will be calling it manually. The method returns an int value, which will either be -1, 0, or 1. Values of zero indicate that either the user is not pressing a button, or that we are waiting for a repeat event.

Inside the Update method, I declare a local variable called retValue which is the value which will be returned from the function. It will only change from zero under special circumstances. Next we get the value this object is tracking from the Unity’s Input Manager using GetAxisRaw as we did earlier. I put the method inside of another method which rounds the result and casts it to an int value type.

The if condition basically asks if there is user input or not. When the value field is not zero the user is providing input. Inside this body we do another if condition check which verifies that sufficient time has passed to allow an input event. On the first press of a button, Time.time will always be greater than _next which will be zero at the time. Inside of the inner condition body, we set the retValue to match the value reported by the Input Manager, and then set our time target to the current time plus an additional amount of time to wait. This means that subsequent calls into this method will not pass the inner condition check until some time in the future. Some of you may not be familiar with the conditional operator (?:) used here – it is very similar to an if condition where the condition to validate is to the left of the question mark, the value to the right of the question mark is used when the condition is true, and the value after the colon is used when the condition is false. Finally, I mark _hold as being true.

The first (outer) if condition has an else clause – whenever the user is NOT providing input, this will mark our _hold value as false and reset the time for future events to zero so that they can immediately fire with the next press of the button.

Now its time to put our Repeater class to good use. Add two fields inside the InputController class as follows:

Repeater _hor = new Repeater("Horizontal");
Repeater _ver = new Repeater("Vertical");

Whenever our Repeaters report input, I will want to share this as an event. I will make it static so that other scripts merely need to know about this class and not its instances. We will implement this EventHandler using generics so that we can specify the type of EventArgs – we will use our InfoEventArgs and specify its type as a Point. Don’t forget that you will need to add a using statement for the System namespace in order to use the EventHandler.

public static event EventHandler<InfoEventArgs<Point>> moveEvent;

We will need to tie our repeaters into Unity’s Update loop, and actually fire the event we just declared at the appropriate time:

void Update () 
{
	int x = _hor.Update();
	int y = _ver.Update();
	if (x != 0 || y != 0)
	{
		if (moveEvent != null)
			moveEvent(this, new InfoEventArgs<Point>(new Point(x, y)));
	}
}

Next I want to add events which watch for the various Fire button presses. I don’t need these to repeat, because I wont consider the input as complete until it is actually released. I will use one Fire button for confirmation, one for cancellation and will add a third, just in case I think of a reason to have it.

The event we send for these will also use InfoEventArgs but instead of passing a Point struct for direction, it will just pass an int representing which Fire button was pressed

public static event EventHandler<InfoEventArgs<int>> fireEvent;

Add a string array to your class to hold the buttons you wish to check for:

string[] _buttons = new string[] {"Fire1", "Fire2", "Fire3"};

In our Update loop after we check for movement, let’s add the following to loop through each of our Fire button checks:

for (int i = 0; i < 3; ++i)
{
	if (Input.GetButtonUp(_buttons[i]))
	{
		if (fireEvent != null)
			fireEvent(this, new InfoEventArgs<int>(i));
	}
}

Using Our Input Controller

Now that we’ve completed the Input Controller, let’s test it out. Create a temporary script somewhere in your project and add it to an object in the scene. You will also need to make sure to add the Input Controller to an object in the scene. I created a script called Demo in the root of the Scripts folder.

I usually connect to events in OnEnable and disconnect from events in OnDisable. Remember that cleanup is very important – particularly when using static events, because they maintain strong references to your objects. This means they keep the objects from going out of scope and being truly destroyed, and could for example trigger events on scripts whose GameObject’s are destroyed.

void OnEnable ()
{
	InputController.moveEvent += OnMoveEvent;
	InputController.fireEvent += OnFireEvent;
}

void OnDisable ()
{
	InputController.moveEvent -= OnMoveEvent;
	InputController.fireEvent -= OnFireEvent;
}

When you have added statements like this, but have not yet implemented the handler, you can have MonoDevelop auto-implement them for you with the correct signatures. Right-click on the OnMoveEvent and then choose Refactor->Create Method. A line will appear indicating where the implementation will be inserted which you can move up or down with the arrow keys, and then confirm the placement by hitting the return key. You should see something like the following:

void OnMoveEvent (object sender, InfoEventArgs<Point> e)
{
	throw new System.NotImplementedException ();
}

I dont want to crash the program so I will replace the throw exception statement with a simple Debug Log indicating the direction of the input.

Debug.Log("Move " + e.info.ToString());

Use the same trick to implement the OnFireEvent handler and use a Debug message to indicate which button index was used.

Debug.Log("Fire " + e.info);

Run the scene and trigger input and watch the console to verify that everything works.

Summary

In this lesson we reviewed making a custom, generic subclass of EventArgs to use with our EventHandler based events. We discussed Unity’s Input Manager and how it can provide unified input across multiple devices, and then we wrapped it up with a new Controler class to listen for special input events specific to our game. In the end I showed a simple implementation that would listen to the events and take an action.

25 thoughts on “Tactics RPG User Input Controller

  1. Suppose we don’t pass in a Point for the moveEvent, but instead want to handle a forward/back/left/right input based on the Unity Input Manager axes (horizontal and vertical). How would that be done?

    1. I suppose I would define an enum with the various directions you listed, and then pass a type of the enum based on the axis values. For example, if the ‘x’ value was greater than a certain threshold, I would pass along “right”, or if the ‘y’ value was greater than a certain threshold, I would pass along “up”.

      As an alternative to the enum, you could also pass along a KeyCode value such as “UpArrow”, see the docs:
      http://docs.unity3d.com/ScriptReference/KeyCode.html

      1. Just wondering – would declaring bools for each direction and trying to pass them in instead of an enum be bad practice? Sorry, I’m new to events and wondering why you’d choose enums.

        I’m also curious where I’m going wrong in trying to pass in an enum.

        moveEvent(this, new InfoEventArgs(MovementDirs.Left));

        MovementDirs is a public enum.

        1. I wouldn’t declare a bool for each direction because it just feels wasteful. If the idea is that you want to support two directions simultaneously such as Up and Right, then you can use an enum as Flags (see my post here, if you are unfamiliar with that).

          When you are using the generic InfoEventArgs, you can help it understand what it is creating like this:
          new InfoEventArgs LessThan MovementDirs GreaterThan (MovementDirs.Left)” Sorry, wordpress keeps modifying my comment and stripping the generic line out. I am not sure how to display it correctly in a comment so you will have to replace the LessThan and GreaterThan bits with the appropriate character.

  2. Hey @thejon2014. Again, great tutorials.

    Here are some suggestions (for the newbies sake):
    > you may be interested in adding a note/link to the repository on every post, or maybe at the Project posts list (a different looking one, to make it obvious)
    > I could only know for sure where the EventHandler goes because I confirmed it at the Repository, so it might be good if you stated that more clearly.

  3. Hi, I’ve hit a bit of a road block following this tutorial and I think it has to do with my event handlers. I am getting no errors, but nothing is added to the debug log when I give input.

    This is my Demo.cs

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

    public class Demo : MonoBehaviour {

    void OnEnable () {
    Debug.Log (“This line is logged properly”);
    InputController.moveEvent += OnMoveEvent;
    InputController.fireEvent += OnFireEvent;
    }

    void OnDisable () {
    InputController.moveEvent -= OnMoveEvent;
    }

    void OnMoveEvent(object sender, InfoEventArgs e) {
    Debug.Log (“Move ” + e.info.ToString ());
    }

    void OnFireEvent(object sender, InfoEventArgs e){
    Debug.Log (“Fire ” + e.info);

    }

    }

    Does this look like it should be working? Is the problem elsewhere?

    1. The code looks ok (but don’t forget to also unsubscribe from the fireEvent). Since you are able to see the Debug.Log where you subscribe for events but aren’t receiving anything, your next check should be that you are actually sending something. I would put some Debug.Log statements in the InputController script around your input where you expect the event to be posted.

      Your problem could be as simple as forgetting to have an InputController component in your scene.

    2. I noticed that you’re missing the info type in your method declarations. So OnMoveEvent parameter should be InfoEventArgs e, and OnFireEvent should be InfoEventArgs e.

      Hope this helps!

  4. Hi =)

    I’ve been so sorry for google translate but I can not speak English well.

    My question is:

    How do I get it to play the game on my mobile?

    As I know it is not possible that a UI button simulates a button print or am I wrong with this statement?

    How do I best deal with this? Can you help me or do you have an idea how I can manage this?

    What maybe still important is I am not really good in the coding I make it only half a year. And have given me everything myself.

    Thanks in advance =)

    1. Unity has a new UI system with Unity 5. Their UI Button does respond to touches for a mobile screen in the same way as it would respond to a mouse click. I have a short post that shows how to link button events to your code here:
      https://theliquidfire.wordpress.com/2015/10/05/sorted-shop/

      Note that the architecture is pretty different than the setup for the Tactics RPG project which was geared more toward using a game controller or keyboard so you could highlight menu options and then confirm them etc. With a touch input you don’t need a highlight state.

      1. I once tried a little, and did not come to any sly results. The only thing I could use but not 100% works is the following:
        Public float Move = 1.0f;

        Void Update ()
        {
        // Up
        If (Input.GetKeyDown (KeyCode.U))
        {
        Transform.Translate (0, 0, Move);
        }
        // Down
        If (Input.GetKeyDown (KeyCode.J))
        {
        Etc
        If I pull it on the TileSelection Indicator, I can use it and could modify it using buttons. But when I drive 2 Tile’s to the left and it selects it does not go there. If I then but normally with (Key A) 1 left it goes 1 links although the Tile Selection Indicator is on 2 links. Then I remembered that you in the input controller believe the position update.
        Int x = _hor.Update ();
        Int y = _ver.Update ();
        If (x! = 0 || y! = 0)
        {
        If (moveEvent! = Null)
        MoveEvent (this, new InfoEventArgs (new point (x, y));
        Could this be the reason that it does not work?
        And how can I rewrite or paste that it works?

        1. I thought I understood what you were asking, but now I am not sure. I thought you wanted to modify the Tactics RPG project so that it would work with a touch-screen interface on mobile rather than the keyboard input it currently uses.

          This response makes it sound like you are still working with keyboard input, but I think it wasn’t translated well enough for me to understand what the problem is or what your goal is.

  5. I’m sorry, as I said, I translate everything with Google Translate. You have already understood me the first time. I would like to play the game on my mobile phone. I wanted to explain to you that I made some attempts, but I do not find a working solution. With the above written code, I can indeed control the Tile Selection Indicator over the buttons. But when I move the Tile Selection Indicator with my code above, and then confirm the destination, it does not move. If I drive with the old control with keyboard 1 Tile to the left, and then still take another step with my code. If he ignores the position of the TileSelection Indicator (which in the game is now 2 on the left) and moves only 1 to the left. Perhaps I go completely wrong the problem, I hope they understand me now. I would also have a button to confirm the destination or to select the actions. Also there I have unfortunately no idea how I should do that. The same would be with the back button. In the tutorial of this project I thought step by step I understand what they are doing. But now I doubt slowly to myself. I am quite a newcomer in coding, and am in the end with my knowledge have never synonymous nor have been specially adapted for mobile phone. But there is always the first time =) Thanks for their help and excuse

    1. Ok, that helps clarify. I think your problem is that you are simply moving the tile selection indicator’s transform position. You are controlling where it appears in the scene, but the rest of the game logic also needs to be informed of the change. See the BattleController’s “pos” field. This is the data that the game references to determine what tile is actually selected. The object you moved is just a view that represents that information to the player, but it is up to you to keep them in sync.

    2. I’m not sure how much of a newcomer you are to coding, but it is worth noting that this is a pretty advanced project. Changing the input architecture may “sound” easy, but is actually a pretty involved process and will have a large impact on the flow of the states and look of the screens as well as a host of architectural challenges you’ll have to consider, like what happens if I tap two places on the screen simultaneously – this can be a much larger problem than you might think. I wouldn’t recommend attempting this sort of challenge until you have a solid understanding of programming, and fully understand the code I have already provided.

      Until then, I am working on another project which will be touch-screen friendly. I hope to start publishing it in a few weeks. Stay tuned because it will probably be a better starting point for you.

  6. I’ve seen I’ve written the wrong code as I’ve still tested it with the keyboard. This is the code I use with the UI button.
    public class TileSelectionController : MonoBehaviour {

    public float Move = 1.0f;

    public void UP ()
    {
    transform.Translate(0, 0, Move);
    }

  7. So here I am again XD after the last error was stupid and this one must be too… well im getting this strange error on the “demo” script which i name InputHelper…. the on enable onfireevent retuns this error and does not let my enter play test mode :

    “Assets/Scripts/InputHelper.cs(10,19): error CS0123: A method or delegate `InputHelper.OnFireEvent(object, InfoEventArgs)’ parameters do not match delegate `System.EventHandler<InfoEventArgs>(object, InfoEventArgs)’ parameters”

    My code is the following :

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

    public class InputHelper : MonoBehaviour {

    void OnEnable ()
    {
    InputController.moveEvent += OnMoveEvent;
    InputController.fireEvent += OnFireEvent;
    }

    void OnDisable ()
    {
    InputController.moveEvent -= OnMoveEvent;
    }

    void OnMoveEvent (object sender, InfoEventArgs e)
    {
    Debug.Log(“Move ” + e.info.ToString());
    }

    void OnFireEvent (object sender, InfoEventArgs e)
    {
    Debug.Log(“Fire ” + e.info);
    }

    }

    If i remove the inputcontroller.fireevent += onfireevent from the onenable method it lets me play test but only the move event works…

    1. Unfortunately wordpress always modifies the code in the comments so I can’t verify that you had the correct generic type applied to the InfoEventArgs in the OnFireEvent handler. Make sure that it is “int” instead of “Point”, otherwise it looks correct at my first glance. If necessary, copy a fire event handler from somewhere else in the code that is working and that should help you understand what you missed.

  8. Hi Jon,

    First of all, great tutorial, I really like the way you describe your thought process during decisions, so it’s easy to understand why it’s better to do it this way than the other way. Much better than the video tutorials where they just give you the fastest path, although many times the wrong one for a full project.

    So, my question is in the Update method for the Repeater class. Why do you use Mathf.RoundToInt instead of a simple cast to int? Since Input.GetAxisRaw will always be 0, 1, or -1, I didn’t understand the reason for that. Is it just a matter of preference?

    Thanks!

    1. Glad you are enjoying it! To answer your question, although the project lends itself to keyboard input, the Input class can accept joystick input as well. In that case, the rounding will help pick the value nearest to the intent of the user.

  9. Woah! I’m really enjoying the template/theme of this blog. It’s simple, yet
    effective. A lot of times it’s tough to get that “perfect balance” between superb usability and visual appeal.
    I must say you’ve done a awesome job with this.
    Also, the blog loads extremely quick for me
    on Safari. Outstanding Blog!

Leave a Reply

Your email address will not be published. Required fields are marked *