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

Создать игру Space Invaders

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

Условие

Создайте игру типа Space Invaders на Unity.

Требования

  1. Три визуально различных типа врагов
  2. Враги могут стрелять, если перед ними нет другого врага
  3. UFO периодически пролетает сверху экрана для бонусных очков
  4. Враги движутся группой и меняют направление у края экрана
  5. Скорость группы врагов увеличивается по мере их уничтожения
  6. Игрок может двигаться влево-вправо и стрелять
  7. Подсчет очков и отображение рекорда
  8. Меню старт, пауза, game over

Бонус

  • Укрытия которые разрушаются от попаданий
  • Несколько жизней у игрока

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

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

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

Решение Space Invaders

Архитектура игры

Space Invaders требует координации нескольких систем: управления группой врагов, стрельбы, столкновений, UI и score. Я использую паттерны, которые были в предыдущих задачах: Object Pool для пуль, Event System для событий, и модульную архитектуру.

1. Менеджер игры и состояния

using UnityEngine;
using System;

public class SpaceInvadersGameManager : Singleton<SpaceInvadersGameManager>
{
    [Header("Game Settings")]
    [SerializeField] private int initialLives = 3;
    [SerializeField] private float gameSpeed = 1f;
    [SerializeField] private int scorePerKill = 10;
    [SerializeField] private int scoreUFO = 100;
    
    [Header("Prefabs")]
    [SerializeField] private Player playerPrefab;
    [SerializeField] private EnemyGroup enemyGroupPrefab;
    [SerializeField] private UFO ufoPrefab;
    [SerializeField] private AudioClip gameOverSound;
    
    // Состояние игры
    public enum GameState { Menu, Playing, Paused, GameOver }
    private GameState currentState = GameState.Menu;
    
    private int currentScore = 0;
    private int highScore = 0;
    private int currentLives = 0;
    private int enemiesKilled = 0;
    
    // Объекты
    private Player player;
    private EnemyGroup enemyGroup;
    private ObjectPool<Bullet> bulletPool;
    
    // События
    public static event Action<int> OnScoreChanged;
    public static event Action<int> OnLivesChanged;
    public static event Action<GameState> OnGameStateChanged;
    public static event Action OnGameOver;
    
    protected override void Awake()
    {
        base.Awake();
        LoadHighScore();
    }
    
    void Start()
    {
        // Подписываемся на события
        Player.OnPlayerDied += OnPlayerDied;
        EnemyGroup.OnAllEnemiesDied += OnAllEnemiesDied;
    }
    
    /// <summary>Начать новую игру</summary>
    public void StartGame()
    {
        currentScore = 0;
        currentLives = initialLives;
        enemiesKilled = 0;
        gameSpeed = 1f;
        
        ChangeGameState(GameState.Playing);
        
        // Создаём игрока
        player = Instantiate(playerPrefab);
        
        // Создаём группу врагов
        enemyGroup = Instantiate(enemyGroupPrefab);
        
        // Создаём пул пуль
        bulletPool = new ObjectPool<Bullet>(playerPrefab.GetComponent<Player>().bulletPrefab, transform);
        bulletPool.Prewarm(20);
        
        OnLivesChanged?.Invoke(currentLives);
        OnScoreChanged?.Invoke(currentScore);
        
        // Запускаем UFO
        StartCoroutine(SpawnUFOPeriodically());
    }
    
    /// <summary>Пауза/Возобновление</summary>
    public void TogglePause()
    {
        if (currentState == GameState.Playing)
        {
            ChangeGameState(GameState.Paused);
            Time.timeScale = 0f;
        }
        else if (currentState == GameState.Paused)
        {
            ChangeGameState(GameState.Playing);
            Time.timeScale = 1f;
        }
    }
    
    /// <summary>Добавить очки</summary>
    public void AddScore(int points)
    {
        currentScore += points;
        OnScoreChanged?.Invoke(currentScore);
        
        // Увеличиваем скорость по мере прогресса
        gameSpeed = 1f + (enemiesKilled * 0.1f);
    }
    
    /// <summary>Когда игрок погиб</summary>
    private void OnPlayerDied()
    {
        currentLives--;
        OnLivesChanged?.Invoke(currentLives);
        
        if (currentLives <= 0)
        {
            GameOver();
        }
        else
        {
            // Возрождаем игрока
            Destroy(player.gameObject);
            player = Instantiate(playerPrefab);
        }
    }
    
    /// <summary>Все враги уничтожены</summary>
    private void OnAllEnemiesDied()
    {
        enemiesKilled++;
        gameSpeed = 1f + (enemiesKilled * 0.1f);
        
        // Создаём новую волну врагов
        Destroy(enemyGroup.gameObject);
        enemyGroup = Instantiate(enemyGroupPrefab);
    }
    
    /// <summary>Конец игры</summary>
    private void GameOver()
    {
        ChangeGameState(GameState.GameOver);
        Time.timeScale = 0f;
        
        // Сохраняем рекорд
        if (currentScore > highScore)
        {
            highScore = currentScore;
            SaveHighScore();
        }
        
        OnGameOver?.Invoke();
        
        if (gameOverSound != null)
            AudioManager.Instance?.PlaySFX(gameOverSound);
    }
    
    /// <summary>Смена состояния игры</summary>
    private void ChangeGameState(GameState newState)
    {
        currentState = newState;
        OnGameStateChanged?.Invoke(newState);
    }
    
    /// <summary>Спавн UFO с интервалом</summary>
    private System.Collections.IEnumerator SpawnUFOPeriodically()
    {
        while (currentState != GameState.GameOver)
        {
            yield return new WaitForSeconds(Random.Range(15f, 30f));
            
            if (currentState == GameState.Playing && enemyGroup != null)
            {
                UFO ufo = Instantiate(ufoPrefab);
                ufo.SetReward(scoreUFO);
            }
        }
    }
    
    private void SaveHighScore() => PlayerPrefs.SetInt("HighScore", highScore);
    private void LoadHighScore() => highScore = PlayerPrefs.GetInt("HighScore", 0);
    
    // Getters
    public int GetCurrentScore() => currentScore;
    public int GetHighScore() => highScore;
    public int GetLives() => currentLives;
    public float GetGameSpeed() => gameSpeed;
    public GameState GetGameState() => currentState;
    public ObjectPool<Bullet> GetBulletPool() => bulletPool;
}

2. Система врагов

public class Enemy : MonoBehaviour
{
    [Header("Settings")]
    [SerializeField] protected float moveSpeed = 2f;
    [SerializeField] protected int pointValue = 10;
    [SerializeField] protected AudioClip deathSound;
    [SerializeField] protected GameObject deathEffectPrefab;
    
    [Header("Shooting")]
    [SerializeField] protected float shootCooldown = 2f;
    [SerializeField] protected Bullet bulletPrefab;
    
    protected bool canShoot = true;
    protected float shootTimer = 0f;
    protected bool hasEnemyInFront = false;
    protected Rigidbody2D rb;
    
    public event Action OnDied;
    
    protected virtual void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }
    
    protected virtual void Update()
    {
        shootTimer += Time.deltaTime;
        if (shootTimer >= shootCooldown)
        {
            shootTimer = 0f;
            canShoot = true;
        }
    }
    
    public virtual void Move(Vector3 direction)
    {
        rb.velocity = direction * moveSpeed * SpaceInvadersGameManager.Instance.GetGameSpeed();
    }
    
    public virtual void TryShoot()
    {
        if (canShoot && !hasEnemyInFront)
        {
            Shoot();
            canShoot = false;
        }
    }
    
    protected virtual void Shoot()
    {
        Bullet bullet = Instantiate(bulletPrefab, transform.position, Quaternion.identity);
        bullet.SetDirection(Vector2.down);
    }
    
    public virtual void Die()
    {
        SpaceInvadersGameManager.Instance.AddScore(pointValue);
        
        if (deathEffectPrefab != null)
            Instantiate(deathEffectPrefab, transform.position, Quaternion.identity);
        
        if (deathSound != null)
            AudioManager.Instance?.PlaySFX(deathSound);
        
        OnDied?.Invoke();
        Destroy(gameObject);
    }
    
    public void SetHasEnemyInFront(bool has) => hasEnemyInFront = has;
}

/// <summary>Группа врагов (движутся вместе)</summary>
public class EnemyGroup : MonoBehaviour
{
    [Header("Formation")]
    [SerializeField] private int rowCount = 2;
    [SerializeField] private int columnCount = 8;
    [SerializeField] private float spacingX = 1.5f;
    [SerializeField] private float spacingY = 1.5f;
    [SerializeField] private Vector2 spawnPosition = Vector2.zero;
    
    [Header("Prefabs")]
    [SerializeField] private Enemy[] enemyTypePrefabs; // 3 типа врагов
    
    [Header("Movement")]
    [SerializeField] private float moveDistance = 0.5f;
    [SerializeField] private float moveInterval = 0.75f;
    
    private Enemy[,] formation;
    private Vector2 direction = Vector2.right;
    private float moveTimer = 0f;
    private bool shouldDropDown = false;
    private int totalEnemies = 0;
    private int enemiesAlive = 0;
    
    public static event Action OnAllEnemiesDied;
    
    void Start()
    {
        CreateFormation();
    }
    
    void Update()
    {
        moveTimer += Time.deltaTime;
        
        if (moveTimer >= moveInterval)
        {
            moveTimer = 0f;
            MoveGroup();
            TryShoot();
        }
    }
    
    private void CreateFormation()
    {
        formation = new Enemy[rowCount, columnCount];
        totalEnemies = rowCount * columnCount;
        enemiesAlive = totalEnemies;
        
        for (int row = 0; row < rowCount; row++)
        {
            for (int col = 0; col < columnCount; col++)
            {
                int enemyType = row % enemyTypePrefabs.Length;
                Vector2 spawnPos = spawnPosition + new Vector2(
                    col * spacingX,
                    -row * spacingY
                );
                
                Enemy enemy = Instantiate(enemyTypePrefabs[enemyType], spawnPos, Quaternion.identity, transform);
                enemy.OnDied += () => OnEnemyDied(row, col);
                formation[row, col] = enemy;
            }
        }
    }
    
    private void MoveGroup()
    {
        // Проверяем границы
        float leftmost = float.MaxValue;
        float rightmost = float.MinValue;
        
        for (int row = 0; row < rowCount; row++)
        {
            for (int col = 0; col < columnCount; col++)
            {
                if (formation[row, col] != null)
                {
                    leftmost = Mathf.Min(leftmost, formation[row, col].transform.position.x);
                    rightmost = Mathf.Max(rightmost, formation[row, col].transform.position.x);
                }
            }
        }
        
        // Если достигли края, меняем направление и опускаемся
        if ((direction == Vector2.right && rightmost >= 8f) || 
            (direction == Vector2.left && leftmost <= -8f))
        {
            direction = -direction;
            shouldDropDown = true;
        }
        
        // Двигаем группу
        Vector2 moveVector = direction * moveDistance;
        if (shouldDropDown)
        {
            moveVector.y = -0.5f;
            shouldDropDown = false;
        }
        
        for (int row = 0; row < rowCount; row++)
        {
            for (int col = 0; col < columnCount; col++)
            {
                if (formation[row, col] != null)
                {
                    formation[row, col].transform.position += (Vector3)moveVector;
                }
            }
        }
    }
    
    private void TryShoot()
    {
        // Враги в нижнем ряду стреляют случайно
        for (int col = 0; col < columnCount; col++)
        {
            for (int row = rowCount - 1; row >= 0; row--)
            {
                if (formation[row, col] != null)
                {
                    // Проверяем есть ли враг впереди
                    bool hasEnemyInFront = row < rowCount - 1;
                    formation[row, col].SetHasEnemyInFront(hasEnemyInFront);
                    
                    if (!hasEnemyInFront && Random.value < 0.1f)
                    {
                        formation[row, col].TryShoot();
                    }
                    break;
                }
            }
        }
    }
    
    private void OnEnemyDied(int row, int col)
    {
        formation[row, col] = null;
        enemiesAlive--;
        
        if (enemiesAlive <= 0)
        {
            OnAllEnemiesDied?.Invoke();
        }
    }
}

3. Игрок и UFO

public class Player : MonoBehaviour
{
    [Header("Movement")]
    [SerializeField] private float moveSpeed = 8f;
    [SerializeField] private float minX = -8f;
    [SerializeField] private float maxX = 8f;
    
    [Header("Shooting")]
    [SerializeField] public Bullet bulletPrefab;
    [SerializeField] private float shootCooldown = 0.3f;
    [SerializeField] private AudioClip shootSound;
    
    [Header("Settings")]
    [SerializeField] private AudioClip deathSound;
    
    private Rigidbody2D rb;
    private float shootTimer = 0f;
    private int lives = 3;
    
    public static event Action OnPlayerDied;
    
    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }
    
    void Update()
    {
        HandleMovement();
        HandleShooting();
    }
    
    private void HandleMovement()
    {
        float input = Input.GetAxis("Horizontal");
        rb.velocity = new Vector2(input * moveSpeed, rb.velocity.y);
        
        // Ограничиваем границы
        Vector3 pos = transform.position;
        pos.x = Mathf.Clamp(pos.x, minX, maxX);
        transform.position = pos;
    }
    
    private void HandleShooting()
    {
        shootTimer += Time.deltaTime;
        
        if (Input.GetKey(KeyCode.Space) && shootTimer >= shootCooldown)
        {
            Shoot();
            shootTimer = 0f;
        }
    }
    
    private void Shoot()
    {
        Bullet bullet = Instantiate(bulletPrefab, transform.position + Vector3.up * 0.5f, Quaternion.identity);
        bullet.SetDirection(Vector2.up);
        
        if (shootSound != null)
            AudioManager.Instance?.PlaySFX(shootSound);
    }
    
    public void Die()
    {
        if (deathSound != null)
            AudioManager.Instance?.PlaySFX(deathSound);
        
        OnPlayerDied?.Invoke();
        Destroy(gameObject);
    }
    
    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("EnemyBullet"))
        {
            Die();
        }
    }
}

public class UFO : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5f;
    [SerializeField] private AudioClip deathSound;
    
    private int reward = 100;
    
    void Start()
    {
        // Спауним слева или справа случайно
        float startX = Random.value > 0.5f ? -10f : 10f;
        transform.position = new Vector3(startX, 8f, 0);
        
        GetComponent<Rigidbody2D>().velocity = new Vector2((startX > 0 ? -1 : 1) * moveSpeed, 0);
    }
    
    void Update()
    {
        // Удаляем если ушёл за границы
        if (Mathf.Abs(transform.position.x) > 12f)
        {
            Destroy(gameObject);
        }
    }
    
    public void Die()
    {
        SpaceInvadersGameManager.Instance.AddScore(reward);
        
        if (deathSound != null)
            AudioManager.Instance?.PlaySFX(deathSound);
        
        Destroy(gameObject);
    }
    
    public void SetReward(int amount) => reward = amount;
}

public class Bullet : MonoBehaviour
{
    [SerializeField] private float speed = 10f;
    private Vector2 direction = Vector2.up;
    private Rigidbody2D rb;
    
    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }
    
    void Start()
    {
        rb.velocity = direction * speed;
    }
    
    public void SetDirection(Vector2 dir) => direction = dir.normalized;
    
    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Enemy"))
        {
            Enemy enemy = other.GetComponent<Enemy>();
            if (enemy != null)
                enemy.Die();
            Destroy(gameObject);
        }
        else if (other.CompareTag("UFO"))
        {
            UFO ufo = other.GetComponent<UFO>();
            if (ufo != null)
                ufo.Die();
            Destroy(gameObject);
        }
        else if (other.CompareTag("Bunker"))
        {
            // Разрушение укрытия
            Destroy(gameObject);
        }
    }
}

4. UI Manager

public class SpaceInvadersUI : MonoBehaviour
{
    [SerializeField] private Text scoreText;
    [SerializeField] private Text highScoreText;
    [SerializeField] private Text livesText;
    [SerializeField] private GameObject gameOverPanel;
    [SerializeField] private GameObject pausePanel;
    [SerializeField] private Button startButton;
    [SerializeField] private Button pauseButton;
    [SerializeField] private Button resumeButton;
    [SerializeField] private Button restartButton;
    
    void Start()
    {
        SpaceInvadersGameManager.OnScoreChanged += UpdateScore;
        SpaceInvadersGameManager.OnLivesChanged += UpdateLives;
        SpaceInvadersGameManager.OnGameStateChanged += OnGameStateChanged;
        SpaceInvadersGameManager.OnGameOver += ShowGameOver;
        
        startButton.onClick.AddListener(() => SpaceInvadersGameManager.Instance.StartGame());
        pauseButton.onClick.AddListener(() => SpaceInvadersGameManager.Instance.TogglePause());
        resumeButton.onClick.AddListener(() => SpaceInvadersGameManager.Instance.TogglePause());
        restartButton.onClick.AddListener(() => {
            Time.timeScale = 1f;
            UnityEngine.SceneManagement.SceneManager.LoadScene(0);
        });
        
        gameOverPanel.SetActive(false);
        pausePanel.SetActive(false);
    }
    
    private void UpdateScore(int score)
    {
        scoreText.text = $"Score: {score}";
    }
    
    private void UpdateLives(int lives)
    {
        livesText.text = $"Lives: {lives}";
    }
    
    private void OnGameStateChanged(SpaceInvadersGameManager.GameState state)
    {
        pausePanel.SetActive(state == SpaceInvadersGameManager.GameState.Paused);
    }
    
    private void ShowGameOver()
    {
        gameOverPanel.SetActive(true);
        highScoreText.text = $"High Score: {SpaceInvadersGameManager.Instance.GetHighScore()}";
    }
}

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

1. Система врагов — формация с координированным движением и стрельбой.

2. Object Pooling — пулинг пуль для оптимизации.

3. Scoring — динамический подсчёт очков с рекордом.

4. Game States — Menu, Playing, Paused, GameOver.

5. Wave System — повышение сложности по мере уничтожения волн.

6. UFO bonus — периодический бонус сверху.

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