Preview

This project is a Unity-based turn-based strategy game prototype featuring multi-floor level design. The core features include grid-based movement, interactive doors, dynamic unit visibility (such as hiders and enemies), and event-driven level scripting. The game leverages Unity’s new Input System for camera controls (movement, rotation, and zoom) and mouse interactions, providing a modern and flexible user experience.

Features and Mechanics

Technical Implementation

The prototype is implemented in C# using Unity’s component-based architecture. It demonstrates the use of serialized fields for inspector configuration, event handling for game logic, singleton patterns for managers, and integration with Unity’s Input System. By developing this project, you will gain practical experience in structuring turn-based game logic, managing game state, implementing responsive input, and applying modular programming principles in Unity.

https://youtu.be/5OODENjU1-Q

Codebase

Summary The project includes scripts that handle unit animations and projectile behaviors, focusing on creating a dynamic and interactive gameplay experience. The UnitAnimator script manages animations based on unit actions, while the BulletProjectile and GrenadeProjectile scripts handle the movement and effects of projectiles.

Key Components

  1. Turn System: • Manages the turn-based mechanics of the game. • Keeps track of the current turn number and whether it is the player's turn. • Provides methods to advance to the next turn and check the current turn state. • Raises an event (OnTurnChanged) whenever the turn changes.

        public class TurnSystem : MonoBehaviour
        {
            public static TurnSystem Instance { get; private set; }
            public event EventHandler OnTurnChanged;
            private int turnNumber = 1;
            private bool isPlayerTurn = true;
    
            private void Awake()
            {
                if (Instance != null)
                {
                    Debug.LogError("There's more than one TurnSystem! " + transform + " - " + Instance);
                    Destroy(gameObject);
                    return;
                }
                Instance = this;
            }
    
            public void NextTurn()
            {
                turnNumber++;
                isPlayerTurn = !isPlayerTurn;
                OnTurnChanged?.Invoke(this, EventArgs.Empty);
            }
    
            public int GetTurnNumber() => turnNumber;
            public bool IsPlayerTurn() => isPlayerTurn;
        }
        
    
  2. Turn System UI: • Handles the user interface related to the turn system. • Updates the turn number display, enemy turn visual, and end turn button visibility based on the current turn state. • Listens to the OnTurnChanged event from TurnSystem to update the UI accordingly.

        public class TurnSystemUI : MonoBehaviour
        {
            [SerializeField] private Button endTurnBtn;
            [SerializeField] private TextMeshProUGUI turnNumberText;
            [SerializeField] private GameObject enemyTurnVisualGameObject;
    
            private void Start()
            {
                endTurnBtn.onClick.AddListener(() => TurnSystem.Instance.NextTurn());
                TurnSystem.Instance.OnTurnChanged += TurnSystem_OnTurnChanged;
                UpdateTurnText();
                UpdateEnemyTurnVisual();
                UpdateEndTurnButtonVisibility();
            }
    
            private void TurnSystem_OnTurnChanged(object sender, EventArgs e)
            {
                UpdateTurnText();
                UpdateEnemyTurnVisual();
                UpdateEndTurnButtonVisibility();
            }
    
            private void UpdateTurnText() => turnNumberText.text = "TURN " + TurnSystem.Instance.GetTurnNumber();
            private void UpdateEnemyTurnVisual() => enemyTurnVisualGameObject.SetActive(!TurnSystem.Instance.IsPlayerTurn());
            private void UpdateEndTurnButtonVisibility() => endTurnBtn.gameObject.SetActive(TurnSystem.Instance.IsPlayerTurn());
        }
        
    
  3. Unit: • Represents a unit in the game, which can be either a player or an enemy. • Manages the unit's action points, health, and position on the grid. • Listens to the OnTurnChanged event to reset action points at the start of the unit's turn. • Raises events when the unit's action points change, when the unit is spawned, and when the unit dies.

        public class Unit : MonoBehaviour
        {
            private const int ACTION_POINTS_MAX = 9;
            public static event EventHandler OnAnyActionPointsChanged;
            public static event EventHandler OnAnyUnitSpawned;
            public static event EventHandler OnAnyUnitDead;
    
            [SerializeField] private bool isEnemy;
            private GridPosition gridPosition;
            private HealthSystem healthSystem;
            private BaseAction[] baseActionArray;
            private int actionPoints = ACTION_POINTS_MAX;
    
            private void Awake()
            {
                healthSystem = GetComponent<HealthSystem>();
                baseActionArray = GetComponents<BaseAction>();
            }
    
            private void Start()
            {
                gridPosition = LevelGrid.Instance.GetGridPosition(transform.position);
                LevelGrid.Instance.AddUnitAtGridPosition(gridPosition, this);
                TurnSystem.Instance.OnTurnChanged += TurnSystem_OnTurnChanged;
                healthSystem.OnDead += HealthSystem_OnDead;
                OnAnyUnitSpawned?.Invoke(this, EventArgs.Empty);
            }
    
            private void Update()
            {
                GridPosition newGridPosition = LevelGrid.Instance.GetGridPosition(transform.position);
                if (newGridPosition != gridPosition)
                {
                    GridPosition oldGridPosition = gridPosition;
                    gridPosition = newGridPosition;
                    LevelGrid.Instance.UnitMovedGridPosition(this, oldGridPosition, newGridPosition);
                }
            }
    
            public T GetAction<T>() where T : BaseAction
            {
                foreach (BaseAction baseAction in baseActionArray)
                {
                    if (baseAction is T)
                    {
                        return (T)baseAction;
                    }
                }
                return null;
            }
    
            public GridPosition GetGridPosition() => gridPosition;
            public Vector3 GetWorldPosition() => transform.position;
            public BaseAction[] GetBaseActionArray() => baseActionArray;
    
            public bool TrySpendActionPointsToTakeAction(BaseAction baseAction)
            {
                if (CanSpendActionPointsToTakeAction(baseAction))
                {
                    SpendActionPoints(baseAction.GetActionPointsCost());
                    return true;
                }
                return false;
            }
    
            public bool CanSpendActionPointsToTakeAction(BaseAction baseAction) => actionPoints >= baseAction.GetActionPointsCost();
    
            private void SpendActionPoints(int amount)
            {
                actionPoints -= amount;
                OnAnyActionPointsChanged?.Invoke(this, EventArgs.Empty);
            }
    
            public int GetActionPoints() => actionPoints;
    
            private void TurnSystem_OnTurnChanged(object sender, EventArgs e)
            {
                if ((IsEnemy() && !TurnSystem.Instance.IsPlayerTurn()) || (!IsEnemy() && TurnSystem.Instance.IsPlayerTurn()))
                {
                    actionPoints = ACTION_POINTS_MAX;
                    OnAnyActionPointsChanged?.Invoke(this, EventArgs.Empty);
                }
            }
    
            public bool IsEnemy() => isEnemy;
    
            public void Damage(int damageAmount)
            {
                healthSystem.Damage(damageAmount);
            }
    
            private void HealthSystem_OnDead(object sender, EventArgs e)
            {
                LevelGrid.Instance.RemoveUnitAtGridPosition(gridPosition, this);
                Destroy(gameObject);
                OnAnyUnitDead?.Invoke(this, EventArgs.Empty);
            }
    
            public float GetHealthNormalized() => healthSystem.GetHealthNormalized();
        }
        
    
  4. Unit Action System: • Manages the actions that units can perform. • Handles unit selection and action execution. • Ensures that actions are only performed during the player's turn and when the unit has enough action points. • Raises events when the selected unit or action changes, when an action starts, and when the system is busy.

        public class UnitActionSystem : MonoBehaviour
        {
            public static UnitActionSystem Instance { get; private set; }
            public event EventHandler OnSelectedUnitChanged;
            public event EventHandler OnSelectedActionChanged;
            public event EventHandler<bool> OnBusyChanged;
            public event EventHandler OnActionStarted;
    
            [SerializeField] private Unit selectedUnit;
            [SerializeField] private LayerMask unitLayerMask;
    
            private BaseAction selectedAction;
            private bool isBusy;
    
            private void Awake()
            {
                if (Instance != null)
                {
                    Debug.LogError("There's more than one UnitActionSystem! " + transform + " - " + Instance);
                    Destroy(gameObject);
                    return;
                }
                Instance = this;
            }
    
            private void Start()
            {
                SetSelectedUnit(selectedUnit);
            }
    
            private void Update()
            {
                if (isBusy || !TurnSystem.Instance.IsPlayerTurn() || EventSystem.current.IsPointerOverGameObject())
                {
                    return;
                }
    
                if (TryHandleUnitSelection())
                {
                    return;
                }
    
                HandleSelectedAction();
            }
    
            private void HandleSelectedAction()
            {
                if (InputManager.Instance.IsMouseButtonDownThisFrame())
                {
                    GridPosition mouseGridPosition = LevelGrid.Instance.GetGridPosition(MouseWorld.GetPositionOnlyHitVisible());
    
                    if (!selectedAction.IsValidActionGridPosition(mouseGridPosition) || !selectedUnit.TrySpendActionPointsToTakeAction(selectedAction))
                    {
                        return;
                    }
    
                    SetBusy();
                    selectedAction.TakeAction(mouseGridPosition, ClearBusy);
                    OnActionStarted?.Invoke(this, EventArgs.Empty);
                }
            }
    
            private void SetBusy()
            {
                isBusy = true;
                OnBusyChanged?.Invoke(this, isBusy);
            }
    
            private void ClearBusy()
            {
                isBusy = false;
                OnBusyChanged?.Invoke(this, isBusy);
            }
    
            private bool TryHandleUnitSelection()
            {
                if (InputManager.Instance.IsMouseButtonDownThisFrame())
                {
                    Ray ray = Camera.main.ScreenPointToRay(InputManager.Instance.GetMouseScreenPosition());
                    if (Physics.Raycast(ray, out RaycastHit raycastHit, float.MaxValue, unitLayerMask))
                    {
                        if (raycastHit.transform.TryGetComponent<Unit>(out Unit unit) && unit != selectedUnit && !unit.IsEnemy())
                        {
                            SetSelectedUnit(unit);
                            return true;
                        }
                    }
                }
                return false;
            }
    
            private void SetSelectedUnit(Unit unit)
            {
                selectedUnit = unit;
                SetSelectedAction(unit.GetAction<MoveAction>());
                OnSelectedUnitChanged?.Invoke(this, EventArgs.Empty);
            }
    
            public void SetSelectedAction(BaseAction baseAction)
            {
                selectedAction = baseAction;
                OnSelectedActionChanged?.Invoke(this, EventArgs.Empty);
            }
    
            public Unit GetSelectedUnit() => selectedUnit;
            public BaseAction GetSelectedAction() => selectedAction;
        }
        
    
  5. Unit Action System UI: • Manages the user interface for unit actions. • Creates buttons for each action the selected unit can perform. • Updates the action points display and the visual state of action buttons. • Listens to various events to update the UI accordingly.

        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;
                UnitActionSystem.Instance.OnSelectedActionChanged += UnitActionSystem_OnSelectedActionChanged;
                UnitActionSystem.Instance.OnActionStarted += UnitActionSystem_OnActionStarted;
                TurnSystem.Instance.OnTurnChanged += TurnSystem_OnTurnChanged;
                Unit.OnAnyActionPointsChanged += Unit_OnAnyActionPointsChanged;
    
                UpdateActionPoints();
                CreateUnitActionButtons();
                UpdateSelectedVisual();
            }
    
            private void CreateUnitActionButtons()
            {
                foreach (Transform buttonTransform in actionButtonContainerTransform)
                {
                    Destroy(buttonTransform.gameObject);
                }
    
                actionButtonUIList.Clear();
    
                Unit selectedUnit = UnitActionSystem.Instance.GetSelectedUnit();
    
                foreach (BaseAction baseAction in selectedUnit.GetBaseActionArray())
                {
                    Transform actionButtonTransform = Instantiate(actionButtonPrefab, actionButtonContainerTransform);
                    ActionButtonUI actionButtonUI = actionButtonTransform.GetComponent<ActionButtonUI>();
                    actionButtonUI.SetBaseAction(baseAction);
                    actionButtonUIList.Add(actionButtonUI);
                }
            }
    
            private void UnitActionSystem_OnSelectedUnitChanged(object sender, EventArgs e)
            {
                CreateUnitActionButtons();
                UpdateSelectedVisual();
                UpdateActionPoints();
            }
    
            private void UnitActionSystem_OnSelectedActionChanged(object sender, EventArgs e)
            {
                UpdateSelectedVisual();
            }
    
            private void UnitActionSystem_OnActionStarted(object sender, EventArgs e)
            {
                UpdateActionPoints();
            }
    
            private void UpdateSelectedVisual()
            {
                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();
            }
        }