Мобильная 3D игра на Unity3D менее чем за 90 часов

в 7:52, , рубрики: Gamedev, unity3d, разработка игр, уроки

image Приветствую! Сегодня я расскажу вам о своем опыте разработки игры на Unity для платформы Android, менее чем за 90 часов, на примере создания простенького «раннера». В процессе повествования я затрону некоторые детали и ключевые этапы, с описанием всех возможных подводных камней и методов их решения. Данная история описывает процесс создания игры для мобильных платформ, начиная от концепции и заканчивая готовым продуктом. Надеюсь, она вдохновит вас на создание собственного проекта, либо поможет пролить свет на некоторые особенности движка Unity. Без лишних слов, приступим к делу!

Этап-1: концепция

Как правило, начинающие разработчики, наступают на свои первые и самые значимые грабли уже на данном этапе, потому что перед тем, как приступить к созданию чего-либо, неплохо было бы оценить собственные возможности. Просто задайте себе вопрос: хватит ли у вас сил, времени и умений на создание проекта ААА класса? Ответ – нет! Отбросьте эту идею в долгий ящик, и не возвращайтесь к ней до тех пор, пока не реализуете чертову дюжину удачных проектов. К слову, под удачей мы подразумеваем количество установок от 500 тысяч, рейтинг свыше 3,5 по 5-ти бальной шкале и коммерческий успех. Для начала, займитесь более простыми, я бы даже сказал приземленными проектами, вроде аркад в стиле addictive games, сочетающих в себе все необходимые нами критерии «удачного» проекта.

Преимущества стиля addictive games:

  1. Затягивающий, «залипающий» геймплей;
  2. Отсутствие сюжета;
  3. Простое и интуитивно понятное управление, требующее от игрока минимум действий;
  4. Минимальные требования к графике.

Последний пункт стоит особняком, так как львиная доля времени уходит не на написание кода, а на полировку визуальной составляющей игры, которая в нашем случае особой роли не играет. Учитывая вышесказанное, я решил остановиться на 3D-раннере, а именно на поделке в роде знаменитой игры ZIGZAG, только лучше.

Этап-2: создание наброска

Вот мы и подошли к самому главному этапу разработки, и пока с вашего лица еще не сошла разочаровывающая улыбка, позвольте вам напомнить, что набросок – это видение продукта. Создав его, вы фактический утверждаете техническое задание будущей игры, благодаря которому все дальнейшие шаманства и танцы будут исходить именно из этого задания. Степень проработки эскиза определять вам и только вам. В конце концов, этот эскиз вы создаете для себя, а не для галереи искусств. На данном этапе, я просто беру ручку и блокнот, после чего начинаю рисовать, изредка оставляя краткие комментарии и пояснения:

image

Из наброска видно, что игра предназначена для мобильных платформ, и запускается она будет в портретном режиме. Геймплей также бесхитростен: задача игрока заключается в преодолении опасного машрута на предоставленном игрой автомобиле, попутно собирая кристаллы. За каждый собранный кристалл и удачно пройденный поворот, игрок получает вознаграждение в виде очков бонуса. Касание по экрану заставляет изменять направление движения автомобиля по осям X и Z.

Этап-3: создание прототипа

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

image

В левой части экрана расположились редактор Scene и Game. Последний отображает то, как именно игра выглядит на устройствах. В правой части: панели Hierarchy и Inspector, а чуть ниже расположены панели Project и Console.

Этап-3.1: под капотом

Внимание! Ниже будет описан простейший код реализации игры, рассчитанный на новичков, и демонстрирующий то, насколько быстро и просто можно добиться результата в Unity. Финальный код игры реализован на более глубоких познаниях языка, включающий проблемы хранения данных, оптимизации и монетизации проекта, однако, по понятным причинам, в данной статье о них говориться не будет. Все скрипты мы будем писать на C#, а тем, кому это не интересно, предлагаю смело переходить к Этапу-4: визуальный дизайн

Мое прототипирование всегда начинается с болванок, то есть в качестве актеров я всегда использую примитивные элементы, вроде кубов и сфер. Такой подход заметно упрощает процесс разработки, позволяя абстрагироваться от всего, что не связано с механикой игры. На первом шаге мы формируем базовое понимание облика будущей игры, а так как по задумке наша игра создается в изометрическом стиле, первое что нам необходимо проделать, это настроить камеру. Тут мы подходим к одной из ключевой особенности Unity. Дело в том, что можно долго экспериментировать с параметрами настройки камеры, подбирая нужные значения… Но проще просто выставить понравившийся ракурс с помощью панели View, а затем активировать GameObject -> Align With View, после чего ваша камера тотчас же примет необходимые значения. Вот такой вот shortcut от создателей Unity.

image

Итак, сцена готова, но как придать персонажу движение? Для начала, произведем некоторые манипуляции с объектом Sphere, добавив в него такие компоненты как Rigidbody и только что созданный скрипт sphereBehavior. Не забудьте отключить галочку Use Gravity, так как на данном этапе он нам не понадобится.

image

Если вкратце, то компонент Rigidbody позволяет объекту ощутить на себе все прелести физического мира, таких как масса, гравитация, сила тяжести, ускорение и.т.д. Вот почему для нас он так важен! А теперь, чтобы заставить тело двигаться в нужном нам направлении, нам всего лишь нужно слегка изменить параметр velocity, но делать это мы будем при помощи кода. Давайте заставим сферу двигаться по оси Х, для этого внесём изменения в скрипт sphereBehavior:

using UnityEngine;
using System.Collections;

public class sphereBehavior : MonoBehaviour {

   private Rigidbody rb; // Объявление новой переменной Rigidbody
   private float speed = 5f; // Скорость движения объекта
   
   void Start() {
      rb = GetComponent<Rigidbody> (); // Получение доступа к Rigidbody
   }
   void Update() {
      rb.velocity = new Vector3 (speed, 0f,0f);
   }

}

В Unity, тела описывают своё положение и направление, посредством специальных векторов, хранящих значения по осям x, y и z. Изменяя эти значения, мы добиваемся необходимого нам направления или положения конкретного тела. Строка rb.velocity = new Vector3(speed, 0f,0f) задает новое направление телу по оси X, тем самым придавая нашей сфере нужное нам направление.

Если вы сделали всё в точности, как и я, то ваша сфера отправится в бесконечное путешествие по оси X, со скоростью speed.

Теперь давайте заставим нашу сферу изменять свое направление, при каждом клике левой клавиши мыши так, как это реализовано в игре ZIGZAG. Для этого мы вновь вернемся к коду sphereBehavior и изменим его следующим образом:

using UnityEngine;
using System.Collections;

public class sphereBehavior : MonoBehaviour {

   private Rigidbody rb; // Объявление новой переменной Rigidbody
   private bool isMovingRight = true; // переменная, отражающая условное направление объекта
   private float speed = 5f; // Скорость движения объекта

   void Start() {
      rb = GetComponent<Rigidbody> (); // Получение доступа к Rigidbody
   }

   void changeDirection() {
      if (isMovingRight) {
         isMovingRight = false;
      } else {
         isMovingRight = true;
      }
   }

   void Update() {

      if(Input.GetMouseButtonDown(0)) {
         changeDirection();
      }

      if (isMovingRight) {
         rb.velocity = new Vector3 (speed, 0f, 0f);
      } else {
         rb.velocity = new Vector3 (0f, 0f, speed);
      }

   }

}

Условимся, что когда сфера движется по оси X, то это движение называется движением «вправо», а по оси Z – «влево». Таким образом мы легко можем описать направление нашего тела специальной булевой переменной isMovingRight.

if(Input.GetMouseButtonDown(0)) {
   changeDirection();
}

Этот кусочек кода отслеживает нажатие левой клавиши мыши, и если данная клавиша все же была нажата, запускает функцию changeDirection(), с простой логикой: если на момент нажатия левой клавиши мыши, переменная isMovingRight имела значение true, то теперь она стала false и наоборот. Напомню, что булевая переменная позволяет нам ответить на один простой вопрос: истинно ли утверждение о том, что тело движется по оси X, или нет? Иными словами, нажатие на левую клавишу мыши постоянно изменяет значение isMovingRight, то на true(тело движется вправо), то на false(тело движется влево).

Альтернативно, функцию changeDirection() можно записать в одну строку:

void changeDirection() {
      isMovingRight = !isMovingRight;
}

И последнее, что необходимо сделать, это переписать метод направления движения с учетом переменной isMovingRight:

if (isMovingRight) {
   rb.velocity = new Vector3 (speed, 0f, 0f);
} else {
   rb.velocity = new Vector3 (0f, 0f, speed);
}

Если isMovingRight имеет значение true (если сфера действительно движется вправо), тогда значение velocity принимает новый вектор направления rb.velocity = new Vector3 (speed, 0f, 0f); Если isMovingRight имеет значение false, значит тело более не движется вправо, а значит пришло время изменить вектор направления на rb.velocity = new Vector3 (0f, 0f, speed);

Запустите игру, проделайте несколько кликов мыши, и если вы сделали все в точности, как и я, то увидите, как сфера начнет описывать зигзаги.

Круто? Конечно нет! Ведь сфера движется, а мы стоим на месте. Давайте доработаем игру так, чтобы мы могли двигаться вместе со сферой и не упускали её из виду. Для этого нам нужно создать скрипт cameraFollow и прикрепить его к объекту Main Camera:

image

А вот код скрипта cameraFollow:

using UnityEngine;
using System.Collections;

public class cameraFollow : MonoBehaviour {

public GameObject player;
public Vector3 offset;

   void Start () {
      offset = transform.position - player.transform.position;
   }
	
   void Update () {
      transform.position = player.transform.position + offset;
   }

}

Как же осуществить слежение за объектом? Для начала нам нужно рассчитать разницу смещения между объектами Camera и Sphere. Для этого достаточно вычесть от позиции камеры, координаты сферы, а полученную разницу сохранить в переменной offset. Но прежде, необходимо получить доступ к координатам сферы. Для этого нам необходима переменная player, представляющая собой простой GameObject. Так как наша сфера находится в постоянном движении, мы должны синхронизировать координаты камеры с координатами сферы, приплюсовав полученное ранее смещение. Осталось только указать в поле player наш объект слежения, и можно смело любоваться результатом. Просто перетащите объект Sphere в поле Player, скрипта cameraFollow, как это показано на картинке (Main Camera при этом должна оставаться выделенной):

image

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

image

Если в вашем списке нет тэга Ground, то его необходимо создать во вкладке Add Tag...

Следующее, что нам предстоит совершить, это создать в корне проекта специальную папку с названием Prefabs, и перетащить в нее наш Cube, прямо из инспектора. Если после этого, имя объекта Cube стало синего цвета, значит вы все сделали правильно.

image

Префабы – это особый тип объектов, позволяющий хранить GameObject, а также все его значения и свойства в одном месте. Префабы позволяют создавать бесконечное множество объекта, а любое его изменение немедленно отражаются на всех его копиях. Иными словами, теперь мы можем вызывать участок пути Cube, прямо из папки Prefabs, столько раз, сколько необходимо.

Теперь давайте создадим пустой GameObject, (щелчок правой кнопки мыши по Hierarchy) переименуем его в RoadContainer и прикрепим к нему только что созданный скрипт roadBehavior:

image

image

А вот и сам код roadBehavior:

using UnityEngine;
using System.Collections;

public class roadBehavior : MonoBehaviour {

   public GameObject road; // Префаб участка пути
   private Vector3 lastpos = new Vector3 (0f,0f,0f); // Координаты установленного префаба

   void Start() {

      for(int i=0; i<10; i++) {
         GameObject _platform = Instantiate (road) as GameObject;
         _platform.transform.position = lastpos + new Vector3 (1f,0f,0f);
         lastpos = _platform.transform.position;
      }

   }

}

Что же тут на самом деле происходит? Как видите, у нас есть переменная, которая позже, вручную будет привязана к нашему префабу Cube, и есть объект Vector3, хранящий координаты последнего установленного префаба (сейчас значения равны нулю).

for(int i=0; i<10; i++) {
   GameObject _platform = Instantiate (road) as GameObject;
   _platform.transform.position = lastpos + new Vector3 (1f,0f,0f);
   lastpos = _platform.transform.position;
}

Этот участок кода выполняет следующее: до тех пор, пока i < 10, мы будем брать префаб, устанавливать его позицию с учетом последней позиции lastpos + позиция с учетом смещения по X, сохранять последнюю позицию. То есть в результате, мы получим 10 префабов Cube установленных в точности друг за другом. Перед проверкой, не забываем назначить переменной road наш объект Cube из папки Prefabs:

image

image

Ок, но что делать дальше? А дальше нам нужно продолжить установку блоков в произвольном порядке. Для этого нам понадобится генератор псевдослучайных чисел random. Подправим скрипт roadBehavior с учетом нововведений:

using UnityEngine;
using System.Collections;

public class roadBehavior : MonoBehaviour {

   public GameObject road; // Префаб участка пути
   private Vector3 lastpos = new Vector3 (0f,0f,0f); // Координаты установленного префаба

   void Start() {

      for(int i=0; i<10; i++) {
         GameObject _platform = Instantiate (road) as GameObject;
         _platform.transform.position = lastpos + new Vector3 (1f,0f,0f);
         lastpos = _platform.transform.position;
      }

      InvokeRepeating ("SpawnPlatform", 1f, 0.2f);

   }

   void SpawnPlatform() {

      int random = Random.Range (0, 2);
      if (random == 0) { // Установить префаб по оси X
         GameObject _platform = Instantiate (road) as GameObject;
         _platform.transform.position = lastpos + new Vector3 (1f,0f,0f);
         lastpos = _platform.transform.position;
      } else { // Установить префаб по оси Z
         GameObject _platform = Instantiate (road) as GameObject;
         _platform.transform.position = lastpos + new Vector3 (0f,0f,1f);
         lastpos = _platform.transform.position;
      }

   }

}

Строчка InvokeRepeating («SpawnPlatform», 1f, 0.2f) предназначена для активации функции SpawnPlatform() спустя 1 секунду после начала игры, и повторного её вызова каждые 0.2 секунды. Что касается самой функции, то тут, как говорится все проще пареной репы! Каждые 0.2 секунды, система загадывает случайное число между цифрами от 0 до 1. Если система загадала 0 – мы устанавливаем новый префаб по оси X, а если 1 – то по оси Z. Вот и вся магия!

image

И наконец, давайте заставим сферу падать каждый раз, когда она сходит с дистанции. Для этого создадим новый скрипт playerFalls и прикрепим его к нашему объекту Sphere:

image

А вот и сам код скрипта playerFalls:

using UnityEngine;
using System.Collections;

public class playerFalls : MonoBehaviour {

   private Rigidbody rb;

   void Start() {

      rb = GetComponent<Rigidbody> ();

   }

   void Update() {

      RaycastHit hit;
      if(Physics.Raycast (transform.position, Vector3.down, out hit, 5f) && hit.transform.gameObject.tag == "Ground") {
         rb.useGravity = false;
      } else {
         rb.useGravity = true;
      }

   }

}

Raycast – специальный луч на подобии лазера, который излучается по направлению к сцене. В случае, если луч отражается от объекта, он возвращает информацию об объекте, с которым столкнулся. И это очень круто, потому что именно так, посредством такого луча, направленного из центра сферы вниз, мы будем проверять, находимся ли мы на платформе Cube или нет (проверяем, имеет ли объект тэг «Ground»). И как только мы покинем регионы дорожного полотна, мы автоматом активируем параметр Gravity нашей сферы (помните, как мы заведомо отключили его в самом начале?), после чего сфера, под воздействием гравитации, рухнет вниз, ха-ха!

Этап-4: визуальный дизайн

Когда все работы по игровой механике закончены, в пору переходить к визуальной части проекта. Все таки геймплей – это хорошо, а приятный геймплей – еще лучше. И несмотря на то, что в самом начале мы обозначили графику, как далеко не самое главное, хочется все же привнести некоторую изюминку, добавив красок в создаваемую игру. После недолгих раздумий, в голову пришла следующая идея:

image

По замыслу, вы управляете автомобилем, несущимся по бескрайним морским просторам, спасаясь от надвигающего катаклизма. Промедление сродни смерти, так как платформы то и дело норовят опрокинуться в морскую пучину, увлекая игрока в бездну позора и разочарования. Плюс ко всему, время от времени, платформы начинают менять цвет, а автомобиль, самопроизвольно увеличивать скорость. Всё это призвано привнести в игру некое подобие «челленджа». Как и было сказано, за каждый удачно пройденный поворот или собранный кристалл, игрок вознаграждается «инкамом» — местным подобием зарплаты. Зарплату в последствии можно обменять в лавке на авто с более высоким «инкамом». Концепция подарила звучное название «Income Racer».

Все ассеты были смоделированы в Blender’е – бесплатном 3D редакторе. В нём же были созданы необходимые текстуры, впоследствии доведенные до приемлемого вида в Photoshop'е. Приятным моментом оказалось то, что Unity легко импортирует 3D модели из Blender’а, без лишней головной боли, делая процесс создания приятным и безболезненным.

Этап-5: полировка

Доводка проекта – те еще грабли, ведь всегда найдется место тому, что можно улучшить, или переделать. Зачастую, случается так, что именно на этапе полировки и доводки, процесс разработки значительно теряет во времени, а то и вовсе заходит в тупик. Причина заключается в том, что вы уже заметно подустали: игра вам кажется однообразной и недостаточно интересной. Иногда, под конец разработки, приходит откровение того, что вы способны на переделку игры с нуля, улучшив её как минимум в два раза! Отбросьте эти мысли и вспомните о плане, о том, с чего все начиналось. Лучше дополнять игру уже после релиза, путем выкатки обновлений, чем затягивать разработку на неопределенный срок. В противном случае, вы рискуете погубить проект, поставив на нём жирный крест. К примеру, на момент написания этих строк, игра даже не имела вступительного экрана. Причиной тому тот факт, что по плану я не мог выйти за рамки в 90 часов, отведенные на процесс разработки. Конечно, можно было бы потратить еще несколько часов на создание вступительного экрана, однако на то он и план, чтобы ему следовать. И это нормально, что некоторые моменты добавляются в игру уже после её релиза.

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

В итоге получилось то, что получилось. На всё про всё было затрачено чуть более 90 часов, что по меркам современного геймдева не так уж и много. По прошествии этого времени, игра была загружена в Play Market и выставлена на всеобщий суд, вот такая история! Если вам понравилась статья, или просто есть о чем поговорить, то добро пожаловать в комментарии. Буду рад ответить на ваши вопросы.

Автор: noekintaro

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js