• J. J. Allen

Using InControl with Opsive's Ultimate Character Controller

Completing this article will integrate InControl with Opsive's Ultimate Character Controller for your Unity 3d project. The completed character can be controlled using Keyboard & Mouse or a Gamepad like the Xbox controller.


Pre-Requisites


  • InControl imported into your Unity 3d project.

  • InControl Input Manager Settings have been set up.

  • InControl Input Manager added to your scene.

  • Opsive Ultimate Character Controller (UCC) imported into your project.

  • Opsive UCC character created and added to your Unity 3d scene.

  • Opsive UCC camera created and added to your Unity 3d scene.


You can purchase these assets directly from the Unity Asset Store by following the links below!




Setup


The folder structure of a Unity 3d project is a combination of organic growth and personal preference. The structure I use in my projects is inspired from many years of REST Api web development and some newer conventions followed by Microsoft within their .Net Core libraries.


The full folder structure used in my projects is below. For this article, our concerns reside within the Integrations folder, however; I thought it may be useful to demonstrate what a full project folder structure looks like.


+ Assets

| + __{{Project Name}}

| + + __Scripts

| + + + Abstractions - Abstract classes that can be used globally across the project

| + + + Components - MonoBehaviours providing SOLID-level functionality

| + + + Data - ScriptableObjects used for POCO (plain old CLR object)

| + + + Editor - Unity 3d editor extensions

| + + + Extensions - C# extension methods

| + + + Gameplay - MonoBehaviours providing gameplay-specific functionality

| + + + Integrations - Classes using the Adapter pattern to bridge 3rd-party libraries

| + + + Services - MonoBehaviours providing service-level functionality.


Infrastructure


Opsive provides a .unitypackage for an integration between InControl and their Ultimate Character Controller. This integration is useful to get InControl and UCC working together very quickly but I knew that its use would be short-lived.


To start, I created three new classes similar to those used in the InControl UCC integration provided by Opsive; PlayerActionSetBase, OpsiveActions, and OpsiveInputSource. I placed these class files within the Integrations folder, using the structure below.


+ Integrations

| + OpsiveActions.cs

| + OpsiveInputSource.cs

| + Abstractions

| + + PlayerActionSetBase.cs


Implementing PlayerActionSetBase.cs


The abstract PlayerActionSetBase class inherits from the InControl.PlayerActionSet class. We use an internal dictionary to map Opsive UCC actions to InControl inputs.


We provide an abstract method in this class, CreateBindings which is called from the constructor. This pattern forces any inheriting classes to provide a method body for this class. We use this method to register all of our input actions and bindings within the OpsiveActions class.


using InControl;
using Opsive.UltimateCharacterController.Integrations.InControl;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace Studio.OverOne.Integrations.Abstractions
{
    internal abstract class PlayerActionSetBase : PlayerActionSet,
        IBindings
    {
        protected Dictionary<string, IInputControl> InputBindingMap { get; private set; } = new Dictionary<string, IInputControl>();

        public PlayerActionSetBase() => CreateBindings();

        public abstract void CreateBindings();

        public IInputControl GetInputControl(string rAction)
        {
            if (InputBindingMap.TryGetValue(rAction.ToLowerInvariant(), out var lControl))
                return lControl;

            Debug.LogWarning($"Warning: Unable to retrieve input for {nameof(rAction)} '{rAction}'");
            return null;
        }

        public PlayerAction GetInputAction(string rAction)
        {
            if (InputBindingMap.TryGetValue(rAction.ToLowerInvariant(), out var lControl))
                if (lControl is PlayerAction lAction)
                    return lAction;

            Debug.LogWarning($"Warning: Unable to retrieve input for {nameof(rAction)} '{rAction}'");
            return null;
        }

        public bool TryGetPlayerInputAction(string rAction, out PlayerAction action)
        {
            action = GetInputAction(rAction);
            return action != null;
        }

        protected void ConfigAction(string rAction, IInputControl action)
        {
            if (InputBindingMap.ContainsKey(rAction.ToLowerInvariant()))
                return;

            InputBindingMap.Add(rAction.ToLowerInvariant(), action);
        }

        protected PlayerAction ConfigBinding(string rAction, params BindingSource[] inputSources)
        {
            if (!TryGetPlayerInputAction(rAction, out PlayerAction lAction))
                lAction = CreatePlayerAction(rAction);

            inputSources.ToList()
                .ForEach(lAction.AddDefaultBinding);

            ConfigAction(rAction, lAction);
            return lAction;
        }

        protected PlayerAction ConfigBinding(string rAction, params Mouse[] inputSources)
        {
            if (!TryGetPlayerInputAction(rAction, out PlayerAction lAction))
                lAction = CreatePlayerAction(rAction);

            inputSources.ToList()
                .ForEach(lAction.AddDefaultBinding);

            ConfigAction(rAction, lAction);
            return lAction;
        }

        protected PlayerAction ConfigBinding(string rAction, params InputControlType[] inputSources)
        {
            if (!TryGetPlayerInputAction(rAction, out PlayerAction lAction))
                lAction = CreatePlayerAction(rAction);

            inputSources.ToList()
                .ForEach(lAction.AddDefaultBinding);

            ConfigAction(rAction, lAction);
            return lAction;
        }

        protected PlayerAction ConfigBinding(string rAction, params Key[] keys)
        {
            if (!TryGetPlayerInputAction(rAction, out PlayerAction lAction))
                lAction = CreatePlayerAction(rAction);

            lAction.AddDefaultBinding(keys);

            ConfigAction(rAction, lAction);
            return lAction;
        }
    }
}

Implementing OpsiveActions.cs


The class OpsiveActions inherits from the PlayerActionSetBase class and uses the inherited methods to configure our InControl input bindings to Opsive actions. This is a pretty big class so we use regions to organize our configuration calls by actions.


Notice how all of our bindings and actions are registered within the overridden CreateBindings methods.


using InControl;

namespace Studio.OverOne.Integrations
{
    using Abstractions;

    internal sealed class OpsiveActions : PlayerActionSetBase        
    {
        #region " Internal Variables "

        private const string FIRE_1 = "Fire1";

        private const string FIRE_2 = "Fire2";

        private const string JUMP = "Jump";

        private const string CROUCH = "Crouch";

        private const string CHANGE_SPEEDS = "Change Speeds";

        private const string SECONDARY_USE = "SecondaryUse";

        private const string RELOAD = "Reload";

        private const string GRENADE = "Grenade";

        private const string ACTION = "Action";

        private const string EQUIP_FIRST_ITEM = "Equip First Item";

        private const string EQUIP_SECOND_ITEM = "Equip Second Item";

        private const string EQUIP_THIRD_ITEM = "Equip Third Item";

        private const string EQUIP_FOURTH_ITEM = "Equip Fourth Item";

        private const string EQUIP_FIFTH_ITEM = "Equip Fifth Item";

        private const string EQUIP_SIXTH_ITEM = "Equip Sixth Item";

        private const string EQUIP_SEVENTH_ITEM = "Equip Seventh Item";

        private const string EQUIP_EIGHTH_ITEM = "Equip Eighth Item";

        private const string EQUIP_NINTH_ITEM = "Equip Ninth Item";

        private const string EQUIP_TENTH_ITEM = "Equip Tenth Item";

        private const string EQUIP_NEXT_ITEM = "Equip Next Item";

        private const string EQUIP_PREVIOUS_ITEM = "Equip Previous Item";

        private const string TOGGLE_ITEM_EQUIP = "Toggle Item Equip";

        private const string DROP = "Drop";

        private const string TOGGLE_PERSPECTIVE = "Toggle Perspective";

        private const string LEAN_LEFT = "Lean Left";

        private const string LEAN_RIGHT = "Lean Right";

        private const string LEAN = "Lean";

        private const string MOUSE_SCROLLWHEEL_UP = "Mouse ScrollWheel Up";

        private const string MOUSE_SCROLLWHEEL_DOWN = "Mouse_ScrollWheel Down";

        private const string MOUSE_SCROLLWHEEL = "Mouse ScrollWheel";

        private const string MOVE_LEFT = "Move Left";

        private const string MOVE_UP = "Move Up";

        private const string MOVE_RIGHT = "Move Right";

        private const string MOVE_DOWN = "Move Down";

        private const string HORIZONTAL = "Horizontal";

        private const string VERTICAL = "Vertical";

        private const string MOUSE_LEFT = "Mouse Left";

        private const string MOUSE_UP = "Mouse Up";

        private const string MOUSE_RIGHT = "Mouse Right";

        private const string MOUSE_DOWN = "Mouse Down";

        private const string MOUSE_X = "Mouse X";

        private const string MOUSE_Y = "Mouse Y";

        #endregion

        public override void CreateBindings()
        {
            #region " Fire1 "

            ConfigBinding(FIRE_1, InputControlType.RightTrigger);
            ConfigBinding(FIRE_1, Mouse.LeftButton);

            #endregion

            #region " Fire2 "

            ConfigBinding(FIRE_2, InputControlType.LeftTrigger);
            ConfigBinding(FIRE_2, Mouse.RightButton);

            #endregion

            #region " Jump "

            ConfigBinding(JUMP, InputControlType.Action1);
            ConfigBinding(JUMP, Key.Space);

            #endregion

            #region " Crouch "

            ConfigBinding(CROUCH, InputControlType.Action2);
            ConfigBinding(CROUCH, Key.C);

            #endregion

            #region " Change Speeds "

            ConfigBinding(CHANGE_SPEEDS, InputControlType.LeftStickButton);
            ConfigBinding(CHANGE_SPEEDS, Key.LeftShift);

            #endregion

            #region " SecondaryUse "

            ConfigBinding(SECONDARY_USE, Key.B);

            #endregion

            #region " Reload"

            ConfigBinding(RELOAD, InputControlType.Action3);
            ConfigBinding(RELOAD, Key.R);

            #endregion

            #region " Grenade "

            ConfigBinding(GRENADE, Key.G);

            #endregion

            #region " Action "

            ConfigBinding(ACTION, InputControlType.Action3);
            ConfigBinding(ACTION, Key.F);

            #endregion

            #region " Equip # Item "

            ConfigBinding(EQUIP_FIRST_ITEM, Key.Key1);
            ConfigBinding(EQUIP_SECOND_ITEM, Key.Key2);
            ConfigBinding(EQUIP_THIRD_ITEM, Key.Key3);
            ConfigBinding(EQUIP_FOURTH_ITEM, Key.Key4);
            ConfigBinding(EQUIP_FIFTH_ITEM, Key.Key5);
            ConfigBinding(EQUIP_SIXTH_ITEM, Key.Key6);
            ConfigBinding(EQUIP_SEVENTH_ITEM, Key.Key7);
            ConfigBinding(EQUIP_EIGHTH_ITEM, Key.Key8);
            ConfigBinding(EQUIP_NINTH_ITEM, Key.Key9);
            ConfigBinding(EQUIP_TENTH_ITEM, Key.Key0);

            #endregion

            #region " Equip Next Item "

            ConfigBinding(EQUIP_NEXT_ITEM, Key.E);

            #endregion

            #region " Equip Previous Item "

            ConfigBinding(EQUIP_PREVIOUS_ITEM, Key.Q);

            #endregion

            #region " Toggle Item "

            ConfigBinding(TOGGLE_ITEM_EQUIP, Key.T);

            #endregion

            #region " Drop "

            ConfigBinding(DROP, Key.Y);

            #endregion

            #region " Toggle Perspective "

            ConfigBinding(TOGGLE_PERSPECTIVE, Key.V);

            #endregion

            #region " Lean Left "

            ConfigBinding(LEAN_LEFT, Key.Z);

            #endregion

            #region " Lean Right "

            ConfigBinding(LEAN_RIGHT, Key.X);

            #endregion

            #region " Lean "

            IInputControl lLeanAction = CreateOneAxisPlayerAction(
                GetInputAction(LEAN_LEFT),
                GetInputAction(LEAN_RIGHT));

            ConfigAction(LEAN, lLeanAction);

            #endregion

            #region " Mouse ScrollWheel Up "

            ConfigBinding(MOUSE_SCROLLWHEEL_UP, Mouse.PositiveScrollWheel);

            #endregion

            #region " Mouse ScrollWheel Down "

            ConfigBinding(MOUSE_SCROLLWHEEL_DOWN, Mouse.NegativeScrollWheel);

            #endregion

            #region " Mouse ScrollWheel "

            IInputControl lScrollWheelAction = CreateOneAxisPlayerAction(
                GetInputAction(MOUSE_SCROLLWHEEL_UP),
                GetInputAction(MOUSE_SCROLLWHEEL_DOWN));

            ConfigAction(MOUSE_SCROLLWHEEL, lScrollWheelAction);

            #endregion

            #region " Move Left "

            ConfigBinding(MOVE_LEFT, InputControlType.LeftStickLeft);
            ConfigBinding(MOVE_LEFT, Key.A);
            ConfigBinding(MOVE_LEFT, Key.LeftArrow);

            #endregion

            #region " Move Up "

            ConfigBinding(MOVE_UP, InputControlType.LeftStickUp);
            ConfigBinding(MOVE_UP, Key.W);
            ConfigBinding(MOVE_UP, Key.UpArrow);

            #endregion

            #region " Move Right "

            ConfigBinding(MOVE_RIGHT, InputControlType.LeftStickRight);
            ConfigBinding(MOVE_RIGHT, Key.D);
            ConfigBinding(MOVE_RIGHT, Key.RightArrow);

            #endregion

            #region " Move Down "

            ConfigBinding(MOVE_DOWN, InputControlType.LeftStickDown);
            ConfigBinding(MOVE_DOWN, Key.S);
            ConfigBinding(MOVE_DOWN, Key.DownArrow);

            #endregion

            #region " Horizontal "

            IInputControl lHorizontalAction = CreateOneAxisPlayerAction(
                GetInputAction(MOVE_LEFT),
                GetInputAction(MOVE_RIGHT));

            ConfigAction(HORIZONTAL, lHorizontalAction);

            #endregion

            #region " Vertical "

            IInputControl lVerticalAction = CreateOneAxisPlayerAction(
                GetInputAction(MOVE_DOWN),
                GetInputAction(MOVE_UP));

            ConfigAction(VERTICAL, lVerticalAction);

            #endregion

            #region " Mouse Left "

            ConfigBinding(MOUSE_LEFT, InputControlType.RightStickLeft);
            ConfigBinding(MOUSE_LEFT, Mouse.NegativeX);

            #endregion

            #region " Mouse Up "

            ConfigBinding(MOUSE_UP, InputControlType.RightStickUp);
            ConfigBinding(MOUSE_UP, Mouse.PositiveY);

            #endregion

            #region " Mouse Right "

            ConfigBinding(MOUSE_RIGHT, InputControlType.RightStickRight);
            ConfigBinding(MOUSE_RIGHT, Mouse.PositiveX);

            #endregion

            #region " Mouse Down "

            ConfigBinding(MOUSE_DOWN, InputControlType.RightStickDown);
            ConfigBinding(MOUSE_DOWN, Mouse.NegativeY);

            #endregion

            #region " Mouse X "

            IInputControl lMouseXAction = CreateOneAxisPlayerAction(
                GetInputAction(MOUSE_LEFT),
                GetInputAction(MOUSE_RIGHT));

            ConfigAction(MOUSE_X, lMouseXAction);

            #endregion

            #region " Mouse Y "

            IInputControl lMouseYAction = CreateOneAxisPlayerAction(
                GetInputAction(MOUSE_DOWN),
                GetInputAction(MOUSE_UP));

            ConfigAction(MOUSE_Y, lMouseYAction);

            #endregion
    }
}


Implementing PlayerInputSource.cs

The OpsiveInputSource class inherits from the Opsive UCC PlayerInput class and overrides the methods used by UCC to check for player input to use the methods / actions available within our OpsiveActions class.




using InControl;
using Opsive.UltimateCharacterController.Input;
using UnityEngine;

namespace Studio.OverOne.Integrations
{
    internal sealed class OpsiveInputSource : PlayerInput
    {
        #region " Inspector Variables "

        [Tooltip("Should the cursor be hidden?")]
        [SerializeField] private bool _hideCursor = true;

        [Tooltip("Should the cursor be disabled?")]
        [SerializeField] private bool _disableCursor = true;

        [Tooltip("The CursorLockMode used when the cursor is disabled.")]
        [SerializeField] private CursorLockMode _disabledLockMode;

        [Tooltip("Should the cursor be enabled when the ESCAPE KEY is pressed?")]
        [SerializeField] private bool _enableCursorWithEscape = true;

        #endregion

        #region " Internal Variables "

        private OpsiveActions _actions;

        #endregion

        protected override void Awake()
        {
            base.Awake();

            _actions = new OpsiveActions();
        }

        private void OnEnable()
        {
            if(!_disableCursor) { return; }

            Cursor.lockState = _disabledLockMode;
            Cursor.visible = false;
        }

        private void OnDisable()
        {
            Cursor.lockState = CursorLockMode.None;
            Cursor.visible = true;
        }

        private void LateUpdate()
        {
            // Enable the cursor if the escape key is pressed. Disable the cursor if it is visbile but should be disabled upon press.
            if (_enableCursorWithEscape && UnityEngine.Input.GetKeyDown(KeyCode.Escape)) {
                Cursor.lockState = CursorLockMode.None;
                Cursor.visible = true;
            } else if (Cursor.visible && _disableCursor && !IsPointerOverUI() && UnityEngine.Input.GetKeyDown(KeyCode.Mouse0)) {
                Cursor.lockState = _disabledLockMode;
                Cursor.visible = false;
            }
        #if UNITY_EDITOR
            // The cursor should be visible when the game is paused.
            if (!Cursor.visible && Time.deltaTime == 0) {
                Cursor.lockState = CursorLockMode.None;
                Cursor.visible = true;
            }
        #endif            
        }

        /// <summary>
        /// Internal method which returns true if the button is being pressed.
        /// </summary>
        /// <param name="name">The name of the button.</param>
        /// <returns>True of the button is being pressed.</returns>
        protected override bool GetButtonInternal(string name)
        {
            var control = _actions.GetInputControl(name);
            if (control == null) {
                return false;
            }
            return control.IsPressed;
        }

        /// <summary>
        /// Internal method which returns true if the button was pressed this frame.
        /// </summary>
        /// <param name="name">The name of the button.</param>
        /// <returns>True if the button is pressed this frame.</returns>
        protected override bool GetButtonDownInternal(string name)
        {
            var control = _actions.GetInputControl(name);
            if (control == null) {
                return false;
            }
            return control.WasPressed;
        }

        /// <summary>
        /// Internal method which returnstrue if the button is up.
        /// </summary>
        /// <param name="name">The name of the button.</param>
        /// <returns>True if the button is up.</returns>
        protected override bool GetButtonUpInternal(string name)
        {
            var control = _actions.GetInputControl(name);
            if (control == null) {
                return false;
            }
            return control.WasReleased;
        }

        /// <summary>
        /// Internal method which returns the value of the axis with the specified name.
        /// </summary>
        /// <param name="name">The name of the axis.</param>
        /// <returns>The value of the axis.</returns>
        protected override float GetAxisInternal(string name)
        {
            var control = _actions.GetInputControl(name);
            if (control == null) {
                return 0;
            }
            if (control is OneAxisInputControl) {
                return (control as OneAxisInputControl).Value;
            } else if (control is TwoAxisInputControl) {
                Debug.LogWarning("Warning: The Ultimate Character Controller doesn't support TwoAxisInputControl. Please create two OneAxisInputControl objects");
            }
            return 0.0f;
        }

        /// <summary>
        /// Internal method which returns the value of the raw axis with the specified name.
        /// </summary>
        /// <param name="name">The name of the axis.</param>
        /// <returns>The value of the raw axis.</returns>
        protected override float GetAxisRawInternal(string name)
        {
            var control = _actions.GetInputControl(name);
            if (control == null) {
                return 0;
            }
            if (control is OneAxisInputControl) {
                return (control as OneAxisInputControl).RawValue;
            } else if (control is TwoAxisInputControl) {
                Debug.LogWarning("Warning: The Ultimate Character Controller doesn't support TwoAxisInputControl. Please create two OneAxisInputControl objects");
            }
            return 0.0f;
        }

        /// <summary>
        /// Enables or disables gameplay input. An example of when it will not be enabled is when there is a fullscreen UI over the main camera.
        /// </summary>
        /// <param name="enable">True if the input is enabled.</param>
        protected override void EnableGameplayInput(bool enable)
        {
            base.EnableGameplayInput(enable);

            if (enable && _disableCursor) {
                Cursor.lockState = _disabledLockMode;
                Cursor.visible = false;
            }
        }

        #if !UNITY_EDITOR
        /// <summary>
        /// Does the game have focus?
        /// </summary>
        /// <param name="hasFocus">True if the game has focus.</param>
        protected override void OnApplicationFocus(bool hasFocus)
        {
            base.OnApplicationFocus(hasFocus);

            if (_disableCursor) {
                Cursor.lockState = _disabledLockMode;
                Cursor.visible = false;
            }
        }
        #endif
    }
}


Usage


To use these classes, select your Opsive UCC character in your scene and remove the default Unity Input script from your character. You can then can add the OpsiveInputSource class to your character using the Add Component button, or dragging your script from the Project Window to your gameObject within the Inspector Window.


You should now be able to click the Play Button within the inspector! The image below is using the Third Person Top Down setup.




If you would like to start with the Opsive provided integration with InControl, you can download it from the following link; https://opsive.com/support/documentation/ultimate-character-controller/integrations/incontrol/


Disclosure: This post may contain affiliate links, which means we may receive a commission if you click a link and purchase something that we have recommended. While clicking these links won't cost you any money, they will help me fund my development projects while recommending great assets!

2018-2020 © Over One Studio LLC.