vlada_maestro / shutterstock
В этой статье мы создадим простую гонку на Unity, в которой будут:
- управление машиной;
- аварии;
- музыка и звуковые эффекты;
- бесконечная дорога;
- очки;
- меню.
- Мы уже несколько раз писали, как реализовать такие вещи, поэтому в этой статье сосредоточимся на том, как использовать Unity для создания самой гонки, не вдаваясь в подробности работы с интерфейсом движка.
- Если вы раньше не работали с Unity, рекомендуем ознакомиться с этими статьями:
- Финальную версию проекта со всеми ассетами можно найти в этом репозитории на GitHub.
Для начала нужно создать 3D-проект в Unity и импортировать модели и звуки. Вы можете использовать свои или взять те, что находятся в репозитории.
Музыка и звуки найдены на бесплатных сайтах. Машины скачаны из Asset Store, а всё остальное я смоделировал самостоятельно (да, это всего лишь дорожный блок и монетка, но я старался).
Когда всё будет скачано и добавлено в проект, можно начинать.
Создайте пустой объект и назовите его Road — в нём будут размещаться все машины и дорожные блоки.
Добавьте в него первый блок:
У объекта Road координаты должны быть по нулям, а у блока X можно поставить на ноль либо на -24.69 — столько он занимает места. Эта цифра нам понадобится, чтобы добавлять новые блоки.
Теперь для блока нужно подключить коллайдеры Box Collider. Но добавлять их нужно не на сам блок, а на дорожное полотно (Plane) и бордюры (Plane_002).
Для полотна сразу установите тег Road (его нужно создать, нажав на Add Tag). Затем приступайте к бордюрам. Это один объект, поэтому нужно просто добавить два коллайдера:
Для бордюров установите тег Wall (его тоже нужно создать). Теги пригодятся для того, чтобы определять, с чем именно сталкивается машина игрока.
После того как будет добавлен игрок, можно будет приступить к генерации бесконечной дороги.
Внутри объекта Road нужно создать ещё один пустой объект и назвать его Player. Внутри него добавьте пустой объект Model и камеру. В Model нужно поместить модель машины, а затем установить камеру сзади модели.
Для объекта Player добавьте компонент Rigidbody и два коллайдера:
Один из коллайдеров нужно установить как триггер — с его помощью будут проверяться столкновения с разными объектами.
Теперь нужно заставить машину двигаться. Для этого создадим скрипт 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 wheels;
Теперь в Unity нужно заполнить все публичные переменные. Пустыми пока можно оставить Control и Broken Prefab, потому что они ещё не готовы.
Переменная Rb будет добавляться скриптом в методе Start():
void Start() { rb = GetComponent();
}
Внутри этого скрипта он использоваться не будет, но нужен, чтобы генерировать дорогу.
Вернёмся к движению:
void Update()
{ if(isAlive) { float newSpeed = speed; float sideSpeed = 0f; if(newSpeed > maxSpeed) { newSpeed = maxSpeed; } if(newSpeed < minSpeed) { newSpeed = minSpeed; } 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); } } if(tag == «Car») { if(transform.position.y < -50f) { Destroy(gameObject);
}
}
}
}
Можно запустить сцену и проверить, как движется машина. Камеру перед этим лучше сместить вбок, чтобы видеть, вращаются ли колёса.
Теперь нужно написать скрипт, который позволит управлять машиной. Назовём его Controls и добавим несколько переменных:
public float speed = 0f; public float maxSpeed = 0.5f; public float sideSpeed = 0f;
Само управление выглядит так:
void Update()
{ float moveSide = Input.GetAxis(«Horizontal»); 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:
Затем нужно добавить в метод Update() скрипта Moving следующий код:
if(control != null)
{
newSpeed += control.speed;
sideSpeed = control.sideSpeed;
}
Его надо вставить сразу после объявления переменных newSpeed и sideSpeed. Теперь машиной можно управлять:
Чтобы машина не падала в пропасть, нужно добавить бесконечную генерацию дороги.
- Перед машиной будет добавляться столько дорожных блоков, сколько нужно, чтобы не было видно пропасти.
- Когда машина проезжает достаточное расстояние от определённого блока, он удаляется, чтобы освободить оперативную память.
Для этого создадим скрипт RoadBlock и прикрепим его к дорожному блоку. В нём будет всего два метода:
public bool Fetch(float x) { bool result = false; if(x > transform.position.x + 100f) { result = true; } return result;
} public void Delete() { Destroy(gameObject);
}
Сохраните блок как префаб. А инстанцироваться новые объекты будут в скрипте Road:
public List blocks; public GameObject player; public GameObject roadPrefab; public GameObject carPrefab; public GameObject coinPrefab; private System.Random rand = new System.Random(); void Update()
{ float x = player.GetComponent().rb.position.x; var last = blocks[blocks.Count — 1]; if(x > last.transform.position.x — 24.69f * 10f) { 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); blocks.Add(block); } foreach (GameObject block in blocks) { bool fetched = block.GetComponent().Fetch(x); if(fetched) { blocks.Remove(block); block.GetComponent().Delete();
}
}
}
Те блоки, которые есть на сцене, нужно поместить в коллекцию Blocks. Также надо сразу заполнить остальные переменные.
Теперь можно проверить, как это выглядит:
Чтобы генерировать машины, нужно сначала создать префаб. Для этого можно скопировать объект 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);
Перед этим не забудьте добавить префаб в переменную carPrefab.
Чтобы игра стала интереснее, можно добавить монеты, при столкновении с которыми игрок будет получать очки.
Для этого поместите на сцену модель монеты, к ней подключите коллайдер с триггером (он обрабатываться не будет, но нужен для того, чтобы машины NPC проходили сквозь монету).
Потом создайте скрипт 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) { 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);
}
Можно проверять:
Чтобы игрок мог получать очки, в скрипт Controls нужно добавить следующие переменные:
public float scores = 0f; public float highScore = 0f;
Добавление очков будет происходить в файле Moving в блоке работы со скриптом Controls:
Чтобы выводить, сколько очков набрал пользователь, нужно создать элемент Canvas и добавить в него текстовый блок:
Для него создайте скрипт Scores со следующим кодом:
using UnityEngine;
using TMPro;
using System; public class Scores : MonoBehaviour
{ private TextMeshProUGUI text; private Controls controls; public GameObject player; void Start() { text = GetComponent(); controls = player.GetComponent(); } void Update() { if(controls != null) { text.text = $»Highscore: {Math.Floor(controls.highScore)} | Scores: {Math.Floor(controls.scores)}»;
}
}
}
Обратите внимание, что тут используется не стандартный текстовый элемент, а объект из библиотеки TextMeshPro.
Теперь нужно добавить логику столкновений с другими объектами. Для начала создадим префаб сломанной машины:
Это та же модель, но здесь для каждого колеса добавлен коллайдер и 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; other.GetComponent().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); SceneManager.LoadScene("Menu");
}
Переход в меню можно пока закомментировать, потому что оно ещё не готово.
Вот что должно получиться:
Теперь можно добавить звуки:
- музыку;
- звук мотора;
- звук столкновения с машиной;
- звук столкновения с монетой.
Чтобы добавить музыку, к объекту Player подключите компонент Audio Source:
Укажите файл, громкость и поставьте галочку возле Loop, чтобы мелодия повторялась.
Затем добавим звук мотора. Для этого найдите звук, в котором есть монотонный рёв. Его высота будет меняться программным путём, когда игрок будет ускоряться или замедляться.
Добавить к модели машины Audio Source:
А затем в файле Moving добавьте такой код:
car.GetComponent().pitch = 2 + newSpeed;
Звук столкновения нужно добавить в префаб сломанной машины, убрав зацикливание. А звук монеты — в пустой объект, который мы инстанцировали в скрипте монеты при столкновении.
Последний штрих — главное меню. Оно будет находиться в отдельной сцене. Создайте скрипт, который будет называться Menu:
using UnityEngine;
using UnityEngine.SceneManagement; public class Menu : MonoBehaviour
{ public void Play() { SceneManager.LoadScene(«Main»); } public void Exit() { Application.Quit();
}
}
Добавьте кнопки и укажите методы, которые будут вызываться при нажатии на них:
При нажатии на Play будет вызываться метод Play(), а при нажатии на Exit — Exit(). Можно добавить монетки и несколько дорожных блоков, чтобы меню выглядело красивее.
Получилась довольно простая игра. Для разработчика это интересный опыт, но игрокам она быстро наскучит. Чтобы этого не произошло, можно добавить несколько простых улучшений:
- накопление денег;
- покупку новых машин;
- более высокие скорость и манёвренность для более дорогих машин;
- смену полос для машин и так далее.
Если же вы хотите делать игры покруче, записывайтесь на наш курс «Профессия Разработчик игр на Unity». На нём вы научитесь работать с шейдерами, графикой высокого разрешения и разными механиками, а также многим другим.
Создание псевдотрёхмерной гоночной игры
В детстве я редко ходил в залы аркадных автоматов, потому что особо в них не нуждался, ведь дома у меня были потрясающие игры для C64… но есть три аркадные игры, на которые у меня всегда находились деньги — Donkey Kong, Dragons Lair и Outrun… … и я очень любил Outrun — скорость, холмы, пальмы и музыка, даже на слабой версии для C64. Поэтому я решил попробовать написать олдскульную псевдотрёхмерную гоночную игру в стиле Outrun, Pitstop или Pole position. Я не планирую собрать полную и завершённую игру, но мне кажется, будет интересно заново изучить механики, при помощи которых эти игры реализовывали свои трюки. Кривые, холмы, спрайты и ощущение скорости… Итак, вот мой «проект на выходные», который в итоге занял пять или шесть недель по выходным
- Сыграть в игру
- Посмотреть исходный код
Играбельная версия больше напоминает техническое демо, чем реальную игру. На самом деле, если вы хотите создать настоящую псевдотрёхмерную гонку, то это будет самый минимальная основа, которую нужно постепенно превратить в игру. Она не отшлифована, немного уродлива, но полностью функциональна. Я покажу, как реализовать её самостоятельно за четыре простых шага. Можно также поиграть
- в демо с прямой дорогой
- в демо с кривыми
- в демо с холмами
- в готовую версию
О производительности
Производительность этой игры очень сильно зависит от машины/браузера. В современных браузерах она работает хорошо, особенно в тех, где есть GPU-ускорение canvas, но плохой графический драйвер может привести к зависанию. В игре можно менять разрешение рендеринга и расстоянием отрисовки.
О структуре кода
Так получилось, что проект реализован на Javascript (из-за простоты прототипирования), но он не предназначен для демонстрации техник или рекомендованных приёмов Javascript.
На самом деле, для простоты понимания Javascript каждого примера встроен непосредственно в HTML-страницу (ужас!); хуже того, в нём используются глобальные переменные и функции.
Если бы я создавал реальную игру, то код был бы гораздо более структурирован и упорядочен, но так как это техническое демо гоночной игры, я решил придерживаться KISS.
Часть 1. Прямые дороги
Итак, как же нам приступить к созданию псевдотрёхмерной гоночной игры? Ну, нам потребуется
- Повторить тригонометрию
- Вспомнить основы 3d-проецирования
- Создать игровой цикл
- Загрузить спрайтовые изображения
- Построить геометрию дороги
- Отрендерить фон
- Отрендерить дорогу
- Отрендерить машину
- Реализовать поддержку клавиатуры для управления машиной
Но прежде чем мы начнём, давайте прочитаем Lou’s Pseudo 3d Page [перевод на Хабре] — единственный источник информации (который мне удалось найти) о том, как создать псевдотрёхмерную гоночную игру. Закончили читать статью Лу? Отлично! Мы будем создавать вариацию его техники «Realistic Hills Using 3d-Projected Segments». Мы будем делать это постепенно, на протяжении следующих четырёх частей. Но начнём мы сейчас, с версии v1, и создадим очень простую геометрию прямой дороги, спроецировав её на HTML5-элемент canvas. Демо можно посмотреть здесь.
Немного тригонометрии
Прежде чем заняться реализацией, давайте воспользуемся основами тригонометрии, чтобы вспомнить, как спроецировать точку в 3D-мире на 2D-экран.
В самом простейшем случае, если не касаться векторов и матриц, для 3D-проецирования используется закон подобных треугольников.
Используем следующие обозначения:
- h = высота камеры
- d = расстояние от камеры до экрана
- z = расстояние от камеры до машины
- y = координата Y экрана
Тогда мы можем использовать закон подобных треугольников для вычисления y = h*d/z как показано на схеме: Можно было также нарисовать похожую схему в виде сверху вместо вида сбоку, и вывести похожее уравнение для вычисления координаты X экрана: x = w*d/z Где w = половина ширины дороги (от камеры до края дороги).
Как видите, для x, и y мы выполняем масштабирование на коэффициент
d/z
Системы координат
В виде схемы это выглядит красиво и просто, но начав кодить, вы можете немного запутаться, потому что мы выбрали произвольные наименования, и непонятно, чем мы обозначили координаты 3D-мира, а чем координаты 2D-экрана.
Также мы предполагаем, что камера находится в центре начала координат мира, хотя в реальности она будет следовать за машиной.
Если подходить более формально, то нам нужно выполнять:
- преобразование из координат мира в координаты экрана
- проецирование координат камеры на нормализованную плоскость проекции
- масштабирование спроецированных координат на координаты физического экрана (в нашем случае это canvas)
Примечание: в настоящей 3d-системе между этапами 1 и 2 выполняется этап поворота, но поскольку мы будем имитировать кривые, то поворот нам не нужен.
Проецирование
Формальные уравнения проецирования можно представить следующим образом:
- Уравнения преобразования (translate) вычисляют точку относительно камеры
- Уравнения проецирования (project) являются вариациями показанного выше «закона подобных треугольников»
- Уравнения масштабирования (scale) учитывают разность между:
- математикой, где 0,0 находится в центре, а ось y направлена вверх, и
- компьютерами, где 0,0 находится в левом верхнем углу, а ось y направлена вниз:
Примечание: в полнофункциональной 3d-системе нам нужно было бы более формально задать класс Vector и Matrix для выполнения более надёжной 3d-математики, и если мы бы сделали это, то стоило бы просто использовать WebGL (или его аналог)… но в нашем проекте это не требуется. Я хотел придерживаться олдскульной псевдотрёхмерности для создания игры в стиле Outrun.
Ещё немного тригонометрии
Последним куском головоломки станет способ вычисления d — расстояния от камеры до плоскости проецирования.
Вместо того, чтобы просто прописывать жёстко заданное значение d, более полезно будет вычислять его из нужной вертикальной области обзора. Благодаря этому мы сможем при необходимости «зумить» камеру.
Если предположить, что мы выполняем проецирование на нормализованную плоскость проецирования, координаты которой находятся в интервале от -1 до +1, то d можно вычислить следующим образом:
d = 1/tan(fov/2) Задавая fov как одну (из множества) переменных, мы сможем настраивать область обзора для тонкой подстройки алгоритма рендеринга.
Структура кода на Javascript
В начале статьи я уже сказал, что код не совсем соответствует рекомендациям по написанию Javascript — это «быстрое и грязное» демо с простыми глобальными переменными и функциями. Однако поскольку я собираюсь создать четыре отдельные версии (прямые, кривые, холмы и спрайты), то буду хранить некоторые многократно используемые методы внутри common.
js в рамках следующих модулей:
- Dom — несколько мелких вспомогательных функций DOM.
- Util — общие утилиты, в основном вспомогательные математические функции.
- Game — общие игровые вспомогательные функции, например, загрузчик изображений и игровой цикл.
- Render — вспомогательные функции рендеринга на canvas.
Подробно я буду объяснять методы из common.js, только если они относятся к самой игре, а не являются просто вспомогательными математическими или DOM-функциями. Надеюсь, из названия и контекста будет понятно, что должны делать методы.
Как обычно, исходный код находится в окончательной документации.
Простой игровой цикл
Прежде чем что-то рендерить, нам нужен игровой цикл.
Если вы читали любую из моих предыдущих статей про игры (pong, breakout, tetris, snakes или boulderdash), то уже видели примеры моего любимого игрового цикла с фиксированным шагом времени.
Я не буду вдаваться глубоко в подробности, и просто повторно использую часть кода из предыдущих игр, чтобы создать игровой цикл с фиксированным шагом времени при помощи requestAnimationFrame.
Принцип заключается в том, что каждый из моих четырёх примеров может вызывать Game.run(…) and и использовать собственные версии
- update — обновление игрового мира с фиксированным шагом времени.
- render — обновление игрового мира, когда позволяет браузер.
run: function(options) {
Game.loadImages(options.images, function(images) {
var update = options.update, // method to update game logic is provided by caller
render = options.render, // method to render the game is provided by caller
step = options.step, // fixed frame step (1/fps) is specified by caller
now = null,
last = Util.timestamp(),
dt = 0,
gdt = 0;
function frame() {
now = Util.timestamp();
dt = Math.min(1, (now — last) / 1000); // using requestAnimationFrame have to be able to handle large delta's caused when it 'hibernates' in a background or non-visible tab
gdt = gdt + dt;
while (gdt > step) {
gdt = gdt — step;
update(step);
}
render();
last = now;
requestAnimationFrame(frame);
}
frame(); // lets get this party started
});
} Повторюсь, это переделка идей из моих предыдущих игр на canvas, поэтому если вам непонятно, как работает игровой цикл, то вернитесь к одной из предыдущих статей.
Изображения и спрайты
Прежде чем начнётся игровой цикл, мы загружаем два отдельных спрайтшита (листа спрайтов):
- background — три параллаксных слоя для неба, холмов и деревьев
- sprites — спрайты машины (плюс деревья и билборды, которые будут добавлены в окончательную версию)
Спрайтшит был сгенерирован с помощью небольшого таска Rake и Ruby Gem sprite-factory.
Этот таск генерирует объединённые спрайтшиты, а также координаты x,y,w,h, которые будут храниться в константах BACKGROUND и SPRITES.
Примечание: фоны созданы мной при помощи Inkscape, а большинство спрайтов — это графика, позаимствованная из старой версии Outrun для Genesis и использованная в качестве обучающих примеров.
Игровые переменные
В дополнение к изображениям фонов и спрайтов нам понадобится несколько игровых переменных, а именно: var fps = 60; // how many 'update' frames per second
var step = 1/fps; // how long is each frame (in seconds)
var width = 1024; // logical canvas width
var height = 768; // logical canvas height
var segments = []; // array of road segments
var canvas = Dom.get('canvas'); // our canvas…
var ctx = canvas.getContext('2d'); // …and its drawing context
var background = null; // our background image (loaded below)
var sprites = null; // our spritesheet (loaded below)
var resolution = null; // scaling factor to provide resolution independence (computed)
var roadWidth = 2000; // actually half the roads width, easier math if the road spans from -roadWidth to +roadWidth
var segmentLength = 200; // length of a single segment
var rumbleLength = 3; // number of segments per red/white rumble strip
var trackLength = null; // z length of entire track (computed)
var lanes = 3; // number of lanes
var fieldOfView = 100; // angle (degrees) for field of view
var cameraHeight = 1000; // z height of camera
var cameraDepth = null; // z distance camera is from screen (computed)
var drawDistance = 300; // number of segments to draw
var playerX = 0; // player x offset from center of road (-1 to 1 to stay independent of roadWidth)
var playerZ = null; // player relative z distance from camera (computed)
var fogDensity = 5; // exponential fog density
var position = 0; // current camera Z position (add playerZ to get player's absolute Z position)
var speed = 0; // current speed
var maxSpeed = segmentLength/step; // top speed (ensure we can't move more than 1 segment in a single frame to make collision detection easier)
var accel = maxSpeed/5; // acceleration rate — tuned until it 'felt' right
var breaking = -maxSpeed; // deceleration rate when braking
var decel = -maxSpeed/5; // 'natural' deceleration rate when neither accelerating, nor braking
var offRoadDecel = -maxSpeed/2; // off road deceleration is somewhere in between
var offRoadLimit = maxSpeed/4; // limit when off road deceleration no longer applies (e.g. you can always go at least this speed even when off road) Некоторые из них можно настраивать при помощи элементов управления UI для изменения критически важных значений в процессе выполнения программы, чтобы можно было увидеть, как они влияют на рендеринг дороги. Другие повторно вычисляются из настраиваемых UI значений в методе reset().
Управляем Ferrari
Мы выполняем привязку клавиш для Game.run, которая обеспечивает простой ввод с клавиатуры, задающий или сбрасывающий переменные, сообщающие о текущих действиях игрока: Game.run({
…
keys: [
{ keys: [KEY.LEFT, KEY.A], mode: 'down', action: function() { keyLeft = true; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'down', action: function() { keyRight = true; } },
{ keys: [KEY.UP, KEY.W], mode: 'down', action: function() { keyFaster = true; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'down', action: function() { keySlower = true; } },
{ keys: [KEY.LEFT, KEY.A], mode: 'up', action: function() { keyLeft = false; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'up', action: function() { keyRight = false; } },
{ keys: [KEY.UP, KEY.W], mode: 'up', action: function() { keyFaster = false; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'up', action: function() { keySlower = false; } }
],
…
} Состоянием игрока управляют следующие переменные:
- speed — текущая скорость.
- position — текущая позиция по Z на трассе. Заметьте, что это позиция камеры, а не Ferrari.
- playerX — текущая позиция игрока по X на дороге. Нормализована в интервале от -1 до +1, чтобы не зависеть от действительного значения roadWidth.
Эти переменные задаются внутри метода update, который выполняет следующие действия:
- обновляет position на основании текущей speed.
- обновляет playerX при нажатии на клавиши «влево» или «вправо».
- увеличивает speed, если нажата клавиша «вверх».
- уменьшает speed, если нажата клавиша «вниз».
- уменьшает speed, если не нажаты клавиши «вверх» и «вниз».
- уменьшает speed, если playerX находится за краем дороги и на траве.
Программирование на скретч: как создать игру-гонки на Scratch
Scratch — это удобное приложение, которое можно использовать для создания мультфильмов и простых симуляторов. Язык пользуется особой популярностью у детей и подростков.
По принципу «Лего» юные пользователи познают принципы программирования и создают анимацию без написания сложного кода.
Для реализации проекта достаточно задать необходимое количество команд и алгоритмов объектам среды путем перетаскивания блоков в область скрипта.
Составными частями Scratch значатся:
- Графические объекты (спрайты), состоящие из кадров-костюмов с заданным сценарием (скриптом).
- Сцена с координатной плоскостью 480×360 пикселей.
- Палитра блоков, распределенная по 10 категориям, а именно:
- движение — регулируют перемещение персонажа;
- внешность — задают визуальные эффекты;
- звук — добавляют аудио;
- перо — реализует дополнительные расширения;
- данные — формируют переменные и списки;
- события — направляют сигналы ко всем объектам и создают события;
- управление — регулируют конструкции;
- сенсоры — задают имя и таймеры;
- операторы — вставляют формулы и арифметические операции;
- другие — создают свои варианты и преобразуют несколько элементов в один.
Помимо разновидности команд, строительные элементы подразделяются на:
- Стеки с выемкой и выступом для сцепления с другими компонентами.
- Циклы (варианты стеков), имеющие С-образную форму, способные охватывать большие и малые комбинации.
- Заголовки с выпуклым верхним краем и выступом для замыкания внизу.
- Ссылки, служащие наполнением внутреннего пространства других элементов.
Начало работы
Сегодня мы создадим гоночный симулятор в простой вариации с обозрением пространства сверху. В уроке будет реализовано управление каром с функцией торможения при выезде за пределы трека, таймером и счетчиком кругов.
Открываем новый план в Scratch и добавляем фон через иконку на верхней панели. В качестве основной локации используем трассу, по которой будут кататься гоночные автомобили.
Персонаж котенок перед заездом будет произносить три фразы: «На старт», «Внимание», «Марш», что будет служить сигналом начала гонок.
Добавляем первый скрипт
Для того чтобы котенок озвучил команды, необходимо перетащить в рабочую область скрипт «когда щелкнуть по флагу» и сцепить его с блоком «говорить «_» в течение «_» секунд».
После этого вписать в пустое пространство фразу «На старт» и поставить время — 1 секунда, а следом создать дубли со словами «Внимание» и «Марш». Завершает сцепку опция «передать старт» другим объектам.
Это и послужит отправной точкой для гонки.
Ввод в пространство других объектов
Первым из спрайтов добавляем «Старт», обозначающий начало и финиш трассы, следом загружаем в среду «Игрока» в виде автомобиля. Для этого открываем папку с изображениями, кликнув на иконку, указанную стрелкой.
Для гоночного кара сразу добавим две переменных:
- «Игрок. Максимальная скорость» послужит величиной предельной быстроты движения, которую машина не сможет превысить.
- «Игрок. Скорость» задаст темп по умолчанию.
После ввода новых объектов разметка должна выглядеть, как на картинке ниже.
Добавляем скрипт для машины
Чтобы правильно стартовать, автомобилю необходимо встать на исходную позицию и развернуться в нужном направлении. Для этого перенесите в рабочую область под заголовок «когда щелкнут по флажку» соответствующие координаты, значения переменных максимальной и стандартной скорости.
Управлять машиной пользователь сможет тремя клавишами:
- Стрелка вверх. Удерживание добавит ускорения, а при отпускании будет происходить торможение.
- Стрелки влево или вправо повернут руль в соответствующую сторону.
Для контроля потребуется создать комбинацию из компонентов в указанной последовательности:
- Перетащите условие «когда я получу старт», добавьте цикл «всегда» и сцепите его с С-образным стеком «если иначе», а после составьте конструкцию из условий и ограничений.
- Когда пользователь нажал клавишу «вверх», машина газует, а скорость увеличивается. При этом персонаж должен пройти заданное количество шагов с текущей скоростью. Здесь же ставим ограничение, добавив переменную максимальной скорости. Таким образом, машина не сможет разогнаться быстрее заданного темпа.
- Когда пользователь отпускает клавишу, авто должно тормозить. Для этого условие «иначе» склеиваем с элементом «Игрок. Скорость» и устанавливаем значение -1. Чтобы предотвратить самопроизвольный задний ход, обозначаем, что если скорость меньше 0, то она равна 0.
- Для поворотов строем скрипт: если нажата клавиша влево, то машина поворачивает против часовой стрелки на заданное количество градусов. Дублируем команду для заруливания вправо.
В итоге должна получиться структура, как на иллюстрации:
Первая анимация готова. Чтобы протестировать результат, нужно кликнуть на зеленый флаг, расположенный сверху игрового поля, и оценить удобство управления.
Если машина едет слишком быстро, рекомендуется уменьшить шаг, с которым двигается персонаж. В соответствующем элементе следует добавить арифметический компонент и частное «Игрок. Скорость», деленное на 3.
После того как управление машиной запрограммировано, переходим к следующему этапу.
Торможение автомобиля
Чтобы лучше контролировать движение в пределах трека, добавляем функцию тормоза следующим образом.
Если авто касается темно-зеленого цвета, то скорость меняется на -2. Также добавляем ограничение, чтобы не уйти в отрицательное значение и не поехать назад.
На этом работа над управлением персонажем закончена. Запускаем игру и видим, что автомобиль свободно едет по трассе, но при этом тормозит при заносах на зеленую область, что и было задумано.
Мы проехали цикл и пришли к финишу, однако это еще не конец нашего проекта.
Конструируем счетчик
По условиям персонаж должен объехать трек три раза. Для воплощения задумки потребуется ввести переменные: «Проехать кругов» и «Игрок. Осталось кругов».
Перед построением алгоритма добавим в стартовый сценарий комбинацию:
задать «Проехать кругов» значение 3.
Для персонажа гонщика состыкуем переменную «Игрок. Осталось кругов» со значением «Проехать кругов» в самом начале игры.
Добавим условие: если касается старта, то уменьшаем переменную «Игрок. Осталось кругов» на -1.
После этого можно запустить симулятор и протестировать счетчик.
При оценке результата можно заметить, что пересечение стартовой черты из любого положения на поле автоматически уменьшает счетчик на одну единицу. В связи с этим нам понадобится защита от читерства. Нужно проконтролировать, что машина проехала весь трек и только после этого пересекла финишную черту.
Для воплощения цели необходимо определить контрольные точки на трассе, через которые автомобиль обязан пройти.
Создаем новый скрипт «Когда я получу старт», а далее:
- перетаскиваем в рабочую область цикл «повторить» и накладываем на него переменную «Проехать кругов»;
- определяем начальную зону контроля на окончании первой трети гоночного трека;
- вкладываем в скрипт цикл «ждем» и прописываем первую проверочную точку, где Х больше 120;
- дублируем предыдущий пункт и обозначаем вторую точку как Y равное 0;
- третья координата будет параллельна первой, поэтому в дубле указываем Х меньше, чем -120.
- завершает цикл условие касание старта.
Теперь только после прохождения всех координат зачтется полный трек, и можно уменьшить их количество на единицу, обозначив это в скрипте.
Также следует удалить первое условие прохождения круга, обозначенное выше.
Запускаем заезд и видим, что счетчик меняется только после прохождения точек на треке и пересечения финиша. Цель сценария достигнута.
Создаем таймер
Финальным штрихом мы добавим в проект таймер и узнаем, за сколько нам удалось пройти гонку.
В начале игры мы делаем перезапуск времени в Scratch и располагаем скрипт «перезапустить таймер» в сценарий для котенка. Для удобства создадим переменную «мой таймер», присвоив начальное значение, равное нулю, и разместим ее над вступительными фразами котенка.
Следом составим сценарий для гоночного трека. Когда начинается игра, мы каждый раз будем обновлять время, беря его из текущего значения Scratch. Трансляция значения переменной «мой таймер» будет исходить из текущего сенсора.
А теперь модифицируем скрипт авто. Для этого напишем переменную «Игрок. Результат» для отображения достижений. И достроим сценарий «Когда я получу старт» следующим образом:
- Переменной «Игрок. Результат» присваиваем текущее значение «Мой таймер».
- Приклеиваем элемент «говорить в течение одной секунды» + «Игрок. Результат».
После этого игра завершается.
Симулятор готов, давайте проедем гонку от начала до конца. На старт! Внимание! Марш!