Уроки:Змейка Scaven'а на Unity

Материал из Blitz3D to Unity3D Wiki Project
Перейти к: навигация, поиск

Вступление

Цель данного урока - осветить основные принципы программирования в Unity3D, а так же показать отличия в методе программирования между платформами Blitz3D и Unity3D.

Основой данного урока является известный всем пользователям Blitz3D урок Scaven'a Первая 3Д игра

Многие привыкли, что большинство вещей в проекте на Blitz3D надо делать (программировать) руками, в Unity3D это не так. Unity3D имеет готовый редактор сцен (уровней/карт - это в принципе название одного и того же), который позволяет расставлять объекты визуально, не прибегая к подбору координат при загрузке модели в коде. Можно собрать целый уровень, например в 3ds Max, загрузить в Blitz3D как отдельную модель, но разобрать уровень на составляющие придется все равно руками (кодом), находя объекты по именам и создавая игровые объекты. Unity позволяет сделать уровень, наполненный уже готовыми игровыми объектами непосредственно в редакторе. За эту возможность Unity часто называют конструктором, но это не так. Да, можно создавать игровые объекты, расставлять в нужные места, настраивать параметры, добавлять объектам поведение - конструировать игровые уровни. Но есть несколько но:

  • Чтобы настраивать параметры объектам - эти параметры надо создать. Unity имеет набор стандартных компонентов, которые позволят настроить физику, материалы объектам, но Вы ничего не сделаете по части игровой логики, не прибегая к программированию. Да есть набор стандартных скриптов, например First Person Controller (контроллер от первого лица), но он даст возможность только побегать по уровню, и ничего более.
  • Поведение объектам добавляют компоненты, выполняющие отдельные задачи. Некоторые Вы найдете в стандартной поставке (например Mouse Look - компонент управления камерой при помощи мышки, который используется в FPS контролере), но все остальное Вам придется программировать самостоятельно.

Чем отличается программирование в Unity от Blitz'a

  • Первое на что хочется обратить внимание - это язык. Unity дает возможность писать скрипты на трех языках: Boo (Python для .Net), Unity Script (вариация Java Script под .Net, разработанная Unity Technologies для Unity) и C# - современный быстроразвивающийся язык программирования, разработанный компанией Microsoft. На каком из этих языков программировать - решать Вам. Для данного ресурса выбран язык C#, как самый гибкий и имеющий больше перспектив, чем остальные языки. Так же на нем больше всего примеров, которые можно найти в сети.
  • Второе - в Unity нет явного главного цикла приложения, в котором Вы привыкли писать функции UpdateWorld, RenderWorld, UpdateGame и т.д. Это непривычно, но это дает большую гибкость разработки приложений и их масштабируемость.
  • В Unity нет твининга - технологии позволяющей делать интерполяции между положениями и ориентациями объектов между обновлениями мира. Эта технология хорошо распространена у пользователей Блица, но многие не до конца понимают как она работает. Теперь это не страшно, т.к. ее больше не будет :). Работу с объектами придется выполнять с учетом прошедшего времени.
  • В Unity принят модульный принцип программирования, который позволяет использовать написанные модули (компоненты) для конструирования игровых объектов.

Разработка архитектуры игры

Итак, руководствуясь тем, чего я написал выше приступим к разработке змейки, написанной ранее Scaven'ом на Блице в качестве урока, который читали практически все, кто программировал на Блице. Я не буду писать урок похожим образом - добавляя по команде в скрипты, постепенно их модифицируя. Я надеюсь Вы уже у умеете вставлять строчки в нужные места, Вам необходимо понять только принцип программирования. Еще я надеюсь, что вы прочитали мою статью C чего начать?, не важно, поняли ли Вы там что-то или нет. И походу прочитаете статью Как писать скрипты, чтобы лучше понять принципы создания компонентов игры.


Прежде всего, перед началом самого программирования мы разобьем игру на модули, которые выполняют свои задачи. Это позволит в дальнейшем легче понять как работает Unity и принципы создания приложений в ней.

Я предлагаю выделить следующие три состояния игры:

  1. Главное меню
  2. Игра
  3. Экран Game Over!

Каждое состояние мы реализуем с помощью игровых сцен. Т.е. при запуске игры будет загружена игровая сцена с главным меню. При нажатии на кнопку Play будет загружена сцена с уровнем. При врезании змейки в стену будет загружена на несколько секунд сцена Game Over, а потом снова сцена с главным меню.

Сцена Главное меню должно выполнять функции отрисовки меню и обрабатывать нажатия на кнопки. Для отрисовки меню средствами GUI мы напишем специальный компонент.

Сцена Игра содержит следующие составляющие:

  • Уровень с препятствиями, который генерируется при старте уровня.
  • Подсчет и отображение очков.
  • Змейку, управляемую игроком.
  • Еду, которая произвольно ставится на уровне и которую ест змейка.

Дополнительно:

  • Еда будет вращаться
  • За змейкой будет тянуться хвост из трех кубиков.


Сцена Game Over! будет отображать надпись "Game Over" по центру экрана несколько секунд и загружать сцену главного меню.


Разработка игры

Я надеюсь Вы прочитали мою статью С чего начать? и уже можете создавать ресурсы в проекте.

Нам понадобятся следующие ресурсы:

Snake Resources.png


  • Папка Fonts содержит два импортированных шрифта. Точнее это один и тот же шрифт, но двух размеров. Для того чтобы их импортировать, просто перетащите любой ttf-шрифт в проект. Если Вы хотите импортировать шрифты с русскими буквами - указывайте в настройках шрифта Character->UNICODE.
  • Папка Materials
    • FoodMaterial - материал зеленого цвета для листьев яблока (еды)
    • FoodMaterial2 - материал красного цвета для самого яблока (еды)
    • SnakeEye - материал черного цвета для глаз змейки
    • SnakeHead - материал желтого цвета для головы змейки
    • WallMaterial - материал для стен (пурпурного цвета)
  • Папка Resources
    • Подпапка Prefabs
      • Food - префаб еды. Сделать его можно следующим образом:
        1. Создать сферу в нуле координат
        2. Перетащить на сферу материал FoodMaterial2
        3. Добавить сфере компонент RigidBody и установить ему галочку Is Kinematic (это нужно, чтобы не было лишней нагрузки на физику, когда мы будем вращать еду)
        4. Создать куб, сделать его дочерним к сфере, удалить Box коллайдер, подобрать масштаб, чтобы было похоже на лист, перетащить на него материал FoodMaterial
        5. Сделать две копии куба (Ctrl + D), расположить два новых "листа" как Вам нравится.
        6. Должно получиться нечто вроде:
          Snake Food.png
        7. Перетащить объект Shpere из окна Hierarchy на созданный заранее префаб Food в окне Project.
        8. Заготовка для префаба еды готова, можно ее удалить из сцены пока.
  • Папка Scenes - создайте три сцены и сохраните под именами MainMenu, Level и GameOver. К ним мы еще вернемся.
    Создав все три сцены перейдите в окно Build Settings (File -> Build Settings...) и перетащите все созданные сцены в окно Scenes In Build. Нулевой сценой сделайте MainMenu, т.к. с нее будет начинаться игра.
    BuildSettings.png
  • Папка Scripts - создайте в ней скрипты C# со следующими именами:
    • Food - скрипт поведения еды.
    • Game - скрипт игры, будет выполнять задачи подсчета очков, подготовки уровня.
    • LoadLevelByTimer - скрипт, загружающий уровень с определенным именем через заданный промежуток времени. Его мы будем использовать для перехода из окна GameOver в главное меню.
    • LookAt - скрипт камеры уровня, будет ориентировать камеру на змейку.
    • MainMenu - скрипт будет выполнять отрисовку главного меню.
    • Player - скрипт, управляющий змейкой
    • Tail - скрипт частей хвоста.


Итак у нас есть все ресурсы, но их еще надо настроить.

Сцена MainMenu

Открываем пока пустую сцену MainMenu, перетаскиваем скрипт MainMenu.cs на камеру (которая в сцене по умолчанию). Можно конечно сделать отдельный объект в сцене для скрипта MainMenu.cs, но в этом нет нужды. Меню мы будем рисовать с помощью средств GUI и возможности авторасчетов размеров элементов управления с помощью GUILayout.

Разработка скрипта MainMenu.cs

Открываем скрипт MainMenu.cs на редактирование (щелкаем по нему два раза) и приводим к следующему виду:

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";

Выполнение отрисовки выполняется в функции OnGUI

    public void OnGUI()
    {
       // тут отрисовка
    }

То что у нас получилось на данный момент:

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()
    {
        // тут отрисовка
    }
}

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

Snake MenuParameters.png

Отрисовка меню

Приступим к отрисовке. Добавтье в метод 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);
        {
            // фигурные скобочки тут стоят для более лучшей организации кода
        }
        GUILayout.EndArea();

Данный код выполнит отрисовку заднего фона меню по центру экрана используя заданный размер.

Весь следующий код отрисовки меню будем писать в промежутке между вызовами функций GUILayout.BeginArea() и GUILayout.EndArea().

Внутри данной области GUILayout может автоматически рассчитать некоторые параметры расположения элементов управления. Например чтобы расположить заголовок по центу можно воспользоваться стилем label.

            // создаем стиль заголовка на основе стиля label стандартного скина
            GUIStyle captionStyle = new GUIStyle(GUI.skin.label);
            // устанавливаем стилю заголовка шрифт captionFont
            captionStyle.font = captionFont;
            // Рассположение текста по центру
            captionStyle.alignment = TextAnchor.MiddleCenter;

            // текст заголовка (отрисовка)
            GUILayout.Label(mainMenuText, captionStyle);

Сохраните скрипт и посмотрите что получилось. Для этого назначьте шрифты параметрам меню перетащив нужный шрифт на нужное поле и нажмите кнопку Play.

Snake MainMenu Title.png

Отрисовка кнопок похожа на отрисовку текста, но функция отрисовки кнопки возвращает так же результат - нажал ли пользователь на кнопку или нет. Поэтому мы можем поместить отрисовку кнопки в условие и выполнить действие при нажатии.

Далее мы создадим стиль для кнопок с указанием шрифта и отступов от края и нарисуем две кнопки с автоматически рассчитанным пространством между ними с указанием размера кнопок.

           // создаем стиль кнопки на основе стиля button стандартного скина
            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
            // устанавливаем стилю кнопки шрифт buttonFont
            buttonStyle.font = buttonFont;
            // отступы кнопок от краев
            buttonStyle.margin = new RectOffset(20, 20, 3, 3);


            // 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(); // динамическое пространство между кнопкой Exit и низом области меню

Результат работы:

Snake MainMenu Final.png

Финальная версия скрипта MainMenu.cs
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);
        {
            // создаем стиль заголовка на основе стиля label стандартного скина
            GUIStyle captionStyle = new GUIStyle(GUI.skin.label);
            // устанавливаем стилю заголовка шрифт captionFont
            captionStyle.font = captionFont;
            // Рассположение текста по центру
            captionStyle.alignment = TextAnchor.MiddleCenter;

            // текст заголовка
            GUILayout.Label(mainMenuText, captionStyle);

            // создаем стиль кнопки на основе стиля button стандартного скина
            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
            // устанавливаем стилю кнопки шрифт buttonFont
            buttonStyle.font = buttonFont;
            // отступы кнопок от краев
            buttonStyle.margin = new RectOffset(20, 20, 3, 3);


            // 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(); // динамическое пространство между кнопкой Exit и низом области меню

        }
        GUILayout.EndArea();

    }
}

Сцена GameOver

Открываем пустую сцену GameOver, перетаскиваем пустой пока скрипт LoadLevelByTimer.cs на камеру. Нам нужно так же показать игроку сообщение "Game Over!". Для этого создаем объект GUIText, пишем ему текст "Game Over!" и устанавливаем шрифт verdana 72.

Выглядеть это будет следующим образом:

Snake GameOver.png

Разработка скрипта LoadLevelByTimer.cs

Здесь я приведу полный скрипт, потому что он не сложный

LoadLevelByTimer.cs

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. Параметру LevelName созданного скрипта устанавливаем текст "MainMenu". Сохраняем сцену и запускаем. Через заданное время у нас загружается сцена с именем MainMenu.

Сцена Level

Эта сцена самая сложная, т.к. в ней происходит игра.

В сцене у нас присутствуют следующие элементы:

  • Камера - смотрит на змейку, следовательно ей нужно дать это поведение.
  • Змейка - передвигается, врезается в объекты и в зависимости что за объект выбирает действие. если врезалась в еду, то еду надо съесть. Если врезалась в другое препятствие, то игрок проиграл - надо загрузить уровень GameOver.
  • Еда - появляется в любом свободном месте в пределах уровня. Может быть съедена.
  • Препятствия уровня, и бортики. Бортики можно создать заранее, а препятствия должны генерироваться.


Разверните камеру на 90 градусов по оси X и поместите в точку (0,40,0). Бортики расставьте на расстоянии 50 единиц от центра и назначьте им материал Wall перетаскиванием. Должно получиться примерно как на следующей картинке:

Snake Level Start.png


Создайте в сцене сферу с координатами 0,0,0 и скейлом 2,2,2. Это будет голова змейки. Как и в примере с едой, добавьте голове змейки глаза. Назначьте соответствующие материалы голове и глазам. У глаз и у головы удалите коллайдеры. Голове назначьте компонент CharacterController с помощью этого компонента мы будем двигать нашу змейку и она не будет проходить сквозь препятствия. Этот компонент - аналог коллизий в Blitz3D (эллипсоид к чему-то), имеет форму капсулы.

Snake Head.png

На камеру повесьте пустой еще скрипт с названием LookAt.cs, змейке на "голову" повесьте пустой скрипт Player.cs.

Скрипт Камеры

Скрипт камеры очень простой, каждый кадр вызывается функция ориентации камеры на цель, если цель существует.

LookAt.cs

using UnityEngine;

public class LookAt : MonoBehaviour
{
    // цель, на которую должен смотреть объект
    public Transform target;

    public void Update()
    {
        if (target != null)
        {
            // Смотрим всегда на цель
            transform.LookAt(target);
        }
    }
}

Сохраните скрипт и перейдите в Unity3D. У скрипта камеры появится параметр Target, на который надо перетащить голову змейки. Таким образом камера будет следить за змейкой.


Игровые скрипты

Далее откроем скрипт Player.cs.

Начнем с того, что сделаем управление по принципу Blitz3D - то есть обработкой кодов клавиш.

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>();

    }

    public void Update()
    {
        // по аналогии с блитцем - управление 
        // кодами кнопок, но такой вариант не очень гибкий
        float vertical = 0;
        float horizontal = 0;

        if (Input.GetKey(KeyCode.UpArrow)) vertical = 1;
        if (Input.GetKey(KeyCode.DownArrow)) vertical = -1;

        if (Input.GetKey(KeyCode.RightArrow)) horizontal = 1;
        if (Input.GetKey(KeyCode.LeftArrow)) horizontal = -1;

        // вращаем трансформ вокруг оси Y 
        transform.Rotate(0, rotationSpeed * Time.deltaTime * horizontal, 0);

        // двигаем змею постоянно
        _controller.Move(transform.forward * speed * Time.deltaTime * vertical);
    }
}

Сохраните и попробуйте запустить сцену со змейкой.

Но есть способ более гибкий - использовать "оси". Найти и посмотреть их параметры, а так же создать новые можно через меню Edit -> Project Settings -> Input. Подробно о них я рассказывать не буду. Используя оси можно сделать универсальное правление, работающее как на клавиатуре, так и на джойстике.

Поэтому перепишем скрипт следующим образом (дополнительно отключим управление движением вперед, пускай змейка двигается сама):

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>();

    }

    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*/);
    }
}

Получилось даже проще, чем в предыдущем варианте.


Теперь попробуем добавить хвост змее

По аналогии с туториалом Scaven'а я не смог сделать змее хвост в виде конуса, т.к. в Unity нету такого стандартного примитива. Поэтому я решил сделать хвост змейке чуть по интереснее. Сделаем его из трех кубиков, которые следуют за змейкой и друг за другом на одинаковом состоянии.

У вас уже должен был быть пустой скрипт Tail.cs, если же его еще нет, то создайте. Поведение элемента хвоста должно быть следующим:

  • если хвост ближе указанного расстояния к цели - он не двигается;
  • если дальше, то поворачивается на цель и перемещается так, чтобы сделать расстояние между элементом хвоста и целью равным установленному.

Замените код скрипта Tail.cs следующим:

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);
        }
    }
}

Кроме самого поведения хвоста, хвост еще надо создать. Создадим мы его программно при инициализации змейки из кубиков. Заодно научимся добавлять компоненты поведения объектам непосредственно кодом, а не в редакторе.

Итак в конец функции Start скрипта Player.cs добавляем следующий код:

        // создаем хвост
        // 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);
            // следующим хозяином будет новосозданный элемент хвоста
            current = tail.transform;
        }

Теперь при запуске сцены у змейки появится 3 кубика, следующие за ней.

Snake Head And Tail.png

Очки и их отображение

Прежде чем заняться едой для змейки, нам надо где-то хранить количество очков, которое набрала змейка за уровень и их отображать.

За это будет отвечать скрипт Game.cs, если у вас он еще не создан - создайте его.


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

Game.cs

using UnityEngine;

public class Game : MonoBehaviour
{
    // набранные очки
    public static int points;

    private string _pointsString;
    private int _lastPonts = -1;

    // генерируем уровень при загрузке сцены
    public void Awake()
    {
        // обнуляем очки
        points = 0;
    }

    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 ?? "");
    }
}


Переменная points сделана статической за тем, чтобы можно было получить очки из любого места, например, при съедании еды их увеличить или отобразить на экране GameOver (это будет вашим домашним заданием ;)).

Теперь надо куда-то повесить скрипт Game.cs. Для таких скриптов, которые являются общими обычно создается объект со специальным именем, чтобы его было легче найти. Создайте пустой объект в сцене и назовите его GameManager. На него и повесьте данный скрипт.

Запустив сцену Вы увидите на экране строку "Score: 0000".

Теперь можно заняться едой

Откройте пустующий скрипт Food.cs (или создайте, если он еще не создан).

Поместите в него следующий код:


using UnityEngine;

public class Food : MonoBehaviour
{
    // количество очков, которое дает еда при съедании
    public int points = 10;

    public void Update()
    {
        // вращаем еду со скоростью 60 градусов в секунду
        transform.Rotate(Vector3.up, 60 * Time.deltaTime);
    }
}

Сохраните скрипт и перейдите в Unity.

Перетащите ранее созданный префаб еды в сцену. И назначьте скрипт еды ему. Unity скажет, что данное действие разорвет связь с префабом. Соглашайтесь, а потом нажмите кнопочку Apply над настройками компонента Transform, которая применит изменения префабу еды (т.е. сохранит назначенный компонент в префаб). Постарайтесь, чтобы еда была в сцене с координатой y = 0.

Запустите игру. Змейка сможет врезаться в еду, но пока не сможет ее съесть.

Т.к. еду можно съесть, то добавим еде функцию ее съедания? которая должна прибавить очков и уничтожить еду.

    public void Eat()
    {
        // прибавляем очки еды к общему числу очков
        Game.points += points;

        // уничтожаем объект еды
        Destroy(gameObject);
    }

Но сама по себе функция не вызовется. Для того, чтобы змейка съела еду, надо обработать столкновение змейки с чем-либо. Для таких целей есть событие OnControllerColliderHit. Данное событие вызывается каждый кадр столько раз, сколько столкновений произошло с объектом. Т.е. при столкновении с едой может случиться несколько столкновений (несколько точек касания), поэтому добавим флаг для предотвращения этого.

Изменяем скрипт Palyer.cs:

  • добавляем флаг
    private bool _testing = false;
  • добавляем включение флага перед перемещением (перед вызовом функции Move)
        _testing = true; // маленкий хинт, для того, чтобы не обрабатывать несколько коллизий за кадр
  • Функция обработки столкновений
    // В данную функцию будут передаваться все объекты, с которыми
    // CharacterController вступает в столкновения
    public void OnControllerColliderHit(ControllerColliderHit hit)
    {
        if (_testing)
        {
            Food food = hit.collider.GetComponent<Food>();
            if (food != null)
            {
                // врезались в еду, "съедаем" ее
                food.Eat();
            }
            else
            {
                // врезались не в еду
                Application.LoadLevel("GameOver");
            }
            _testing = false;
        }
    }

Сохраняем. Пробуем. При столкновении с едой она должна исчезнуть, а если столкнемся с бортиками, то проиграем.

Полный скрипт Player.cs

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);
            // следующим хозяином будет новосозданный элемент хвоста
            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);

        _testing = true; // маленкий хинт, для того, чтобы не обрабатывать несколько коллизий за кадр

        // движение выполняем с помощью контроллера в сторону, куда смотрит трансформ игрока
        // двигаем змею постоянно
        _controller.Move(transform.forward * speed * Time.deltaTime /* * vertical*/);
    }


    // В данную функцию будут передаваться все объекты, с которыми
    // CharacterController вступает в столкновения
    public void OnControllerColliderHit(ControllerColliderHit hit)
    {
        if (_testing)
        {
            Food food = hit.collider.GetComponent<Food>();
            if (food != null)
            {
                // врезались в еду, "съедаем" ее
                food.Eat();
            }
            else
            {
                // врезались не в еду
                Application.LoadLevel("GameOver");
            }
            _testing = false;
        }
    }
}


Добавим еде функцию создания еды в свободном пространстве уровня:

    // функция создания новой еды
    public static void GenerateNewFood()
    {
        // создаем экземпляр еды, предварительно загружая префаб из ресурсов
        GameObject food = (GameObject)Instantiate(Resources.Load("Prefabs/Food", typeof(GameObject)));

        // цикл подбора положения еды
        while (true)
        {
            // ставим еду в рандомное место
            food.transform.position = new Vector3(Random.Range(-40, 41), 0, Random.Range(-40, 41));
            // получаем размер ее колайдера в мировых координатах
            Bounds foodBounds = food.collider.bounds;

            bool intersects = false;

            // Проверяем со всеми колайдерами кроме колайдера самой еды.
            // Данная фукнция использует габаритные контейнеры колайдеров для
            // сравнения. Если используются сложные колайдеры в уровне, то
            // данное сравнение будет не верным.
            foreach (Collider objectColiider in FindObjectsOfType(typeof(Collider)))
            {
                // проверяем со всеми коллайдерами, кроме коллайдера созданной еды 
                if (objectColiider != food.collider)
                {
                    // если пересекается, то завершаем цикл, досрочно
                    if (objectColiider.bounds.Intersects(foodBounds))
                    {
                        intersects = true;
                        break;
                    }
                }
            }

            // установили в нужное место, останавливаем цикл установки
            if (!intersects)
            {
                break;
            }
        }
    }

И вызовем ее после удаления еды при ее съедании.

Полный скрипт еды

using UnityEngine;

public class Food : MonoBehaviour
{
    // количество очков, которое дает еда при съедании
    public int points = 10;

    public void Update()
    {
        // вращаем еду со скоростью 60 градусов в секунду
        transform.Rotate(Vector3.up, 60 * Time.deltaTime);
    }

    public void Eat()
    {
        // прибавляем очки еды к общему числу очков
        Game.points += points;

        // уничтожаем объект еды
        Destroy(gameObject);

        // Генерируем новую еду
        GenerateNewFood();
    }

    // функция создания новой еды
    public static void GenerateNewFood()
    {
        // создаем экземпляр еды, предварительно загружая префаб из ресурсов
        GameObject food = (GameObject)Instantiate(Resources.Load("Prefabs/Food", typeof(GameObject)));

        // цикл подбора положения еды
        while (true)
        {
            // ставим еду в рандомное место
            food.transform.position = new Vector3(Random.Range(-40, 41), 0, Random.Range(-40, 41));
            // получаем размер ее колайдера в мировых координатах
            Bounds foodBounds = food.collider.bounds;

            bool intersects = false;

            // Проверяем со всеми колайдерами кроме колайдера самой еды.
            // Данная фукнция использует габаритные контейнеры колайдеров для
            // сравнения. Если используются сложные колайдеры в уровне, то
            // данное сравнение будет не верным.
            foreach (Collider objectColiider in FindObjectsOfType(typeof(Collider)))
            {
                if (objectColiider != food.collider)
                {
                    // если пересекается, то завершаем цикл, досрочно
                    if (objectColiider.bounds.Intersects(foodBounds))
                    {
                        intersects = true;
                        break;
                    }
                }
            }

            // установили в нужное место, останавливаем цикл установки
            if (!intersects)
            {
                break;
            }
        }
    }
}


Теперь в уровне появляется новая еда при съедании.

Нам осталось модифицировать скрипт Game.cs так, чтобы при старте генерировались препятствия и еда.

Полный скрипт Game.cs

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();

        // ставим первую еду
        Food.GenerateNewFood();
    }

    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;
        }

    }
}

Назначаем скрипту материал стены, который мы создали за ранее, теперь он их создает при старте. А так же он создает и первую еду, поэтому существующую в сцене еду удаляем.

Заключение

Фух, думал не хватит сил закончить этот урок! Надеюсь он вам поможет в освоении Unity.

Вот что получилось:

Snake Game Complete.png

Поиграть можно тут: Snake