Documentation Gael - j5x/PvB2025 GitHub Wiki

Unity Game Mechanic Documentation

1. Loading Screen System

1.1. Overview

A self-contained scene and manager that asynchronously loads any target scene while updating a UI progress bar and optional percentage text. It enforces a minimum display time so the bar never snaps instantly to 100%.


1.2. GameLoader Utility

A tiny static helper for passing “which scene to load next” into the LoadingScene.

public static class GameLoader
{
    /// <summary>
    /// Assign this before loading the LoadingScene.
    /// </summary>
    public static string NextSceneName;
}

1.3. LoadingScreenManager

Responsibilities

  • Read GameLoader.NextSceneName on Start().

  • Kick off an AsyncOperation for that scene.

  • Update a Slider (progressBar) and TextMeshProUGUI (progressText) each frame.

  • Enforce a minimum load-screen duration (minLoadTime) even if the target scene loads faster.

  • Activate the new scene once both loading and the delay have completed.

public class LoadingScreenManager : MonoBehaviour
{
    [Header("UI References")]
    [SerializeField] private Slider progressBar;
    [SerializeField] private TextMeshProUGUI progressText;

    [Header("Timing")]
    [Tooltip("Minimum seconds to show the bar filling to 100%")]
    [SerializeField] private float minLoadTime = 2f;

    private void Start() {
        string scene = GameLoader.NextSceneName;
        if (string.IsNullOrEmpty(scene)) {
            Debug.LogError("NextSceneName not set!");
            return;
        }
        StartCoroutine(LoadWithMinimumTime(scene));
    }

    private IEnumerator LoadWithMinimumTime(string targetScene) {
        var op = SceneManager.LoadSceneAsync(targetScene);
        op.allowSceneActivation = false;
        float start = Time.time;

        while (true) {
            float raw = Mathf.Clamp01(op.progress / 0.9f);
            float timed = Mathf.Clamp01((Time.time - start) / minLoadTime);
            float display = Mathf.Min(raw, timed);

            progressBar.value    = display;
            progressText.text    = $"{Mathf.RoundToInt(display * 100f)}%";

            if (op.progress >= 0.9f && Time.time - start >= minLoadTime) {
                op.allowSceneActivation = true;
                yield break;
            }
            yield return null;
        }
    }
}

1.4. Inspector Setup

  1. GameLoader
  • No Inspector component; just call
GameLoader.NextSceneName = "YourGameplayScene";
SceneManager.LoadScene("LoadingScene");

1.5. LoadingScreenManager (placed in LoadingScene)

  • Progress Bar → assign your UI Slider.

  • Progress Text → assign any TextMeshProUGUI (optional).

  • Min Load Time → tweak how long the bar takes to fill.


2. Round-Timer & Enemy Spawner

2.1. Overview

A simple “three-round” timer that counts down, spawns up to N enemies per round, advances rounds on enemy death, and triggers game-over if time expires first.

2.2. RoundTimer

Responsibilities

  • Hold an array of round durations (float[] roundDurations = {45, 45, 60}).

  • Expose two UnityEvents:

  • OnRoundStart(int roundNumber) → UI can update “Round X” displays.

  • OnGameOver() → UI can show game-over screen.

  • Count down in a coroutine.

  • Watch a flag isEnemyDead (set externally) to advance to the next round.

public class RoundTimer : MonoBehaviour
{
    public UnityEvent<int> OnRoundStart;
    public UnityEvent       OnGameOver;
    [SerializeField] private TMP_Text timerText;

    private float[] roundDurations = {45f, 45f, 60f};
    private int    currentRound;
    private float  timeRemaining;
    private bool   isTimerRunning;
    private bool   isEnemyDead;

    private void Start() => StartRound(0);

    private void StartRound(int idx) {
        if (idx >= roundDurations.Length) {
            Debug.Log("All rounds complete!");
            return;
        }
        currentRound = idx;
        timeRemaining = roundDurations[idx];
        isTimerRunning = true;
        isEnemyDead = false;

        OnRoundStart.Invoke(idx + 1);
        UpdateTimerUI();
        StartCoroutine(TimerCoroutine());
    }

    private IEnumerator TimerCoroutine() {
        while (isTimerRunning && timeRemaining > 0) {
            yield return new WaitForSeconds(1f);
            timeRemaining--;
            UpdateTimerUI();
            if (isEnemyDead) {
                StartNextRound();
                yield break;
            }
        }
        if (timeRemaining <= 0f) OnGameOver.Invoke();
    }

    private void StartNextRound() {
        isTimerRunning = false;
        StartRound(currentRound + 1);
    }

    private void UpdateTimerUI() {
        if (timerText) timerText.text = $"Time: {timeRemaining}s";
    }

    /// <summary>
    /// Call this from Enemy’s OnDeath to signal the round is won.
    /// </summary>
    public void EnemyDefeated() => isEnemyDead = true;
}

2.3. EnemySpawner

Responsibilities

  • Spawn up to maxSpawns instances of a given enemyPrefab, each after respawnDelay seconds.

  • Assigns itself (spawner) into the new Enemy so the Enemy can notify it on death.

public class EnemySpawner : MonoBehaviour
{
    [SerializeField] private GameObject enemyPrefab;
    [SerializeField] private Transform  spawnPoint;
    [SerializeField] private float      respawnDelay = 2f;
    [SerializeField] private int        maxSpawns = 3;

    private int spawnCount;
    private GameObject currentEnemy;

    private void Start() => SpawnEnemy();

    public void SpawnEnemy() {
        if (spawnCount >= maxSpawns) return;
        StartCoroutine(SpawnWithDelay());
    }

    private IEnumerator SpawnWithDelay() {
        yield return new WaitForSeconds(respawnDelay);
        currentEnemy = Instantiate(enemyPrefab, spawnPoint.position, spawnPoint.rotation);
        var e = currentEnemy.GetComponent<Enemy>();
        if (e != null) e.spawner = this;
        spawnCount++;
    }
}

2.4. Wiring Them Together

1. RoundTimer in your scene:

  • Hook its OnRoundStart(int) → update your “Round X” UI.

  • Hook its OnGameOver() → show game-over screen or return to menu.

2. EnemySpawner placed somewhere in the scene:

  • Set Enemy Prefab, Spawn Point, Respawn Delay, Max Spawns.

3. Enemy script (derived from Character) calls both:

  • On its HealthComponent.OnDeath, call both:
spawner?.SpawnEnemy();
FindObjectOfType<RoundTimer>()?.EnemyDefeated();
  • This signals the RoundTimer that the current enemy died, advancing the round, and tells the spawner to spawn the next.

2.5. Per-Round Stat Boosts & Victory

  • When OnRoundStart(roundIndex) fires, you can:

  • Increase the player’s stats (e.g. healthComponent.ApplyHealthBuff(50, 0)).

  • Or adjust the next enemy’s config (e.g. enemyPrefab.GetComponent().attackConfigs[0].attackDamage += 10).

  • After the third round’s enemies spawn and die, StartRound(3) will “out of bounds” and simply log “All rounds complete!” — tie that to your victory UI or scene-transition call in OnRoundStart or via another UnityEvent.


3. Health Configuration (HealthConfig)

3.1. Overview

  • ScriptableObject that defines per-character health parameters:

    • maxHealth

    • canRegenerate

    • regenRate & regenAmount

Each character (player or enemy) points at one of these configs so you can tweak HP and regen in the Inspector.

3.2. HealthComponent

* MonoBehaviour you add (or auto-add) to every character.

  • Fields

    • baseMaxHealth, currentHealth

    • regenMultiplier, damageMultiplier

* Initialization

  • InitializeHealth(HealthConfig) reads the SO into baseMaxHealth & currentHealth.

  • Starts a repeating regen coroutine if enabled.

* Public API

  • TakeDamage(int amount) → applies multipliers, clamps at 0, fires OnHealthChanged(int) and OnDeath when HP hits zero.

  • Heal(int amount) → clamps at max, fires OnHealthChanged.

* Events

  • OnHealthChanged(int) → subscribe UI bars or other feedback.

  • OnDeath → triggers character cleanup or round logic.

3.3. Character Base Class

* Abstract Character : MonoBehaviour that both Player and Enemy inherit.

* Responsibility

  • In Awake() it ensures there’s a HealthComponent, runs InitializeHealth, and hooks OnDeath → Die().

  • Exposes public virtual void TakeDamage(float) which simply calls the HealthComponent.

  • Defines two abstract methods Attack() and Defend() for subclasses to implement.

3.4. AttackComponent

* MonoBehaviour you attach alongside Character.

* Config

  • A list of AttackConfig ScriptableObjects, each defining an animation trigger, damage amount, and delay.

  • A flag isAIControlled to auto-loop attacks on enemies.

* PerformAttack

  • Chooses an AttackConfig, fires the Animator trigger, and uses Invoke(ExecuteAttackLogic, delay) to schedule the hit.

* ExecuteAttackLogic

  • Looks at character (Player or Enemy) and finds the opposing actor in the scene (FindObjectOfType() or ()).

  • Calls that actor’s TakeDamage(attackDamage).

  • Logs the hit.

3.5. Runtime Flow

  1. Match-3 trigger (or a button press) calls Player.Attack(), which under the hood does attackComponent.PerformAttack().

  2. The Animator plays the punch/kick animation.

  3. After attackDelay seconds, ExecuteAttackLogic() runs and applies damage to the target’s HealthComponent.

  4. HealthComponent.TakeDamage() reduces HP, fires OnHealthChanged (UI updates), and if HP ≤ 0, fires OnDeath.

  5. OnDeath in the Character base class calls Die(), which cleans up the GameObject (and informs the spawner or round timer).

⚠️ **GitHub.com Fallback** ⚠️