Игра “Snake” на Unity3D (The game “Snake” on Unity3D)

Автор: | 27.01.2019

Архитектура проекта
Подготовка ресурсов
Сцена MainMenu
Сцена GameOver
Сцена Level

Скрипт LookAt
Скрипт Tail
Скрипт Player
Скрипт Game

Контрольное задание

Архитектура проекта

Сюжет игры и ее программная реализация на языке Java рассмотрены в разделе Игровое приложение ”Snake”.   Также см.  Snake Game with OpenCV Python  and the Python code.

Ниже рассмотрено, как можно создать эту игру в Unity 3D.

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

  1. Главное меню
  2. Игра на определенном уровне
  3. Меню «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

В этой папке будет сохраняться префаб яблока. Последовательность создания префаба:

  1. Создать сферу в нуле координат.
  2. Перетащить на сферу материал FoodMaterial2.
  3. Выделить сферу и добавить компонент RigidBody (Component -> Physics -> Rigidbody). Установить ему галочку Is Kinematic (это нужно, чтобы не было лишней нагрузки на физику).
  4. Перетащить объект Shpere из окна Hierarchy в  папку Prefabs в окне Project и там переименовать его в Food.
  5. Заготовку для префаба еды (объект 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 при съедании каждого яблока растет хвост змейки. Дополнить игру, чтобы прибавлялся элемент хвоста (кубик) при съедании каждого яблока.

Полезные ссылки:

 

Автор: Николай Свирневский