Создать endless runner игру
Условие
Создайте endless runner игру.
Требования
- Персонаж бежит автоматически
- Управление: прыжок и slide
- Процедурная генерация уровня
- Разные типы препятствий
- Сбор монет
- Увеличение скорости со временем
- Подсчет дистанции и очков
UI
- Текущий счет
- Монеты
- Пауза
- Game Over с рекордом
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Архитектура 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 с хорошей производительностью и аддиктивным геймплеем.