Архитектура проекта
Подготовка ресурсов
Сцена MainMenu
Сцена GameOver
Сцена Level
Скрипт LookAt
Скрипт Tail
Скрипт Player
Скрипт Game
Архитектура проекта
Сюжет игры и ее программная реализация на языке Java рассмотрены в разделе Игровое приложение ”Snake”. Также см. Snake Game with OpenCV Python and the Python code.
Ниже рассмотрено, как можно создать эту игру в Unity 3D.
Прежде всего, разобьем игру на модули, которые выполняют свои задачи. Выделим следующие три состояния игры:
- Главное меню
- Игра на определенном уровне
- Меню «Game Over!»
Каждое состояние реализуем с помощью игровых сцен. Т.е., при запуске игры будет загружена игровая сцена с главным меню. При нажатии на кнопку Play будет загружена сцена с игрой. При столкновении змейки со стеной будет загружена на несколько секунд сцена Game Over, а потом снова сцена с главным меню.
Сцена «Главное меню» должна выполнять функции создания меню и обрабатывать нажатия на кнопки.
Сцена «Игра» содержит следующие составляющие:
- уровень с препятствиями (генерируется при старте уровня);
- подсчет и отображение очков;
- змейку, управляемую игроком;
- еду, которая произвольно ставится на уровне.
За змейкой будет тянуться хвост из трех кубиков.
Сцена «Game Over!» будет отображать надпись «Game Over» по центру экрана несколько секунд и загружать сцену главного меню.
Вначале создадим необходимые в проекте ресурсы.
Подготовка ресурсов
Дополнительно к папке Scenes создайте в проекте еще 3 папки (Assest->Create->Folder) — Materials, Prefabs и Scripts.
Папка Materials
В этой папке создайте материалы (Assest->Create->Material) однотонного цвета:
- FoodMaterial – материал красного цвета для яблока;
- SnakeEye – материал черного цвета для глаз змейки;
- SnakeHead – материал желтого цвета для головы змейки;
- WallMaterial – материал пурпурного цвета для стен.
Цвет материала определяется в окне Color, которое вызывается из окна Inspector кнопкой рядом с изображением пипетки.
Папка Prefabs
В этой папке будет сохраняться префаб яблока. Последовательность создания префаба:
- Создать сферу в нуле координат.
- Перетащить на сферу материал FoodMaterial2.
- Выделить сферу и добавить компонент RigidBody (Component -> Physics -> Rigidbody). Установить ему галочку Is Kinematic (это нужно, чтобы не было лишней нагрузки на физику).
- Перетащить объект Shpere из окна Hierarchy в папку Prefabs в окне Project и там переименовать его в Food.
- Заготовку для префаба еды (объект Shpere) можно удалить со сцены (еда будет создаваться на основе префаба Food в скрипте Food).
Папка Scenes
Сцену, в которой создавали заготовку для префаба еды, сохраните под именем Level в папке Scenes. Создайте и сохраните там же еще две сцены: MainMenu и GameOver.
Откройте окно Build Settings (File -> Build Settings…) и перетащите все созданные сцены в окно Build Settings. Нулевой сценой сделайте MainMenu, т.к. с нее будет начинаться игра. Закройте окно.
Папка Scripts
Создайте и сохраните в этой папке C# скрипты (пока еще пустые):
- Game – скрипт игры, подсчета очков и подготовки уровня.
- LoadLevelByTimer – скрипт для перехода из сцены GameOver в сцену MainMenu.
- LookAt – скрипт будет ориентировать камеру на змейку.
- MainMenu – скрипт для отрисовки главного меню.
- Player – скрипт, управляющий змейкой.
- Tail – скрипт частей хвоста.
Итак, у нас есть все ресурсы, но их еще надо настроить.
Сцена MainMenu
Открываем сцену MainMenu (DblClick на имени сцены в окне Assets->Scenes). Открываем скрипт MainMenu.cs на редактирование (DblClick на имени скрипта в окне Assets->Scripts), открывается VS редактор с заготовкой кода. Меняем код на следующий:
using UnityEngine; public class MainMenu : MonoBehaviour { // Размер меню public Vector2 menuSize = new Vector2(500, 300); // минимальная высота кнопки public float buttonMinHeight = 60f; // шрифт заголовка public Font captionFont; // шрифт кнопок public Font buttonFont; // тексты меню public string mainMenuText = "Main menu"; public string startButtonText = "Start game"; public string exitButtonText = "Exit"; public void OnGUI() { // рассчитываем прямоугольник по центру экрана Rect rect = new Rect( Screen.width / 2f - menuSize.x / 2, Screen.height / 2f - menuSize.y / 2, menuSize.x, menuSize.y); // область меню GUILayout.BeginArea(rect, GUI.skin.textArea); { // создаем стиль заголовка GUIStyle captionStyle = new GUIStyle(GUI.skin.label); // устанавливаем стиль заголовка шрифт captionFont captionStyle.font = captionFont; // Рассположение текста по центру captionStyle.alignment = TextAnchor.MiddleCenter; captionStyle.fontSize = 70; // текст заголовка GUILayout.Label(mainMenuText, captionStyle); // создаем стиль кнопки GUIStyle buttonStyle = new GUIStyle(GUI.skin.button); // устанавливаем стилю кнопки шрифт buttonFont buttonStyle.font = buttonFont; // отступы кнопок от краев buttonStyle.margin = new RectOffset(20, 20, 3, 3); buttonStyle.fontSize = 40; // FlexibleSpace - автоматически рассчитанное место для // заполнения пустого пространства между элементами GUILayout.FlexibleSpace(); // отрисовка кнопки Start и обработка ее нажатия if (GUILayout.Button(startButtonText, buttonStyle, GUILayout.MinHeight(buttonMinHeight))) { // загрузка сцены с именем Level Application.LoadLevel("Level"); } GUILayout.FlexibleSpace(); // отрисовка кнопки Exit и обработка ее нажатия if (GUILayout.Button(exitButtonText, buttonStyle, GUILayout.MinHeight(buttonMinHeight))) { // выход Application.Quit(); } GUILayout.FlexibleSpace(); } GUILayout.EndArea(); } }
Сохраните скрипт, перейдите в Unity и перетащите скрипт на камеру. Компонент меню камеры будет выглядеть следующим образом:
Подключаем скрипт и нажимаем Play. Видим следующий результат:
Сцена GameOver
Открываем сцену GameOver, создаем объект Text (GameObject->UI->Text), пишем ему текст «Game Over!», задаем положение (Pos…) в точке (0,0,0), устанавливаем размер шрифта (FontSize = 24). Запускаем проект
Перетаскиваем скрипт LoadLevelByTimer на камеру и заполняем его следующим кодом:
using UnityEngine; using System.Collections; public class LoadLevelByTimer : MonoBehaviour { // время до загрузки уровня public float delay = 3; // имя загружаемого уровня public string levelName; // типа IEnumerator из простр. имен System.Collections. // для поддержки функцией Start механизма сопрограмм public IEnumerator Start() { // задержка на заданное число секунд yield return new WaitForSeconds(delay); // загрузка уровня с указанным именем Application.LoadLevel(levelName); } }
Подключаем скрипт к Unity и в окне Inspector инициализируем значения открытых переменных скрипта. Переменная Delay определяет время задержки (3 сек) сцены GameOver. Переменная LevelName определяет имя подгружаемой сцены — «MainMenu».
Сохраняем сцену и запускаем ее (Play). Через заданное время (3 сек) после загрузки сцены GameOver загружается сцена с именем MainMenu.
Сцена Level
Эта сцена самая сложная, т.к. в ней происходит игра. В сцене у нас присутствуют следующие элементы:
- Камера – смотрит на змейку, следовательно, ей нужно дать это поведение.
- Змейка – передвигается, врезается в объекты и, в зависимости от объекта, выбирает действие. Если врезалась в еду, то еду надо съесть. Если врезалась в другое препятствие, то игрок проиграл — надо загрузить уровень GameOver.
- Еда – появляется в любом свободном месте в пределах уровня. Она может быть съедена.
- Препятствия уровня, и бортики. Бортики можно создать заранее, а препятствия должны генерироваться.
Каждый из 4-х бортиков (объекты Cube) ставится на расстоянии 50 единиц от центра. Подбирается соответствующий масштаб по осям.
На бортики перетащите материал WallMaterial.
Разверните камеру. Должно получиться примерно, как на следующей картинке:
Создайте в сцене сферу с координатами 0,0,0 и масштабом 2,2,2. Это будет голова змейки. Добавьте глаза (сферы), чтобы они смотрели в направлении оси Z головы. Сделайте глаза дочериными (child objects) к голове, подберите масштаб. Назначьте соответствующие материалы голове и глазам. У глаз и у головы удалите компонент Collider. Голове назначьте компонент CharacterController (Component->Physics->CharacterController), с помощью этого компонента мы будем двигать змейку, и она не будет проходить сквозь препятствия. Поверните голову (ось Z сферы) навстречу камере — на 180 градусов вокруг оси Y.
При запуске сцены будет следующий вид:
Скрипт LookAt
На камеру повесьте скрипт LookAt. Скрипт камеры очень простой, каждый кадр вызывает функцию ориентации камеры на цель, если цель существует.
using UnityEngine; public class LookAt : MonoBehaviour { // цель, на которую должен смотреть объект public Transform target; public void Update() { if (target != null) { // Смотрим всегда на цель transform.LookAt(target); } } }
В VS подключите скрипт и перейдите в Unity3D. У скрипта камеры появится параметр Target, на который надо перетащить голову змейки. Таким образом, камера будет следить за змейкой. Чтобы это проверить переместите голову за пределы бортиков ближе к камере и запустите сцену (Play). Голова останется в центре поля наблюдения камерой.
Скрипт Tail
Поведение хвоста следующее:
- хвост должен следовать за головой (целью) на установленном расстоянии;
- если хвост ближе указанного расстояния к цели – он не двигается;
- если дальше, то поворачивается на цель и перемещается так, чтобы сделать расстояние равным установленному.
Хвост пока определим только одним кубиком (объект Cube). Установите для него Y=0. Повесьте скрипт Tail на кубик-хвост и заполните скрипт следующим кодом:
using UnityEngine; public class Tail : MonoBehaviour { public Transform target; public float targetDistance; public void Update() { // направление на цель Vector3 direction = target.position - transform.position; // дистанция до цели float distance = direction.magnitude; // если расстояние до цели хвоста больше заданного if (distance > targetDistance) { // двигаем хвост transform.position += direction.normalized * (distance - targetDistance); // смотрим на цель transform.LookAt(target); } } }
У скрипта кубика появятся параметр Target и параметр Target Distance. Первый параметр инициализируется перетаскиванием к нему головы змейки, для второго задается значение 3.
Протестируйте код, переместив перед запуском сцены голову в какую-либо точку сцены — отдельно от хвоста.
Как видим из рисунка после запуска сцены хвост (кубик) переместился вслед за головой.
После тестирования кубик-хвост удалите. Хвост будет создаваться программно из 3-х кубиков в скрипте Player.
Скрипт Player
Перетащите ранее созданный префаб Food на сцену, установите для него положение y = 0, координаты x и z в пределах поля между бортиками. Змейке на «голову» повесьте скрипт Player со следующим кодом:
using UnityEngine; // скрипту игрока необходим на объекте компонент CharacterController // с помощью этого компонента будет выполняться движение [RequireComponent(typeof(CharacterController))] public class Player : MonoBehaviour { // скорость перемещения - 6 единиц в секунду по умолчанию // в редакторе можно поменять public float speed = 6; // аналогично скорость вращения 60 градусов в секунду по умолчанию public float rotationSpeed = 60; // локальная переменная для хранения ссылки на компонент CharacterController private CharacterController _controller; public void Start() { // получаем компонент CharacterController и // записываем его в локальную переменную _controller = GetComponent<CharacterController>(); // создаем хвост // current - текущая цель элемента хвоста, начинаем с головы Transform current = transform; for (int i = 0; i < 3; i++) { // создаем примитив куб и добавляем ему компонент Tail Tail tail = GameObject.CreatePrimitive(PrimitiveType.Cube).AddComponent<Tail>(); // помещаем "хвост" за "хозяином" tail.transform.position = current.transform.position - current.transform.forward * 2; // ориентация хвоста как ориентация хозяина tail.transform.rotation = transform.rotation; // элемент хвоста должен следовать за хозяином, поэтому передаем ему ссылку на его tail.target = current.transform; // дистанция между элементами хвоста - 2 единицы tail.targetDistance = 2; // удаляем с хвоста коллайдер, так как он не нужен //Destroy(tail.collider); Destroy(tail.GetComponent<Collider>()); // следующим хозяином будет новосозданный элемент хвоста current = tail.transform; } } private bool _testing = false; public void Update() { /* * Гибкий способ - использовать оси * Unity имеет набор предустановленных осей, которые можно использовать * следующий код будет работать как на клавиатуре (стрелки и WSAD), так и на геймпаде */ // получаем значение вертикальной оси ввода /* float vertical = Input.GetAxis("Vertical"); */ // получаем значение горизонтальной оси ввода float horizontal = Input.GetAxis("Horizontal"); // вращаем трансформ вокруг оси Y transform.Rotate(0, rotationSpeed * Time.deltaTime * horizontal, 0); // движение выполняем с помощью контроллера в сторону, куда смотрит трансформ игрока // двигаем змею постоянно _controller.Move(transform.forward * speed * Time.deltaTime /* * vertical*/); } GameObject food; // В данную функцию будут передаваться все объекты, с которыми // CharacterController вступает в столкновения public void OnControllerColliderHit(ControllerColliderHit hit) { if (hit.collider.gameObject.name == "Food") { // прибавляем очки еды к общему числу очков //Game.points += 10; //Врезались в еду, "съедаем" ее и создаем новую в пределах поля. //На самом деле перемещаем еду в Random положение food = GameObject.Find("Food"); //Destroy(food); var pos = new Vector3(Random.Range(-40, 41), 0, Random.Range(-40, 41)); food.transform.position = pos; } else { //врезались не в еду Application.LoadLevel("GameOver"); } } }
Запускайте игру, вот что получилось.
Змейка кроме головы имеет 3 элемента хвоста. Она двигается вперед и меняет направление движения под управлением клавиш влево/вправо (или же a/d).
При столкновении с яблоком змейка его съедает. При этом появляется новое яблоко в произвольном месте поля, игра продолжается.
При столкновении змейки с бортиками игра заканчивается — появляется сообщение «GameOver», а через 3 сек — MainMenu.
В скрипте код подсчета очков, пока закомментирован //Game.points += 10; , поскольку используются данные скрипта Game. Раскомментируйте его, когда подключите этот скрипт.
Скрипт Game
Теперь нам надо где-то хранить количество очков, которое набрала змейка за уровень, и их отображать. За это будет отвечать скрипт Game.
using UnityEngine; public class Game : MonoBehaviour { // материал стен public Material wallMaterial; // набранные очки public static int points; // количество стен в уровне public int countWals = 10; private string _pointsString; private int _lastPonts = -1; // генерируем уровень при загрузке сцены public void Awake() { // обнуляем очки points = 0; // генерируем уровень GenerateLevel(); } public void Update() { // обновление текста очков только при их изменении if (_lastPonts == points) return; _lastPonts = points; // очки в формате четырех цифр, начинающихся с нулей _pointsString = "Score: "+ points.ToString("0000"); } // отрисовка набранных очков public void OnGUI() { GUI.color = Color.yellow; GUI.Label(new Rect(20, 20, 200, 20), _pointsString ?? ""); } // функция генерации уровня private void GenerateLevel() { for (int i = 0; i < countWals; i++) { // создаем куб GameObject wall = GameObject .CreatePrimitive(PrimitiveType.Cube); // называем его "Wall" wall.name = "Wall"; // увеличиваем его габариты wall.transform.localScale = new Vector3(2,2,2); // расставляем его так, чтобы координаты были не в центре поля var pos = new Vector3(Random.Range(-40, 41), 0, Random.Range(-40, 41)); while (Mathf.Abs(pos.x) < 10 || Mathf.Abs(pos.z) < 10) { pos = new Vector3(Random.Range(-40, 41), 0, Random.Range(-40, 41)); } wall.transform.position = pos; // и назначаем материал //wall.renderer.material = wallMaterial; wall.GetComponent<Renderer>().material = wallMaterial; } } }
Теперь надо куда-то повесить скрипт Game. Для таких скриптов, которые являются общими, обычно создается объект со специальным именем, чтобы его было легче найти. Создайте пустой объект и назовите его GameManager. На него и повесьте скрипт Game.
Запускайте игру, вот что получилось:
Контрольное задание:
В классической постановке игры Snake при съедании каждого яблока растет хвост змейки. Дополнить игру, чтобы прибавлялся элемент хвоста (кубик) при съедании каждого яблока.
Полезные ссылки:
Автор: Николай Свирневский