My turn system approach - Kraddak/UnityStudy GitHub Wiki

All turn-based games have this concept in common, unrelated actions/commands simply cannot happen when an animation/event is running in a turn. If you have confirmed an action, you can't at the same time issue another action, you will have to wait for the character to finish what is doing. Of course this concept is much deeper than this, many things can actually happen during a turn (let's just think about the overwatch mechanic of XCOM enemy unknown), but the concept is actually simple, we want a way to issue commands only one by one, and if one command is running, then the user will simply have to wait. To us this means that we want a system that monitors player input, records command, and blocks other actions (for example another user command) until the current one is finished.

A consideration about the MonoBehaviour.Update() function

Who is familiar with Unity, know that when we want to make any kind of action during a period of time, then writing code in Update is a possibility. Therefore if we want to have a system that monitors the player input, we would need to check here for user command and the rest. While studying the strategy game course by CodeMonkey a similar notion to the below is proposed.

Update example

    private void Update(){ 
        // if this boolean is true, then no command is registered right? The update function will terminate here
        if(isBusy) return;

        // of course there will be a enemy turn, and the player can't interact while the enemy is moving
        if(!TurnSystem.Instance.IsPlayerTurn())
            return;

        // a way of asking Unity if the cursor is over a game object and not the "void"
        if(EventSystem.current.IsPointerOverGameObject())
            return;

        // we may have some conditions to check before issuing the user's command
        if(TryHandleUnitSelection()) return;

        // we finally execute the action
        HandleSelectedAction();
    }

This is a good starting point, we funnel every user input through this code, and if we don't mess things up, everything would go right. I have nothing against this approach, it is simple and effective, but I am not really satisfied by this for the following reasons. This approach requires a few consideration. Mainly that the "boolean isBusy" is touched every time really carefully. Every action needs to put it to true when starts, and false when it ends. And the course propose a really good way to do it by the usage of events. We can see that every time the HandleSelectedAction() is called we indeed set the isBusy to true. We can also expect the called action to put isBusy to true, and it's exactly what happens.


We can expect that also actions would do things over time, and therefore use their Update function (which is why they are inheriting from MonoBehaviour). We might therefore want to check if the action can operate or maybe be blocked by some conditions. That's why we see for example in the move action the check of isActive a protected field of the abstract class BaseAction (and they could be more). We now have even two variables that will interact with the flow of the user input. Again this might work depending on the project, but I prefer to have more control over this flow, and not to have everything so dependant on two boolean variables. Let's say for example that a user action triggers another event from the environment, and maybe that would provoke another animation, remember that in all this time no other user can be issued. I tremble at the consideration of having to control for every event to finish.

A small Task review

Task are a similar concept to Coroutines in Unity. In any case a function denoted with the "async" keyword is eligible to run as an asynchronous task (no surprises here). To put it simply, an async function must always contain the await keyword, which can be used in many ways. The most common is to use the "await Task.Yield();" command. With this we are simply instructing the task to restart from the beginning every new frame. An Example:

    public static async Task MoveTo(Transform movingObject, Vector3 destination, float speed, bool shouldRotate, Action action){
        Vector3 origin = movingObject.position;
        Vector3 direction = Direction(origin, destination);
        float distance = Vector3.Distance(origin, destination);
        float remainingDistance = distance;
        float completeness;

        while (remainingDistance > 0){

            // Rotation
            if(shouldRotate)
                movingObject.forward = Vector3.Lerp(
                    movingObject.forward, 
                    new Vector3(direction.x,0,direction.z), 
                    Time.deltaTime * speed * 3);

            completeness = 1 - (remainingDistance / distance);
            movingObject.position = Vector3.Lerp(origin, destination, completeness);
            remainingDistance -= speed * Time.deltaTime;
            action();
            await Task.Yield();
        }
    }

With this function we can basically move any object in any direction, no Update function needed. I have a Util class which contains all these kind of functions.

My turn system approach

The CharacterController class might be a little overloaded, but there are many good practices in Unity that I still need to learn. This time the Update function is only checking for user input, in this case either if the left click or right click input has been sent. This is because in strategy games selecting a unit with left click and ordering it around with the right click is a commonplace practice. This is a minimal approach, but we could later insert more commands that the player may use to interact with the game. We can see that both the left click and the right click call the same function but with different parameters. Notice that the called function (it is also a task), being an higher order function, requires a another task as a parameter.

    public async void TryNextTask(Func<Task> task, int delay = 0){
        if(tasks.Count > 0) return;

        // This will also start the added task
        this.tasks.Add(task());

        // Wait for all the current running tasks to finish
        await Task.WhenAll(this.tasks);
        await Task.Delay(delay);
        this.tasks.Clear();
    }

This task will simply TRY to add task to be executed to a list of tasks, and wait for them to finish. Now if there are tasks running, this will simply do nothing. The thing I like the most about this approach, is that if we want to have more concurrent actions/event happening during the current action playing, we will simply add the needed task to this list! Let's see how the MoveAction looks like now (I purposely commented some code to grey things out, and make more relevant things more easy to read).

    public override async Task Take(){/*
        currentPositionIndex = 0;
        this.pathAsVectors = Pathfinder.Instance.GetCurrentPath();
        Pathfinder.Instance.Deactivate();
        if(currentPositionIndex > pathAsVectors.Length){
            this.isWaitingForAnimationToFinish = false;
            return;
        }

        Task recalculateLOS = this.unit.RecalculateLOS(
            pathAsVectors[pathAsVectors.Length-1], this.unit.GetHeight());
            CharacterController.Instance.AddTask(recalculateLOS);*/

        OnStartMoving?.Invoke(this.unit, this.movingAnimationSpeed);
        this.unit.animator.SetBool(s_isMoving, true);
        // The unit can follow a path, a number of points to reach by moving
        for(int i=0; i<pathAsVectors.Length; i++)
            // A call of MoveTo will finish with the unit reached the destination
            await Util.MoveTo(unit.transform, pathAsVectors[i], movingAnimationSpeed, unit.MantainFootsOnTerrain);
        

        this.unit.animator.SetBool(s_isMoving, false);
        OnStopMoving?.Invoke(this, EventArgs.Empty);
        this.unit.CheckForCover();/*
        this.pathAsVectors = null;
        Pathfinder.Instance.Activate(this.unit.transform.position, this.unit.GetRemainingMovement());*/
    }

The video below (sorry for the poor resolution, the upload limit is pretty severe) shows how, when a character is selected and in standby, we can select other actions. But once we issue, for example the command to move, we cannot issue the other command to aim (I am pressing the second button continously while the character is moving). Only when the moving animation is finished the aim command is finally issued. NoCommandOvverride (1)

⚠️ **GitHub.com Fallback** ⚠️