Bomberman - это игра, где игроки сражаются, стратегически размещая бомбы по полю, чтобы взорвать друг друга. В этом уроке мы сделаем такую же игру на Unity.
Изучив материалы урока, вы узнаете:
- Сброс бомб и привязка их к квадратикам игрового поля.
- Создание взрывов с помощью лучей, которые ищут пустые квадратики.
- Реакция игрока на взрывы.
- Реакция бомб на взрывы.
- Определение ничьи или победы игрока.
Приступаем к созданию игры на Unity
Скачайте стартовый проект и распакуйте его в любую папку по вашему выбору. Откройте Starter Project в Unity. Ассеты содержатся в следующих папках:
- Animation Controllers
- Содержит контроллер анимации игрока, в том числе логику для анимирования конечностей игроков, когда те шагают. Если вам нужно освежить знания по анимации, изучите наш урок Введение в анимацию в Unity
- Materials
- Содержит материалы для создания блоков
- Models
- Содержит модели игроков, уровней и бомб, также как и их материалы
- Music
- Содержит саундтреки
- Physics Materials
- Соержит физический материал игроков - это специальные виды материалов, которые добавляют физические свойства поверхности. Для этого урока он используется, чтобы позволить игрокам двигаться без усилий arund the level without friction.
- Prefabs
- Содержит префабы бомбы и взрыва
- Scenes
- Содержит игровые сцены
- Scripts
- Содержит начальные скрипты; не забудьте открыть их изучить комментарии к коду
- Sound Effects
- Содержит звуковые эффекты для бомбы и взрыва
- Textures
- Содержит текстуры обоих игроков
Бросаем бомбы в Unity
Если сцена Game еще не открыта, то откройте ее и запустите игру.
Оба игрока могут ходить по карте, используя клавиши W
, A
, S
, D
и клавиши со стрелками. Когда игрок 1 (красный) нажимает на клавишу "пробел" он помещает бомбу у своих ног, игрок 2 должен в состоянии сделать то же самое, нажав на клавишу Enter
. Тем не менее, это пока еще не работает, поэтому откройте скрипт Player.cs в редакторе. Этот скрипт обрабатывает все движения игрока и логику анимации. Он также включает в себя метод, названный DropBomb, который просто проверяет добавлен ли игровой объект bombPrefab:
private void DropBomb() {
if (bombPrefab) { //Проверяет, присвоен ли первым делом префаб бомбы
}
}
Для того, чтобы бомба падала под игроком, добавьте следующую строку внутри if
:
Instantiate(
bombPrefab,
myTransform.position,
bombPrefab.transform.rotation
);
Теперь бомба падает как и задумывалось. Запустите сцену, чтобы убедиться в этом:
Есть небольшая проблема с тем, как сбрасываются бомбы: их можно сбросить абсолютно где угодно, а это создаст некоторые проблемы с подсчетами того, где должны происходить взрывы. Далее в этом уроке мы разберем эту специфику.
Расположение
Следующая наша задача - сделать так, чтобы бомбы располагались прямо посередине ячейки сетки на полу. Каждая плитка на этой сетке равна 1×1, так что эта задача не вызовет у нас затруднений. В Player.cs отредактируйте Instantiate():
Instantiate(
bombPrefab, new Vector3(
Mathf.RoundToInt(myTransform.position.x),
bombPrefab.transform.position.y,
Mathf.RoundToInt(myTransform.position.z)
),
bombPrefab.transform.rotation
);
Mathf.RoundToInt
вызывает переменные позиции игрока x
и z
, округляет все переменные до целочисленных int а затем размещает бомбы ровно на квадратиках:
Запустите игру и попробуйте побегать, разбрасывая бомбы. Они будут появляться ровно на квадратиках:
Создаем взрывы
Нам потребуется новый скрипт:
- Выберите папку Scripts в браузере проекта.
- Нажмите на кнопку Create.
- Выберите C# Script.
- Назовите новый скрипт Bomb.
Теперь прикрепите скрипт Bomb к префабу Bomb:
- В папке Prefabs выберите игровой объект Bomb.
- Нажмите на кнопку Add Component.
- Введите bomb в поле поиска.
- Выберите скрипт Bomb, который вы только что сделали.
И, наконец, откройте скрипт Bomb в редакторе кода. Внутри Start()
добавьте следующую строку кода:
Invoke("Explode", 3f);
Invoke() принимает 2 параметра: первый - имя метода, который вы хотите вызвать, второй - задержка перед вызовом этого метода. В нашем коде мы делаем так, чтобы бомба взрывалась через три секунды. Добавьте следующее в Update():
void Explode() { }
Перед созданием игровых объектов взрыва Explosion GameObjects, вам понадобится публичная переменная типа GameObject, так что вы можете назначить префаб Explosion в редакторе. Добавьте следующий код прямо над Start():
public GameObject explosionPrefab;
Сохраните файл и вернитесь в редактор. Выберите префаб Bomb в папке Prefabs и перетащите префаб Explosion в слот Explosion Prefab:
После того, как вы сделали это, вернитесь в редактор кода. Вы наконец-то написали скрипт, который делает взрывы! Внутри Explode() добавьте следующие строки:
Instantiate(
explosionPrefab,
transform.position,
Quaternion.identity
); //1
GetComponent<MeshRenderer>().enabled = false; //2
transform.FindChild("Collider").gameObject.SetActive(false); //3
Destroy(gameObject, .3f); //4
Этот фрагмент кода выполняет следующие действия:
- Создает взрыв на месте, где была бомба.
- Отключает визуализацию, делая бомбы невидимыми.
- Отключает коллайдер, позволяя игрокам бегать и взрываться.
- Уничтожает бомбу после 0,3 секунды; это гарантирует, что все взрывы будут появляться перед уничтожением GameObject.
Сохраните ваш скрипт Bomb и запустите вашу игру. Положите несколько бомб и согрейтесь в пламени взрыва!
Крутые парни не смотрят на взрывы!
Больше! Взрывов должно быть больше!
Следующим делом надо добавить действия расширения рядов взрывов. Чтобы сделать это, вам нужно создать сопрограмму.
Сопрограмма, по существу, это функция, которая позволяет приостановить выполнение и вернуть управление Unity. На более позднем этапе, выполнение этой функции будет возобновлено с места последней остановки. Люди часто путают с сопрограммы с многопоточностью. Это не одно и тоже: сопрограммы выполняются в том же потоке и возобновляются в заданных промежутках времени. Чтобы узнать больше о сопрограммамах ознакомьтесь с документацией по Unity.
Вернитесь в редактор кода и отредактируйте скрипт Bomb. Под Explode() добавьте новый IEnumerator по имени CreateExplosions:
private IEnumerator CreateExplosions(Vector3 direction) {
return null // текущий плейсхолдер
}
Создание Сопрограммы
Добавьте следующие четыре строки кода между вызовом Instantiate и отключением MeshRenderer в Explode():
StartCoroutine(CreateExplosions(Vector3.forward));
StartCoroutine(CreateExplosions(Vector3.right));
StartCoroutine(CreateExplosions(Vector3.back));
StartCoroutine(CreateExplosions(Vector3.left));
StartCoroutine вызывается перед запуском CreateExplosions IEnumerator
один раз для каждого направления.
Сейчас будет самое интересное. Внутри CreateExplosions() добавьте следующий кусок кода:
//1
for (int i = 1; i < 3; i++) {
//2
RaycastHit hit;
//3
Physics.Raycast(transform.position + new Vector3(0,.5f,0), direction, out hit, i, levelMask);
//4
if (!hit.collider) {
Instantiate(explosionPrefab, transform.position + (i * direction),
//5
explosionPrefab.transform.rotation);
//6
} else {
//7
break;
}
//8
yield return new WaitForSeconds(.05f);
}
Этот фрагмент кода выглядит довольно сложным, но на самом деле довольно просто. Вот, что мы делаем на каждом шаге:
- Повторяет цикл
for
для каждой единицы измерения расстояния, которую вы хотите охватить взрывом. В этом случае взрыв будет достигать двух метров.
- Объект RaycastHit содержит всю информацию о рейкастах Raycast и том, во что они упираются, а во что – нет.
- Эта важная строка кода посылает Raycast от центра бомбы в направлении, пройденном через вызов StartCoroutine Затем она выводит результат объекту RaycastHit object. Параметр
i
3. показывает расстояние, которое должен пройти луч. И, наконец, он использует LayerMask (названный levelMask
) 3. чтобы убедиться, что луч проверяет только блоки на уровне и игнорирует игрока и другие коллайдеры.
- Если Raycast ни во что не попал, то это свободная плитка.
- Создает взрыв там, где отмечается рейкаст.
- Луч Raycast упирается в блок.
- Как только луч Raycast 7. упирается в блок, прекращается выполнение цикла
for
. Это гарантирует, что взрыв не сможет перепрыгивать через стены.
- Ждем 0.05 секунды перед выполнением следующего повторения цикла
for
. Это делает взрыв более убедительным, как будто он расширяется наружу.
Вот как это выглядит в действии:
Красная линия - луч (рейкаст). Он проверяет плитки вокруг бомбы в поисках свободного пространства и, если находит, то порождает взрыв. Когда он попадает на блок, то ничего не происходит и проверка на свободное место в этом направлении прекращается. Теперь вы знаете причину, почему бомбы должны быть привязанным к центру плитки. Если бы бомбы могли находиться где угодно, то в некоторых случаях луч упирался бы в блок и не создавал взрывов, т.к. был бы размещен некорректно:
Добавлям слой маски
В наш скрипт Bomb закралась ошибка — LayerMask еще не существует. Прямо под задекларированной переменной explosionPrefab добавьте дополнительную строку, так чтобы это выглядело следующим образом:
public GameObject explosionPrefab;
public LayerMask levelMask;
A LayerMask выборочно берет слои, используемые с рейкастами. В этом случае нужно выбирать только блоки, чтобы лучи не зацепляли что-либо другое. Сохраните скрипт бомбы и вернитесь в редактор Unity. Кликните по кнопке Layers правом верхнем углу, а затем выберите Edit Layers…
Кликните на текстовое поле рядом с User Layer 8 и введите Blocks
. Это определяет новый слой, который вы можете использовать.
Внутри вкладки Иерархия выберите игровой объект Blocks.
Измените слой для вновь созданного слоя Blocks:
Когда появится диалоговое окно Change Layer, нажмите на кнопку "Yes, change children", чтобы применить изменения ко всем желтым блокам, разбросанным по карте.
И наконец, выберите префаб Bomb в папке Prefabs во вкладке project и измените Level Mask на Blocks.
Запустите сцену снова и бросьте несколько бомб. Смотрите как ваши взрывы распространяются вокруг блоков:
Цепные реакции
Когда взрыв от одной бомбы коснется другой, она должна взорваться - эта особенность делает игру более стратегической и захватывающей.
Откройте скрипт Bomb.cs в редакторе кода. Добавьте новый метод OnTriggerEnter после CreateExplosions():
public void OnTriggerEnter(Collider other) { }
Здесь OnTriggerEnter - предопределенный метод в MonoBehaviour, который вызывается при столкновении коллайдера триггера и rigidbody (жесткого тела). Параметр Collider по имени other, это коллайдер игрового объекта (GameObject) в триггере.
Нам нужно проверить сталкивающийся объект и сделать так, чтобы бомба разрывалась при взрыве. Для начала, нам нужно знать взорвалась ли бомба. Сперва нужно объявить переменную exploded
, поэтому добавьте следующий код сразу под объявлением переменной levelMask:
private bool exploded = false;
Внутри OnTriggerEnter() добавьте следующий фрагмент кода:
if (!exploded && other.CompareTag("Explosion")) { // 1 & 2
CancelInvoke("Explode"); // 2
Explode(); // 3
}
Он делает три вещи:
- Проверяет взорвалась ли бомба.
- 2. Проверяет, присвоен ли коллайдеру триггера тег Explosion.
- Отменяет уже вызванный Explode путем «сброса» бомбы — если этого не сделать, бомба может взрываться дважды.
- Взрыв!
Теперь у нас есть переменная, но помимо этого нужно чтобы она где-нибудь изменялась. Наиболее логичное место для изменения значения переменной - внутри Explode(), сразу же после отключения компонента MeshRenderer:
...
GetComponent<MeshRenderer>().enabled = false;
exploded = true;
...
Теперь все настроено, поэтому сохраните файл и запустите сцену снова. Бросьте несколько бомб рядом друг с другом и посмотрете, что происходит:
Теперь у нас есть кое-что по-настоящему взрывное! Один маленький взрыв может добавить игре огня, ведь он также детонирует другие бомбы, создавая самую настоящую цепную реакцию! Осталось лишь разобраться с реакцией игроков на взрывы (ее едва ли можно назвать положительной) и тем, как в игре определяется исход.
Смерть игрока и что с ней делать
Откройте скрипт Player.cs в вашем редакторе кода. Сейчас у нас нет ни одной переменной, указывающей жив игрок или нет, поэтому добавьте логическую переменную в верхней части скрипта, прямо под переменной canMove:
public bool dead = false;
Эта переменная используется для отслеживания умер ли игрок в результате взрыва. Теперь добавьте эту строку выше всех остальных объявлений переменных:
public GlobalStateManager GlobalManager;
Это ссылка на скрипт GlobalStateManager, который получает уведомление обо всех умерших игроках и определяет победителя.
Внутри OnTriggerEnter() уже есть проверка того, задело ли игрока взрывом, но пока что она лишь записывает это в окне консоли.
Добавьте этот фрагмент кода под вызовом Debug.Log:
dead = true; // 1
GlobalManager.PlayerDied(playerNumber); // 2
Destroy(gameObject); // 3
Этот фрагмент кода делает следующие вещи:
- Устанавливает переменную dead, чтобы можно было следить за смертью игрока.
- Уведомляет глобального мереджера состояния, что игрок умер.
- Уничтожает игровой объект игрока (GameObject).
Сохраните этот файл и вернуться в редактор Unity. Вам нужно связать GlobalStateManager с обоими игроками:
- В окне иерархии, выберите оба игровых объекта Player.
- Перетащите игровой объект Global State Manager в слот Global Manager.
Запустите сцену снова и убедитесь, что один из игроков стерт с лица земли взрывом.
Каждый игрок, который встает на пути взрыва погибает мгновенно. Сама игра не знает, кто выиграл, т.к. GlobalStateManager еще не использует получаемую информацию. Пришло время изменить эту ситуацию.
Объявление победителя
Откройте GlobalStateManager.cs в редакторе кода. Чтобы GlobalStateManager следил, умер(ли) ли игрок(и), Вам понадобятся две переменные. Добавьте их в верхней части скрипта выше PlayerDied():
private int deadPlayers = 0;
private int deadPlayerNumber = -1;
Прежде всего, deadPlayers будет хранить число умерших игроков. Переменная deadPlayerNumber устанавливается, как только первый игрок умирает, а также она показывает, какой это был игрок.
Теперь можно добавлять уже саму логику. В PlayerDied() добавьте следующий кусок кода::
deadPlayers++; // 1
if (deadPlayers == 1) { // 2
deadPlayerNumber = playerNumber; // 3
Invoke("CheckPlayersDeath", .3f); // 4
}
Этот фрагмент кода выполняет следующие действия:
- Добавляет умершего игрока.
- Если умер первый игрок...
- Присваивает первому умершему игроку.
- Через 0.3 секунды проверяет, умер только один игрок или оба.
Эта последняя задержка необходима для проверки ничьейного результата игры. Если бы вы проверяли сразу же, вы не могли бы видеть, что все умерли. 0,3 секунды достаточно, чтобы определить остались ли живые игроки.
Победа, Поражение, или Ничья
Вы близки концу как никогда! Нам осталось создать логику выбора между выигрышем и ничьей! Создайте новый метод по имени CheckPlayersDeath в скрипте GlobalStateManager:
void CheckPlayersDeath() {
// 1
if (deadPlayers == 1) {
// 2
if (deadPlayerNumber == 1) {
Debug.Log("Игрок 2 победил!");
// 3
} else {
Debug.Log("Игрок 1 победил!");
}
// 4
} else {
Debug.Log("Игра закончилась вничью!");
}
}
Логика разных операторов if
в этом методе:
- Единственный игрок в игре умер и он в проигрыше.
- Игрок 1 умер, игрок 2 победил.
- Игрок 2, игрок 1 победил.
- Оба игрока умерли - ничья.
Сохраните код, запустите игру еще раз и проверьте, показывается ли в консоли, кто выиграл, либо ничейный результат:
Скачайте итоговый результат работы над Bomberman-игры на Unity и побросайтесь бомбами).
Теперь вы знаете, как сделать Bomberman-игру с помощью Unity. В этом руководстве использовальсь системы частиц для бомбы и взрыва. Если вы хотите узнать больше о системах частиц, загляните в статью Введение в Unity: Система частиц.
В заключение, я настоятельно рекомендую вам самостоятельно продолжить работу над этой игрой, добавляя новые функции, например:
- Сделать бомбы "толкаемыми", так что вы можете спастись от бомбы рядом с вами толкнув ее к сопернику
- Ограничьте количество бомб, которые могут быть брошены
- Сделайте чтобы можно было легко и быстро перезагружать игру
- Добавьте блоки, которые разрушаются от взрывов
- Создайте интересные бонусы
- Добавьте параметр "жизнь" и способ поправлять здоровье
- Элементы пользовательского интерфейса, показывающие, что игрок выиграл
- Найдите способ позволить играть в игру большему количеству игроков