Найти в Дзене
KNL Games

Исходный код для игры про BomberMan

BomberMan В контексте разработки игр, особенно в проектах, где несколько систем должны обмениваться информацией о общем состоянии игры, GlobalStateManager (или GameManager, GameStateManager) является чрезвычайно важным и распространенным паттерном проектирования. Вот для чего он нужен и какие функции он выполняет: Основное Назначение GlobalStateManager GlobalStateManager — это централизованный, обычно единственный (Singleton) скрипт, который управляет основными, сквозными данными и состоянием всей игры, которые не относятся к конкретному объекту (как игрок или бомба). Его основная цель — централизация и управление состоянием игры, чтобы избежать хаотичного обмена данными между множеством несвязанных объектов. Ключевые Функции GlobalStateManager 1. Управление Состоянием Игры (Game State Management) Это самая очевидная функция. Менеджер отслеживает, в каком режиме находится игра в данный момент: Меню (MainMenu) Игра (Playing) Пауза (Paused) Окончание раунда/игры (GameOver) Загрузка уров
Оглавление
BomberMan
BomberMan

В контексте разработки игр, особенно в проектах, где несколько систем должны обмениваться информацией о общем состоянии игры, GlobalStateManager (или GameManager, GameStateManager) является чрезвычайно важным и распространенным паттерном проектирования.

Вот для чего он нужен и какие функции он выполняет:

Основное Назначение GlobalStateManager

GlobalStateManager — это централизованный, обычно единственный (Singleton) скрипт, который управляет основными, сквозными данными и состоянием всей игры, которые не относятся к конкретному объекту (как игрок или бомба).

Его основная цель — централизация и управление состоянием игры, чтобы избежать хаотичного обмена данными между множеством несвязанных объектов.

Ключевые Функции GlobalStateManager

1. Управление Состоянием Игры (Game State Management)

Это самая очевидная функция. Менеджер отслеживает, в каком режиме находится игра в данный момент:

  • Меню (MainMenu)
  • Игра (Playing)
  • Пауза (Paused)
  • Окончание раунда/игры (GameOver)
  • Загрузка уровня (Loading)

Когда происходит переход между этими состояниями (например, игрок нажимает паузу), GlobalStateManager оповещает все остальные скрипты о необходимости отреагировать (например, остановить движение объектов или скрыть UI).

2. Отслеживание Сквозных Данных (Global Data Tracking)

Это данные, которые должны быть доступны и актуальны в любом месте игры:

  • Счет: Текущий счет игрока или всех игроков.
  • Жизни/Ресурсы: Общее количество жизней, оставшееся время раунда, собранные монеты.
  • Уровень: Текущий загруженный уровень.

3. Обработка Событий Смерти Игрока (В вашем примере)

В вашем коде игрока (Player) есть метод:

csharp

globalStateManager.PlayerDied(playerNumber);

Это идеальный пример:

  • Скрипт игрока не знает, что делать после смерти (отключить UI, показать экран Game Over, начать новый раунд).
  • Он просто сообщает менеджеру: “Игрок номер N умер”.
  • GlobalStateManager принимает это событие, уменьшает счетчик живых игроков, и если все умерли, запускает логику GameOver.

4. Управление Слоями (Singleton/Access Point)

Поскольку этот менеджер должен быть доступен отовсюду, он часто реализуется как Singleton. Это гарантирует, что в сцене существует только один экземпляр этого класса, и любой другой скрипт может легко получить к нему доступ (например, GlobalStateManager.Instance.CurrentScore).

5. Управление Загрузкой Сцен и Переходами

Если игра состоит из нескольких сцен (Главное меню, Уровень 1, Экран результатов), GlobalStateManager отвечает за корректную последовательную загрузку этих сцен, сохраняя при этом ключевые данные (например, счет) между ними, если это необходимо (используя DontDestroyOnLoad).

Почему это лучше, чем прямое обращение?

Если бы скрипт Player напрямую вызывал метод в скрипте UI, а скрипт UI вызывал бы метод в скрипте ScoreTracker, у вас возник бы “спагетти-код”:

  1. Зависимость: Player жестко привязан к UIController. Если вы переименуете UIController, сломается и Player.
  2. Сложность: Если 10 объектов должны знать о счете, 10 объектов должны иметь ссылки на ScoreTracker.

GlobalStateManager выступает в роли посредника или диспетчера:

  1. Player сообщает Менеджеру о смерти.
  2. Менеджер сам вызывает UI, чтобы обновить экран, и сам вызывает спаун новой жизни, если это необходимо.
  3. Скрипты становятся слабосвязанными — они знают только о Менеджере, а не о десятке других систем.

Код из игры GlobalStateManager:

using UnityEngine;

using System.Collections.Generic;

public class GlobalStateManager : MonoBehaviour

{

public List<GameObject> Players = new List<GameObject>();

private int deadPlayers = 0;

private int deadPlayerNumber = -1;

public void PlayerDied(int playerNumber)

{

deadPlayers++;

if(deadPlayers == 1)

{

deadPlayerNumber = playerNumber;

Invoke("CheckPlayersDeath", 0.3f);

}

}

private void OnGUI()

{

if (deadPlayers == 1)

{

if (deadPlayers == 1)

{

if (deadPlayerNumber == 1)

{

GUI.Label(new Rect(10, 10, 200, 20), "Player 2 - победитель");

}

else

{

GUI.Label(new Rect(10, 10, 200, 20), "Player 1 - победитель");

}

}

else

{

GUI.Label(new Rect(10, 10, 200, 20), "Ничья");

}

}

}

private void CheckPlayersDeath()

{

if(deadPlayers == 1)

{

if(deadPlayers == 1)

{

if(deadPlayerNumber == 1)

{

Debug.Log("Player 2 - победитель");

}

else

{

Debug.Log("Player 1 - победитель");

}

}

else

{

Debug.Log("Игра окончена! Ничья");

}

}

}

}

В контексте разработки игр, Player (Игрок) — это центральный скрипт (или набор скриптов), который управляет всеми аспектами поведения и представления сущности, которой игрок (человек, управляющий игрой) контролирует напрямую.

В вашем примере скрипт Player выполняет функции, необходимые для его существования и взаимодействия с миром игры.

Вот основные задачи, которые выполняет скрипт Player, и почему он необходим:

1. Управление Вводом (Input Handling)

Игрок — это точка входа для всех команд, идущих от пользователя (клавиатуры, геймпада, мыши).

  • Что делает: Скрипт Player постоянно слушает нажатия клавиш (Input.GetKey, Input.GetKeyDown).
  • В вашем коде: Он отвечает за обработку клавиш W, A, S, D для движения и Space для сброса бомбы.

2. Физическое Управление и Движение

Игрок должен перемещаться в игровом мире в соответствии с командами ввода.

  • Что делает: Принимает команды ввода и преобразует их в физические действия (изменение скорости, вращение).
  • В вашем коде: Использует Rigidbody.velocity и myTransform.rotation для перемещения и ориентации персонажа в соответствии с moveSpeed.

3. Анимация и Визуальное Представление

Скрипт отвечает за то, как персонаж выглядит, когда он выполняет действия.

  • Что делает: Обновляет состояние Animator (например, включает или выключает анимацию ходьбы) на основе того, двигается ли игрок.
  • В вашем коде: Управляет параметром "Walking" в Animator.

4. Игровая Логика, Специфичная для Игрока

Это логика, которая касается только самого персонажа:

  • Состояния: Отслеживание состояний canMove, canDropBombs, и самое главное — dead.
  • Сброс Бомбы: Содержит логику, определяющую, где и как должна быть создана (инстанцирована) новая бомба.

5. Взаимодействие с Окружением и Другими Сущностями

Игрок должен реагировать на мир вокруг него.

  • Что делает: Использует коллайдеры для обнаружения столкновений или триггеров.
  • В вашем коде (Смерть): Метод OnTriggerEnter проверяет, не столкнулся ли игрок с объектом, помеченным тегом "Explosion".

6. Связь с Глобальными Системными Менеджерами

Игрок должен сообщать центральным системам о критических событиях, которые влияют на всю игру.

  • В вашем коде: При смерти игрок информирует GlobalStateManager о своем состоянии, позволяя менеджеру обновить счет, проверить условия победы/поражения и т.д.

Резюме

Player — это контроллер и модель для управляемой игроком сущности. Он инкапсулирует всю логику, необходимую для того, чтобы эта сущность могла двигаться, действовать, выглядеть и реагировать на события в игровом мире, выступая в роли посредника между вводом игрока и игровым миром.

Код из игры Player:

using UnityEngine;

using System;

public class Player : MonoBehaviour

{

public GlobalStateManager globalManager;

[Range(1, 2)]

public int playerNumber = 1;

public float moveSpeed = 5f;

public bool canDropBombs = true;

public bool canMove = true;

public bool dead = false;

public GameObject bombPrefab;

private Rigidbody rigidBody;

private Transform myTransform;

private Animator animator;

void Start()

{

rigidBody = GetComponent<Rigidbody>();

myTransform = transform;

animator = myTransform.Find("PlayerModel").GetComponent<Animator>();

}

void Update()

{

UpdateMovement();

}

private void UpdateMovement()

{

animator.SetBool("Walking", false);

if (!canMove)

{

return;

}

if (playerNumber == 1)

{

UpdatePlayer1Movement();

}

else

{

UpdatePlayer2Movement();

}

}

private void UpdatePlayer1Movement()

{

if (Input.GetKey(KeyCode.W))

{

rigidBody.velocity = new Vector3(rigidBody.velocity.x, rigidBody.velocity.y, moveSpeed);

myTransform.rotation = Quaternion.Euler(0, 0, 0);

animator.SetBool("Walking", true);

}

if (Input.GetKey(KeyCode.A))

{

rigidBody.velocity = new Vector3(-moveSpeed, rigidBody.velocity.y, rigidBody.velocity.z);

myTransform.rotation = Quaternion.Euler(0, 270, 0);

animator.SetBool("Walking", true);

}

if (Input.GetKey(KeyCode.S))

{

rigidBody.velocity = new Vector3(rigidBody.velocity.x, rigidBody.velocity.y, -moveSpeed);

myTransform.rotation = Quaternion.Euler(0, 180, 0);

animator.SetBool("Walking", true);

}

if (Input.GetKey(KeyCode.D))

{

rigidBody.velocity = new Vector3(moveSpeed, rigidBody.velocity.y, rigidBody.velocity.z);

myTransform.rotation = Quaternion.Euler(0, 90, 0);

animator.SetBool("Walking", true);

}

if (canDropBombs && Input.GetKeyDown(KeyCode.Space))

{

DropBomb();

}

}

private void UpdatePlayer2Movement()

{

if (Input.GetKey(KeyCode.UpArrow))

{

rigidBody.velocity = new Vector3(rigidBody.velocity.x, rigidBody.velocity.y, moveSpeed);

myTransform.rotation = Quaternion.Euler(0, 0, 0);

animator.SetBool("Walking", true);

}

if (Input.GetKey(KeyCode.LeftArrow))

{

rigidBody.velocity = new Vector3(-moveSpeed, rigidBody.velocity.y, rigidBody.velocity.z);

myTransform.rotation = Quaternion.Euler(0, 270, 0);

animator.SetBool("Walking", true);

}

if (Input.GetKey(KeyCode.DownArrow))

{

rigidBody.velocity = new Vector3(rigidBody.velocity.x, rigidBody.velocity.y, -moveSpeed);

myTransform.rotation = Quaternion.Euler(0, 180, 0);

animator.SetBool("Walking", true);

}

if (Input.GetKey(KeyCode.RightArrow))

{

rigidBody.velocity = new Vector3(moveSpeed, rigidBody.velocity.y, rigidBody.velocity.z);

myTransform.rotation = Quaternion.Euler(0, 90, 0);

animator.SetBool("Walking", true);

}

if (canDropBombs && (Input.GetKeyDown(KeyCode.KeypadEnter) || Input.GetKeyDown(KeyCode.Return)))

{

DropBomb();

}

}

private void DropBomb()

{

if (bombPrefab)

{

Instantiate(bombPrefab,

new Vector3(Mathf.RoundToInt(myTransform.position.x), bombPrefab.transform.position.y, Mathf.RoundToInt(myTransform.position.z)),

bombPrefab.transform.rotation);

}

}

public void OnTriggerEnter(Collider other)

{

if (!dead && other.CompareTag("Explosion"))

{

Debug.Log("P" + playerNumber + " hit by explosion!");

dead = true;

globalManager.PlayerDied(playerNumber);

Destroy(gameObject);

}

}

}

Скрипт Bomb (Бомба) в игре типа Bomberman выполняет роль таймера, активатора и источника воздействия в игровом мире. Он управляет всем жизненным циклом бомбы, от момента ее установки до момента взрыва и распространения урона.

Вот ключевые функции, которые выполняет скрипт Bomb:

1. Тайминг и Инициирование Взрыва

Главная задача бомбы — взорваться через заданное время или по внешнему триггеру.

  • Задержка: В вашем коде используется Invoke("Explode", 3f);. Это устанавливает таймер, который гарантирует, что бомба взорвется через 3 секунды, даже если игрок не предпримет никаких дальнейших действий.
  • Отмена Таймера: Метод OnTriggerEnter позволяет отменить запланированный взрыв (CancelInvoke("Explode")) и взорвать бомбу немедленно, если она столкнулась с уже существующей волной взрыва (например, если одна бомба активирует другую).

2. Создание Визуальных и Звуковых Эффектов

Взрыв должен быть зрелищным и слышимым.

  • Звук: Проигрывается explosionSound (звук взрыва).
  • Визуальный эффект (VFX): Создается префаб взрыва (explosionPrefab), который обычно содержит систему частиц (как вы настраивали в Уроке №4).

3. Логика Распространения Взрыва (Цепная Реакция)

В играх типа Bomberman взрыв не просто происходит в одной точке, он распространяется по прямым линиям, пока не встретит препятствие.

  • Корутина CreateExplosion (или PropagateExplosion в исправленной версии): Эта часть кода отвечает за “распространение” эффекта.Она использует Physics.Raycast для проверки наличия препятствий (levelMask) на определенном расстоянии (i).
    Если препятствие найдено, распространение в этом направлении останавливается (break).
    Если препятствия нет, инстанцируется новый эффект взрыва на следующем участке пути.

4. Управление Состоянием Самого Объекта Бомбы

Поскольку бомба — это временный объект в сцене, скрипт управляет ее окончательным удалением.

  • Скрытие: GetComponent<MeshRenderer>().enabled = false; — бомба становится невидимой, чтобы не мешать зрелищу самого взрыва.
  • Отключение Коллайдера: Отключение коллайдера предотвращает случайное срабатывание триггеров другими объектами.
  • Уничтожение: Destroy(gameObject, 0.3f); — сам объект бомбы удаляется из сцены через короткое время после того, как эффекты взрыва уже созданы.

5. Взаимодействие с Другими Объектами

Бомба влияет на других участников игры.

  • Триггер для Игроков: Коллайдер бомбы, когда она взрывается, должен активировать логику смерти у игроков (через тег "Explosion").
  • Влияние на Другие Бомбы (Цепная реакция): Если взрыв от одной бомбы доходит до другой, он немедленно активирует ее взрыв.

Резюме

Скрипт Bomb — это самостоятельный агент в игре. Он несет ответственность за запуск события взрыва, распространение этого взрыва по правилам игры (через лучи), генерацию звука и VFX, и, наконец, самоуничтожение после завершения своей миссии.

Код из игры Bomb:

using System.Collections;

using UnityEngine;

public class Bomb : MonoBehaviour

{

public AudioClip explosionSound;

public GameObject explosionPrefab;

public LayerMask levelMask;

private bool exploded = false;

private void Start()

{

Invoke("Explode", 3f);

}

private void Explode()

{

AudioSource.PlayClipAtPoint(explosionSound, transform.position);

Instantiate(explosionPrefab, transform.position, Quaternion.identity);

StartCoroutine(CreateExplosion(Vector3.forward));

StartCoroutine(CreateExplosion(Vector3.right));

StartCoroutine(CreateExplosion(Vector3.back));

StartCoroutine(CreateExplosion(Vector3.left));

GetComponent<MeshRenderer>().enabled = false;

exploded = true;

transform.Find("Collider").gameObject.SetActive(false);

Destroy(gameObject, 0.3f);

}

private void OnTriggerEnter(Collider other)

{

if(!exploded && other.CompareTag("Explosion"))

{

CancelInvoke("Explode");

Explode();

}

}

private IEnumerator CreateExplosion(Vector3 direction)

{

for(int i = 1;i < 3; i++)

{

RaycastHit hit;

Physics.Raycast(transform.position + new Vector3(0, 0.5f, 0), direction, out hit, i, levelMask);

if (!hit.collider)

{

Instantiate(explosionPrefab, transform.position + (i * direction), explosionPrefab.transform.rotation);

}

else

{

break;

}

yield return new WaitForSeconds(0.05f);

}

}

}

В контексте Unity и объектно-ориентированного программирования, функция, которая самоуничтожается или удаляет себя из сцены, называется самоуничтожением (self-destruction).

Если в вашем коде есть метод, который называется, например, DestroySelf(), или если вы используете встроенную функцию Unity Destroy(gameObject) внутри самого скрипта, он предназначен для одной основной цели:

Удалить текущий игровой объект (вместе с его компонентами и скриптом), на котором он прикреплен, из памяти и сцены.

В вашем скрипте Bomb эта логика уже реализована:

csharp

// В конце метода Explode():
Destroy(gameObject, 0.3f);

Конкретные причины, по которым нужен Destroy(gameObject) в скрипте Bomb:

  1. Ограниченное Время Жизни (TTL): Бомба — это временный объект. После того как она выполнила свою функцию (взорвалась), она больше не нужна сцене. Если ее не удалить, она будет занимать память и ресурсы.
  2. Визуальное Устранение: Эффекты взрыва (частицы) могут существовать несколько секунд, чтобы выглядеть красиво. Поскольку сам объект бомбы уже не нужен, мы удаляем его немедленно (Destroy(gameObject)), но даем небольшую задержку (0.3f), чтобы прежде всего убедиться, что визуальные эффекты (которые могут быть инстанцированы как отдельные объекты) успеют воспроизвестись.
  3. Очистка Памяти: Удаление объекта из иерархии сцены освобождает память, которую он занимал (MeshRenderer, Collider, Rigidbody и сам скрипт Bomb).

Когда еще используют DestroySelf()?

  • Враги: Враги, которые были уничтожены игроком, самоуничтожаются.
  • Снаряды/Пули: Снаряды, которые попали в цель или улетели слишком далеко.
  • Таймеры: Объекты, которые должны исчезнуть через определенное время (например, выпадающие бонусы, которые не были подобраны).

Итог: В скрипте Bomb, вызов Destroy(gameObject, 0.3f) — это команда объекту бомбы: “После того как пройдет 0.3 секунды, полностью удали себя из игры”.

Код из игры DestroySelf:

using UnityEngine;

public class DestroySelf : MonoBehaviour

{

public float Delay = 3f;

void Start()

{

Destroy(gameObject, Delay);

}

}

Скрипт с названием DisableTriggerOnPlayerExit (Отключить триггер при выходе игрока) почти наверняка служит для управления областью действия (зоной) или поведением, которое должно быть активно только тогда, когда игрок находится внутри определенного объема.

Это очень распространенный шаблон, используемый для реализации:

  1. Зоны активации (Activation Zones): Триггер, который активирует событие (например, запуск катсцены, появление врага, изменение освещения) только когда игрок входит в зону, и отключает это событие, когда игрок выходит.
  2. Зоны с особыми правилами: Зоны, где игрок замедляется, ускоряется, или где ему запрещено ставить бомбы.
  3. Триггеры выхода: В случае с Bomberman это может быть триггер, который, если игрок покидает определенную безопасную зону, запускает таймер или вызывает эффект.

Как это работает (Предполагаемая логика)

Скрипт DisableTriggerOnPlayerExit будет использовать два ключевых метода Unity, связанных с коллайдерами, настроенными как триггеры:

  1. OnTriggerEnter(Collider other):Скрипт проверяет, является ли вошедший объект (other) игроком (например, по тегу "Player").
    Если это игрок, скрипт
    активирует ту функциональность, за которую он отвечает (например, включает MeshRenderer на объекте, который должен быть виден только в этой зоне, или запускает отсчет таймера).
  2. OnTriggerExit(Collider other):Скрипт снова проверяет, покинул ли зону именно игрок.
    Если это игрок, скрипт
    деактивирует ту функциональность, которую он активировал при входе (например, отключает таймер, прячет объект, сбрасывает скорости и т.д.).

Примеры использования в игре типа Bomberman:

  • Зона “Усиления”: Игрок входит в триггер, и на 10 секунд получает возможность ставить бомбы большего радиуса. Когда он выходит, радиус сбрасывается.
  • Зона Смерти (за пределами карты): Если игрок случайно выходит за пределы игровой арены, скрипт на невидимой стене-триггере немедленно вызывает смерть игрока, вместо того чтобы ждать, пока он упадет в пустоту.
  • Переключение камеры: Активация второстепенной камеры, которая следит за боссом, только когда игрок заходит в зону его появления.

Таким образом, DisableTriggerOnPlayerExit — это механизм контроля области, который позволяет логике (триггеру) быть активной или неактивной в зависимости от присутствия игрока в заданном пространстве.

Код из игры DisableTriggerOnPlayerExit:

using UnityEngine;

public class DisableTriggerOnPlayerExit : MonoBehaviour

{

public void OnTriggerExit(Collider other)

{

if (other.gameObject.CompareTag("Player"))

{

GetComponent<Collider>().isTrigger = false;

}

}

}

Это все скрипты, которые использовались в проекте, их можно просто скопировать в свой проект.