Создание системы управления в unity, да и в играх в целом, очень специфичная область программирования. Ведь система подстраивается под нужды проекта, а значит и сложность ее будет зависеть от сложности проекта и уровня ваших знаний и подготовки.
Конечно, когда мы говорим о жанре платформер, тем более в 2D играх, то система управления здесь реализуется на порядок проще, чем в более крупных 3D играх, к тому же, Unity предлагает множество уже готовых решений и все, что вам требуется – только правильно их использовать.
Если на ПК управление в большей части реализуется за счет мыши и клавиатуры, то с сенсорными экранами все немножко сложнее. На экране не расположишь все кнопки, поэтому приходится прибегать к дополнительным системам отлавливания действий игрока и управления персонажем.
В этой статье попробуем рассмотреть один из примеров такой системы управления.
И так, начнем с небольшой программной части, а именно – с событийной части взаимодействия игрока и персонажа через команды.
Система команд
Так как игра предположительно будет работать на смартфонах, то кнопки управления персонажем будут находится на экране. Для создания пользовательского интерфейса используем систему Unity UI, поэтому создаем новый канвас на сцене, и три изображения кнопок управления: бежать влево Left run, вправо Right run и прыжок Jump.
Заметьте, что в примере используются не кнопки Button, а простые Image – дальше мы добавим весь необходимый функционал этим изображениям, чтобы они работали как кнопки.
Все команды по управлению персонажем будут поступать с панели UI. Чтобы разделять команды, заведем небольшое перечисление enum, где укажем, какие действия может принимать персонаж.
Создаем скрипт с перечислениями ActionType в котором будут находится команды.
- public enum ActionType {
- None = 0,
- MoveRight = 1,
- MoveLeft = 2,
- Jump = 3,
- Die = 4,
- }
Теперь создадим сам скрипт персонажа Character.
- public sealed class Character : MonoBehaviour {
- }
Добавим в этот скрипт метод, через который будут поступать команды персонажу.
- public sealed class Character : MonoBehaviour {
- public void OnDoAction(ActionType action) {
- print(“Выполнить действие: “ + action);
- }
- }
Метод OnDoAction будет принимать определенною команду из перечисления ActionType.
Дальше создадим персонажа на сцене.
Мы используем бесплатный набор для персонажа и других объектов отсюда.
Дальше кидаем скрипт Character на персонажа на сцене и возвращаемся к канвасу, где продолжим настраивать наши кнопки.
Так как мы не используем стандартные кнопки Button по причине того, что они могут отловить только событие нажатия, мы будем настраивать изображения через компонент EventTrigger, который позволяет отлавливать кучу других полезных действий.
Чтобы добавить на изображение компонент EventTrigger, необходимо выбрать Image, перейти по вкладке Component -> Event и добавить компонент EventTrigger, ну или его можно найти по названию.
Этот компонент EventTrigger имеет все необходимые действия для работы с изображениями. Нам понадобится только PointerUp и PointerDown, для событий нажатия на изображение и события “отжатия”.
Поэтому нажимаем кнопку AddNewEventType в компоненте EventTrigger и ищем эти события, после чего добавляем их.
Почему именно PointerUp и PointerDown?
Потому что персонаж будет передвигаться только пока вы “зажали” кнопку передвижения, после ее отпускания необходимо будет вызвать команду остановки, чтобы персонаж остановился.
Поэтому кнопкам Left run и Right run (влево и вправо), в компоненте EventTrigger добавим по одному действию к каждый список: в Pointer Down действие “начала движения”, а в Pointer Up действие “остановки движения”. Через эти действия мы будем передавать команды персонажу в метод OnDoAction.
Передавать команды в виде их названий через кнопки нельзя, поэтому возвращаемся в скрипт Character и создадим еще один, похожий на OnDoAction метод, в который будем передавать числовой номер команды actionId.
- public sealed class Character : MonoBehaviour {
- public void OnDoAction(int actionId) {
- OnDoAction((ActionType)actionId);
- }
- public void OnDoAction(ActionType action) {
- print(“Выполнить действие: “ + action);
- }
- }
Теперь можно добавить действия кнопкам. В каждой кнопке, в списках Pointer Down и Pointer Up в качестве цели выбираем скрипт нашего персонажа Character, после чего, в раскрывающемся списке ищем метод OnDoAction.
Добавляем его в список действий.
Дальше в этих действиях указываем числовой номер команды из перечисления ActionType. К примеру, кнопка Right run в списке Pointer Down будет содержать метод OnDoAction с номером команды 1 – то есть, что в перечислении обозначает команду MoveRight, а в списке Pointer Up будет содержать тот же метод OnDoAction с номером команды 0, что обозначает исходное положение None из того-же перечисления. Кнопка Left run, в списке Pointer Down будет иметь номер команды 2 – MoveLeft.
С кнопкой Jump все еще проще, ей нужно добавить только одно событие Pointer Click, так как кнопка будет срабатывать один раз. Также добавляем в список действие и числовой номер команды – 3 Jump для прыжка.
После этого можно запустить и протестировать вызов команд в игре, нажатием на разные кнопки.
Обработка команд
Теперь, когда мы закончили с визуальной частью управления, переходим к самой обработке поступивших команд.
Возвращаемся в скрипт Character, где для начала добавим несколько переменных: переменную направления directionRight, и переключатели onMoving и onMakeJump для движения и совершения прыжка.
- public sealed class Character : MonoBehaviour {
- private bool directionRight = true;
- private bool onMoving, onMakeJump;
- public void OnDoAction(int actionId) {
- OnDoAction((ActionType)actionId);
- }
- public void OnDoAction(ActionType action) {
- print(“Выполнить действие: “ + action);
- }
- }
Переменная directionRight будет указывать, что объект движется вправо когда ее значение будет true и влево, когда значение станет false, по умолчанию наш персонаж на сцене смотрит вправо, значит и переменная directionRight также будет иметь, по умолчанию, значение true. Другая переменная onMoving будет указывать на то что поступила команда двигаться с панели управления, то же самое и для переменной onMakeJump, только для команды прыжка.
Теперь переходим к методу OnDoAction, где разберем действия персонажа на поступившие команды.
Для начала разберем команды бега MoveRight и MoveLeft.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- public void OnDoAction(ActionType action) {
- if (action == ActionType.MoveRight) {
- this.onMoving = this.directionRight = true;
- } else if (action == ActionType.MoveLeft) {
- this.directionRight = false;
- this.onMoving = true;
- }
- }
- }
Используем простые условия: если поступила команда двигаться вправо MoveRight, то переменная directionRight становится true. Это значит, что персонаж будет двигаться вправо, то же самое делаем и для команды двигаться влево MoveLeft, только тогда значение directionRight будет false.
При поступлении команды None мы должны будем остановить любое движение персонажа – то есть перевести значение переменной onMoving в false.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- public void OnDoAction(ActionType action) {
- if (action == ActionType.MoveRight) {
- this.onMoving = this.directionRight = true;
- } else if (action == ActionType.MoveLeft) {
- this.directionRight = false;
- this.onMoving = true;
- } else if (action == ActionType.None) {
- this.onMoving = false;
- }
- }
- }
Далее переходим к команде прыжка Jump.
Перед тем как выполнить эту команду, необходимо дополнительно проверить, стоит ли на “земле” наш персонаж. Для этого добавим простой коллайдер BoxCollider2D персонажу и настроим его.
В скрипте Character создадим новый метод CheckGround для проверки нахождения поверхности и переменную коллайдера objCollider.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- private BoxCollider2D objCollider;
- private bool CheckGround() {}
- public new BoxCollider2D collider {
- get {
- if (this.objCollider == null) this.objCollider = GetComponent<BoxCollider2D>();
- return this.objCollider;
- }
- }
- }
Так как в игре персонаж будет иметь самый простой тип коллайдера – BoxCollider2D, то свойство collider будет возвращать именно этот компонент.
В методе CheckGround будем проверять, находится ли персонаж на поверхности. Для этого проверим коллайдер персонажа на соприкосновение с землей.
- private bool CheckGround() {
- Bounds bounds = this.collider.bounds;
- Vector2 point = bounds.center;
- Vector2 size = bounds.size;
- bool result = Physics2D.OverlapBox(point, size, 0f);
- return result;
- }
Для этого используем позицию point и размер size коллайдера персонажа.
Далее методом Physics2D.OverlapBox проверим наличие поверхности под “ногами”.
Продолжаем работать с командой Jump, для этого вернемся в метод OnDoAction, где добавим новое условие.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- public void OnDoAction(ActionType action) {
- if (action == ActionType.MoveRight) {
- this.onMoving = this.directionRight = true;
- } else if (action == ActionType.MoveLeft) {
- this.directionRight = false;
- this.onMoving = true;
- } else if (action == ActionType.None) {
- this.onMoving = false;
- } else if (action == ActionType.Jump) {
- if (CheckGround()) this.onMakeJump = true;
- }
- }
- }
Проверим с помощью метода CheckGround поверхность и установим переменной onMakeJump значение true, чтобы совершить прыжок.
Вот так наш персонаж будет обрабатывать поступающие команды от игрока, теперь можно переходить ко второй части статьи – к методам передвижения персонажа.
Методы передвижения. Физика.
Для того, чтобы четко определить какую систему для перемещения объекта выбрать, Transform или Rigidbody, нужно решить? какими свойства этот объект будет обладать, ведь если он просто падает сверху вниз, это еще не повод сразу же использовать Rigidbody.
Физический движок Unity – это самостоятельная система обработки физических объектов и чем больше этих объектов будет на сцене? тем больше ресурсов уйдет на их обработку, так что для лучшей работы игры следует максимально минимизировать их кол-во.
Для персонажа выделим его главные требования: он должен четко взаимодействовать с окружением, которое состоит из коллайдеров, в первую очередь это пол и стены.
Для любого физического взаимодействия между объектами в Unity, эти объекты должны иметь компонент Rigidbody, ну или по крайней мере тот, кто совершает действие – в нашем случае это персонаж.
Самое главное, с чего нужно начать, так это выбрать методы обработки перемещения персонажа.
Если вы уже знакомы с компонентом Rigidbody, то значит замечали, что этот компонент похож на стандартный Transform. Оба этих компонента могут работать с позицией и поворотом персонажа, оба этих компонента можно использовать для перемещения объекта. Transform чаще используют для перемещения не физических объектов, например, для полета в игре огненного шара используется простое перемещение через Transform, но для физического объекта такое движение использовать не рекомендуется.
Методы обработки
Для обработки движения двух разных типов объектов (Transform и Rigidbody) существует два разных метода Update и FixedUpdate – каждый из них используется для своих целей.
Общее у этих методов то, что они оба вызываются через определенное время, Update – каждый кадр, а FixedUpdate – каждый промежуток времени.
В первом случае метод Update вызывается столько раз, сколько позволяет частота кадров в игре, то есть если у вас частота к примеру 60 кадров в 1 секунду, значит, что и метод Update сработает 60 раз за 1 секунду.
Метод Update отлично подходит для обработки движений с помощью Transform’а, так как движения, в этом случае, будут синхронизированы с обработкой графической составляющей игры, то есть все вычисления по перемещению будут выполнены в момент отрисовки кадра, отсюда следует, что объект будет занимать фактически именно ту позицию, которую вы видите на экране в данный момент.
Во втором случае имеем метод FixedUpdate. Этот метод выполняется через каждый промежуток времени, по умолчанию стоит значение – раз в 0.02 секунды, то есть вызов метода FixedUpdate происходит интервалом в 0.02 секунды. Физический движок работает отдельно от системы графической обработки в Unity и он не обязательно выполняет обработку физики каждый кадр, как это делает метод Update для движения Transform’а. При низкой частоте кадров (к примеру 1 кадр в 1 секунду) в игре могут возникать рывки и дерганья объектов Rigidbody во время перемещения. Поскольку расчет физики будет так же производится каждые 0.02 секунды – графическая система просто не будет успевать обновлять кадры с движением объекта, но при нормальной частоте кадров можно получить плавное и четкое взаимодействие всех физических объектов на сцене.
Главное различие между этими двумя методами, которое нужно запомнить это то, что FixedUpdate выполняет расчет физических параметров объекта, а Update, в первую очередь – графических.
Многие начинающие разработчики часто работают с Rigidbody в разных методах: ускоряют, замедляют их, придают им импульсы, бросают и так далее. Но с физическими телами рекомендуется работать именно в методе FixedUpdat. Даже если вы 10 раз измените позицию тела Rigidbody в том же методе Update или в любом другом методе, то все равно эти изменения вступят в силу только в момент вызова метода FixedUpdate, какая частота кадров у вас бы ни была.
Поэтому в примере работа с Transform’ом происходит только в методе Update, а с Rigidbody только в методе FixedUpdate.
Тело
В отличие от Transform’а у физического тела Rigidbody есть несколько способов перемещения.
- AddForce. Этот метод используется для придания импульса физическому объекту методом толчка. Для постоянного перемещения этот метод плохо подходит, так как им лучше пользоваться единожды, к примеру – для броска объекта или прыжка.
- MovePosition. Этот метод аналогичен методу SetPositionAndRotation компонента Transform, который просто перемещает физический объект в указанную позицию. Метод отлично подходит для равномерного движения, которое не предусматривает использования какого либо ускорения в режиме Kinematic. Для перемещения персонажа этот метод тоже не подходит, так как не позволяет использовать естественную силу притяжения на объекте.
- Velocity. Это свойство физического тела, которое показывает величину ускорения объекта. Свойство отлично подходит для задач, когда нужно замедлить или ускорить объект без резких толчков. Именно это свойство мы будем использовать для перемещения персонажа, так как оно позволяет управлять движением тела учитывая окружающее воздействие – гравитацию или преграды.
Движение с ускорением
Теперь попробуем применить выбранный тип движения для персонажа, для этого вернемся в скрипт персонажа Character, где создадим новый метод FixedUpdate и пару переменных для перемещения: moveSpeed для скорости движения и jumpForce для силы прыжка.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- [Range(0, 10)]
- public float moveSpeed = 5;
- [Range(0, 20)]
- public float jumpForce = 10;
- private Rigidbody2D objBody;
- private void FixedUpdate() {}
- public Rigidbody2D body {
- get {
- if (this.objBody == null) this.objBody = GetComponent<Rigidbody2D>();
- return this.objBody;
- }
- }
- /*Остальной код…*/
- }
Перед движением сначала проверим, поступила ли команда движения персонажу с помощью переменной onMoving.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- private void FixedUpdate() {
- if (this.onMoving) {
- }
- }
- /*Остальной код…*/
- }
Так как свойство ускорения тела velocity – это двумерный вектор Vector2, то нам нужно будет указывать ускорение по двум осям – X и Y соответственно.
Сначала определим, в какую сторону персонажу нужно совершать ускорение по оси X. Для этого с помощью переменной directionRight проверим направление персонажа, если ее значение true, значит ускорять объект нужно вправо с положительным значением, если же значение false, то будем ускорять объект влево с отрицательным значением.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- private void FixedUpdate() {
- if (this.onMoving) {
- float xVelocity, yVelocity;
- if (this.directionRight) xVelocity = 1f;
- else xVelocity = –1f;
- }
- }
- /*Остальной код…*/
- }
Теперь укажем скорость движения персонажа, используя переменную moveSpeed умноженную на дельту Time.fixedDeltaTime, чтобы учитывать время задержки между вызовами метода FixedUpdate.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- private void FixedUpdate() {
- if (this.onMoving) {
- float xVelocity, yVelocity;
- if (this.directionRight) xVelocity = 1f;
- else xVelocity = –1f;
- float speed = this.moveSpeed * Time.fixedDeltaTime * 100f;
- }
- }
- /*Остальной код…*/
- }
И в конце, умножим полученную скорость движения на ускорение xVelocity.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- private void FixedUpdate() {
- if (this.onMoving) {
- float xVelocity, yVelocity;
- if (this.directionRight) xVelocity = 1f;
- else xVelocity = –1f;
- float speed = this.moveSpeed * Time.fixedDeltaTime * 100f;
- xVelocity = xVelocity * speed;
- }
- }
- /*Остальной код…*/
- }
Готово, у нас теперь есть скорость и направление движения персонажа по оси X, осталось добавить движение по оси Y, чтобы полностью составить вектор ускорения персонажа.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- private void FixedUpdate() {
- if (this.onMoving) {
- float xVelocity, yVelocity;
- if (this.directionRight) xVelocity = 1f;
- else xVelocity = –1f;
- float speed = this.moveSpeed * Time.fixedDeltaTime * 100f;
- xVelocity = xVelocity * speed;
- yVelocity = this.body.velocity.y;
- }
- }
- /*Остальной код…*/
- }
Здесь ускорение по оси Y оставляем неизменным – персонаж сам будет обрабатывает вертикальное ускорение.
Осталось только применить полученный вектор ускорения телу Rigidbody.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- private void FixedUpdate() {
- if (this.onMoving) {
- float xVelocity, yVelocity;
- if (this.directionRight) xVelocity = 1f;
- else xVelocity = –1f;
- float speed = this.moveSpeed * Time.fixedDeltaTime * 100f;
- xVelocity = xVelocity * speed;
- yVelocity = this.body.velocity.y;
- this.body.velocity = new Vector2(xVelocity, yVelocity);
- }
- }
- /*Остальной код…*/
- }
После проделанных действий наш персонаж сможет двигаться по горизонтали учитывая силу притяжения.
Чтобы проверить, добавим на сцену поверхность с коллайдером.
Теперь попробуем запустить игру и убедиться, что движение работает согласно командам.
Так, сейчас персонаж скользит по поверхности будто бы на льду, чтобы это исправить, как вариант, можно добавить физический материал коллайдеру поверхности и увеличить его коэффициент трения Friction.
Перекинем физический материал в поле Material коллайдера поверхности и снова тестируем.
Изменяя коэффициент трения Friction физического материала можно добиться нужно результата движения по поверхности.
Импульс
Теперь займемся прыжком персонажа. Как было сказано выше, для совершения прыжка физическим телом, будем использовать метод AddForce который придаст объекту импульс движения по вертикали.
Возвращаемся в метод FixedUpdate скрипта Character.
Импульс, как и ускорение, задается двумерным вектором Vector2, но в отличие от ускорения, импульсу, помимо направления, еще необходимо передавать силу толчка.
Итак, получив команду прыжка Jump в методе FixedUpdate, после выполнения движения будем проверять переменную onMakeJump для совершения прыжка.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- private void FixedUpdate() {
- if (this.onMoving) {
- float xVelocity, yVelocity;
- if (this.directionRight) xVelocity = 1f;
- else xVelocity = –1f;
- float speed = this.moveSpeed * Time.fixedDeltaTime * 100f;
- xVelocity = xVelocity * speed;
- yVelocity = this.body.velocity.y;
- this.body.velocity = new Vector2(xVelocity, yVelocity);
- }
- if (this.onMakeJump) {
- print(“Прыжок”);
- }
- }
- /*Остальной код…*/
- }
Здесь, как и при движении, также будем использовать две переменные для работы с направлениями, только теперь мы будем изменять направление по оси Y придавая ей силу прыжка с помощью переменной jumpForce, а по оси X объект будет двигаться согласно своему ускорению движения.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- [Range(0, 20)]
- public float jumpForce = 10;
- private void FixedUpdate() {
- if (this.onMoving) {
- float xVelocity, yVelocity;
- if (this.directionRight) xVelocity = 1f;
- else xVelocity = –1f;
- float speed = this.moveSpeed * Time.fixedDeltaTime * 100f;
- xVelocity = xVelocity * speed;
- yVelocity = this.body.velocity.y;
- this.body.velocity = new Vector2(xVelocity, yVelocity);
- }
- if (this.onMakeJump) {
- float xVelocity = this.body.velocity.x;
- float yVelocity = this.jumpForce;
- }
- }
- /*Остальной код…*/
- }
Далее передаем телу Rigidbody импульс через метод AddForce.
- public sealed class Character : MonoBehaviour {
- /*Остальной код…*/
- private void FixedUpdate() {
- if (this.onMoving) {
- float xVelocity, yVelocity;
- if (this.directionRight) xVelocity = 1f;
- else xVelocity = –1f;
- float speed = this.moveSpeed * Time.fixedDeltaTime * 100f;
- xVelocity = xVelocity * speed;
- yVelocity = this.body.velocity.y;
- this.body.velocity = new Vector2(xVelocity, yVelocity);
- }
- if (this.onMakeJump) {
- float xVelocity = this.body.velocity.x;
- float yVelocity = this.jumpForce;
- Vector2 force = new Vector2(xVelocity, yVelocity);
- this.body.AddForce(force, ForceMode2D.Impulse);
- this.onMakeJump = false;
- }
- }
- /*Остальной код…*/
- }
Теперь снова вернем переменной onMakeJump исходное значение false для следующего прыжка. Наш персонаж может прыгать по поверхностям, попробуйте запустить и проверить результат.
Чтобы персонаж не прыгал так высоко и приземлялся быстрее можно в настройках физики Unity повысить силу гравитации. Эти настройки можно найти выбрав Edit -> Project settings -> Physics2D, в параметре Gravity увеличиваем Y составляющую.
Снова проверим результат.
Заключение
Вот так легко и просто можно создать систему управления персонажем с помощью команды игрока. Теперь вы запросто сможете подключить к готовому персонажу анимации и добавить в игру другие объекты.
Не понимаю у меня персонаж при сталкновении со стеной начинает дёргаться, обьясните как исправить пожалуйста
Задайте пожалуйста Ваш вопрос на почту в поддержку сайта со скриншотами проблемы support@unity3dschool.com
Спасибо!
в ответ, скиньте готовый скрипт пж)
Кто-нибудь, скиньте готовый скрипт пожалуйста) можно на почту
К сожалению исходники удалены с облака, дубликатов нет.
Что бы правильно заработал метод CheckGround нужно добавить номер слоя в котором находится земля, отличающийся от слоя героя например так :
bool rezult = Physics2D.OverlapBox(point,size, 0f,1);
Передрал все наголо(кроме пака ассетов),код повторил до идеала.в итоге граундчек не работает,персонаж прыгает в полете еще,управляется во все стороны.что то пропустиЛ?и на else xVelocity = –1f; ругалось,пришлось хитрить else xVelocity = 1f*(-1);
Напишите в поддержку, покажите код и вкратце суть проблемы – посмотрим – подскажем.
support@unity3dschool.com
По поводу xVelocity, это потому-что в статье вместо минуса стоит дефис: “–1f”, а необходимо: “-1f” 🙂
Можете скинуть исходники?
отправьте запрос через почту.
Можете скинуть исходник, ссылка неактуальна
Напишите в поддержку, ответим.
По ссылке: http://rgho.st/8qrH8MghQ