Nasty Bug with disabling PlayerController

This code presented in this lecture has a very nasty bug that becomes apparent if:

  • The game object containing this script is not disabled in your scene hierarchy by default, AND
  • you subscribe to events in your playercontroller in OnEnable and OnDisable
  • you rely upon events as part of restore / scene loading
   private void OnEnable()
   {
      playerController.enabled = false;
      Time.timeScale = 0;
   }
   
   private void OnDisable()
   {
      playerController.enabled = true;
      Time.timeScale = 1;
   }

With the bug, what happens is your player controller will get disabled when the scene loads, which will make it unsubscribe from events. You will then miss events needed as part of scene loading / restore state. The window will eventually get disabled by the ShowHide script but in the meantime you will miss important events needed as part of scene restoration. It is very hard to track this bug down if you don’t know to look for it.

I implemented a simple (but not robust) fix - move ShowHide code from Awake to Start. One can also disable the Pause UI gameobject in the scene view.

My fix is not robust in my mind because we are using playerController.enabled in many places.

Question: What’s a more robust way of preventing user input when you don’t want it?

I was thinking of modifying PlayerController with a boolean and methods to ascertain if we want to accept or not accept user input. But after getting burned by weird race conditions, I am seeking advice. I’d also like the solution to be compatible with this from Brian’s tips and tricks.

In the past, I might have reached for a GameManager singleton class to centralize coordination of game state and make sure pause, resume, suspend user input are all handled properly. I am increasingly leaning that way, especially seeing how often we call static methods like GameObject.Find* for the player and saving wrapper. Looking for suggestions.

Option 1: Updated PlayerController (no game manager)

        private bool isInputSuspended;  
//by default we want input not to be suspended I think

        private void Update()
        {
            if (IsInputSuspended()) return;

            // normal update code continues
        }

        public bool IsInputSuspended()
        {
            return isInputSuspended;
        }

        public void SuspendInput()
        {
            isInputSuspended = true;
        }

        public void AcceptInput()
        {
            isInputSuspended = false;
        }

In the course code, there are no events being subscribed or unsubscribed in PlayerController.cs

PlayerController.cs
using RPG.Combat;
using RPG.Movement;
using UnityEngine;
using RPG.Attributes;
using System;
using UnityEngine.EventSystems;
using UnityEngine.AI;
using GameDevTV.Inventories;

namespace RPG.Control
{
    public class PlayerController : MonoBehaviour
    {
        Health health;
        ActionStore actionStore;

        [System.Serializable]
        struct CursorMapping
        {
            public CursorType type;
            public Texture2D texture;
            public Vector2 hotspot;
        }

        [SerializeField] CursorMapping[] cursorMappings = null;
        [SerializeField] float maxNavMeshProjectionDistance = 1f;
        [SerializeField] float raycastRadius = 1f;
        [SerializeField] int numberOfAbilities = 6;

        bool isDraggingUI = false;

        private void Awake() {
            health = GetComponent<Health>();
            actionStore = GetComponent<ActionStore>();
        }

        private void Update()
        {
            if (InteractWithUI()) return;
            if (health.IsDead()) 
            {
                SetCursor(CursorType.None);
                return;
            }

            UseAbilities();

            if (InteractWithComponent()) return;
            if (InteractWithMovement()) return;

            SetCursor(CursorType.None);
        }

        private bool InteractWithUI()
        {
            if (Input.GetMouseButtonUp(0))
            {
                isDraggingUI = false;
            }
            if (EventSystem.current.IsPointerOverGameObject())
            {
                if (Input.GetMouseButtonDown(0))
                {
                    isDraggingUI = true;
                }
                SetCursor(CursorType.UI);
                return true;
            }
            if (isDraggingUI)
            {
                return true;
            }
            return false;
        }

        private void UseAbilities()
        {
            for (int i = 0; i < numberOfAbilities; i++)
            {
                if (Input.GetKeyDown(KeyCode.Alpha1 + i))
                {
                    actionStore.Use(i, gameObject);
                }
            }
        }

        private bool InteractWithComponent()
        {
            RaycastHit[] hits = RaycastAllSorted();
            foreach (RaycastHit hit in hits)
            {
                IRaycastable[] raycastables = hit.transform.GetComponents<IRaycastable>();
                foreach (IRaycastable raycastable in raycastables)
                {
                    if (raycastable.HandleRaycast(this))
                    {
                        SetCursor(raycastable.GetCursorType());
                        return true;
                    }
                }
            }
            return false;
        }
        

        RaycastHit[] RaycastAllSorted()
        {
            RaycastHit[] hits = Physics.SphereCastAll(GetMouseRay(), raycastRadius);
            float[] distances = new float[hits.Length];
            for (int i = 0; i < hits.Length; i++)
            {
                distances[i] = hits[i].distance;
            }
            Array.Sort(distances, hits);
            return hits;
        }

        private bool InteractWithMovement()
        {
            Vector3 target;
            bool hasHit = RaycastNavMesh(out target);
            if (hasHit)
            {
                if (!GetComponent<Mover>().CanMoveTo(target)) return false;

                if (Input.GetMouseButton(0))
                {
                    GetComponent<Mover>().StartMoveAction(target, 1f);
                }
                SetCursor(CursorType.Movement);
                return true;
            }
            return false;
        }

        private bool RaycastNavMesh(out Vector3 target)
        {
            target = new Vector3();

            RaycastHit hit;
            bool hasHit = Physics.Raycast(GetMouseRay(), out hit);
            if (!hasHit) return false;

            NavMeshHit navMeshHit;
            bool hasCastToNavMesh = NavMesh.SamplePosition(
                hit.point, out navMeshHit, maxNavMeshProjectionDistance, NavMesh.AllAreas);
            if (!hasCastToNavMesh) return false;

            target = navMeshHit.position;

            return true;
        }

        private void SetCursor(CursorType type)
        {
            CursorMapping mapping = GetCursorMapping(type);
            Cursor.SetCursor(mapping.texture, mapping.hotspot, CursorMode.Auto);
        }

        private CursorMapping GetCursorMapping(CursorType type)
        {
            foreach (CursorMapping mapping in cursorMappings)
            {
                if (mapping.type == type)
                {
                    return mapping;
                }
            }
            return cursorMappings[0];
        }

        public static Ray GetMouseRay()
        {
            return Camera.main.ScreenPointToRay(Input.mousePosition);
        }
    }
}

Yes - I added something so that I didn’t have to rely on hard coded numberOfAbilities = 6.

I am happy to convert to a “talk” if it’s out of scope for course. Figured it would be useful. I always get something out of reading everyone’s community contributions.

Is the event being received from the ActionStore that is on the PlayerController’s GameObject?
It might make more sense to subscribe to the event in Awake() and unsubscribe in OnDestroy()

Yes. Exactly.

Got it. So that would be preferred over trying to have some kind of interface for pausing /suspending user input.

Absolutely. Technically, you don’t even need to unsubscribe since the PlayerController and the ActionStore are on the same GO and will be destroyed at the same time.

1 Like

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms