When it comes to software, controlled destruction breeds confidence

When it comes to software, controlled destruction breeds confidence

“Get Out Alive” is a prototype I’ve been working on for a 2D turn-based game. It has been a few months since I started and has been a lot of fun so far, so I figured I would give a progress update.

Lately I’ve been working on “search areas” and item “finding” for units, so the unit can enter a search area, and then during the unit’s next turn you can have that unit perform a search to find a random item (e.g. first aid, weapon, etc).

Demo of basic unit item searching and usage:

Handling items still has a lot of work to go but I got the basic ScriptableObject groundwork for it done.

After all this progress, I realized that I coded myself into a hole of sorts. What do I mean? Well, my main Player class is called TurnPlayer, and over time, as I started implementing new things, I ended up just implementing a lot of them into this one class. Now this TurnPlayer class is responsible for managing the UI by enabling/disabling UI elements, reacting to UI events, starting unit turns and handling what the current unit is. Man! I really messed up that one. I’ve noticed this the entire time of the project but I was too focused on getting small stuff working that I didn’t want to change it since I was afraid of breaking everything else and having to spend more time fixing things I already had working. It may feel OK in the short term to not bother refactoring code, but it is really biting me in the butt in this project right now.

If you’re curious what this TurnPlayer class looks like, here is the code for it. It is absolutely horrible and I’m pretty embarrassed by it (not even to mention issues with server authority, I know I need to refactor that). My non-game projects aren’t nearly this bad since I feel more confident with structure, design and everything outside of Unity but I’m still getting used to game development.

public class TurnPlayer : NetworkBehaviour
{
    public delegate void OnPlayerTurnChangedEventHandler(bool isOurTurn);

    const short cTurnPlayer = 1002;

    const int cMaxUnits = 6;

    #region Serialied Variables
    [SerializeField]
    public Sprite playerPortrait;

    [SerializeField]
    public bool spawnUnitsAtEndOfTurn = false;

    [SerializeField]
    protected GameObject[] unitPrefab;

    [SerializeField]
    private GameObject unitCameraPrefab;
    #endregion  

    #region SyncVars
    [SyncVar]
    public int playerId;

    [SyncVar]
    public NetworkInstanceId playerNetId;

    [SyncVar]
    public bool isEnemy;

    //[SyncVar]
    public bool hasTurn = false;

    [SyncVar(hook = "OnMaxMovesChanged")]
    public int maxMoves;

    //[SyncVar]
    private int currentUnitIndex = 0;

    [SyncVar]
    private NetworkInstanceId currentUnitId;

    [SyncVar]
    private bool hasSpawnedUnits = false;

    [SyncVar]
    private int currentUnitCount = 0;
    #endregion

    #region UI Elements
    private GameObject localUICanvas;

    // Button Bar Buttons
    private Button localButtonBarBtnSearch;
    private Button localButtonBarBtnAction;
    private Button localButtonBarBtnNextUnit;
    private Button localButtonBarBtnEndTurn;
    private Button localButtonBarBtnCombat;

    // FOR DEBUGGING
    private Text localUnitTurnState;
    #endregion

    private TurnPlayerUI turnPlayerUI;
    private List<Unit> units = new List<Unit>();
    private List<GameObject> unitObjs = new List<GameObject>();
    private List<Transform> spawnPositions = new List<Transform>();
    private LobbyManager lobbyManager;
    private CinemachineVirtualCamera playerCamera;
    private CinemachineVirtualCamera currentUnitCamera;
    private CinemachineVirtualCamera lastUnitCamera;
    private List<CinemachineVirtualCamera> unitCameras = new List<CinemachineVirtualCamera>();

    private UnitSelector unitSelector;

    private TurnPlayer_UnitManager turnPlayer_UnitManager;
    private TurnPlayer_State turnPlayer_State;
    private TurnPlayer_Spawner turnPlayer_Spawner;

    #region Properties
    public bool IsEnemy
    {
        get { return isEnemy; }
    }

    public CinemachineVirtualCamera CurrentCamera
    {
        get { return currentUnitCamera; }
    }

    public float CameraOrthographicSize
    {
        get { return playerCamera.m_Lens.OrthographicSize; }
        set
        {
            playerCamera.m_Lens.OrthographicSize = value;

            for (int i = 0; i < unitCameras.Count; i++)
            {
                unitCameras[i].m_Lens.OrthographicSize = value;
            }
        }
    }
    #endregion

    #region Events
    public event OnPlayerTurnChangedEventHandler OnPlayerTurnChanged;

    private void OnTurnChange(bool isOurTurn)
    {
        if (OnPlayerTurnChanged != null)
        {
            OnPlayerTurnChanged(isOurTurn);
        }
    }
    #endregion

    #region SyncVar Event Handlers
    private void OnMaxMovesChanged(int value)
    {
        maxMoves = value;

        if (isLocalPlayer)
        {
            //Debug.Log("TurnPlayer.OnMaxMovesChanged() - " + value);

            turnPlayerUI.SetLocalPlayerMaxMoves(value);

            var unit = units[currentUnitIndex];
            unit.SetMaxMoves(value);
        }
    }
    #endregion

    #region Unity Overrides
    public override void OnStartLocalPlayer()
    {
        localUICanvas = GameObject.Find("LocalUICanvas");

        playerCamera = GameObject.Find("CM vcam1").GetComponent<CinemachineVirtualCamera>();
        playerCamera.transform.position = gameObject.transform.position;
        playerCamera.m_Follow = gameObject.transform;

        turnPlayerUI = GetComponent<TurnPlayerUI>();

        unitSelector = GetComponent<UnitSelector>();

        turnPlayer_UnitManager = GetComponent<TurnPlayer_UnitManager>();
        turnPlayer_State = GetComponent<TurnPlayer_State>();
        turnPlayer_Spawner = GetComponent<TurnPlayer_Spawner>();

        localButtonBarBtnSearch = localUICanvas.FindObject("btnSearch").GetComponent<Button>();
        localButtonBarBtnSearch.onClick.AddListener(delegate () { this.BtnSearch_Click(); });

        localButtonBarBtnAction = localUICanvas.FindObject("btnAction").GetComponent<Button>();
        localButtonBarBtnAction.onClick.AddListener(delegate () { this.BtnAction_Click(); });

        localButtonBarBtnEndTurn = localUICanvas.FindObject("btnEndTurn").GetComponent<Button>();
        localButtonBarBtnEndTurn.onClick.AddListener(delegate () { this.BtnEndTurn_Click(); });

        localButtonBarBtnNextUnit = localUICanvas.FindObject("btnNextUnit").GetComponent<Button>();
        localButtonBarBtnNextUnit.onClick.AddListener(delegate () { this.BtnNextUnit_Click(); });

        localButtonBarBtnCombat = localUICanvas.FindObject("btnCombat").GetComponent<Button>();
        localButtonBarBtnCombat.onClick.AddListener(delegate () { this.BtnCombat_Click(); });

        EnableLocalEndTurnButton(false);

        // FOR DEBUGGING
        localUnitTurnState = localUICanvas.FindObject("UnitTurnState").GetComponent<Text>();

        hasTurn = false;
    }

    public override void OnStartClient()
    {
        this.playerNetId = this.netId;
    }

    public override void OnStartServer()
    {
        base.OnStartServer();
    }

    void Start()
    {
        lobbyManager = GameObject.Find("LobbyManager").GetComponent<LobbyManager>();

        if (isServer)
        {
            this.playerNetId = this.netId;

            GameManager.singleton.AddPlayer(this.playerNetId, gameObject);
        }

        if (isLocalPlayer)
        {
            // Disable turn elements on start
            turnPlayerUI.EnableLocalButtonBar(false);

            if (NetworkClient.active)
            {
                GameManager.singleton.EventOnPlayerTurnChange += GameManager_OnPlayerTurnChange;
                GameManager.singleton.EventOnPlayerUnitStartTurn += GameManager_OnPlayerUnitStartTurn;
                GameManager.singleton.EventOnPlayerUnitEndTurn += GameManager_OnPlayerUnitEndTurn;
                GameManager.singleton.EventOnGameStart += GameManager_OnGameStart;
                GameManager.singleton.EventOnGameSpawnUnits += GameManager_OnSpawnUnits;
            }   
        }

        // Generate spawn positions
        if (isEnemy)
        {
            var enemySpawn = Utils.GetEnemySpawnPositions();

            int posIndex = 0;
            for (int i = 0; i < unitPrefab.Length; i++)
            {
                posIndex = i;

                if (posIndex >= enemySpawn.Count)
                    posIndex = 0;

                spawnPositions.Add(enemySpawn[posIndex]);
            }
        }
        else
        {
            for (int i = 0; i < unitPrefab.Length; i++)
            {
                spawnPositions.Add(lobbyManager.GetStartPosition());
            }
        }
    }

    protected virtual void OnDestroy()
    {
        if (isLocalPlayer)
        {
            localButtonBarBtnSearch.onClick.RemoveListener(delegate () { this.BtnSearch_Click(); });
            localButtonBarBtnAction.onClick.RemoveListener(delegate () { this.BtnAction_Click(); });
            localButtonBarBtnEndTurn.onClick.RemoveListener(delegate () { this.BtnEndTurn_Click(); });
            localButtonBarBtnNextUnit.onClick.RemoveListener(delegate () { this.BtnNextUnit_Click(); });
            localButtonBarBtnCombat.onClick.RemoveListener(delegate () { this.BtnCombat_Click(); });

            GameManager.singleton.EventOnPlayerTurnChange -= GameManager_OnPlayerTurnChange;
            GameManager.singleton.EventOnPlayerUnitStartTurn -= GameManager_OnPlayerUnitStartTurn;
            GameManager.singleton.EventOnPlayerUnitEndTurn -= GameManager_OnPlayerUnitEndTurn;
            GameManager.singleton.EventOnGameStart -= GameManager_OnGameStart;
            GameManager.singleton.EventOnGameSpawnUnits -= GameManager_OnSpawnUnits;
        }

        if (hasAuthority)
        {
            for (int i = 0; i < units.Count; i++)
            {
                units[i].EventOnUnitStartTurn -= OnUnitStartTurn;
                units[i].EventOnUnitEndTurn -= OnUnitEndTurn;
                units[i].EventOnUnitStateChanged -= OnUnitStateChanged;
                units[i].EventOnUnitDeath -= OnUnitDeath;

                units[i].EventOnUnitStartup -= OnUnitStartup;

                // Unsubscribe to unit's searchable area events
                units[i].EventOnUnitEnterSearchableArea -= OnUnitEnterSearchableArea;
                units[i].EventOnUnitLeaveSearchableArea -= OnUnitLeaveSearchableArea;

                units[i].EventOnUnitSearchFoundItem -= OnUnitSearchFoundItem;
                units[i].EventOnUnitEquipItem -= OnUnitEquipItem;
            }
        }
    }

    protected virtual void OnDisable()
    {
        if (isLocalPlayer)
        {
            localButtonBarBtnSearch.onClick.RemoveListener(delegate () { this.BtnSearch_Click(); });
            localButtonBarBtnAction.onClick.RemoveListener(delegate () { this.BtnAction_Click(); });
            localButtonBarBtnEndTurn.onClick.RemoveListener(delegate () { this.BtnEndTurn_Click(); });
            localButtonBarBtnNextUnit.onClick.RemoveListener(delegate () { this.BtnEndTurn_Click(); });
            localButtonBarBtnCombat.onClick.RemoveListener(delegate () { this.BtnCombat_Click(); });

            GameManager.singleton.EventOnPlayerTurnChange -= GameManager_OnPlayerTurnChange;
            GameManager.singleton.EventOnPlayerUnitStartTurn -= GameManager_OnPlayerUnitStartTurn;
            GameManager.singleton.EventOnPlayerUnitEndTurn -= GameManager_OnPlayerUnitEndTurn;
            GameManager.singleton.EventOnGameStart -= GameManager_OnGameStart;
            GameManager.singleton.EventOnGameSpawnUnits -= GameManager_OnSpawnUnits;
        }

        if (hasAuthority)
        {
            for (int i = 0; i < units.Count; i++)
            {
                units[i].EventOnUnitStartTurn -= OnUnitStartTurn;
                units[i].EventOnUnitEndTurn -= OnUnitEndTurn;
                units[i].EventOnUnitStateChanged -= OnUnitStateChanged;
                units[i].EventOnUnitDeath -= OnUnitDeath;

                units[i].EventOnUnitStartup -= OnUnitStartup;

                // Unsubscribe to unit's searchable area events
                units[i].EventOnUnitEnterSearchableArea -= OnUnitEnterSearchableArea;
                units[i].EventOnUnitLeaveSearchableArea -= OnUnitLeaveSearchableArea;

                units[i].EventOnUnitSearchFoundItem -= OnUnitSearchFoundItem;
                units[i].EventOnUnitEquipItem -= OnUnitEquipItem;
            }
        }
    }

    protected virtual void Update()
    {
        if (isServer && this.connectionToClient.isReady && !hasSpawnedUnits)
        {
            CmdSpawnUnits();
        }

        if (!isLocalPlayer)
            return;

        if (!hasSpawnedUnits)
            return; // Don't start until we've spawned units.

        if (GameManager.singleton.currentTurnId != this.netId && hasTurn)
        {
            // Opps, it isn't our turn but we haven't ended it yet?
            //Debug.LogWarning("Opps, it isn't our turn but we haven't ended it yet?");

            //hasTurn = false;
        }
        else if (GameManager.singleton.currentTurnId == this.netId && hasTurn)
        {
            if (currentUnitIndex > units.Count)
                return;

            //TODO Sometimes this displays an index out of bounds exception when a unit dies and a new one is spawned.
            var currentUnit = units[currentUnitIndex];

            if (currentUnit == null)
                return;

            // It is our turn and we've already started it
            if (currentUnit.TurnState == GameConstants.UnitTurnState.PerformMoveActionOrSearch)
            {
                if (Input.GetMouseButtonDown(0) && Utils.IsOnScreen(Input.mousePosition))
                {
                    if (EventSystem.current.IsPointerOverGameObject() && EventSystem.current.currentSelectedGameObject != null)
                    {
                        Debug.LogWarning("IsPointerOverGameObject == true; name = " + EventSystem.current.currentSelectedGameObject.name);
                        return;
                    }

                    Vector3 moveTarget = Camera.main.ScreenToWorldPoint(Input.mousePosition);

                    MoveUnit(moveTarget);
                }
            }
            else if (currentUnit.TurnState == GameConstants.UnitTurnState.Combat)
            {

            }
        }
    }

    private void LateUpdate()
    {
        if (!isLocalPlayer)
            return;

        if (!hasSpawnedUnits)
            return; // Don't start until we've spawned units.


        float scrollSpeed = 2;
        float dragSpeed = 0.5f;

        var mousePos = Input.mousePosition;

        // Do camera movement by keyboard
        transform.Translate(new Vector3(Input.GetAxis("Horizontal") * scrollSpeed * Time.deltaTime,
                                      Input.GetAxis("Vertical") * scrollSpeed * Time.deltaTime, 0));


        if (currentUnitCamera == null)
            return;

        if ((Input.GetKey("left alt") || Input.GetKey("right alt")) || Input.GetMouseButton(1))
        {
            currentUnitCamera.Priority = 10;
            playerCamera.Priority = 12;

            transform.Translate(-new Vector3(Input.GetAxis("Mouse X") * dragSpeed, Input.GetAxis("Mouse Y") * dragSpeed, 0));
        }
        else
        {
            currentUnitCamera.Priority = 10;
            playerCamera.Priority = 1;
        }
    }
    #endregion

    #region UI Event Handlers
    protected virtual void BtnSearch_Click()
    {
        var unit = units[currentUnitIndex];
        unit.PerformSearch();

        //Debug.Log("BtnSearch_Click()");
    }

    protected virtual void BtnAction_Click()
    {
        var unit = units[currentUnitIndex];
        unit.PerformAction(0);

        //Debug.Log("BtnAction_Click()");
    }

    protected virtual void BtnCombat_Click()
    {
        //Debug.Log("BtnSkipUnit_Click()");
        //TODO: When this is clicked, it should check for nearby units to do combat with
        // And that will trigger an event that will display a UI with a list of units.
        // In the UI you can click the unit and it will deal combat damage to that unit.
        // That ui will go away and turn will continue like normal.

        var unitObj = unitObjs[currentUnitIndex];
        var unitCombat = unitObj.GetComponent<UnitCombat>();
        var unit = units[currentUnitIndex];

        //if (unitCombat != null)
        //{
        //    unitCombat.HighlightValidTargets();
        //}

        unit.DoCombat();        
    }

    protected virtual void BtnNextUnit_Click()
    {
        //Debug.Log("BtnNextUnit_Click()");

        EndUnitTurn();
    }

    protected virtual void BtnEndTurn_Click()
    {
        //Debug.Log("BtnEndTurn_Click()");

        CompleteTurn();
    }
    #endregion

    #region GameManager Event Handlers
    private void GameManager_OnPlayerTurnChange(GameObject playerObject, NetworkInstanceId playerId)
    {
        var player = playerObject.GetComponent<TurnPlayer>();

        if (player != null)
        {
            //Debug.Log("TurnPlayer.GameManager_OnPlayerTurnChange() - playerId=" + playerId + "; netId=" + this.netId);

            if (playerId == this.netId)
            {
                BeginTurn();
            }
            else
            {
                if (hasAuthority)
                {
                    localUnitTurnState.text = "";
                }
            }
        }
    }

    private void GameManager_OnPlayerUnitStartTurn(GameObject unitObj, NetworkInstanceId unitId)
    {
        var unit = unitObj.GetComponent<Unit>();

        if (unit != null)
        {
            if (unit.ownerNetId != netId)
            {
                //Debug.Log("TurnPlayer.GameManager_OnPlayerUnitStartTurn() " + unitObj.name);

                var collider = unitObj.GetComponent<BoxCollider2D>();

                GraphUpdateObject guo = new GraphUpdateObject(collider.bounds);
                guo.modifyWalkability = true;
                guo.setWalkability = true;
                guo.updatePhysics = false;

                AstarPath.active.UpdateGraphs(guo, 0.5f);
                AstarPath.active.Scan();
            }

            turnPlayerUI.EnableTurnPlayerUI(true);
            turnPlayerUI.SetTurnPlayerUnit(unit);
        }
        else
        {
            turnPlayerUI.EnableTurnPlayerUI(false);
        }
    }

    private void GameManager_OnPlayerUnitEndTurn(GameObject unitObj, NetworkInstanceId unitId)
    {
        var unit = unitObj.GetComponent<Unit>();

        if (unit != null)
        {
            if (unit.ownerNetId != netId)
            {
                //Debug.Log("TurnPlayer.GameManager_OnPlayerUnitEndTurn() " + unitObj.name);

                var collider = unitObj.GetComponent<BoxCollider2D>();

                GraphUpdateObject guo = new GraphUpdateObject(collider.bounds);
                guo.modifyWalkability = true;
                guo.setWalkability = false;
                guo.updatePhysics = false;

                AstarPath.active.UpdateGraphs(guo, 0.5f);
                AstarPath.active.Scan();
            }
        }
    }

    private void GameManager_OnGameStart()
    {
        if (!isLocalPlayer)
            return;

        playerCamera.gameObject.transform.position = new Vector3(0, 1, 0);
        playerCamera.m_Follow = gameObject.transform;
        
        currentUnitCamera = unitCameras[0];
        currentUnitCamera.enabled = true;

        ShowAllUnitInfo(true);
    }

    private void GameManager_OnSpawnUnits()
    {
        if (!isLocalPlayer)
            return;

        //Debug.Log("TurnPlayer.GameManager_OnSpawnUnits()");

        //TODO Should display a message to all other players that they sense a change to indicate that the enemy is spawning

        CmdHandleUnitSpawning();
    }
    #endregion

    #region Unit Event Handlers
    private void OnUnitStartup(GameObject unitObj)
    {
        //Debug.Log("OnUnitStartup(" + unitObj.name + "; hasAuthority="+ hasAuthority  + "; isServer=" + isServer + ") - " + unitObj.name);

        if (!hasAuthority)
            return;

        var unit = unitObj.GetComponent<Unit>();
        turnPlayerUI.UnitPanel_Add(unitObj, unit.netId);
    }

    private void OnUnitStartTurn(GameObject unitObj)
    {
        //Debug.Log("OnUnitStartTurn");
    }

    private void OnUnitEndTurn(bool performedMove, bool softEndTurn)
    {
        //Debug.Log("OnUnitEndTurn - " + performedMove);

        if (softEndTurn)
        {
            CmdGameManagerPlayerUnitEndTurn(unitObjs[currentUnitIndex], units[currentUnitIndex].netId);

            return;
        }

        // On unit end turn, set unit state to Inactive
        var unit = units[currentUnitIndex];

        StartCoroutine(DelayNextTurn(1.0f));
    }

    private void OnUnitStateChanged(GameConstants.UnitTurnState state)
    {
        //Debug.Log("TurnPlayer.OnUnitStateChanged(" + gameObject.name + ") " + state);

        HandleUnitTurnState(state);
    }

    private void OnUnitDeath(GameObject deadUnit, GameObject attackerObj)
    {
        if (!isLocalPlayer)
            return;

        if (deadUnit == null)
            return;

        Debug.Log("TurnPlayer.OnUnitDeath(" + gameObject.name + "; isLocalPlayer=" + isLocalPlayer + ") - unit.Name: " + deadUnit.name + "; DiedFrom=" + attackerObj.name);

        RemoveUnit(deadUnit);
    }

    private void RemoveUnit(GameObject deadUnit)
    {
        Debug.Log("TurnPlayer.RemoveUnit(" + gameObject.name + "; isLocalPlayer=" + isLocalPlayer + ") - deadUnit.Name: " + deadUnit.name);

        var unit = deadUnit.GetComponent<Unit>();

        if (unit != null)
        {
            // Unsubscribe from unit events
            unit.EventOnUnitStartTurn -= OnUnitStartTurn;
            unit.EventOnUnitEndTurn -= OnUnitEndTurn;
            unit.EventOnUnitStateChanged -= OnUnitStateChanged;
            unit.EventOnUnitDeath -= OnUnitDeath;

            unit.EventOnUnitStartup -= OnUnitStartup;

            // Unsubscribe to unit's searchable area events
            unit.EventOnUnitEnterSearchableArea += OnUnitEnterSearchableArea;
            unit.EventOnUnitLeaveSearchableArea += OnUnitLeaveSearchableArea;

            // Get index of deadUnit
            int deadUnitIndex = units.IndexOf(unit);
            var unitId = unit.netId;

            // Remove unit from units panel
            turnPlayerUI.UnitPanel_Remove(unitId);

            // Remove unit from collection
            units.RemoveAt(deadUnitIndex);

            // Remove unit object from collection
            unitObjs.RemoveAt(deadUnitIndex);

            var deadUnitCamera = unitCameras[deadUnitIndex];

            // Remove unit's camera
            unitCameras.RemoveAt(deadUnitIndex);

            // Destroy unit camera
            Destroy(deadUnitCamera.gameObject);

            // Destroy unit object
            CmdDestroyUnit(deadUnit);
        }
    }

    private void OnUnitEnterSearchableArea(GameObject unitObj)
    {
        if (!isLocalPlayer)
            return;

        SpeechBubbleManager.Instance.AddSpeechBubble(
            unitObj.gameObject.transform,
            "I can search this area....", SpeechBubbleManager.SpeechbubbleType.NORMAL);

        //Debug.Log("TurnPlayer.OnUnitEnterSearchableArea() - " + unitObj.name);
    }

    private void OnUnitLeaveSearchableArea(GameObject unitObj)
    {
        if (!isLocalPlayer)
            return;

        SpeechBubbleManager.Instance.AddSpeechBubble(
            unitObj.gameObject.transform,
            "Leaving search area", SpeechBubbleManager.SpeechbubbleType.NORMAL);

        //Debug.Log("TurnPlayer.OnUnitLeaveSearchableArea() - " + unitObj.name);
    }

    private void OnUnitSearchFoundItem(GameObject unitObj, string itemId, bool canEquip)
    {
        var unit = unitObj.GetComponent<Unit>();

        if (canEquip)
        {
            //TODO Show UI where player can choose to equip item or not

            unit.EquipItem(itemId);
        }
    }

    private void OnUnitEquipItem(GameObject unitObj, string itemId)
    {
        BaseItem item = ItemManager.singleton.GetItem(itemId);

        if (item != null)
        {
            EnableLocalUnitActionBar(true);

            turnPlayerUI.ActionBar_AddItem(unitObj, item);
        }
    }
    #endregion

    #region Unit Methods
    public void SpawnUnits()
    {
        CmdSpawnUnits();
    }
    #endregion

    #region Command Methods
    [Command]
    private void CmdUpdateCurrentUnitId(NetworkInstanceId id)
    { 
        currentUnitId = id;
    }

    [Command]
    private void CmdSetMaxMoves(int moves)
    {
        this.maxMoves = moves;
    }

    [Command]
    private void CmdCompleteTurn()
    {
        // Tell GameManager on server to move to next turn
        GameManager.singleton.NextTurn(this.netId);
    }

    [Command]
    private void CmdGameManagerPlayerUnitStartTurn(GameObject unit, NetworkInstanceId unitId)
    {
        GameManager.singleton.PlayerUnitStartTurn(unit, unitId);
    }

    [Command]
    private void CmdGameManagerPlayerUnitEndTurn(GameObject unit, NetworkInstanceId unitId)
    {
        GameManager.singleton.PlayerUnitEndTurn(unit, unitId);
    }

    [Command]
    private void CmdSpawnUnits()
    {
        if (!NetworkServer.active)
            return;

        // Spawn units
        for (int i = 0; i < unitPrefab.Length; i++)
        {
            var spawnPosition = spawnPositions[i];

            var unitObj = (GameObject)Instantiate(
               unitPrefab[i],
               spawnPosition.position,
               Quaternion.identity);

            unitObj.name = string.Format("{0}-{1}-{2}", i, gameObject.name, unitObj.name);

            NetworkServer.SpawnWithClientAuthority(unitObj, this.connectionToClient);

            currentUnitCount++;

            RpcAddUnit(unitObj);
        }

        hasSpawnedUnits = true;
    }

    [Command]
    private void CmdDestroyUnit(GameObject unit)
    {
        Debug.Log("TurnPlayer.CmdDestroyUnit(" + gameObject.name + ") - deadUnit.Name: " + unit.name);

        if (!hasAuthority)
            return;

        currentUnitCount--;

        Destroy(unit);
    }

    [Command]
    private void CmdHandleUnitSpawning()
    {
        //Debug.Log("TurnPlayer.CmdHandleUnitSpawning()");

        if (spawnUnitsAtEndOfTurn == false)
            return;

        // Logic to see if player can spawn units
        bool canSpawn = false;

        // If the number of units we have is greater than  the number of available units
        // then the player cannot spawn any more units.
        if (currentUnitCount > unitPrefab.Length)
            return;

        int spawnRoll = Random.Range(1, unitPrefab.Length);

        // If the spawn roll is greater than the number of current units then we can spawn.
        if (spawnRoll > currentUnitCount)
            canSpawn = true;

        if (canSpawn)
        {
            // Get random spawn position
            int randomIndex = Random.Range(0, spawnPositions.Count - 1);
            var spawnPosition = spawnPositions[randomIndex];

            var unitObj = (GameObject)Instantiate(
               unitPrefab[0],
               spawnPosition.position,
               Quaternion.identity);

            unitObj.name = string.Format("{0}-{1}-SPAWNED", gameObject.name, unitObj.name);
            NetworkServer.SpawnWithClientAuthority(unitObj, gameObject);

            currentUnitCount++;

            RpcAddUnit(unitObj);
        }
    }
    #endregion

    #region ClientRPC Methods
    [ClientRpc]
    void RpcAddUnit(GameObject obj)
    {
        if (!isLocalPlayer)
            return;

        var unit = obj.GetComponent<Unit>();

        //Debug.Log("TurnPlayer.RpcAddUnit(" + obj.name + "; unit=" + unit.UnitName + ")");

        unit.EventOnUnitStartTurn += OnUnitStartTurn;
        unit.EventOnUnitEndTurn += OnUnitEndTurn;
        unit.EventOnUnitStateChanged += OnUnitStateChanged;

        // Subscribe to unit's death event
        unit.EventOnUnitDeath += OnUnitDeath;

        // Subscribe to unit's startup event
        unit.EventOnUnitStartup += OnUnitStartup;

        // Subscribe to unit's searchable area events
        unit.EventOnUnitEnterSearchableArea += OnUnitEnterSearchableArea;
        unit.EventOnUnitLeaveSearchableArea += OnUnitLeaveSearchableArea;

        unit.EventOnUnitSearchFoundItem += OnUnitSearchFoundItem;
        unit.EventOnUnitEquipItem += OnUnitEquipItem;

        unit.ownerNetId = this.netId;
        unit.name = obj.name;

        unitObjs.Add(obj);
        units.Add(unit);

        var unitCameraObject = Instantiate(unitCameraPrefab);
        var unitCamera = unitCameraObject.GetComponent<CinemachineVirtualCamera>();

        unit.Camera = unitCamera;

        // Unit camera confiner
        var confineShape = GameObject.Find("Background").GetComponent<PolygonCollider2D>();
        var unitCameraConfiner = unitCameraObject.GetComponent<CinemachineConfiner>();
        unitCameraConfiner.m_BoundingShape2D = confineShape;

        unitCamera.m_Follow = obj.transform;
        unitCamera.Priority = 10;
        unitCamera.enabled = false;
        unitCamera.gameObject.transform.position = new Vector3(0, 1, 0);

        unitCameras.Add(unitCamera);
    }
    #endregion

    #region Local UI Methods
    private void EnableLocalUI(bool enabled)
    {
        turnPlayerUI.EnableLocalButtonBar(enabled);
    }

    private void EnableLocalUnitActionBar(bool enabled)
    {
        //turnPlayerUI.EnableLocalButtonBar(enabled);
    }

    private void EnableLocalActionButton(bool enabled)
    {
        localButtonBarBtnAction.interactable = enabled;
    }

    private void EnableLocalSearchButton(bool enabled)
    {
        localButtonBarBtnSearch.interactable = enabled;
    }

    private void EnableLocalCombatButton(bool enabled)
    {
        localButtonBarBtnCombat.interactable = enabled;
    }

    private void EnableLocalNextUnitButton(bool enabled)
    {
        localButtonBarBtnNextUnit.interactable = enabled;
    }

    private void EnableLocalEndTurnButton(bool enabled)
    {
        localButtonBarBtnEndTurn.interactable = enabled;
    }
    #endregion

    #region Turn Methods
    private IEnumerator DelayStartUnitTurn(Unit unit, int moves, float delay)
    {
        yield return new WaitForSeconds(delay);

        // Populate action bar
        LoadUnit_ItemsIntoActionBar(unit);

        unit.StartTurn(moves);
    }

    private void HandleUnitTurnState(GameConstants.UnitTurnState state)
    {
        if (!hasAuthority)
            return;

        //Debug.Log("TurnPlayer.HandleUnitTurnState(" + gameObject.name + "; Has Turn: " + hasTurn + ") " + state);

        if (state == GameConstants.UnitTurnState.PerformMoveActionOrSearch)
        {
            if (!hasTurn)
                return;

            var unit = units[currentUnitIndex];
            
            EnableLocalActionButton(true);
            
            if (unit.CanUnitPerformSearch)
                EnableLocalSearchButton(true);
            else
                EnableLocalSearchButton(false);

            EnableLocalNextUnitButton(true);
            EnableLocalCombatButton(false);
            EnableLocalEndTurnButton(true);

            // ...and enable movement for current unit
            unit.CanMove(true);

            localUnitTurnState.text = "MOVE/ACTION/SEARCH";
        }
        else if (state == GameConstants.UnitTurnState.Moving)
        {
            if (!hasTurn)
                return;

            EnableLocalActionButton(false);
            EnableLocalSearchButton(false);
            EnableLocalNextUnitButton(false);
            EnableLocalCombatButton(false);
            EnableLocalEndTurnButton(false);

            localUnitTurnState.text = "MOVING";
        }
        else if (state == GameConstants.UnitTurnState.Combat)
        {
            if (!hasTurn)
                return;

            EnableLocalActionButton(false);
            EnableLocalSearchButton(false);
            EnableLocalCombatButton(true);
            EnableLocalNextUnitButton(true);
            EnableLocalEndTurnButton(true);

            localUnitTurnState.text = "COMBAT";
        }
        else if (state == GameConstants.UnitTurnState.ExaustedActions)
        {
            if (!hasTurn)
                return;

            // If we performed a search, then disable the buttons: Action, Search
            EnableLocalActionButton(false);
            EnableLocalSearchButton(false);
            EnableLocalCombatButton(false);
            EnableLocalNextUnitButton(true);
            EnableLocalEndTurnButton(true);

            // ...and disable unit movement
            var unit = units[currentUnitIndex];
            unit.CanMove(false);

            localUnitTurnState.text = "EXAUSTED";
        }
        else if (state == GameConstants.UnitTurnState.EndTurn)
        {
            EnableLocalActionButton(false);
            EnableLocalSearchButton(false);
            EnableLocalCombatButton(false);
            EnableLocalNextUnitButton(false);

            localUnitTurnState.text = "END TURN";
        }
    }

    protected virtual void BeginTurn()
    {
        if (isLocalPlayer)
        {
            //Debug.Log("TurnPlayer.BeginTurn(" + gameObject.name +")");

            // It is our turn, set hasTurn = True on the server
            hasTurn = true;

            // If we have no units left, skip turn
            if (units.Count <= 0)
            {
                Debug.LogWarning("TurnPlayer.BeginTurn(" + gameObject.name + ") - No units, skipping turn");
                CompleteTurn();

                return;
            }

            currentUnitIndex = 0;

            int moves = this.RollMaxMoves();
            CmdSetMaxMoves(moves);

            // Enable local UI if we have our turn
            EnableLocalUI(true);

            var unit = units[currentUnitIndex];

            // Deactivate the previous unit's camera and active the new unit's camera
            lastUnitCamera = currentUnitCamera;

            // If last camera is null, then the unit died
            // and get the last available unit's camera instead
            if (lastUnitCamera == null)
            {
                lastUnitCamera = unitCameras[unitCameras.Count - 1];
            }

            currentUnitCamera = unitCameras[currentUnitIndex];
            lastUnitCamera.enabled = false;
            currentUnitCamera.enabled = true;

            // Set TurnPlayer to unit transform position so TurnPlayer camera will follow
            transform.position = unitObjs[currentUnitIndex].transform.position;

            CmdUpdateCurrentUnitId(unit.netId);

            CmdGameManagerPlayerUnitStartTurn(unitObjs[currentUnitIndex], unit.netId);

            StartCoroutine(DelayStartUnitTurn(unit, moves, 1.0f));
        }
    }

    private void EndUnitTurn()
    {
        if (!isLocalPlayer)
            return;

        //Debug.Log("TurnPlayer.EndUnitTurn(" + gameObject.name + ")");

        EnableLocalNextUnitButton(false);

        turnPlayerUI.ActionBar_Clear();
        EnableLocalUnitActionBar(false);

        var unit = units[currentUnitIndex];

        unit.EndTurn(false);
    }

    private void ShowAllUnitInfo(bool show)
    {
        for (int i = 0; i < units.Count; i++)
        {
            units[i].ShowInfo(show);
        }
    }

    private IEnumerator DelayNextTurn(float delay)
    {
        yield return new WaitForSeconds(delay);

        NextUnit();
    }

    private void NextUnit()
    {
        if (!isLocalPlayer)
            return;

        //Debug.Log("TurnPlayer.NextUnit(" + gameObject.name + ")");

        turnPlayerUI.ActionBar_Clear();
        EnableLocalUnitActionBar(false);

        CmdGameManagerPlayerUnitEndTurn(unitObjs[currentUnitIndex], units[currentUnitIndex].netId);

        currentUnitIndex += 1;

        //Debug.Log("TurnPlayer.NextUnit() currentUnitIndex=" + currentUnitIndex + "; units.Count=" + units.Count);

        if (currentUnitIndex >= units.Count)
        {
            currentUnitIndex = 0;

            CompleteTurn();

            return;
        }

        int moves = this.RollMaxMoves();
        CmdSetMaxMoves(moves);

        var unit = units[currentUnitIndex];

        CmdUpdateCurrentUnitId(unit.netId);

        CmdGameManagerPlayerUnitStartTurn(unitObjs[currentUnitIndex], unit.netId);

        // Deactivate the previous unit's camera and active the new unit's camera
        lastUnitCamera = currentUnitCamera;

        // If last camera is null, then the unit died
        // and get the last available unit's camera instead
        if (lastUnitCamera == null)
        {
            lastUnitCamera = unitCameras[unitCameras.Count - 1];
        }

        currentUnitCamera = unitCameras[currentUnitIndex];
        lastUnitCamera.enabled = false;
        currentUnitCamera.enabled = true;

        // Set TurnPlayer to unit transform position so TurnPlayer camera will follow
        transform.position = unitObjs[currentUnitIndex].transform.position;

        // Populate action bar
        LoadUnit_ItemsIntoActionBar(unit);
        
        unit.StartTurn(moves);
    }

    protected virtual void CompleteTurn()
    {
        if (!isLocalPlayer)
            return;

        //Debug.Log("TurnPlayer.CompleteTurn(" + gameObject.name + ")");

        hasTurn = false;

        // Disable local UI at end of our turn
        EnableLocalUI(false);

        turnPlayerUI.ActionBar_Clear();
        EnableLocalUnitActionBar(false);

        if (units.Count > 0)
        {
            var unit = units[currentUnitIndex];
            unit.SoftEndTurn();
        }

        CmdCompleteTurn();
    }

    private void MoveUnit(Vector3 target)
    {
        if (!isLocalPlayer)
            return;

        var unit = units[currentUnitIndex];

        unit.StartMovement(target);
    }

    public virtual int RollMaxMoves()
    {
        int moves = Random.Range(1, 6);

        return moves;
    }
    #endregion

    public void FocusUnit(NetworkInstanceId unitId)
    {
        if (!isLocalPlayer)
            return;

        bool exists = units.Any(u => u.netId == unitId);

        if (!exists)
            return;

        var unit = units.Find(u => u.netId == unitId);

        Debug.Log("TurnPlayer.FocusUnit() unit.Name=" + unit.UnitName);

    }

    public void UnFocusUnit(NetworkInstanceId unitId)
    {
        if (!isLocalPlayer)
            return;

        bool exists = units.Any(u => u.netId == unitId);

        if (!exists)
            return;

        var unit = units.Find(u => u.netId == unitId);

        Debug.Log("TurnPlayer.UnFocusUnit() unit.Name=" + unit.UnitName);

    }

    private void LoadUnit_ItemsIntoActionBar(Unit unit)
    {
        // Populate unit items in action bar
        EnableLocalUnitActionBar(true);
        turnPlayerUI.ActionBar_Clear();

        ItemInventoryStruct[] unitItems = unit.EquipedItems;

        if (unitItems.Any())
        {
            EnableLocalUnitActionBar(true);

            for (int i  = 0; i < unitItems.Length; i++)
            {
                BaseItem item = ItemManager.singleton.GetItem(unitItems[i].ItemId);

                if (item != null)
                {
                    turnPlayerUI.ActionBar_AddItem(unitObjs[currentUnitIndex], item);
                }
            }
        }
    }
}

I’ve decided to branch my project and start refactoring this TurnPlayer component into several components that are for a specific ‘behavior’. I decided to do this because I was working on unit combat and ran into a wall.

When a unit is next to another unit that it can perform melee combat with, the unit goes into a ‘Combat’ state and currently if you click the ‘Combat’ button, the unit attacks each unit that is near it. I wanted to let the player select which nearby unit to do combat with. This involves prompting the local player during the unit’s ‘Combat’ state to select a valid nearby unit, and once that unit is selected, the current unit will perform combat with it.

Ok, sounds simple enough, but the way this TurnPlayer component was structured didn’t really allow for this to be done easily. I would have to add a new state handler to the TurnPlayer so it knows if it is in a ‘Select combat target’ state so it could handle unit selection during the Update() loop, which is just one more thing added to this already insanely cluttered component. I could add this all as a new component for the unit but that doesn’t feel right since then the unit’s would all have to interact with the local player through a UI and I feel like they shouldn’t and that should be the job of the actual Player object in the game since the units are their own thing and shouldn’t really have any local UI associated with them, but instead should trigger various events that the local Player’s UI can subscribe to.

Eventually I’ll have a component for at least:

  • Local player UI
  • Unit management
  • Camera management (e.g. zooming, panning, etc)
  • Local player state
  • Unit spawning

I’ll eventually have to refactor some of my other classes (GameManager, Unit, etc) but for now I’m starting with this and then working towards everything else.

Selfcriticisms

  • One habit I have noticed I have is that I tend to get an idea and then just start coding. This is a habit that is more with game development and not with other non-game related projects though. I’ll implement this piece, then implement the next then the next and then stop and run it, only to find that nothing works as planned. I need to get better at doing incremental changes.
  • I worry too much about changing code already written if that code took me awhile to get working. I’m afraid to break code. I need to be conscience of this and work towards getting better at it. After all, when it comes to software, controlled destruction breeds confidence.