Создаём простую 3D-гонку на Unity
Гонки — один из самых популярных и простых в реализации игровых жанров. Рассказываем, как за один день создать простую 3D-гонку на Unity.
![](https://248006.selcdn.ru/main/iblock/a8e/a8e23d532e3c96386d0e4ac4c2ceeced/5b1423eb9c7b73fde8ca2fdd33797503.jpg)
![](https://248006.selcdn.ru/main/iblock/a8e/a8e23d532e3c96386d0e4ac4c2ceeced/5b1423eb9c7b73fde8ca2fdd33797503.jpg)
vlada_maestro / shutterstock
В этой статье мы создадим простую гонку на Unity, в которой будут:
- управление машиной;
- аварии;
- музыка и звуковые эффекты;
- бесконечная дорога;
- очки;
- меню.
Мы уже несколько раз писали, как реализовать такие вещи, поэтому в этой статье сосредоточимся на том, как использовать Unity для создания самой гонки, не вдаваясь в подробности работы с интерфейсом движка.
Если вы раньше не работали с Unity, рекомендуем ознакомиться с этими статьями:
- Как создать 2D-игру на Unity — введение в Unity.
- Что такое ассеты в Unity — добавление компонентов игры.
- Как создать 2D-шутер в Unity — работа с префабами.
- Меню для игры на Unity — об интерфейсе и работе с файлами.
Финальную версию проекта со всеми ассетами можно найти в этом репозитории на GitHub.
Подготовка к проекту
Для начала нужно создать 3D-проект в Unity и импортировать модели и звуки. Вы можете использовать свои или взять те, что находятся в репозитории.
Музыка и звуки найдены на бесплатных сайтах. Машины скачаны из Asset Store, а всё остальное я смоделировал самостоятельно (да, это всего лишь дорожный блок и монетка, но я старался).
Когда всё будет скачано и добавлено в проект, можно начинать.
Добавление дороги
Создайте пустой объект и назовите его Road — в нём будут размещаться все машины и дорожные блоки.
![](https://248006.selcdn.ru/main/upload/setka_images/11205307112019_93b8888c66d5103a8bb4f5fd89e04622d16558ec.png)
Добавьте в него первый блок:
![](https://248006.selcdn.ru/main/upload/setka_images/11205407112019_152245077ac6dbcf53ad14fb93a3bbc9c8bae8d3.png)
У объекта Road координаты должны быть по нулям, а у блока X можно поставить на ноль либо на -24.69 — столько он занимает места. Эта цифра нам понадобится, чтобы добавлять новые блоки.
Теперь для блока нужно подключить коллайдеры Box Collider. Но добавлять их нужно не на сам блок, а на дорожное полотно (Plane) и бордюры (Plane_002).
![](https://248006.selcdn.ru/main/upload/setka_images/11205407112019_4fb6fa4fbc0158ec7cf1e48acc6733dd567fe239.png)
Для полотна сразу установите тег Road (его нужно создать, нажав на Add Tag). Затем приступайте к бордюрам. Это один объект, поэтому нужно просто добавить два коллайдера:
![](https://248006.selcdn.ru/main/upload/setka_images/11205407112019_8bc1aa9ad985c6b5f9a267787eba176dd734e55f.png)
Для бордюров установите тег Wall (его тоже нужно создать). Теги пригодятся для того, чтобы определять, с чем именно сталкивается машина игрока.
После того как будет добавлен игрок, можно будет приступить к генерации бесконечной дороги.
Добавление игрока
Внутри объекта Road нужно создать ещё один пустой объект и назвать его Player. Внутри него добавьте пустой объект Model и камеру. В Model нужно поместить модель машины, а затем установить камеру сзади модели.
![](https://248006.selcdn.ru/main/upload/setka_images/11205407112019_51f6f1668dfb2cd28d94728ed419d478538b3006.png)
Для объекта Player добавьте компонент Rigidbody и два коллайдера:
![](https://248006.selcdn.ru/main/upload/setka_images/11205407112019_b7f630640f2efffc4c41d0d6466d02c19b3b342c.png)
Один из коллайдеров нужно установить как триггер — с его помощью будут проверяться столкновения с разными объектами.
Теперь нужно заставить машину двигаться. Для этого создадим скрипт Moving и прикрепим его к объекту Player.
В первую очередь добавим переменные:
public Rigidbody rb;
public GameObject car; //Модель машины
public GameObject brokenPrefab; //Префаб сломанной машины
public GameObject modelHolder; //Объект, в который помещается модель
public Controls control; //Скрипт управления, он будет добавлен позже
private float speed = 0.1f; //Скорость на старте
private float maxSpeed = 0.5f; //Максимальная скорость
private float minSpeed = 0.1f; //Минимальная скорость
private bool isAlive = true; //Жива ли машина. Если да, то она будет двигаться
private bool isKilled = false; //Эта переменная нужна, чтобы триггер сработал только один раз
public List<GameObject> wheels; //Колёса машины
Теперь в Unity нужно заполнить все публичные переменные. Пустыми пока можно оставить Control и Broken Prefab, потому что они ещё не готовы.
![](https://248006.selcdn.ru/main/upload/setka_images/11205407112019_4722513d69363eb8e301bcbc5a9b6b244943d87f.png)
Переменная Rb будет добавляться скриптом в методе Start():
void Start()
{
rb = GetComponent<Rigidbody>();
}
Внутри этого скрипта он использоваться не будет, но нужен, чтобы генерировать дорогу.
Вернёмся к движению:
void Update()
{
if(isAlive)
{
float newSpeed = speed; //Скорость движения вперёд
float sideSpeed = 0f; //Скорость движения вбок
if(newSpeed > maxSpeed)
{
newSpeed = maxSpeed; //Проверка на превышение максимальной скорости
}
if(newSpeed < minSpeed)
{
newSpeed = minSpeed; //Проверка на слишком низкую скорость
}
//Изменение положения машины - она двигается вперёд
//Для этого к её положению по оси X прибавляется новая скорость, положение по Y остаётся прежним
//К положение по оси Z прибавляется 0.1f, умноженная на боковую скорость
transform.position = new Vector3(transform.position.x + newSpeed, transform.position.y, transform.position.z + 0.1f * sideSpeed);
if(control != null)
{
control.sideSpeed = 0f; //Сброс боковой скорости
}
if(wheels.Count > 0) //Если есть колёса
{
foreach (var wheel in wheels)
{
wheel.transform.Rotate(-3f, 0f, 0f); //Вращение каждого колеса по оси X
}
}
if(tag == "Car")
{
if(transform.position.y < -50f)
{
Destroy(gameObject); //Если это машина NPC, то она будет удаляться со сцены, если упадёт ниже -50f
}
}
}
}
Можно запустить сцену и проверить, как движется машина. Камеру перед этим лучше сместить вбок, чтобы видеть, вращаются ли колёса.
![](/upload/setka_images/11205407112019_8022b4d7ba55e49277568f23d9bc1ed151747386.gif)
Теперь нужно написать скрипт, который позволит управлять машиной. Назовём его Controls и добавим несколько переменных:
public float speed = 0f; //Скорость
public float maxSpeed = 0.5f; //Максимальная скорость
public float sideSpeed = 0f; //Боковая скорость
Само управление выглядит так:
void Update()
{
float moveSide = Input.GetAxis("Horizontal"); //Когда игрок будет нажимать на стрелочки влево или вправо, сюда будет добавляться 1f или -1f
float moveForward = Input.GetAxis("Vertical"); //То же самое, но со стрелочками вверх и вниз
if(moveSide != 0)
{
sideSpeed = moveSide * -1f; //Если игрок нажал на стрелочки влево или вправо, задаём боковую скорость
}
if(moveForward != 0)
{
speed += 0.01f * moveForward; //Если игрок нажал вверх или вниз
}
else //Если игрок не нажал ни вверх, ни вниз, то скорость будет постепенно возвращаться к нулю
{
if(speed > 0)
{
speed -= 0.01f;
}
else
{
speed += 0.01f;
}
}
if(speed > maxSpeed)
{
speed = maxSpeed; //Проверка на превышение максимальной скорости
}
}
Ссылку на скрипт нужно добавить в Moving:
![](https://248006.selcdn.ru/main/upload/setka_images/11205407112019_321ed7dda7d49b44474fa677da2349e6dadbddbe.png)
Затем нужно добавить в метод Update() скрипта Moving следующий код:
if(control != null) //Если подключён скрипт управления
{
newSpeed += control.speed; //Изменение скорости
sideSpeed = control.sideSpeed; //Изменение направления
}
Его надо вставить сразу после объявления переменных newSpeed и sideSpeed. Теперь машиной можно управлять:
![](/upload/setka_images/11205407112019_c384d3e418c1562c1bbf0adfaf71efc497b93ffd.gif)
Генерация бесконечной дороги
Чтобы машина не падала в пропасть, нужно добавить бесконечную генерацию дороги.
- Перед машиной будет добавляться столько дорожных блоков, сколько нужно, чтобы не было видно пропасти.
- Когда машина проезжает достаточное расстояние от определённого блока, он удаляется, чтобы освободить оперативную память.
Для этого создадим скрипт RoadBlock и прикрепим его к дорожному блоку. В нём будет всего два метода:
public bool Fetch(float x) //Проверка, проехала ли машина игрока этот блок на достаточное расстояние
{
bool result = false;
if(x > transform.position.x + 100f)
{
result = true; //Если машина проехала на 100f от блока, то возвращается true
}
return result;
}
public void Delete()
{
Destroy(gameObject); //Удаление блока
}
Сохраните блок как префаб. А инстанцироваться новые объекты будут в скрипте Road:
public List<GameObject> blocks; //Коллекция всех дорожных блоков
public GameObject player; //Игрок
public GameObject roadPrefab; //Префаб дорожного блока
public GameObject carPrefab; //Префаб машины NPC
public GameObject coinPrefab; //Префаб монеты
private System.Random rand = new System.Random(); //Генератор случайных чисел
void Update()
{
float x = player.GetComponent<Moving>().rb.position.x; //Получение положения игрока
var last = blocks[blocks.Count - 1]; //Номер дорожного блока, который дальше всех от игрока
if(x > last.transform.position.x - 24.69f * 10f) //Если игрок подъехал к последнему блоку ближе, чем на 10 блоков
{
//Инстанцирование нового блока
var block = Instantiate(roadPrefab, new Vector3(last.transform.position.x + 24.69f, last.transform.position.y, last.transform.position.z), Quaternion.identity);
block.transform.SetParent(gameObject.transform); //Перемещение блока в объект Road
blocks.Add(block); //Добавление блока в коллекцию
}
foreach (GameObject block in blocks)
{
bool fetched = block.GetComponent<RoadBlock>().Fetch(x); //Проверка, проехал ли игрок этот блок
if(fetched) //Если проехал
{
blocks.Remove(block); //Удаление блока из коллекции
block.GetComponent<RoadBlock>().Delete(); //Удаление блока со сцены
}
}
}
Те блоки, которые есть на сцене, нужно поместить в коллекцию Blocks. Также надо сразу заполнить остальные переменные.
![](https://248006.selcdn.ru/main/upload/setka_images/12072007112019_9c691ecb712da8ec3b74d53b0e423135ab9adb2d.png)
Теперь можно проверить, как это выглядит:
![](/upload/setka_images/12072007112019_708c0bd27c934cebc233f6fb9ee57098eefce354.gif)
Генерация машин
Чтобы генерировать машины, нужно сначала создать префаб. Для этого можно скопировать объект Player, удалив камеру и скрипт Controls. Также лучше использовать другую модель, чтобы игрок не путался.
Затем в скрипте Road после добавления нового дорожного блока нужно добавить вот такой код:
float side = rand.Next(1, 3) == 1 ? -1f : 1f; //Случайное определение стороны появления машины
//Добавление машины на сцену
var car = Instantiate(carPrefab, new Vector3(last.transform.position.x + 24.69f, last.transform.position.y + 0.20f, last.transform.position.z + 1.30f * side), Quaternion.Euler(new Vector3(0f, 90f, 0f)));
car.transform.SetParent(gameObject.transform); //Добавление машины в объект Road
Перед этим не забудьте добавить префаб в переменную carPrefab.
![](/upload/setka_images/12071907112019_950616b1b3098f817fe7027e1dfa02169666ef55.gif)
Добавление монет
Чтобы игра стала интереснее, можно добавить монеты, при столкновении с которыми игрок будет получать очки.
Для этого поместите на сцену модель монеты, к ней подключите коллайдер с триггером (он обрабатываться не будет, но нужен для того, чтобы машины NPC проходили сквозь монету).
![](https://248006.selcdn.ru/main/upload/setka_images/12072007112019_5b7e8820a7508f239a2f26f703047170b17c3a4f.png)
Потом создайте скрипт Coin с таким кодом:
int direction = 1; //Направление движения монеты
float high = 1.2f; //Наивысшая точка
float low = 0.7f; //Низшая точка
public GameObject coinSound; //Звук монеты
void Update()
{
transform.Rotate(0f, 1f, 0f); //Монета с каждым кадром будет вращаться
if(direction > 0) //Если направление больше нуля, то монета будет двигаться вверх,
{
if(transform.position.y < high) //пока не достигнет наивысшей точки
{
transform.position = new Vector3(transform.position.x, transform.position.y + 0.01f, transform.position.z);
}
else //После направление изменится
{
direction *= -1;
}
}
else
{
if(transform.position.y > low) //И монета будет двигаться вниз
{
transform.position = new Vector3(transform.position.x, transform.position.y - 0.01f, transform.position.z);
}
else
{
direction *= -1; //А когда достигнет низшей точки, снова начнёт двигаться вверх
}
}
}
public void Delete() //Удаление монеты
{
var sound = Instantiate(coinSound, transform.position, transform.rotation); //Добавление звука монеты
Destroy(sound, 2f); //Уничтожение звука через две секунды
Destroy(gameObject); //Сама монета удалится сразу
}
В качестве префаба звука пока можно использовать пустой объект. А сам код генерации можно добавить туда же, где происходит инстанцирование остальных объектов.
if(rand.Next(0, 100) > 70) //Добавление монеты с вероятностью 30%
{
var coin = Instantiate(coinPrefab, new Vector3(last.transform.position.x + 24.69f, last.transform.position.y + 0.20f, last.transform.position.z + 1.50f * side * -1f), Quaternion.identity);
coin.transform.SetParent(gameObject.transform);
}
Можно проверять:
![](/upload/setka_images/12072007112019_b0db57166028d10b861a6ceedc3e58d5dd8b10d0.gif)
Получение очков
Чтобы игрок мог получать очки, в скрипт Controls нужно добавить следующие переменные:
public float scores = 0f; //Очки
public float highScore = 0f; //Лучший результат
Добавление очков будет происходить в файле Moving в блоке работы со скриптом Controls:
control.scores += 0.1f; //Добавление очков
Чтобы выводить, сколько очков набрал пользователь, нужно создать элемент Canvas и добавить в него текстовый блок:
![](https://248006.selcdn.ru/main/upload/setka_images/12072007112019_0aa57be3ed6c6de7e9327e7741fab432239c9554.png)
Для него создайте скрипт Scores со следующим кодом:
using UnityEngine;
using TMPro;
using System;
public class Scores : MonoBehaviour
{
private TextMeshProUGUI text;
private Controls controls;
public GameObject player;
void Start()
{
text = GetComponent<TextMeshProUGUI>();
controls = player.GetComponent<Controls>();
}
void Update()
{
if(controls != null)
{
text.text = $"Highscore: {Math.Floor(controls.highScore)} | Scores: {Math.Floor(controls.scores)}";
}
}
}
Обратите внимание, что тут используется не стандартный текстовый элемент, а объект из библиотеки TextMeshPro.
![](/upload/setka_images/12072007112019_f0646c625095b49e4e4c41332dd1408112ad8d69.gif)
Обработка столкновений
Теперь нужно добавить логику столкновений с другими объектами. Для начала создадим префаб сломанной машины:
![](https://248006.selcdn.ru/main/upload/setka_images/12072007112019_4d91d82c34218935d797c0d03a5469af514ae1bf.png)
Это та же модель, но здесь для каждого колеса добавлен коллайдер и Rigidbody. То есть при столкновении у машины будут отлетать колёса.
Затем в файл Moving нужно добавить следующий код:
void OnTriggerEnter(Collider other)
{
if(other.tag == "Car" || other.tag == "Wall") //Если машина игрока столкнулась со стеной или другой машиной
{
isAlive = false; //Игрок больше не жив
if(car != null) //Если есть модель
{
if(!isKilled) //Если триггер ещё не сработал
{
Destroy(car); //Удалить старую модель
//Добавить новую модель
var broken = Instantiate(brokenPrefab, transform.position, Quaternion.Euler(new Vector3(0f, -270f, 0f)));
broken.transform.SetParent(modelHolder.transform);
isKilled = true; //Указать, что триггер сработал
StartCoroutine("Die"); //Запустить процесс умирания
}
}
}
if(other.tag == "Coin") //Если столкновение с монетой
{
if(control != null) //Если столкнулась машина игрока
{
control.scores += 100f; //Добавить 100 очков
other.GetComponent<Coin>().Delete(); //Удалить монету
}
}
}
IEnumerator Die() //Процесс умирания
{
string path = "highscore"; //Путь к файлу, в котором сохраняется высший результат
using(FileStream fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite))
{
byte [] bytes = new byte[Convert.ToInt32(fs.Length)];
fs.Read(bytes, 0, Convert.ToInt32(fs.Length));
string high = Encoding.UTF8.GetString(bytes);
float highScore = 0f;
try
{
highScore = Convert.ToSingle(high);
}
catch(Exception e)
{
Debug.Log(e.ToString());
}
if(highScore < Math.Floor(control.scores))
{
byte[] newScores = Encoding.UTF8.GetBytes(Math.Floor(control.scores).ToString());
fs.Write(newScores, 0, newScores.Length);
}
}
yield return new WaitForSeconds(2f); //Подождать 2 секунды
SceneManager.LoadScene("Menu"); //Перейти в меню
}
Переход в меню можно пока закомментировать, потому что оно ещё не готово.
Вот что должно получиться:
![](/upload/setka_images/12071907112019_747f9c83ae490f4448d4099c736d74f0edb301fa.gif)
Работа со звуком
Теперь можно добавить звуки:
- музыку;
- звук мотора;
- звук столкновения с машиной;
- звук столкновения с монетой.
Чтобы добавить музыку, к объекту Player подключите компонент Audio Source:
![](https://248006.selcdn.ru/main/upload/setka_images/12072007112019_94b88a6516c8a4023592c2b5c6b666d2745c77bc.png)
Укажите файл, громкость и поставьте галочку возле Loop, чтобы мелодия повторялась.
Затем добавим звук мотора. Для этого найдите звук, в котором есть монотонный рёв. Его высота будет меняться программным путём, когда игрок будет ускоряться или замедляться.
Добавить к модели машины Audio Source:
![](https://248006.selcdn.ru/main/upload/setka_images/12072007112019_d2f39db9dc4025ae1b91cdf5ab66a76e0fc325f4.png)
А затем в файле Moving добавьте такой код:
car.GetComponent<AudioSource>().pitch = 2 + newSpeed; //Изменение высоты звука
Звук столкновения нужно добавить в префаб сломанной машины, убрав зацикливание. А звук монеты — в пустой объект, который мы инстанцировали в скрипте монеты при столкновении.
Главное меню
Последний штрих — главное меню. Оно будет находиться в отдельной сцене. Создайте скрипт, который будет называться Menu:
using UnityEngine;
using UnityEngine.SceneManagement;
public class Menu : MonoBehaviour
{
public void Play()
{
SceneManager.LoadScene("Main");
}
public void Exit()
{
Application.Quit();
}
}
Добавьте кнопки и укажите методы, которые будут вызываться при нажатии на них:
![](https://248006.selcdn.ru/main/upload/setka_images/12072007112019_0c71894c1bf28a402d268cb7d1242603a9a3766b.png)
При нажатии на Play будет вызываться метод Play(), а при нажатии на Exit — Exit(). Можно добавить монетки и несколько дорожных блоков, чтобы меню выглядело красивее.
![](https://248006.selcdn.ru/main/upload/setka_images/12072007112019_4a8d1d7497e309a43b35e6f22b7458da67ef39ee.png)
Итог
Получилась довольно простая игра. Для разработчика это интересный опыт, но игрокам она быстро наскучит. Чтобы этого не произошло, можно добавить несколько простых улучшений:
- накопление денег;
- покупку новых машин;
- более высокие скорость и манёвренность для более дорогих машин;
- смену полос для машин и так далее.
Если же вы хотите делать игры покруче, записывайтесь на наш курс «Профессия Разработчик игр на Unity». На нём вы научитесь работать с шейдерами, графикой высокого разрешения и разными механиками, а также многим другим.