공룡 달리기 2D - 장애물과 게임 매니저

싱글톤 GameManager로 장애물 스폰, 점수, 게임 오버를 관리하고 OnBecameInvisible로 오브젝트를 자동 삭제합니다

Building on tutorial 47 (jump and background scroll), this tutorial adds obstacle spawning, collision, scoring, and game over via GameManager, plus automatic obstacle movement and cleanup via ObstacleController. You will practice the singleton pattern, PlayerPrefs, Time.timeScale, SceneManager.LoadScene, and OnBecameInvisible.


Game Structure

Script Role
GameManager Singleton that manages the whole scene (spawn, score, game over)
ObstacleController Move obstacles left; destroy them when they leave the screen

ObstacleController – Move and Auto-Destroy

Attached to each obstacle prefab. Moves left at a constant speed and destroys itself when it goes off-screen.

Full Code

using UnityEngine;

public class ObstacleController : MonoBehaviour
{
    /// <summary>Horizontal speed (negative = move left)</summary>
    public float moveSpeedX;

    private Rigidbody2D rb;

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        rb.velocity = new Vector2(moveSpeedX, 0);
    }

    private void OnBecameInvisible()
    {
        Destroy(gameObject);
    }
}

Key Concepts

1. Moving with Rigidbody2D.velocity

rb.velocity = new Vector2(moveSpeedX, 0);
  • Setting velocity directly (vs. AddForce) overwrites the current speed every frame, giving a perfectly constant horizontal movement.
  • Y = 0: Keeps the obstacle moving horizontally without being affected by gravity. (Alternatively, set Gravity Scale to 0 on the Rigidbody2D.)
  • Set moveSpeedX to a negative value to move left.

2. OnBecameInvisible – Auto-Destroy Off-Screen

private void OnBecameInvisible()
{
    Destroy(gameObject);
}
  • OnBecameInvisible is called by Unity when this object's Renderer is no longer visible by any camera.
  • The counterpart OnBecameVisible fires when it enters any camera's view.
  • This removes the obstacle without needing to manually check its position, cleanly preventing memory waste.
  • Note: The editor's Scene view counts as a camera. During development, the event may fire later than expected if the Scene view is open. Builds behave correctly.

GameManager – Scene-Wide Control

A singleton accessible anywhere via GameManager.instance. Handles obstacle spawning, scoring, game over, and restart.

Full Code

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public static GameManager instance;

    public Transform[] spawnPoints;
    public GameObject[] obstacles;
    public float spawnDelay;
    private float spawnTimer;
    public bool isSpawning;
    private int spawnTracker;

    public int mainScore;
    public TextMeshProUGUI mainScoreText;
    public GameObject gameOverPanel;
    public TextMeshProUGUI bestScoreText;
    public TextMeshProUGUI endScoreText;

    public AudioClip explosionSound;
    public AudioClip pickupCoinSound;
    public AudioSource SE;

    void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(this.gameObject);
        }
        else
        {
            instance = this;
        }
    }

    void Start()
    {
        SE = GetComponent<AudioSource>();
    }

    void Update()
    {
        if (isSpawning)
        {
            spawnTimer += Time.deltaTime;
            if (spawnTimer >= spawnDelay)
            {
                spawnTimer = 0;
                SpawnObstacle();
            }
        }
    }

    public void GameOver()
    {
        SE.clip = explosionSound;
        SE.Play();
        Time.timeScale = 0;

        if (mainScore > PlayerPrefs.GetInt("BestScore"))
        {
            PlayerPrefs.SetInt("BestScore", mainScore);
        }

        bestScoreText.text = "Best Score : " + PlayerPrefs.GetInt("BestScore").ToString();
        endScoreText.text = "Score : " + mainScore.ToString();
        gameOverPanel.SetActive(true);
    }

    public void GameRestart()
    {
        Time.timeScale = 1;
        SceneManager.LoadScene("GameScene");
    }

    public void ScoreUiUpdate()
    {
        SE.clip = pickupCoinSound;
        SE.Play();
        mainScore++;
        mainScoreText.text = "Score : " + mainScore.ToString("D5");
    }

    void SpawnObstacle()
    {
        spawnTracker = Random.Range(0, obstacles.Length);

        switch (spawnTracker)
        {
            case 0:
                Instantiate(obstacles[0], spawnPoints[0].position, Quaternion.identity);
                break;
            case 1:
                Instantiate(obstacles[1], spawnPoints[1].position, Quaternion.identity);
                break;
            case 2:
                Instantiate(obstacles[2], spawnPoints[1].position, Quaternion.identity);
                break;
            case 3:
                Instantiate(obstacles[3], spawnPoints[1].position, Quaternion.identity);
                break;
            case 4:
                Instantiate(obstacles[4], spawnPoints[Random.Range(2, 4)].position, Quaternion.identity);
                break;
        }

        spawnDelay = Random.Range(1f, 3f);
    }
}

Key Concepts

1. Singleton – Awake

void Awake()
{
    if (instance != null && instance != this)
        Destroy(this.gameObject);
    else
        instance = this;
}
  • static instance: Stores the single instance as a static variable so any script can access it via GameManager.instance.
  • Awake runs before Start: Safe to read GameManager.instance from any script's Start.
  • If a duplicate is created (e.g. on scene reload), it is immediately destroyed, keeping exactly one instance alive.

2. Obstacle Spawning – timer and Instantiate

spawnTimer += Time.deltaTime;
if (spawnTimer >= spawnDelay)
{
    spawnTimer = 0;
    SpawnObstacle();
}
  • Accumulates Time.deltaTime each frame — a simple interval timer without Invoke or coroutines.
  • After each spawn, spawnDelay = Random.Range(1f, 3f) randomizes the next interval to avoid predictable patterns.
Instantiate(obstacles[0], spawnPoints[0].position, Quaternion.identity);
  • Instantiate(prefab, position, rotation): Creates a copy of the prefab at the given position.
  • Quaternion.identity: No rotation — the object spawns facing its default direction.
  • A switch-case selects both the obstacle type and its spawn point. The bird (case 4) picks a random aerial spawn from spawnPoints[2] or spawnPoints[3].

3. Game Over – Time.timeScale and PlayerPrefs

Time.timeScale = 0;

if (mainScore > PlayerPrefs.GetInt("BestScore"))
    PlayerPrefs.SetInt("BestScore", mainScore);
  • Time.timeScale = 0: Pauses the game. Update, physics, and time-based coroutines stop. UI buttons and events still work.
  • PlayerPrefs: Unity's persistent key-value store. Data survives app restarts. SetInt / GetInt work with an int. Good for simple per-device data like high scores or settings.

4. Game Restart – SceneManager.LoadScene

public void GameRestart()
{
    Time.timeScale = 1;
    SceneManager.LoadScene("GameScene");
}
  • Reset timeScale to 1 first: Loading a scene while timeScale is 0 keeps the new scene frozen.
  • SceneManager.LoadScene("name"): Reloads the scene by name. The scene must be registered in File → Build Settings.
  • Requires using UnityEngine.SceneManagement;.

5. Score Display – ToString("D5")

mainScoreText.text = "Score : " + mainScore.ToString("D5");
  • "D5" format pads an integer to at least 5 digits with leading zeros (e.g. 3 → "00003"). Useful for classic arcade-style score displays.

Connecting to DinoController

DinoController.OnTriggerEnter2D calls GameManager.instance to communicate:

private void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.gameObject.CompareTag("Obstacle"))
    {
        anim.SetTrigger("isDie");
        GameManager.instance.GameOver();
    }
    else if (collision.gameObject.CompareTag("Point"))
    {
        GameManager.instance.ScoreUiUpdate();
    }
}
  • CompareTag is faster than == "Obstacle" and will warn you at runtime if the tag doesn't exist.
  • Obstacle tag: Set on every obstacle prefab in the Inspector.
  • Point trigger: A thin Is Trigger Collider in front of each obstacle, tagged "Point", increments the score when the dino passes through.

Scene Setup Summary

Item Recommendation
GameManager object Empty GameObject with GameManager.cs + AudioSource
spawnPoints 4 empty Transforms: 2 ground, 2 aerial — assign in Inspector
obstacles array 5 prefabs (4 cactus types + bird) — assign in Inspector
Obstacle prefabs Rigidbody2D + Collider2D + ObstacleController; tag = "Obstacle"
Score trigger Is Trigger Collider on each obstacle, tag = "Point"
Game over panel Canvas child — Panel + score texts + restart button; disabled initially