Implementing ammo

I’m sorry if this sounds very vague but
I’ve been trying to add an ammo system into my game initially thinking I only needed to make a simple check ammo function within the Shoot action but it seems there’s a lot more I need to do than this. My plan was to make it so that the Shoot Action button should hide itself once all ammo is depleted. And add a separate reload button that would fill the ammo.
I started getting confused while trying to make this because there is no separate Shoot button(as there is only the ActionButtonUI prefab) and got completely lost on what script I need to be modifying first.
I think I can deal with making a new reload button that can be pressed anytime by the unit to reload their weapons but I can’t see how I could make an attached action script on a unit to toggle whether their button should show or not.
I’ll soon provide the codes I have once I get the time but I’m hoping for some advice. Thank you.

I would make the Reload a ReloadAction. This makes it a button available on the menu automatically. It also gives you the opportunity to count reloading as an Action that consumes Action Points.

You might consider adding a method to BaseAction

    public virtual bool CanPerformThisAction()
    {
        return GetActionPointsCost() <- unit.GetActionPoints();
    }

In most cases, this would be enough, but for an Action like ShootAction, you also want to make sure that there is enough ammo

public override bool CanPerformThisAction()
{
    return base.CanPerformThisAction() && ammo.count > 0;
}

And your ReloadAction might override it to be

public bool CanPerformThisAction()
{
   return base.CanPerformThisAction() && ammo.count == 0;
}

Finally, in ActionButtonUI, change the Interactable setting to read

        button.interactable = baseAction.CanPerformThisAction();
2 Likes

Okay, so this will require a lot of explaining to do.
Here is my script for UnitActionSystemUI.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class UnitActionSystemUI : MonoBehaviour
{
    [SerializeField] private Transform actionButtonPrefab;
    [SerializeField] private Transform actionButtonContainerTransform;
    [SerializeField] private TextMeshProUGUI actionPointsText;

    private List<ActionButtonUI> actionButtonUIList;

    private void Awake()
    {
        actionButtonUIList = new List<ActionButtonUI>();
    }
    private void Start()
    {
        UnitActionSystem.Instance.OnSelectedUnitChanged += UnitActionSystem_OnSelectedUnitChanged;//<1>
        UnitActionSystem.Instance.OnSelectedActionChanged += UnitActionSystem_OnSelectedActionChanged;
        UnitActionSystem.Instance.OnActionStarted += UnitActionSystem_OnActionStarted;
        TurnSystem.Instance.OnTurnChanged += TurnSystem_OnTurnChanged;
        Unit.OnAnyActionPointsChanged += Unit_OnAnyActionPointsChanged;

        UpdateActionPoints();
        CreateUnitActionButtons();
        UpdateSelectedVisual();//<4>
    }
    private void CreateUnitActionButtons()
    {
        //Debug.Log(0);
        foreach(Transform buttonTransform in actionButtonContainerTransform)//<2>
        {
            Destroy(buttonTransform.gameObject);
        }

        actionButtonUIList.Clear();

        Unit selectedUnit = UnitActionSystem.Instance.GetSelectedUnit();

        foreach(BaseAction baseAction in selectedUnit.GetBaseActionArray())
        {
            //Debug.Log(baseAction);
            Transform actionButtonTransform = Instantiate(actionButtonPrefab, actionButtonContainerTransform);//<3>
            ActionButtonUI actionButtonUI = actionButtonTransform.GetComponent<ActionButtonUI>();
            actionButtonUI.SetBaseAction(baseAction);

            actionButtonUIList.Add(actionButtonUI);
        }
    }

    private void UnitActionSystem_OnSelectedUnitChanged(object sender, EventArgs e)//<1>
    {
        //Debug.Log(1);
        CreateUnitActionButtons();
        UpdateSelectedVisual();
        UpdateActionPoints();
    }

    private void UnitActionSystem_OnSelectedActionChanged(object sender, EventArgs e)
    {
        //Debug.Log(2);
        UpdateSelectedVisual();
    }

    private void UnitActionSystem_OnActionStarted(object sender, EventArgs e)
    {
        //Debug.Log(3);
        UpdateActionPoints();
    }
    
    private void UpdateSelectedVisual()//<4>
    {
        //Debug.Log(4);
        foreach(ActionButtonUI actionButtonUI in actionButtonUIList)
        {
            actionButtonUI.UpdateSelectedVisual();
        }
    }

    private void UpdateActionPoints()
    {
        //Debug.Log(5);
        Unit selectedUnit = UnitActionSystem.Instance.GetSelectedUnit();

        actionPointsText.text = "Action Points : " + selectedUnit.GetActionPoints();
    }

    private void TurnSystem_OnTurnChanged(object sender, EventArgs e)
    {
        //Debug.Log(6);
        CreateUnitActionButtons();
        UpdateActionPoints();
    }

    private void Unit_OnAnyActionPointsChanged(object sender, EventArgs e)
    {
        //Debug.Log(7);
        UpdateActionPoints();
    }
}

As you can see from here

    private void TurnSystem_OnTurnChanged(object sender, EventArgs e)
    {
        //Debug.Log(6);
        CreateUnitActionButtons();
        UpdateActionPoints();
    }

I’ve added CreateUnitActionButtons(). Reason I did this was because I wanted to make it so that when the unit was out of ammo, upon next turn the Shoot Action would not display in the first place. I used Debug.Log(1~7) to check what functions always comes when a turn was over and I noticed that TurnSystem_OnTurnChanged came only once every turn. So I thought putting CreateUnitActionButtons() here would provide the new buttons depending on the updated information.

Now for providing the updated information on ammo. For this, I made changes on ShootAction.cs and ActionButtonUI.cs.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShootAction : BaseAction
{
    public event EventHandler<OnShootEventArgs> OnShoot;
    [SerializeField] public int ammo = 3;

    public class OnShootEventArgs : EventArgs
    {
        public Unit targetUnit;
        public Unit shootingUnit;
    }

    private enum State
    {
        Aiming,
        Shooting,
        Cooloff,
    }

    private State state;
    private int maxShootDistance = 7;
    private float stateTimer;
    private Unit targetUnit;
    private bool canShootBullet;
    private bool checkAmmo;

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

        stateTimer -= Time.deltaTime;

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

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

    private void NextState()
    {
        switch(state)
        {
            case State.Aiming:
                state = State.Shooting;
                float shootingStateTime = 0.01f;
                stateTimer = shootingStateTime;
                break;
            case State.Shooting:
                state = State.Cooloff;
                float cooloffStateTime = 0.5f;
                stateTimer = cooloffStateTime;
                break;
            case State.Cooloff:
                //Call delegate.
                //This will call ClearBusy in UnitActionSystem.cs which will make isBusy into false.
                //Despite ClearBusy being a private function from a different class, delegates allow this to happen
                ActionComplete();
                break;
        }

        //Debug.Log(state);
    }

    private void Shoot()
    {
        OnShoot?.Invoke(this, new OnShootEventArgs {
            targetUnit = targetUnit,
            shootingUnit = unit
        });
        
        targetUnit.Damage(40);
    }

    public bool CheckAmmo(int ammo)
    {
        if(ammo > 0)
        {
            return true;
        }
        else{
            return false;
        }
    }
    
    public override string GetActionName()
    {
        return "Shoot";
    }

    public override List<GridPosition> GetValidActionGridPositionList()
    {
        List<GridPosition> validGridPositionList = new List<GridPosition>();
//------------------------------------------------------------------------------------
//The part where unit calculates the range of the action.
        GridPosition unitGridPosition = unit.GetGridPosition();

        for(int x = -maxShootDistance; x <= maxShootDistance; x++)
        {
            for(int z = -maxShootDistance; z <= maxShootDistance; z++)
            {
                GridPosition offsetGridPosition = new GridPosition(x, z);
                GridPosition testGridPosition = unitGridPosition + offsetGridPosition;

                if(!LevelGrid.Instance.IsValidGridPosition(testGridPosition))
                {
                    continue;
                }

                //--------------------------------------------------
                //Turns valid range into triangle.
                int testDistance = Math.Abs(x) + Math.Abs(z);
                if(testDistance > maxShootDistance)
                {
                    continue;
                }

                //validGridPositionList.Add(testGridPosition);
                //continue;
                //--------------------------------------------------

                if(!LevelGrid.Instance.HasAnyUnitOnGridPosition(testGridPosition))
                {
                    //Grid Position is empty, no unit
                    continue;
                }

                //With LevelGrid.cs <7>, targetUnit identifies what the unit is on the target position.
                Unit targetUnit = LevelGrid.Instance.GetUnitAtGridPosition(testGridPosition);

                if(targetUnit.IsEnemy() == unit.IsEnemy())
                {
                    //Both units on same 'team'
                    //This is used because the enemy is also using units for their team and they shoud avoid friendly fire.
                    continue;
                }

                validGridPositionList.Add(testGridPosition);
            }
        }

        return validGridPositionList;
//--------------------------------------------------------------------------------------------
    }

    public override void TakeAction(GridPosition gridPosition, Action onActionComplete)
    {
        ActionStart(onActionComplete);

        targetUnit = LevelGrid.Instance.GetUnitAtGridPosition(gridPosition);

        //Debug.Log("Aiming");
        state = State.Aiming;
        float aimingoffStateTime = 1f;
        stateTimer = aimingoffStateTime;

        canShootBullet = true;
    }

    //--------------------------------------------------
    //Used to end turn for player using this action.
    public override int GetActionPointsCost()
    {
        return unit.GetActionPoints();
    }
    //--------------------------------------------------
}

In ShootAction.cs, CheckAmmo() was used as a bool to return true or false if there was ammo left in the unit. I made ammo into a serializefield because I wanted to customize the amount of ammo different types of units that carry guns would have. Finally the ActionButton.cs.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class ActionButtonUI : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI textMeshPro;
    [SerializeField] private Button button;
    [SerializeField] private GameObject selectedGameObject;

    private BaseAction baseAction;

    //Function to know which action this button belongs to.
    //The button's functions is called in UnitActionSystemUI and in the function SetBaseAction
    //you must make that the text within the button can be altered.
    public void SetBaseAction(BaseAction baseAction)//<1>
    {
        bool showButton = true;
        // Show or hide the Shoot button based on the ammo availability
        if (baseAction is ShootAction)
        {
            bool hasAmmo = ((ShootAction)baseAction).CheckAmmo(((ShootAction)baseAction).ammo);
            //Debug.Log(hasAmmo);
            if (!hasAmmo)
            {
                // If ShootAction has no ammo, remove it from baseAction
                showButton = false;
            }
        }
        Debug.Log(baseAction);
        this.baseAction = baseAction;
        if(showButton)
        {
            textMeshPro.text = baseAction.GetActionName().ToUpper();
            button.gameObject.SetActive(true);

            button.onClick.RemoveAllListeners();
            button.onClick.AddListener(() => {
                UnitActionSystem.Instance.SetSelectedAction(baseAction);
            });
        }
        else
        {
            button.gameObject.SetActive(false);
        }
    }

    public void UpdateSelectedVisual()
    {
        BaseAction selectedBaseAction = UnitActionSystem.Instance.GetSelectedAction();
        selectedGameObject.SetActive(selectedBaseAction == baseAction);
    }
}

The return bool for CheckAmmo from ShootAction.cs was used here to depend whether the button should be visualized or hidden.
I knew this would only hide the button and not actually remove the ability to shoot from the unit itself. But I thought that if there was no shoot button to begin with after pressing End Turn, there wouldn’t be any need to remove the shooting action anyway because the player wouldn’t be able to click on the action. And when making the Reload Action, I would make a similar method to toggle the CheckAmmo = false to true and recharge the ammo back to it’s full capacity.
But it turns out implementing ammo and adding a new Reload Action was far more complicated from this and I have the photos showing I was wrong.


This is what I see when I hit play.

And here is me clicking the shoot action and firing at the enemy in range.

And this is what happens when I press End Turn. As you can see, there is already a problem of all my buttons selected even if Shoot Action is the only one being selected.

Upon clicking the Shoot button, the Shoot button then becomes the only one visually selected. I say visually, because Shoot was selected to begin with. Which is where the second problem is seen.

The buttons don’t check if the unit has ammo or not unless I click on any button to begin with. So, in the third image I uploaded, if I don’t click on any button and just pressed the enemy tile I could shoot as much as I want regardless of my action points being depleted.


Now this image is when my unit is all out of ammo to shoot at the target. Visually, the Shoot button is gone as expected. Well, I didn’t expect the problem that all the other buttons are visually selected but this image proves that the buttons checked on the ammo and hid the Shoot button.
I was planning on making the Reload button here that can be used any time while consuming one action point but due to the major problem that’s occurring I never got to that.

Lastly this is an image of the functions that happen in order when I click End Turn in ActionButtonUI.cs. I thought that implementing an update on the buttons within any of the functions that always happen every turn would change what buttons are actually usable for each unit. Like ammo for starters and maybe an interaction button that only appears when close to a door switch etc. Or maybe an update on the unit movement like if the unit gets a debuf and has a reduction to the range it can run.

I think the problem was that I needed to make a new function other than CreateUnitActionButtons() to update the buttons. Also that I should’ve been considering the action itself and not just the UI. So I’m going to backtrack and retry a different way,
However I’d like to know if the way I did was wrong to begin with. Or if I was focusing on the wrong thing. I’d like to know if there was any other problem I missed. Thank you.

There are a lot of moving parts here, so it’s hard to specifically say what’s wrong or right about the approach. I’ll try to go through the code this weekend if time permits, but it’s generally easier me to help work through a problem from a known state (such as the course repo) than it is to work through multiple changes and then press go.

Your report of fired events seems to be missing calls to UnitActionSystemUI.UpdateSelectedVisual, although it looks like the relevant events are subscribed to and the method called from code. It’s possible you’ve not included those calls in the list shown, however.

I don’t think the turn change is the correct place for the CreateActionButtons, because we need to recreate the Action buttons whenever the selected unit changes, (because different Units can have different actions, and we need to re-assign the buttons to the new Unit anyways!).

That being said, this should be fairly simple to implement. I do recommend rather than casting the Action in the ActionSystemUI that you use a little abstraction instead.

You can use the CanPerformThisAction() method override to turn the shoot or reload completely on or off depending on the situation within the SetBaseAction() method.

this.baseAction = baseAction;
if(baseAction.CanPerformThisAction())
{
    ///set the button up
} else
{
    button.gameObject.SetActive(false);
}

That should pretty much do it. ShootAction returns true on CanPerformThisAction() if you have ammo, Reload returns true if you don’t have ammo. You might rename this to “ShowThisAction()” for more clarity.

So I’ve rewritten the code way you’ve suggested and I think the calculation for ammo works now. However I didn’t understand how to implement
button.interactable = baseAction.CanPerformThisAction();
in ActionButtonUI.cs. Which seems to be the main problem of the buttons not updating when CanPerformThisAction() returns true or false.
The situation is this,


as you can see, the action points are 0 but I’m still able to take the action.

In this photo, you can see that all ammo for Shoot has been taken but the button doesn’t hide itself. I clicked Spin to see if upon update the Shoot would recognize CanPerdormThisAction() is false but that doesn’t seem to be the case.

Finally, skipping a few turns ahead, the shoot action still remains.

Here’s the code for ShootAction.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShootAction : BaseAction
{
    public event EventHandler<OnShootEventArgs> OnShoot;
    [SerializeField] private int ammo = 10;

    public class OnShootEventArgs : EventArgs
    {
        public Unit targetUnit;
        public Unit shootingUnit;
    }

    private enum State
    {
        Aiming,
        Shooting,
        Cooloff,
    }

    private State state;
    private int maxShootDistance = 7;
    private float stateTimer;
    private Unit targetUnit;
    private bool canShootBullet;
    private void Update()
    {
        if(!isActive)
        {
            return;
        }

        stateTimer -= Time.deltaTime;

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

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

    private void NextState()
    {
        switch(state)
        {
            case State.Aiming:
                state = State.Shooting;
                float shootingStateTime = 0.01f;
                stateTimer = shootingStateTime;
                break;
            case State.Shooting:
                state = State.Cooloff;
                float cooloffStateTime = 0.5f;
                stateTimer = cooloffStateTime;
                break;
            case State.Cooloff:
                //Call delegate.
                //This will call ClearBusy in UnitActionSystem.cs which will make isBusy into false.
                //Despite ClearBusy being a private function from a different class, delegates allow this to happen
                ActionComplete();
                break;
        }
    }

    private void Shoot()
    {
        Debug.Log("Ammo before shoot = " + ammo);
        OnShoot?.Invoke(this, new OnShootEventArgs {
            targetUnit = targetUnit,
            shootingUnit = unit
        });
        
        targetUnit.Damage(40);
        ammo -= 1;
        Debug.Log("Ammo after shoot = " + ammo);
    }
    
    public override bool CanPerformThisAction()
    {
        //Debug.Log(base.CanPerformThisAction());
        //Debug.Log(ammo);
        return base.CanPerformThisAction() && ammo > 0;
    }

    
    public override string GetActionName()
    {
        return "Shoot";
    }

    public override List<GridPosition> GetValidActionGridPositionList()
    {
        List<GridPosition> validGridPositionList = new List<GridPosition>();
//------------------------------------------------------------------------------------
//The part where unit calculates the range of the action.
        GridPosition unitGridPosition = unit.GetGridPosition();

        for(int x = -maxShootDistance; x <= maxShootDistance; x++)
        {
            for(int z = -maxShootDistance; z <= maxShootDistance; z++)
            {
                GridPosition offsetGridPosition = new GridPosition(x, z);
                GridPosition testGridPosition = unitGridPosition + offsetGridPosition;

                if(!LevelGrid.Instance.IsValidGridPosition(testGridPosition))
                {
                    continue;
                }

                //--------------------------------------------------
                //Turns valid range into triangle.
                int testDistance = Math.Abs(x) + Math.Abs(z);
                if(testDistance > maxShootDistance)
                {
                    continue;
                }

                //validGridPositionList.Add(testGridPosition);
                //continue;
                //--------------------------------------------------

                if(!LevelGrid.Instance.HasAnyUnitOnGridPosition(testGridPosition))
                {
                    //Grid Position is empty, no unit
                    continue;
                }

                //With LevelGrid.cs <7>, targetUnit identifies what the unit is on the target position.
                Unit targetUnit = LevelGrid.Instance.GetUnitAtGridPosition(testGridPosition);

                if(targetUnit.IsEnemy() == unit.IsEnemy())
                {
                    //Both units on same 'team'
                    //This is used because the enemy is also using units for their team and they shoud avoid friendly fire.
                    continue;
                }

                validGridPositionList.Add(testGridPosition);
            }
        }

        return validGridPositionList;
//--------------------------------------------------------------------------------------------
    }

    public override void TakeAction(GridPosition gridPosition, Action onActionComplete)
    {
        ActionStart(onActionComplete);

        targetUnit = LevelGrid.Instance.GetUnitAtGridPosition(gridPosition);

        //Debug.Log("Aiming");
        state = State.Aiming;
        float aimingoffStateTime = 1f;
        stateTimer = aimingoffStateTime;

        canShootBullet = true;
    }

    //--------------------------------------------------
    //Used to end turn for player using this action.
    public override int GetActionPointsCost()
    {
        return unit.GetActionPoints();
    }
    //--------------------------------------------------
}

I’ve tried to change it the way suggested instead of continuing what I initially made
and through Debug I think it’s safe to say that the ammo calculation is working properly. I think I understood that part so I will implement this method when making the ReloadAction.cs as well.
Now here’s ActionButtonUI.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class ActionButtonUI : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI textMeshPro;
    [SerializeField] private Button button;
    [SerializeField] private GameObject selectedGameObject;

    private BaseAction baseAction;

    //Function to know which action this button belongs to.
    //The button's functions is called in UnitActionSystemUI and in the function SetBaseAction
    //you must make that the text within the button can be altered.
    public void SetBaseAction(BaseAction baseAction)//<1>
    {
        this.baseAction = baseAction;
        bool canPerformThisAction = baseAction.CanPerformThisAction();

        if(canPerformThisAction)
        {
            button.interactable = baseAction.CanPerformThisAction();
            Debug.Log(button.interactable);
            Debug.Log("Can perform action");
            //Set button
            textMeshPro.text = baseAction.GetActionName().ToUpper();

            button.gameObject.SetActive(true);

            button.onClick.RemoveAllListeners();
            button.onClick.AddListener(() => {
                UnitActionSystem.Instance.SetSelectedAction(baseAction);
            });
        }
        else
        {
            button.interactable = baseAction.CanPerformThisAction();
            Debug.Log(button.interactable);
            Debug.Log("Cannot perform action");
            button.gameObject.SetActive(false);
        }
    }

    public void UpdateSelectedVisual()
    {
        BaseAction selectedBaseAction = UnitActionSystem.Instance.GetSelectedAction();
        selectedGameObject.SetActive(selectedBaseAction == baseAction);
    }
}

So the code above may look a bit messy because I’ve been having a hard time implementing button.interactable in the correct area. Normally I was hoping to find the Update() function or any function that sends data by frame and add the method to hide the button if requirements don’t meet(meaning if the unit either has no action points, or in Shoot’s case has no ammo) because I thought the problem originated from the fact I’m not giving the updated data to my unit. But it seems like a problem that I lack the knowledge of. I studied about abstraction and what interactable does but there’s something I’m not seeing here which I’d appreciate any sort of advice.
Also here is UnitActionSystemUI.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class UnitActionSystemUI : MonoBehaviour
{
    [SerializeField] private Transform actionButtonPrefab;
    [SerializeField] private Transform actionButtonContainerTransform;
    [SerializeField] private TextMeshProUGUI actionPointsText;

    private List<ActionButtonUI> actionButtonUIList;

    private void Awake()
    {
        actionButtonUIList = new List<ActionButtonUI>();
    }
    private void Start()
    {
        UnitActionSystem.Instance.OnSelectedUnitChanged += UnitActionSystem_OnSelectedUnitChanged;//<1>
        UnitActionSystem.Instance.OnSelectedActionChanged += UnitActionSystem_OnSelectedActionChanged;
        UnitActionSystem.Instance.OnActionStarted += UnitActionSystem_OnActionStarted;
        TurnSystem.Instance.OnTurnChanged += TurnSystem_OnTurnChanged;
        Unit.OnAnyActionPointsChanged += Unit_OnAnyActionPointsChanged;

        UpdateActionPoints();
        CreateUnitActionButtons();
        UpdateSelectedVisual();//<4>
    }
    private void CreateUnitActionButtons()
    {
        foreach(Transform buttonTransform in actionButtonContainerTransform)//<2>
        {
            Destroy(buttonTransform.gameObject);
        }

        actionButtonUIList.Clear();

        Unit selectedUnit = UnitActionSystem.Instance.GetSelectedUnit();

        foreach(BaseAction baseAction in selectedUnit.GetBaseActionArray())
        {
            Transform actionButtonTransform = Instantiate(actionButtonPrefab, actionButtonContainerTransform);//<3>
            ActionButtonUI actionButtonUI = actionButtonTransform.GetComponent<ActionButtonUI>();
            actionButtonUI.SetBaseAction(baseAction);

            actionButtonUIList.Add(actionButtonUI);
        }
    }

    private void UnitActionSystem_OnSelectedUnitChanged(object sender, EventArgs e)//<1>
    {
        CreateUnitActionButtons();
        UpdateSelectedVisual();
        UpdateActionPoints();
    }

    private void UnitActionSystem_OnSelectedActionChanged(object sender, EventArgs e)
    {
        UpdateSelectedVisual();
    }

    private void UnitActionSystem_OnActionStarted(object sender, EventArgs e)
    {
        UpdateActionPoints();
    }
    
    private void UpdateSelectedVisual()//<4>
    {
        foreach(ActionButtonUI actionButtonUI in actionButtonUIList)
        {
            actionButtonUI.UpdateSelectedVisual();
        }
    }

    private void UpdateActionPoints()
    {
        Unit selectedUnit = UnitActionSystem.Instance.GetSelectedUnit();

        actionPointsText.text = "Action Points : " + selectedUnit.GetActionPoints();
    }

    private void TurnSystem_OnTurnChanged(object sender, EventArgs e)
    {
        UpdateActionPoints();
    }

    private void Unit_OnAnyActionPointsChanged(object sender, EventArgs e)
    {
        UpdateActionPoints();
    }
}

As you can see, I’m no longer using CreateUnitActionButtons(); in TurnSystem_OnTurnChanged() because the button should be created when the unit changes, not the turn. I haven’t touched this code after deleting that line but was looking into this thinking the problem may lie in here.
But honestly the biggest concern for me is that I think the solution may be very simple and I’m making this a much bigger problem than it should be. Thank you again for the suggestions and advice. I’ll provide updates on my progress of this problem.

I think I see the problem now…
Here’s the Course version of ActionButtonUI.UpdateSelectedVisual()

public void UpdateSelectedVisual()
    {
        if (UnitActionSystem.Instance.GetSelectedUnit() == null)
        {
            button.interactable = false;
            selectedGameObject.SetActive(false);
            return;
        }
        BaseAction selectedBaseAction = UnitActionSystem.Instance.GetSelectedAction();
        button.interactable = baseAction.GetActionPointsCost() <=
           baseAction.GetUnit().GetActionPoints();
        selectedGameObject.SetActive(selectedBaseAction == baseAction);
    }

As you can see, there’s quite a bit of course code method missing from your UpdateSelectedVisual

The course version updates both the button and the selected outline. It bases the decision of whether it’s interactable or not based on Action Points available vs. ActionPoint cost.
My own version of the game, rather than having an ammo count, has a cooldown for each Action, much like common Squad v Squad games like Marvel Strike Force or Raid: Shadow Legends. So in my case, you can perform one Action and one Move (or just one, or neither) for each character per turn. CanPerformThisAction in Actions other than MoveAction returns (!HasActedThisTurn && Cooldown==0) but in your case, for most actions it should simply return the Action point cost of the ability >= the number of points available, except for shoot, which should include Ammo == 0 and Reload which should include Ammo >0. You might also include the HasActedThisTurn logic as well, resetting the bool on each when the turn changes (the Action would subscribe to TurnManagerOnTurnChanged
Then you would change UpdateSelectedVisual to:

    public void UpdateSelectedVisual()
    {
        if (UnitActionSystem.Instance.GetSelectedUnit() == null)
        {
            button.interactable = false;
            selectedGameObject.SetActive(false);
            return;
        }
        BaseAction selectedBaseAction = UnitActionSystem.Instance.GetSelectedAction();
        button.interactable = baseAction.CanPerformThisAction();
        selectedGameObject.SetActive(selectedBaseAction == baseAction);
    }

Now since you’re goal is to not show the button at all if the player cannot shoort (or conversely Reload), you could include instead

button.gameObject.SetActiveSelf(baseAction.CanPerformThisAction());

This would make the button dissappear, and if the Reload button is after the Shoot button, it would actually make the Reload button appear out of nowhere as well.

In SetBaseAction, rather than working through all of the logic you have now, you can simplify SetBaseAction greatly

    public void SetBaseAction(BaseAction baseAction)
    {
        this.baseAction = baseAction;
        textMeshPro.text = baseAction.GetActionName().ToUpper();

        button.onClick.AddListener(() => {
            UnitActionSystem.Instance.SetSelectedAction(baseAction);
        });
        UpdateSelectedVisual();
    }

because UpdateSelectedVisual is already working through the buttons on and off issues.

One more quick set of changes (verify that this works, and that either ShootAction or ReloadAction is on at any given time (could be neither if both have been pressed).

Your idea is to completely remove the button until the end of the turn, and for the contraindicated button to only appear on the NEXT turn…

  1. Character Shoots, Shoot Action Disappears each turn until out of ammo
  2. Character out of ammo, ShootAction Disappears, no Reload button appears
  3. Next turn Reload button appears, but no shoot button until a turn where Character Reloads
  4. Reload button dissappears, ShootAction does not appear this turn
  5. ShootAction appears on next turn, cycle repeats.

This is actually relatively easy to accomplish, without breaking the the system.
First, in the ShootAction and Reloadactions, put in that hasActedThisTurn bool I mentioned earlier. In this case, both ShootAction and ReloadAction will need an Awake() method, where they subscribe to TurnSystem.Instance.OnTurnChanged;

    //This is ShootAction's handler, ReloadAction would need to be ammo==0
    private void TurnSystem_OnTurnChanged(object sender, EventArgs e)
    {
        hasActedThisTurn = ammo > 0;
    }

Now, during this turn, only the Shoot Action or the Reload action will be visible at the start of the turn, and when shooting or reloading, either one, there will be no converse button appearing until the start of the next turn.

Privacy & Terms