Merging the Third Person Controller Course with the RPG Course

One week later, and we fixed the dynamic crafting tables problem. Anyway, the next thing I want to do, is to make sure that in ‘WindowController.cs’, only when you open the pause menu do you freeze the time scale. In other words, if you open the inventory, quest list, etc… don’t freeze the time. Only freeze it when the pause menu is opened

I’ll work on this, but I figured I should let you know @Brian_Trotter (in case you got any ideas for that one, xD).

Anyway, here’s the ‘WindowController.cs’ script:

using System.Collections.Generic;
using RPG.InputReading;
using UnityEngine;

namespace RPG.UI 
{
    // Generic script, responsible for common behaviour between User Interface Systems

    public abstract class WindowController: MonoBehaviour
    {
        protected static HashSet<WindowController> activeWindows = new();

        public static event System.Action OnAnyWindowOpened;
        public static event System.Action OnAllWindowsClosed;

        protected InputReader InputReader;

        // Protected and Virtual, therefore this method can be overridden (because it's virtual) by any inheriting scripts (because it's protected),
        // and tuned:
        protected virtual void Awake() 
        {
            InputReader = GameObject.FindWithTag("Player").GetComponent<InputReader>();
            Subscribe();
        }

        // Protected and Virtual, therefore this method can be overridden by any inheriting scripts, and tuned:
        protected virtual void OnDestroy()
        {
            Unsubscribe();
        }

        // when any window is open:
        void OnEnable() 
        {
            activeWindows.Add(this);
            Time.timeScale = 0.0f;
            OnAnyWindowOpened?.Invoke();
            Debug.Log($"Window enabled");
        }

        // when any window is closed:
        protected virtual void OnDisable() 
        {
            activeWindows.Remove(this);
            if (activeWindows.Count == 0) {
                Time.timeScale = 1.0f;
                OnAllWindowsClosed?.Invoke();
            }
            Debug.Log($"Window disabled");
        }

        /// <summary>
        /// Override this method to subscribe to any events that will call this implementation of 'WindowController'
        /// </summary>
        protected abstract void Subscribe();

        /// <summary>
        /// Override this method to ubsubscribe to any events that will call this implementation of 'WindowController'
        /// </summary>
        protected abstract void Unsubscribe();

        protected void CloseWindow()
        {
            gameObject.SetActive(false);
            Debug.Log($"Window closed");
        }

        protected void ToggleWindow()
        {
            gameObject.SetActive(!gameObject.activeSelf);
            Debug.Log($"Window toggled to {(gameObject.activeSelf ? "active" : "inactive")}");
        }
    }
}

Anyway, here’s what I came up with:

  1. In ‘InputReader.cs’, I introduced a circular dependency (I realized that one was there from the days of me creating the parrying system… I will need to find a way around this). Apart from that, I swapped out ‘EnableControls’ and ‘DisableControls’ in ‘InputReader.cs’ for new ‘EnableFreeLookCamera’ and ‘DisableFreeLookCamera’. That way, you don’t lose your controls, you just freeze the Free Look camera when you got an inventory of some sort open in your game:
    private PlayerStateMachine playerStateMachine;

    private void Start() 
    {
        controls = new Controls();
        controls.Player.SetCallbacks(this);
        controls.Player.Enable();
        controls.UI.SetCallbacks(this);
        controls.UI.Enable();

        // WindowController.OnAnyWindowOpened += DisableControls;
        // WindowController.OnAllWindowsClosed += EnableControls;

        // TEST - DELETE IF FAILED
        WindowController.OnAnyWindowOpened += DisableFreeLookCamera;
        WindowController.OnAllWindowsClosed += EnableFreeLookCamera;

        playerStateMachine = GetComponent<PlayerStateMachine>();
        animationEventRelay = GetComponent<AnimationEventRelay>();
    }

    /* void EnableControls() 
    {
        controls.Player.Enable();
    }

    void DisableControls() 
    {
        controls.Player.Disable();
    } */

    // TEST FUNCTION - DELETE IF FAILED
    void EnableFreeLookCamera() 
    {
        // (HARD-CODED VALUES. IF YOU CHANGE THEM IN CINEMACHINE FREE LOOK CAMERA, CHANGE THEM HERE TOO!)
        playerStateMachine.PlayerFreeLookCamera.m_XAxis.m_MaxSpeed = 200;
        playerStateMachine.PlayerFreeLookCamera.m_YAxis.m_MaxSpeed = 0;

        // Do the same for the Boat driving camera as well
    }

    void DisableFreeLookCamera() 
    {
        playerStateMachine.PlayerFreeLookCamera.m_XAxis.m_MaxSpeed = 0f;
        playerStateMachine.PlayerFreeLookCamera.m_YAxis.m_MaxSpeed = 0f;

        // Do the same for the Boat driving camera as well
    }

        private void OnDestroy() 
    {
        // WindowController.OnAnyWindowOpened -= DisableControls;
        // WindowController.OnAllWindowsClosed -= EnableControls;

        // TEST - DELETE IF FAILED
        WindowController.OnAnyWindowOpened -= DisableFreeLookCamera;
        WindowController.OnAllWindowsClosed -= EnableFreeLookCamera;

        controls.Player.Disable();
        controls.UI.Disable();
    }
  1. in ‘WindowController.cs’, I removed the time scale freeezing, and strictly placed it in ‘PauseMenuUI.cs’:
        // when any window is open:
        void OnEnable()
        {
            activeWindows.Add(this);
            // Time.timeScale = 0.0f;
            OnAnyWindowOpened?.Invoke();
            Debug.Log($"Window enabled");
        }

        // when any window is closed:
        protected virtual void OnDisable()
        {
            activeWindows.Remove(this);
            if (activeWindows.Count == 0)
            {
                // Time.timeScale = 1.0f;
                OnAllWindowsClosed?.Invoke();
            }
            Debug.Log($"Window disabled");
        }
  1. in ‘PauseMenuUI.cs’, I modified ‘HandleCancelEvent()’, so that we can freeze the time on pause only for pausing, and not anything else:
        private void HandleCancelEvent() 
        {
            if (activeWindows.Count > 0) 
            {
                var windows = activeWindows.ToList();

                for (int i = 0; i < activeWindows.Count; i++) 
                {
                    windows[i].gameObject.SetActive(false);
                }

                // TEST LINE - DELETE IF FAILED
                Time.timeScale = 1.0f;
            }

            else 
            {
                // TEST LINE - DELETE IF FAILED
                Time.timeScale = 0.0f;

                gameObject.SetActive(true);
            }

        }

Now I gotta find which other cameras need to freeze as well (like the boat-driving camera for example), and work on that

But I also noticed something, the Quest UI is not part of the WindowController yet, so it doesn’t play by it’s rules just yet. I’ll need to work on that later

Is this the best approach? I truly don’t know, but time will tell


I also have to mention that the Resume button from the pause menu, along with the quit button, will suffer because of this setup. Here’s how I fixed it:

  1. Place this simple function in ‘PauseMenuUI.cs’:
        public void RestoreTimeSpeedToNormal() 
        {
            Time.timeScale = 1.0f;
        }
  1. Apply this to both the quit button, and the resume button, to restore the game speed:

and I also did the same for my own custom Pickup Inventory UI, where I restore the speed when it’s closed:

(I’ll work on the code of freezing it to begin with)

The one major drawback I believe I will face with my variant, is you’ll be strangled quite a lot by NPCs and your camera will be frozen. I’ll probably just need to make a button for that or something, instead of this setup

That looks like you’re on the right track.

I didn’t do the Quest panel as a WindowController because you might want that quest window up while you walk around looking for Baddy McBadface to get his McGuffin of Ultimate Power.

[IGNORE. I REVERSED EVERYTHING. I ADMIT, THIS WAS A TERRIBLE IDEA FROM MY SIDE!]

I want the Quest Panel in this as well, though, so I can freeze the camera for that as well. I think it would be important not to drive the player to insanity through camera rotations when he’s playing his/her game, and he has an open UI of any sort :slight_smile:

I’m literally debating on either this approach, or get an input reader button to universally block the current camera from rotating, but if their rotation speeds are different this will be very challenging (unless I set them all up to the exact same value, that is)

Something else I noticed, is if you hit the attack button whilst in the Inventory UI, or dragging and dropping stuff around, you do attack, and that’s… not 100% convenient

And after a little bit of thinking, because you can always be attacked by a hostile NPC whilst collecting stuff or something, when you’re attacked, why don’t we automatically set the player to enter his targeting state and aim for whoever just hit him, under the condition that he’s not aiming at someone else? (i.e: ‘stateMachine.Targeter.HasTargets’ is false) That way the player can focus on who just hit him

The one window I’d make an exception to freeze time for, because it would be quite annoying otherwise, would be the pickup window


Edit: Now I see why you chose to freeze the entire game when a window is open…

Edit 2: Ahh nevermind, forget this issue… I reversed everything. This was a terrible idea! (I wanted to do this only because I thought that this would make the game look more polished, but frankly speaking, it made gameplay insanely difficult, and I can see a million bugs coming out of just this one. I’d rather have great gameplay than a stylish-looking game)

Tomorrow I’ll go full insanity mode, and start working on making Animals enemies as well. I recently fixed the last few bugs that I had as of right now. This will take a while to get it right

That could only be happening if you had a state that was not unsubscribing from the attack button…

We? No… You? Probably. :slight_smile:

In the list of high end AAA games that I’m playing off and on over the last six months, four out of four of them (Marvel Midnight Suns, Final Fantasy VII Remake Intergrade, Borderlands, Dragon’s Age: Inqusition) freeze the game when you open a window. Witcher did too, if I recall. MMORPGS (multiplayer) like World of Warcraft do not freeze the game, but that’s sort of the nature of multi-player games.

I chose to freeze the entire game when the window is open because it’s what most gamers expect in single player mode.

I just wanted something a little more lively, but I’m not going down this path again. Last time I did, I had a sense that I’ll be wasting so much time fixing unwanted problems that could’ve easily been avoided, and the last thing I want right now is unwanted drama

Ehh, you learn and you grow

Neither will I, I learned my lesson lah!

HOWEVER… THINKING A LITTLE BIT ABOUT IT, I have a new problem for later to work on. If the inventory pauses the game, this makes eating whilst in battle very easy, and this beats the entire idea of making it a challenge, so I can go in one of two ways:

  1. Unfreeze the time when the inventory is open
  2. Use the Abilities bar as a place as well to hold the food, that way we don’t have to pause anything

I’ll probably go with the second approach, and then I’ll debate on whether I want my food to be stackable or not

I’ll do this later, I’m currently working with the animal death state, to make sure you can’t mount dead animals. This is one troublesome state… :slight_smile:

Hey everyone! I’m working on merging the two courses ‘backwards’ in a way. Using the Third Person Combat as the base, and adding pieces of the RPG course on (and using Brian’s amazing guide as needed). I’ve just done adding Patrolling and Dwell states to the enemies, and saw an interesting bug (that had been present for a long time in the EnemyChasingState, I just never saw it).

In any state where we set the FreeLookSpeed hash or any animator float (I’m guessing here), and the game is paused in any way (inventory screen, pause menu etc), the animation gets ‘lost’ until the enemy changes state. See in the gif below, it begins to ‘slide’ instead of walking / running. This DOESN’T happen in the attacking state, and this is what makes me think it is to do with setting animator floats and then pausing the game with Time.timeScale. The logic of the states works fine, so I don’t think there is an issue there (and the animations work, as long as the state isn’t interrupted by a Time.timeScale = 0 call.

Here is what it looks like:
AnimationBug (2)(1)

Attempted fixes:

Use Time.unscaledDeltaTime - The animation keeps playing during the pause menu, and the enemy will still move toward player or waypoint

Caching animator float value - Fire events for OnPause and OnResume. OnPause caches the animator float value when the game is paused, and OnResume sets in when resumed. Didn’t work.

Any thoughts? Has anyone else seen this bug? Thanks to anyone who can take the time to help out with this :slight_smile:

Disregard, found the fix further up in this thread (not sure why it didn’t show up when I searched for key words earlier)!

Also just wanted to say @Brian_Trotter you have done amazing work here, going well and truly above and beyond!

1 Like

Privacy & Terms