tmp

Home About me Blogroll

Tuesday, March 5, 2013

Unity3D tip #01 - in-game configurable input

Currently unity doesn't provide a way to configure the commands during the game. Once the player starts the program, the command are permanently linked to the keyboard and mouse buttons. For Facade we wanted to give the player the possibility to choose the keyboard configuration from the main menu, like in any other game. In order to do this, I've created a simple class that replaces the Input class that unity provides.
using UnityEngine;
using System.Collections;

public enum Command {
	FORWARD,
	LEFT,
	RIGHT,
	BACK,
	JUMP,
	QMENU,
	PAUSE,
	SWING_FORWARD,
	SWING_BACKWARD,
	ACTIVATE,
	CLOSE_TIP
}

public enum Axis {
	HORIZONTAL,
	VERTICAL
}

public class FInputManager : MonoBehaviour {

	static private KeyCode[] commandsArray;

	static public void SetCommands(KeyCode[] commands) {
		commandsArray = commands;	
	}
	
	static public bool GetCommand(Command com) {
		return Input.GetKey(commandsArray[(int)com]);
	}
	
	static public bool GetCommandDown(Command com) {
		return Input.GetKeyDown(commandsArray[(int)com]);
	}
	
	static public bool GetCommandUp(Command com) {
		return Input.GetKeyUp(commandsArray[(int)com]);
	}
	
	static public float GetAxis(Axis axis) {
		switch (axis) {
		case Axis.HORIZONTAL: 
			return (float)( -System.Convert.ToInt32(Input.GetKey(commandsArray[(int)Command.LEFT])) + 
							 System.Convert.ToInt32(Input.GetKey(commandsArray[(int)Command.RIGHT])));
		case Axis.VERTICAL: 
			return (float)( System.Convert.ToInt32(Input.GetKey(commandsArray[(int)Command.FORWARD])) + 
							 -System.Convert.ToInt32(Input.GetKey(commandsArray[(int)Command.BACK])));
		}
		return 0;
	}
	
	static public KeyCode GetKeyCode(Command com) {
		return commandsArray[(int)com];
	}
	
}
Instead of using strings like unity does, this class uses only an enum, thus it avoid allocations of pointless strings. The mapping between a Command and a unity KeyCode is done through the array commandsArray. This can be set through the SetCommands method (but look out from the out-of-bound errors ;) ). Now let see how the menu and the config file works.
private List> buttonsEntries;

// --- inside the menu loop --

if (buttonsEntriesNeedsRefresh) {
	buttonsEntriesNeedsRefresh = false;
	LoadButtonsEntry();
}

// The user is inserting the new key for a command. 
// Capture it and save it the right button entry
if (waitingForKey) {
	Event e = Event.current;
	if (e.isKey) {
		waitingForKey = false;
		buttonsEntries[entryToUpdateIndex].Item2 = e.keyCode;
	}
}

int entryIndex = 0;

GUILayout.BeginHorizontal();
if (GUILayout.Button("Save")) {
	buttonsEntriesNeedsRefresh = true;
	SaveButtonsEntry();
	UpdateInputManager();
	menuState = MenuState.MAIN;
}
if (GUILayout.Button("Discard Changes")) {
	buttonsEntriesNeedsRefresh = true;
	menuState = MenuState.MAIN;
}
if (GUILayout.Button("Load default configuration")) {
	buttonsEntriesNeedsRefresh = true;
	CreateDefaultConfig();
	menuState = MenuState.MAIN;
}
GUILayout.EndHorizontal();

buttonConfigScroolPosition = GUILayout.BeginScrollView(
	buttonConfigScroolPosition, false, true);

foreach (Tuple entry in buttonsEntries) {
	GUILayout.BeginHorizontal();
	GUILayout.Label(entry.Item1, GUILayout.Width(buttonWidth));
	if (GUILayout.Button(entry.Item2.ToString(), GUILayout.Width(100))) {
                // The player clicked this button, so the button to be updated 
                //is the button specified by entryIndex
		entryToUpdateIndex = entryIndex;
                // From now on we will wait for the user to press a new key.
		waitingForKey = true;
	}
	entryIndex++;
	GUILayout.EndHorizontal();
	}
}
Once the player press the 'Save' button, the methods SaveButtonsEntry() and UpdateInputManager() are called.
private void SaveButtonsEntry() {
	System.IO.File.WriteAllText(buttonsConfig, string.Empty);	
	FileInfo buttonsConfigFileInfo = new FileInfo(buttonsConfig);
	StreamWriter writer = buttonsConfigFileInfo.AppendText();

	foreach (Tuple entry in buttonsEntries) {
                // We want a KISS config file, nothing fancy
                // The first field is the name of the command,
                // the second is the string representation of the KeyCode value.
		writer.WriteLine(entry.Item1 + '=' + entry.Item2.ToString());
	}
	
//		writer.Flush();
	writer.Close();
}

private void UpdateInputManager() {
	System.Array commands = System.Enum.GetValues(typeof(Command));
	KeyCode[] newCommands = new KeyCode[commands.Length];
	int i = 0;
        // Search each possible value of the Command enum inside the config file
	foreach (var com in commands) {
		bool commandFound = false;
		foreach (var entry in buttonsEntries) {
                        // Again, keep it simple, nothing fancy
			if (entry.Item1.ToUpper().Replace(' ','_') == com.ToString()) {
				newCommands[i] = entry.Item2;
				commandFound = true;
				break;
			}
		}
		if (!commandFound) {
			Debug.LogError("Unable to find command '" + com + "' in the buttons configuration file");
		}
		i++;
	}
	FInputManager.SetCommands(newCommands);  // <--- data-blogger-escaped-inputmanager.="" data-blogger-escaped-pre="" data-blogger-escaped-the="" data-blogger-escaped-update="">


It is pretty easy to achieve this in C#: We can get the string representation of each enum value, thus we don't need to do any mapping between string and integers values.