Adding a Variable Cost

Say I wanted to add a new cost called magic points (MP) to an action like the shoot attack and allow the player to add X MP to the attack, so the shoot attack would be one action and X MP. Using and updating the MP with a constant cost (like MP 2) is easy enough, but I am having difficulty figuring out how to let the player choose how much MP to use.

You could use a pair of arrows to increase/decrease the added MP amount…

I would put a script on the MP amount text itself like this:

using TMPro;
using UnityEngine;


public class ValueSelector : MonoBehaviour
    {
        [SerializeField] private int minValue = 0;
        [SerializeField] private int maxValue = 10;
        private TextMeshProUGUI text;

        private int currentValue = 0;
        

        private void Awake()
        {
            text = GetComponent<TextMeshProUGUI>();
        }

        public int CurrentValue
        {
            get => currentValue;
            private set => currentValue = Mathf.Clamp(value, minValue, maxValue);
        }

        public void IncreaseValue()
        {
            currentValue++;
        }

        public void DecreaseValue()
        {
            currentValue--;
        }
    }

Add a + and - button (I like < and > arrows) to the sides of the TextMeshProUGUI component, and wire them to call IncreaseValue() and DecreaseValue in the inspector.

When you call the action, you can get the CurrentValue from the ValueSelector.

You may wish to include a method to setup the ValueSelector with the minimum and maximum values as well.

2 Likes

I want to use arrows to choose like you said, but I want it to happen after selecting the action and the target. I have MP set-up to work through the Unit and UnitActionSystemUI scripts, but it is just a flat cost right now. So I am trying to figure out how to pause the shoot action after selecting the target, let the player choose MP through arrows, and then finish the shoot action. Maybe this should be moved over to the shoot action video?

It’s fine here… Are you literally asking to pause the game mid-animation to set the MP Cost???

No, more like before the animations. The player chooses the target and then selects MP. After that, the animations play out. Possibly a new state in the state machine?

Perhaps a secondary method within the UnitActionSystem… Send it the Action and the Target, and then it caches those values and signals the Mana Selection to activate… when the Mana Selection is complete, carry on with the original workflow, passing along the Mana Selection as well…

I think I figured out how to start the selection when the target is chosen. I put it in HandleSelectedAction like so

       if (!selectedUnit.TrySpendActionPointsToTakeAction(selectedAction)) //Can we spend the action points?
        {
            return;
        }

        if (!selectedUnit.TrySpendMagicPointsToTakeAction(selectedAction)) //Most actions have 0 MP cost.
        {
            return;
        }

        //All checks passed so run the action
        SetBusy();
        //MP select here if shoot
        if (selectedAction.GetActionName() == "Shoot")
        {
            Debug.Log("Shoot MP Here");
        }
        selectedAction.TakeAction(mouseGridPosition, ClearBusy);

Then I need to find a way to let the player select the MP. I am having trouble creating a function to do that. It needs to get the target in the shoot action and also reference the override of MP cost in the shoot action once the player selects the amount. I have something started like this:

private void SelectMagicPoints(BaseAction baseAction, Unit selectedUnit)
{
    
    baseAction.GetMagicPointsCost(); //Get the override
}

But I can’t figure out how to get the target from the shoot action as well as allow the player to choose through the use of arrows.

If your Selected Unit and Selected Action/Target are confirmed and Cached in the UnitActionSystem, you should be able to use something like an Event to tell the Selector that it need to appear and eventually return an MP value… Here’s how I think I would set it up in UnitActionSystem

public struct MPSelectionInfo
{
     public int MPAvailable;
     public System.Action<int> callback;
}
public event EventSystem<MPSelectionInfo> OnMPSelectionNeeded;

Then when it’s time to get the MP selection, first, SetBusy (because we’ll be waiting)
Then raise the event

//Warning, I'm at work and may have made a syntax error here.
OnMPSelectionNeeded?.Invoke(this, new OnMPSelectionNeeded {MPAvailable = selectedUnit.MPAvailible(), callback = SelectionFinished});  

Your Selection interface should subscribe to this and activate when the event is raised, using the max mp to determine the maximum the player can spend. You may need to make a function on the unit to expose the amount of MP available.
Once the selection is made,

callback(MPAmountSelected);

Then you’ll need a SelectionFinished method which will take the amount of the MP used and pass that on to the ShootAction.

2 Likes

I have made progress but I hit a snag. I am able to get the selector to appear but I can’t pause the shoot action to select MP before continuing. Right now the selector appears while the shot is happening but doesn’t stop anything. I used the ActionBusyUI as a model to show the selector. Here’s what I have in UnitActionSystem:

        //All checks passed so run the action
        SetBusy();
        //MP select here if shoot
        
        if (selectedAction.GetActionName() == "Shoot")
        { 
            targetUnit = LevelGrid.Instance.GetUnitAtGridPosition(mouseGridPosition); //What unit is the target (check if works with AI)
            SetActive();
            OnMPSelectionNeeded?.Invoke(this, new MPSelectionInfo { MPAvailable = selectedUnit.GetMagicPoints(), callback = SelectionFinished });
            
        }
        selectedAction.TakeAction(mouseGridPosition, ClearBusy);

        OnActionStarted?.Invoke(this, EventArgs.Empty);

And this is in the selector:

private void Start()
{
    UnitActionSystem.Instance.OnSelectorChanged += UnitActionSystem_OnSelectorChanged;

    Hide(); //Start hidden
}

private void UnitActionSystem_OnSelectorChanged(object sender, bool isActive)
{
    if (isActive)
    {
        Show();
    }
    else
    {
        Hide();
    }
}

So this works and shows the selector with a cached unit, action, and target, but I am not sure how to pause things so the player can choose the MP amount before shooting.

There are a few factors at play here… You need to know at what point in the animation you want to pause, pause the animation, and then invoke the OnMPSelectionNeeded?.Invoke, and then wait for a response, and then continue the animation…

This might be better handled as a combination of the ShootAction itself, since we actually have a psuedo state machine running in ShootAction…

Here’s what I came up with (note that I don’t have my project setup like yours, so some of this is just hard coded numbers, which you’ll replace with your max mp cost and setting the correct values in shoot…

First, remove the OnMPSelectionNeeded?.Invoke() in the if(selectedAction.getActionName==“Shoot”) block…
Create a new method in the UnitActionSystem

    public void RequestMPCost(System.Action<int> callback)
    {
        if (OnMPSelectionNeeded == null)
        {
            callback?.Invoke(0);
            return;
        }
        OnMPSelectionNeeded?.Invoke(this, new MPSelectionInfo(){callback = callback, MPAvailable = selectedUnit.GetMagicPoints()});
    }

Now in ShootAction.cs: We’re going to add another state to the state machine

    private enum State
    {
        Aiming,
        Charging,
        Shooting,
        Cooloff,
    }

Then a few changes in our state handling:

    private void Update()
    {
        if (!isActive)
        {
            return;
        }

        stateTimer -= Time.deltaTime;

        switch (state)
        {
            case State.Aiming:
                Vector3 aimDir = (targetUnit.GetWorldPosition() - unit.GetWorldPosition()).normalized;
                
                float rotateSpeed = 10f;
                transform.forward = Vector3.Lerp(transform.forward, aimDir, Time.deltaTime * rotateSpeed);
                break;
            case State.Charging:
                stateTimer = .1f;
                break;
            case State.Shooting:
                if (canShootBullet)
                {
                    Shoot();
                    canShootBullet = false;
                }
                break;
            case State.Cooloff:
                break;
        }

        if (stateTimer <= 0f)
        {
            NextState();
        }
    }

    private void NextState()
    {
        switch (state)
        {
            case State.Aiming:
                state = State.Charging;
                stateTimer = .1f;
                UnitActionSystem.Instance.RequestMPCost(ConfirmMPCost);
                break;
            case State.Charging:
                state = State.Shooting;
                float shootingStateTime = 0.1f;
                stateTimer = shootingStateTime;
                break;
            case State.Shooting:
                state = State.Cooloff;
                float coolOffStateTime = 0.5f;
                stateTimer = coolOffStateTime;
                break;
            case State.Cooloff:
                ActionComplete();
                break;
        }
    }

    private void ConfirmMPCost(int value)
    {
        Debug.Log($"Magic = {value}"); 
        NextState();
    }

So what this does is that after the Aiming state, we are already paused for the duration of the aiming state, to ensure that the rifle is where it needs to be. Then rather than shooting directly, we pass along a request to the UnitActionSystem to get the Magic cost you’ll be wanting to use, with the callback being the ConfirmMPCost. Once that callback has been received, you can set the cost with the int returned in the callback and continue on with the rest of shooting.

2 Likes

Excellent! The state machine pause works fine though currently I have to disable the NextState() in ConfirmMPCost to get it right. However, I am having trouble getting the MP selection to register. I am not familiar with using:

public struct MPSelectionInfo
    {
        public int MPAvailable;
        public System.Action<int> callback;
    }
    public event EventHandler<MPSelectionInfo> OnMPSelectionNeeded;

or

OnMPSelectionNeeded?.Invoke(this, new MPSelectionInfo(){callback = callback, MPAvailable = selectedUnit.GetMagicPoints()});

So I think I am doing something wrong with how to get the selection working. Right now I have it like this:

private void UnitActionSystem_OnMPSelectionNeeded(object sender, 
UnitActionSystem.MPSelectionInfo e)
    {
        Unit selectedUnit = UnitActionSystem.Instance.GetSelectedUnit();
        maxMP = selectedUnit.GetMagicPoints();
        MPAmount = 1;
        availableMP = maxMP - MPAmount;
        Debug.Log("MaxMP " + maxMP);
        
    }

    public void IncreaseMP()
    {
        Unit selectedUnit = UnitActionSystem.Instance.GetSelectedUnit();
        if(MPAmount < maxMP)
        {
            MPAmount++;
        }

        UpdateMPNumText();
        UpdateMPAvailableText();
        Debug.Log("Amount " + MPAmount);
    }

    public void DecreaseMP()
    {
        if (MPAmount > 1)
        {
            MPAmount--;
        }

        UpdateMPNumText();
        UpdateMPAvailableText();
        Debug.Log("Amount " + MPAmount);
    }

With the increase and decrease wired to buttons. However, it doesn’t work. The MP amount and available are always 0 because the OnMPSelectionNeeded never triggers which is strange because I did what you did in the RequestMPCost. Is there something I am missing how to properly use this public struct and event?

Are you subscribing to the OnMPSelectionNeeded event in your script?

Then your confirm button should be

callback?.Invoke(MPAmount);
Hide();

There’s no need to get the MP from the UnitActionSystem.GetSelectedUnit in any of the methods, the max MP was already passed along in the data structure.

2 Likes

That worked! I added a bit more to show the numbers change for the player but everything is functioning. I just need to make the enemy do it now and it will be fully operational. Thank you!

1 Like

This is extra but I added a selection for the enemy like so to go off at the same time:

public struct MPSelectionInfo
    {
        public int MPAvailable;
        public int enemyMPAvailable;
        public Action<int> callback;
        public Action<int> enemyCallback;
    }
public void RequestMPCost(Action<int> callback, Action<int> enemyCallback)
    {
        if (OnMPSelectionNeeded == null)
        {
            callback?.Invoke(0);
            enemyCallback?.Invoke(0);
            return;
        }
        if (TurnSystem.Instance.IsPlayerTurn())
        {
            //Player turn use shooting unit is selected and target is enemy
            OnMPSelectionNeeded?.Invoke(this, new MPSelectionInfo()
            {
                callback = callback,
                enemyCallback = enemyCallback,
                MPAvailable = selectedUnit.GetMagicPoints(),
                enemyMPAvailable = targetUnit.GetMagicPoints()
            });
        }
        else
        {
            OnMPSelectionNeeded?.Invoke(this, new MPSelectionInfo()
            {
                callback = callback,
                enemyCallback = enemyCallback,
                MPAvailable = targetUnit.GetMagicPoints(),
                enemyMPAvailable = selectedUnit.GetMagicPoints()
            });
        }     
    }

This all seems to be fine except it runs into a problem here in the ShootAction:

case State.Aiming:
                state = State.Choosing;
                stateTimer = 0.1f;
                UnitActionSystem.Instance.RequestMPCost(ConfirmMPCost);
                break;
private void ConfirmMPCost(int value, int num)
    {
        Debug.Log($"MP = {value} and EnemyMP = {num}");

        NextState();
    }

My question is why can’t I use two values in RequestMPCost? It says
There is no argument given that corresponds to the required formal parameter 'enemyCallback' of 'UnitActionSystem.RequestMPCost(Action<int>, Action<int>)'

Why is that? I thought I set it up the same way.

1 Like

Think I figured it out. I created a new function ConfirmEnemyMPCost that is for the enemy and it works. Now it has the right amount of parameters.

1 Like

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

Privacy & Terms