RPG Combat Course - Click to Move with new Input System

Just thought I would share. I implemented the click to move and click and hold to move using the new input system.

First I defined a ClickToMove action in an Input Action Asset named Player Controls as follows:

Next here is my Mover script:

using UnityEngine;
using UnityEngine.AI;
using UnityEngine.InputSystem;

public class Mover : MonoBehaviour
{
    private NavMeshAgent _navAgent;
    private PlayerControls _controls;
    private Animator _anim;
    private Camera _cam;

    private bool _moveShouldContinue;
    private bool _moveHasStarted;

    private void Awake() {
        _controls = new PlayerControls();
        _navAgent = GetComponent<NavMeshAgent>();
        _anim = GetComponent<Animator>();
        _cam = Camera.main;
    }

    private void OnEnable() {
        _controls.Enable();
    }

    private void OnDisable() {
        _controls.Disable();
    }

    private void Start() {
        _controls.Player.ClickToMove.started += OnClickToMoveStarted;
        _controls.Player.ClickToMove.performed += OnClickToMove;
        _controls.Player.ClickToMove.canceled += OnClickToMoveCanceled;
    }

    private void LateUpdate() {
        // check if we are holding the mouse button down so we
        // should keep moving
        if (_moveShouldContinue && _moveHasStarted) {
            MoveToRay();
        }
    }

    private void OnClickToMoveStarted(InputAction.CallbackContext context) {
        // set the boolean flags when we click the mouse
        _moveShouldContinue = true;
        _moveHasStarted = false;
    }
    
    private void OnClickToMove(InputAction.CallbackContext context) {
        // started to actually move
        _moveHasStarted = true;
        MoveToRay();
    }
    
    private void OnClickToMoveCanceled(InputAction.CallbackContext context) {
        // we have released the mouse, so we should not continue movement
        _moveShouldContinue = false;
    }

    // move us to the ray defined by the mouse's current position
    private void MoveToRay() {
        var ray = _cam.ScreenPointToRay(Mouse.current.position.ReadValue());
        var hasHit = Physics.Raycast(ray, out var hit);
        if (hasHit) {
            _navAgent.destination = hit.point;
        }
        UpdateAnimator();
    }
    
   // update the animator
    private void UpdateAnimator() {
        var vel = _navAgent.velocity;
        var localVel = transform.InverseTransformDirection(vel);
        _anim.SetFloat("forwardSpeed", localVel.z);
    }
}

The two booleans _moveShouldContinue and _moveHasStarted work together to let the mover know if we just clicked the mouse or are holding down the mouse. The three methods the ClickToMove action is subscribed to take care of setting those booleans as well as moving the player.

For click and hold I am using LateUpdate which checks for both flags to be true and if so continue moving to where the mouse is pointing to. This is needed because, as I understand it, the input system not yet has implemented continuous callbacks.

Hope this helps!

5 Likes

You’re on the right track, although I would put the OnClick methods in the PlayerController once you get to the PlayerController section. It will make a lot more sense once we get there. I’ll be interested in seeing how you come along with the input system as you progress through the course.

2 Likes

That is exactly what I did. :grin:

I did change things up a bit and now subscribe to a general OnClick method which calls different methods depending on the phase of the callback context. I found this allows me to make sure I am not moving or attack unless I really mean it.

I also implemented an interface (ITakeHits) for things that can take hits that way I can have different health systems for different kinds of objects (for example how I want to handle destructible objects vs. NPCs). So my InteractWithCombat method looks for things with the ITakeHits interface which requires a TakeDamage and Die method. The Health script (which will just be for the player and npcs) implements this interface.

For folks’ reference here is my PlayerController script (which is the only thing that uses the input system logic) as it stands just before the Enemy AI section.

using System;
using UnityEngine;
using UnityEngine.InputSystem;
using RPG.Movement;
using RPG.Combat;
using RPG.Core;

namespace RPG.Control {

    [RequireComponent(typeof(Mover))]
    [RequireComponent(typeof(Fighter))]
    public class PlayerController : MonoBehaviour {
        private PlayerControls _controls;
        private Mover _mover;
        private Fighter _fighter;

        private bool _moveShouldContinue;
        private bool _moveHasStarted;

        private void Awake() {
            _controls = new PlayerControls();
            _mover = GetComponent<Mover>();
            _fighter = GetComponent<Fighter>();
        }

        private void OnEnable() {
            _controls.Enable();

            _controls.Player.Click.started += OnClick;
            _controls.Player.Click.performed += OnClick;
            _controls.Player.Click.canceled += OnClick;
        }

        private void OnDisable() {
            _controls.Player.Click.started -= OnClick;
            _controls.Player.Click.performed -= OnClick;
            _controls.Player.Click.canceled -= OnClick;

            _controls.Disable();
        }

        private void Update() {
            if (_moveShouldContinue && _moveHasStarted) {
                InteractWithMovement();
            }
        }

    #region Input System Methods
        
        // implements what OnClick does in each of the phases
        private void OnClick(InputAction.CallbackContext context) {
            switch (context.phase) {
                case InputActionPhase.Started:
                    OnClickStarted(context);
                    break;
                case InputActionPhase.Performed:
                    OnClickPerformed(context);
                    break;
                case InputActionPhase.Canceled:
                    OnClickCanceled(context);
                    break;
                case InputActionPhase.Disabled:
                    OnClickDisabled(context);
                    break;
                case InputActionPhase.Waiting:
                    OnClickWaiting(context);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private void OnClickStarted(InputAction.CallbackContext context) {
            _moveShouldContinue = true;
            _moveHasStarted = false;
        }

        private void OnClickPerformed(InputAction.CallbackContext context) {
            _moveHasStarted = true;
            if (InteractWithCombat()) return;
            if (InteractWithMovement()) return;
        }

        private void OnClickCanceled(InputAction.CallbackContext context) {
            _moveShouldContinue = false;
        }

        private void OnClickDisabled(InputAction.CallbackContext context) {
            // do nothing
        }

        private void OnClickWaiting(InputAction.CallbackContext context) {
            _moveShouldContinue = false;
            _moveHasStarted = false;
        }
        
    #endregion

        public static Ray GetMouseRay() {
            return Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue());
        }

        private bool InteractWithMovement() {
            var hasHit = Physics.Raycast(GetMouseRay(), out var hit);
            if (hasHit) {
                _mover.StartMovement(hit.point);
                return true;
            }
            return false;
        }

        private bool InteractWithCombat() {
            var hits = Physics.RaycastAll(GetMouseRay());
            foreach (var hit in hits) {
                // see if what we hit, if it can take hits, and it is not ourself
                var target = hit.collider.GetComponent<ITakeHits>();
                if (target == null || hit.collider.gameObject == this.gameObject) continue;
                
                // check if we can attack the target
                if (!GetComponent<Fighter>().CanAttack(hit.collider.gameObject)) continue;
                
                // attack
                _fighter.Attack(hit.collider.gameObject);
                
                // make sure we don't move when we attack
                _moveShouldContinue = false;
                return true;
            }

            return false;
        }

    }

}

4 Likes

You’ll find, later in the course, that we’ll be refactoring everything that is interactable to be an IRayCastable (and then the IRayCastable will take most of the responsibility for what happens… for example, CombatTarget would be an IRayCastable that would instruct the player’s fighter to target, an inventory pickup would instruct the player to pick up the item. )
With the normal input system, each IRayCastable would be responsible for checking to see if the mouse button was engaged. For my project using the new Input system, I passed a “bool shouldInteract” to the IRayCastables.

Good to know. I also moved from using the IAction to a full state machine. (I have been playing with implementations of simple state machines so I wanted to see if I could get it to work here in a more complex case).

Basically I have separated out my functionality so I have an Entity and EntityController base class. The Player and Enemy classes inherit from Entity and the PlayerController and EnemyController (my version of the AIController) inherit from the EntityController. The previous Mover and Fighter classes now are responsible for handling animation and animation events and are required components on the Entities depending on what kind of functionality an entity will have. The Controllers implement the state machine and, in the case of NPCs, is responsible to customizing their behavior. The various states read from and act upon the data on the Controllers, animation handling classes, and for the case of the Death state (which is a special case) the Entities. Once the course gets to implementing things like stats, weapons and spells I am planning on using scriptable objects (if the course doesn’t do it) and have the states read that data as well.

With regards to the Input System, the PlayerControler subscribes to to input system events to determine what state we need to transition to when we click the mouse. This actually gave me a bit of a headache, but I got it working with some refactoring of methods. All the player controlled movement is now in a Moving state since we want to continue to be able to click and hold to move.

1 Like

That is very exciting seeing how you implemented the New Input system. I am still new and only recently started the course. But am interested in implementing the new input system.

Thank you for sharing and once I get farther in and understand more I will try this out. Thank you.

This is nice.
I just wished i was wise enough to be active here before implementing my own version of the input system. Sure my implementation works but… It needs work.

Thank you much for sharing with us.

M. D.

I myself have completed this section of the course and I am creating my own RPG 3rd Person template of sorts. It is combining the New Input System controls and folder organization and scripts from both this course and the other RPG courses using the movement and controls from the Unity 3rd Person Combat & Traversal course. A way of taking the experience of both courses into one template that I can build off of for other projects.

I am adding this as a public repo on my GitHub as well to keep that up to date in development once it is finished.

Privacy & Terms