← Назад к вопросам

Создать endless runner игру

2.0 Middle🔥 151 комментариев
#C# и ООП#UI#Unity Core#Оптимизация#Паттерны проектирования

Условие

Создайте endless runner игру.

Требования

  1. Персонаж бежит автоматически
  2. Управление: прыжок и slide
  3. Процедурная генерация уровня
  4. Разные типы препятствий
  5. Сбор монет
  6. Увеличение скорости со временем
  7. Подсчет дистанции и очков

UI

  • Текущий счет
  • Монеты
  • Пауза
  • Game Over с рекордом

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Архитектура Endless Runner

Endless Runner требует: управления персонажем, процедурной генерации уровня, системы препятствий, монет, и скорости. Я реализую модульную архитектуру с использованием пулинга для оптимизации.

1. Менеджер игры

using UnityEngine;
using System;
using System.Collections;

public class EndlessRunnerManager : Singleton<EndlessRunnerManager>
{
    [Header("Game Settings")]
    [SerializeField] private float initialSpeed = 10f;
    [SerializeField] private float maxSpeed = 25f;
    [SerializeField] private float speedIncreaseRate = 0.5f; // За сек
    [SerializeField] private int coinValue = 10;
    
    // Состояние
    private float currentSpeed = 10f;
    private int currentScore = 0;
    private int currentCoins = 0;
    private float distanceTraveled = 0f;
    private bool isGameOver = false;
    private bool isPaused = false;
    private int highScore = 0;
    
    // События
    public event Action<int> OnScoreChanged;
    public event Action<int> OnCoinsChanged;
    public event Action<float> OnSpeedChanged;
    public event Action<float> OnDistanceChanged;
    public event Action OnGameOver;
    public event Action OnGamePaused;
    public event Action OnGameResumed;
    
    protected override void Awake()
    {
        base.Awake();
        currentSpeed = initialSpeed;
        LoadHighScore();
    }
    
    void Update()
    {
        if (isGameOver || isPaused)
            return;
        
        // Увеличиваем скорость со временем
        currentSpeed = Mathf.Min(
            currentSpeed + speedIncreaseRate * Time.deltaTime,
            maxSpeed
        );
        OnSpeedChanged?.Invoke(currentSpeed);
        
        // Подсчитываем дистанцию
        distanceTraveled += currentSpeed * Time.deltaTime;
        OnDistanceChanged?.Invoke(distanceTraveled);
    }
    
    public void AddScore(int points)
    {
        currentScore += points;
        OnScoreChanged?.Invoke(currentScore);
    }
    
    public void AddCoin(int amount = 1)
    {
        currentCoins += amount;
        OnCoinsChanged?.Invoke(currentCoins);
    }
    
    public void GameOver()
    {
        isGameOver = true;
        
        if (currentScore > highScore)
        {
            highScore = currentScore;
            SaveHighScore();
        }
        
        OnGameOver?.Invoke();
    }
    
    public void PauseGame()
    {
        isPaused = true;
        Time.timeScale = 0f;
        OnGamePaused?.Invoke();
    }
    
    public void ResumeGame()
    {
        isPaused = false;
        Time.timeScale = 1f;
        OnGameResumed?.Invoke();
    }
    
    // Getters
    public float GetCurrentSpeed() => currentSpeed;
    public float GetMaxSpeed() => maxSpeed;
    public int GetScore() => currentScore;
    public int GetCoins() => currentCoins;
    public float GetDistance() => distanceTraveled;
    public int GetHighScore() => highScore;
    public bool IsGameOver => isGameOver;
    public bool IsPaused => isPaused;
    
    private void SaveHighScore() => PlayerPrefs.SetInt("EndlessRunnerHighScore", highScore);
    private void LoadHighScore() => highScore = PlayerPrefs.GetInt("EndlessRunnerHighScore", 0);
}

2. Контроллер персонажа

using UnityEngine;
using System;

public class RunnerPlayer : MonoBehaviour
{
    [Header("Movement")]
    [SerializeField] private float jumpForce = 5f;
    [SerializeField] private float slideForce = 0.5f;
    [SerializeField] private float groundDrag = 5f;
    
    [Header("Ground Check")]
    [SerializeField] private Transform groundCheck;
    [SerializeField] private float groundCheckRadius = 0.2f;
    [SerializeField] private LayerMask groundLayer;
    
    [Header("Lane")]
    [SerializeField] private float laneWidth = 2f;
    [SerializeField] private int currentLane = 1; // 0=left, 1=center, 2=right
    [SerializeField] private float laneSwitchSpeed = 10f;
    
    // Компоненты
    private Rigidbody rb;
    private Animator animator;
    private CapsuleCollider capsuleCollider;
    private CapsuleCollider slideCollider; // Для slide
    
    // Состояние
    private bool isGrounded = false;
    private bool isSliding = false;
    private float slideTimer = 0f;
    private float slideDuration = 0.5f;
    private Vector3 targetLanePosition;
    
    // События
    public event Action OnPlayerDied;
    
    void Awake()
    {
        rb = GetComponent<Rigidbody>();
        animator = GetComponent<Animator>();
        capsuleCollider = GetComponent<CapsuleCollider>();
        
        // Создаём collider для slide
        slideCollider = gameObject.AddComponent<CapsuleCollider>();
        slideCollider.height = capsuleCollider.height * 0.5f;
        slideCollider.center = new Vector3(0, -capsuleCollider.height * 0.25f, 0);
        slideCollider.enabled = false;
        
        SetupLanePosition();
    }
    
    void Update()
    {
        CheckGround();
        HandleInput();
        UpdateLanePosition();
        UpdateSlide();
    }
    
    void FixedUpdate()
    {
        // Персонаж всегда бежит вперёд
        rb.velocity = new Vector3(
            rb.velocity.x,
            rb.velocity.y,
            EndlessRunnerManager.Instance.GetCurrentSpeed()
        );
    }
    
    private void CheckGround()
    {
        isGrounded = Physics.CheckSphere(
            groundCheck.position,
            groundCheckRadius,
            groundLayer
        );
    }
    
    private void HandleInput()
    {
        // Прыжок
        if (Input.GetKeyDown(KeyCode.Space) && isGrounded && !isSliding)
        {
            Jump();
        }
        
        // Slide
        if (Input.GetKeyDown(KeyCode.LeftControl) && isGrounded && !isSliding)
        {
            StartSlide();
        }
        
        // Смена полосы
        if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow))
        {
            MoveLane(-1);
        }
        if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow))
        {
            MoveLane(1);
        }
    }
    
    private void Jump()
    {
        rb.velocity = new Vector3(rb.velocity.x, 0, rb.velocity.z);
        rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
        animator.SetTrigger("Jump");
    }
    
    private void StartSlide()
    {
        isSliding = true;
        slideTimer = 0f;
        
        // Отключаем обычный collider и включаем slide collider
        capsuleCollider.enabled = false;
        slideCollider.enabled = true;
        
        animator.SetBool("IsSliding", true);
    }
    
    private void UpdateSlide()
    {
        if (!isSliding)
            return;
        
        slideTimer += Time.deltaTime;
        
        if (slideTimer >= slideDuration)
        {
            EndSlide();
        }
    }
    
    private void EndSlide()
    {
        isSliding = false;
        capsuleCollider.enabled = true;
        slideCollider.enabled = false;
        animator.SetBool("IsSliding", false);
    }
    
    private void MoveLane(int direction)
    {
        int newLane = Mathf.Clamp(currentLane + direction, 0, 2);
        if (newLane != currentLane)
        {
            currentLane = newLane;
            SetupLanePosition();
        }
    }
    
    private void SetupLanePosition()
    {
        targetLanePosition = new Vector3(
            (currentLane - 1) * laneWidth,
            transform.position.y,
            transform.position.z
        );
    }
    
    private void UpdateLanePosition()
    {
        Vector3 pos = transform.position;
        pos.x = Mathf.Lerp(pos.x, targetLanePosition.x, Time.deltaTime * laneSwitchSpeed);
        transform.position = pos;
    }
    
    public void Die()
    {
        animator.SetTrigger("Die");
        GetComponent<Collider>().enabled = false;
        rb.isKinematic = true;
        OnPlayerDied?.Invoke();
    }
    
    public int GetLane() => currentLane;
}

3. Процедурная генерация уровня

using UnityEngine;
using System.Collections.Generic;

public class LevelGenerator : Singleton<LevelGenerator>
{
    [Header("Prefabs")]
    [SerializeField] private GameObject[] obstaclePrefabs;
    [SerializeField] private GameObject coinPrefab;
    [SerializeField] private Transform levelContainer;
    
    [Header("Generation")]
    [SerializeField] private float segmentLength = 10f;
    [SerializeField] private float spawnDistance = 50f;
    [SerializeField] private float obstacleSpawnChance = 0.7f;
    [SerializeField] private float coinSpawnChance = 0.5f;
    
    private Transform player;
    private float lastSegmentZ = 0f;
    private Queue<GameObject> segmentPool = new Queue<GameObject>();
    private EndlessRunnerManager gameManager;
    
    protected override void Awake()
    {
        base.Awake();
        gameManager = EndlessRunnerManager.Instance;
    }
    
    void Start()
    {
        player = FindObjectOfType<RunnerPlayer>().transform;
    }
    
    void Update()
    {
        if (gameManager.IsGameOver)
            return;
        
        // Генерируем новые сегменты впереди игрока
        if (player.position.z + spawnDistance > lastSegmentZ)
        {
            GenerateSegment();
        }
        
        // Удаляем старые сегменты позади игрока
        if (segmentPool.Count > 20)
        {
            GameObject oldSegment = segmentPool.Dequeue();
            Destroy(oldSegment);
        }
    }
    
    private void GenerateSegment()
    {
        float segmentZ = lastSegmentZ + segmentLength;
        
        // Генерируем препятствия
        if (Random.value < obstacleSpawnChance)
        {
            int lane = Random.Range(0, 3);
            float laneX = (lane - 1) * 2f;
            
            int obstacleType = Random.Range(0, obstaclePrefabs.Length);
            GameObject obstacle = Instantiate(
                obstaclePrefabs[obstacleType],
                new Vector3(laneX, 0, segmentZ),
                Quaternion.identity,
                levelContainer
            );
            
            segmentPool.Enqueue(obstacle);
        }
        
        // Генерируем монеты
        if (Random.value < coinSpawnChance)
        {
            for (int i = 0; i < 3; i++)
            {
                float laneX = (i - 1) * 2f;
                
                GameObject coin = Instantiate(
                    coinPrefab,
                    new Vector3(laneX, 1f, segmentZ + i * 0.5f),
                    Quaternion.identity,
                    levelContainer
                );
                
                segmentPool.Enqueue(coin);
            }
        }
        
        lastSegmentZ = segmentZ;
    }
}

4. Препятствия и монеты

using UnityEngine;

public class Obstacle : MonoBehaviour
{
    [SerializeField] private int damage = 1;
    
    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            RunnerPlayer player = other.GetComponent<RunnerPlayer>();
            if (player != null)
            {
                player.Die();
                EndlessRunnerManager.Instance.GameOver();
            }
        }
    }
}

public class Coin : MonoBehaviour
{
    [SerializeField] private int coinValue = 10;
    [SerializeField] private AudioClip coinSound;
    private bool collected = false;
    
    void OnTriggerEnter(Collider other)
    {
        if (collected)
            return;
        
        if (other.CompareTag("Player"))
        {
            collected = true;
            EndlessRunnerManager.Instance.AddCoin(1);
            EndlessRunnerManager.Instance.AddScore(coinValue);
            
            if (coinSound != null)
                AudioManager.Instance?.PlaySFX(coinSound);
            
            // Эффект сбора
            transform.SetParent(other.transform);
            StartCoroutine(AnimateCollection());
        }
    }
    
    private System.Collections.IEnumerator AnimateCollection()
    {
        yield return new WaitForSeconds(0.3f);
        Destroy(gameObject);
    }
}

5. UI Manager

using UnityEngine;
using UnityEngine.UI;

public class EndlessRunnerUI : MonoBehaviour
{
    [SerializeField] private Text scoreText;
    [SerializeField] private Text coinsText;
    [SerializeField] private Text distanceText;
    [SerializeField] private Text speedText;
    [SerializeField] private Text highScoreText;
    
    [SerializeField] private GameObject gameOverPanel;
    [SerializeField] private GameObject pausePanel;
    [SerializeField] private Button pauseButton;
    [SerializeField] private Button resumeButton;
    [SerializeField] private Button restartButton;
    
    private EndlessRunnerManager gameManager;
    
    void Start()
    {
        gameManager = EndlessRunnerManager.Instance;
        
        // Подписываемся на события
        gameManager.OnScoreChanged += UpdateScore;
        gameManager.OnCoinsChanged += UpdateCoins;
        gameManager.OnSpeedChanged += UpdateSpeed;
        gameManager.OnDistanceChanged += UpdateDistance;
        gameManager.OnGameOver += ShowGameOver;
        gameManager.OnGamePaused += ShowPausePanel;
        gameManager.OnGameResumed += HidePausePanel;
        
        // Кнопки
        pauseButton.onClick.AddListener(() => gameManager.PauseGame());
        resumeButton.onClick.AddListener(() => gameManager.ResumeGame());
        restartButton.onClick.AddListener(() => 
            UnityEngine.SceneManagement.SceneManager.LoadScene(0));
        
        gameOverPanel.SetActive(false);
        pausePanel.SetActive(false);
    }
    
    private void UpdateScore(int score)
    {
        scoreText.text = $"Score: {score}";
    }
    
    private void UpdateCoins(int coins)
    {
        coinsText.text = $"Coins: {coins}";
    }
    
    private void UpdateDistance(float distance)
    {
        distanceText.text = $"Distance: {(distance / 10f):F1}m";
    }
    
    private void UpdateSpeed(float speed)
    {
        speedText.text = $"Speed: {speed:F1}";
    }
    
    private void ShowGameOver()
    {
        gameOverPanel.SetActive(true);
        highScoreText.text = $"High Score: {gameManager.GetHighScore()}";
    }
    
    private void ShowPausePanel()
    {
        pausePanel.SetActive(true);
    }
    
    private void HidePausePanel()
    {
        pausePanel.SetActive(false);
    }
}

Ключевые компоненты

1. Процедурная генерация — препятствия и монеты генерируются перед игроком.

2. Система полос — три дорожки для движения влево-вправо.

3. Управление — прыжок (Space), slide (Ctrl), движение (A/D или стрелки).

4. Динамическая сложность — скорость увеличивается со временем.

5. Scoring — монеты и дистанция дают очки.

6. Оптимизация — пулинг для генерируемых объектов.

Эта реализация обеспечивает полнофункциональный endless runner с хорошей производительностью и аддиктивным геймплеем.

Создать endless runner игру | PrepBro