Bug when the currently selected unit dies

I just came across a bug where the currently selected unit is shot by the enemy units and the game fails to transfer the unit selection over to the next one available, which essentially leaves the currently active unit to be a ghost.
When doing the next action (unless the player first clicks on the other on to select it instead), consequently I get an error:

MissingReferenceException: The object of type 'Unit' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
Unit.GetWorldPosition () (at Assets/Scripts/Unit.cs:94)
CameraManager.BaseAction_OnAnyActionStarted (System.Object sender, System.EventArgs e) (at Assets/Scripts/CameraManager.cs:39)
BaseAction.ActionStart (System.Action onActionComplete) (at Assets/Scripts/Actions/BaseAction.cs:49)
ShootAction.TakeAction (GridPosition gridPosition, System.Action onCompleteAction) (at Assets/Scripts/Actions/ShootAction.cs:112)
UnitActionSystem.HandleSelectedAction () (at Assets/Scripts/UnitActionSystem.cs:92)
UnitActionSystem.Update () (at Assets/Scripts/UnitActionSystem.cs:77)

It seems it was already discussed here: Unit that dies while selected still has move action

I would think it shouldn’t be the UnitActionSystem trying to re-assign the selected unit from within its Update() (safeguarding against “no unit selected” should be added to it, of course), but rather that the OnDead event from the healthsystem should trigger a re-selection in some way, which would avoid the possibility of trying to find another unit in every update…

So these are my changes in the UnitActionSystem:

// In Start()
        Unit.OnAnyUnitDead += Unit_OnAnyUnitDead;

// null protection within Update()
{
       //...
       // Currently active unit has died on the previous enemy turn, or all player units are dead
        if (null == selectedUnit)
        {
            return;
        }

        HandleSelectedAction();
}

// And implement handling the event
    private void Unit_OnAnyUnitDead(object sender, EventArgs e)
    {
        if (TurnSystem.Instance.IsPlayerTurn)
        {
            if (UnitManager.Instance.GetFriendlyUnityList().Count > 0)
            {
                SetSelectedUnit(UnitManager.Instance.GetFriendlyUnityList()[0]);
            }
            else
            {
                Debug.Log("Game Over, all player units died");
                selectedUnit = null;
            }
        }
    }

Since we already have the UnitManager keeping track of units, we can simply ask it to give us the first in the list.

Updating the UI would also need some null safeguard:

    private void CreateUnitActionButtons()
    {
        ClearUnitActionButtons();

        Unit selectedUnit = UnitActionSystem.Instance.GetSelectedUnit();
        if(null == selectedUnit)
        {
            return;
        }
        //...
    }

Hm. It’s more tricky than that. My changes caused the enemy to burst into a pile of ragdolls when I shot him…

MissingReferenceException: The object of type 'MeshRenderer' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
GridSystemVisualSingle.Hide () (at Assets/Scripts/Grid/GridSystemVisualSingle.cs:19)
GridSystemVisual.HideAllGridPositions () (at Assets/Scripts/Grid/GridSystemVisual.cs:92)
GridSystemVisual.UpdateGridVisual () (at Assets/Scripts/Grid/GridSystemVisual.cs:139)
GridSystemVisual.UnitActionSystem_OnSelectedActionChanged (System.Object sender, System.EventArgs e) (at Assets/Scripts/Grid/GridSystemVisual.cs:78)
UnitActionSystem.SetSelectedAction (BaseAction baseAction) (at Assets/Scripts/UnitActionSystem.cs:174)
UnitActionSystem.SetSelectedUnit (Unit unit) (at Assets/Scripts/UnitActionSystem.cs:154)
UnitActionSystem.Unit_OnAnyUnitDead (System.Object sender, System.EventArgs e) (at Assets/Scripts/UnitActionSystem.cs:184)
Unit.HealthSystem_OnDead (System.Object sender, System.EventArgs e) (at Assets/Scripts/Unit.cs:146)
HealthSystem.Die () (at Assets/Scripts/HealthSystem.cs:40)
HealthSystem.Damage (System.Int32 amount) (at Assets/Scripts/HealthSystem.cs:33)
Unit.Damage (System.Int32 damageAmount) (at Assets/Scripts/Unit.cs:139)
ShootAction.Shoot () (at Assets/Scripts/Actions/ShootAction.cs:174)
ShootAction.Update () (at Assets/Scripts/Actions/ShootAction.cs:59)

Alright, I had my logic upside-down, the transfer of control needs to take place, when it’s not currently the player’s turn.

  private void Unit_OnAnyUnitDead(object sender, EventArgs e)
  {
      if (!TurnSystem.Instance.IsPlayerTurn)
      {
        //...
      }
  }

But now, when all player units were dead (and the “game over” appeared in the console) I got a new issue, this time from the UI where I had to add an explicit null check for the “game over” state:

    private void UpdateActionPoints()
    {
        Unit selectedUnit = UnitActionSystem.Instance.GetSelectedUnit();
        if (null != selectedUnit)
        {
            _actionPointsText.text = $"Action Points Available: {selectedUnit.CurrentActionPoints}";
        }
        else
        {
            _actionPointsText.text = "GAME OVER";
        }
    }

I think this is it:

//In    private void Unit_OnAnyUnitDead(object sender, EventArgs e)    
           {
                //Debug.Log($"Game Over, all player units died");
                _selectedUnit = null;
                OnSelectedUnitChanged?.Invoke(this, EventArgs.Empty);
            }

Don’t forget to call out that the unit has changed so the UI buttons actually refresh.

So the whole change as a diff (applicable in my project, anyway) in one go:

diff --git a/TurnBasedStrategyCourse/Assets/Scripts/UI/UnitActionSystemUI.cs b/TurnBasedStrategyCourse/Assets/Scripts/UI/UnitActionSystemUI.cs
index 48c9c72..784813c 100644
--- a/TurnBasedStrategyCourse/Assets/Scripts/UI/UnitActionSystemUI.cs
+++ b/TurnBasedStrategyCourse/Assets/Scripts/UI/UnitActionSystemUI.cs
@@ -39,6 +39,12 @@ public class UnitActionSystemUI : MonoBehaviour
         ClearUnitActionButtons();
 
         Unit selectedUnit = UnitActionSystem.Instance.GetSelectedUnit();
+        if (null == selectedUnit)
+        {
+            //Debug.Log($"No selected Unit, there should be no UI buttons to show...");
+            return;
+        }
+
         foreach (BaseAction baseAction in selectedUnit.GetBaseActionArray())
         {
             Transform actionButtonTransform = Instantiate(_actionButtonPrefab, _actionButtonContainerTransform);
@@ -106,7 +112,14 @@ public class UnitActionSystemUI : MonoBehaviour
     private void UpdateActionPoints()
     {
         Unit selectedUnit = UnitActionSystem.Instance.GetSelectedUnit();
-        _actionPointsText.text = $"Action Points Available: {selectedUnit.CurrentActionPoints}";
+        if (null != selectedUnit)
+        {
+            _actionPointsText.text = $"Action Points Available: {selectedUnit.CurrentActionPoints}";
+        }
+        else
+        {
+            _actionPointsText.text = "GAME OVER";
+        }
     }
 
 }
diff --git a/TurnBasedStrategyCourse/Assets/Scripts/UnitActionSystem.cs b/TurnBasedStrategyCourse/Assets/Scripts/UnitActionSystem.cs
index f0931f3..bb79809 100644
--- a/TurnBasedStrategyCourse/Assets/Scripts/UnitActionSystem.cs
+++ b/TurnBasedStrategyCourse/Assets/Scripts/UnitActionSystem.cs
@@ -46,6 +46,7 @@ public class UnitActionSystem : MonoBehaviour
         // Seems a bit redundant, but going through the proper route is needed in order to trigger all
         // additional logic that is defined for selecting a unit (like activating the default action)
         SetSelectedUnit(_selectedUnit);
+        Unit.OnAnyUnitDead += Unit_OnAnyUnitDead;
     }
 
 
@@ -74,6 +75,12 @@ public class UnitActionSystem : MonoBehaviour
             return;
         }
 
+        // Currently active unit has died on the previous enemy turn, or all player units are dead
+        if (null == _selectedUnit)
+        {
+            return;
+        }
+
         HandleSelectedAction();
     }
 
@@ -166,4 +173,23 @@ public class UnitActionSystem : MonoBehaviour
         OnSelectedActionChanged?.Invoke(this, EventArgs.Empty);
     }
 
+
+    private void Unit_OnAnyUnitDead(object sender, EventArgs e)
+    {
+        if (!TurnSystem.Instance.IsPlayerTurn)
+        {
+            if (UnitManager.Instance.FriendlyUnityList.Count > 0)
+            {
+                //Debug.Log($"selected player unit {(sender as Unit).name} has died, transferring control");
+                SetSelectedUnit(UnitManager.Instance.FriendlyUnityList[0]);
+            }
+            else
+            {
+                //Debug.Log($"Game Over, all player units died");
+                _selectedUnit = null;
+                OnSelectedUnitChanged?.Invoke(this, EventArgs.Empty);
+            }
+        }
+    }
+
 }

Video or it didn’t happen! :smiling_imp:

Great job working through this!

As you can see, sometimes small changes to fix one issue can quickly lead to other unintended consequences. The challenge is tracking those issues down and keeping them from spiraling out of control. Well done.

I had considered to add a screenshot of the piled enemies… :grinning_face_with_smiling_eyes:

Oh, I know that just too well :grimacing:

1 Like

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

Privacy & Terms