Как создать внутриигровое меню в Unity. Делаем простую игру с кнопками, ящиками и дверями на Unity

Бесподобна для быстрого прототипирования игр. Если у вас появилась идея какой-то новой игровой механики, с Unity её можно реализовать и протестировать в кратчайшие сроки. Под катом я расскажу как можно сделать несложную головоломку используя только стандартные компоненты и не залезая в графические редакторы. По ходу статьи я продемонстрирую пару трюков игрового дизайна, а в конце немного расскажу об эстетике, динамике и механике.

Для самых нетерпеливых по ссылкам ниже находится готовый прототип.
Онлайн версия
Скомпилированная версия для Windows [Зеркало ] ~7.5 МБ

Что мы собираемся делать? Двумерную головоломку с колобком в роли главного героя, который может двигать ящики, которые могут нажимать кнопки, которые могут открывать двери, за которыми скрывается выход из уровня, который построил я. Или вы, у нас же здесь туториал как-никак.

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

Грубый набросок

Я соврал, я не буду ждать. Создаём пустой проект без лишних пакетов и выбираем схему расположения окошек на свой вкус, я буду использовать Tall. Добавляем в иерархию сферу, перетаскиваем на неё главную камеру. Теперь камера будет следовать за нашей сферой, если она вдруг захочет погулять. Переименовываем сферу в «Player», перетаскиваем в Project, теперь у нас есть prefab , который мы можем использовать в любых новых сценах, если таковые будут. Не забывайте проверять координаты префабов при создании и использовании, если мы хотим делать игрушку в двух измерениях, то третья ось должна быть выставлена в ноль для всех взаимодействующих объектов.

Теперь добавим источник света, лезем в меню GameObject -> Create Other -> Directional light . Его координаты не имеют значения, он будет освещать наши объекты одинаково из любого места. Однако, имеет смысл поднять его немного над сценой, чтобы не мешался при выделении объектов, поэтому поставим ему координаты (0;0;-10). К слову о сцене, ось X у нас будет расти слева направо, Y - снизу вверх, а Z - от зрителя вглубь экрана. Покликайте по стрелочкам вокруг кубика в правом верхнем углу сцены и поверните её нужным образом.

Добавим на сцену кубик, назовём его «Wall» и перетащим в Assets. Одинокая кубическая стена рядом со сферическим колобком не очень-то впечатляет, да? Три поля Scale в инспекторе позволят нам вытягивать стенку, а комбинация клавиш Ctrl+D создаст её копию. В Unity есть много других полезных горячих клавиш , например зажатый Ctrl ограничивает перемещение объектов единичными интервалами, а клавиша V позволит тягать объект за вершины, и они будут липнуть к вершинам других объектов. Замечательно, не правда ли? И вы всё ещё пишете свой движок? Ну-ну.

Сообразите что-нибудь похожее на комнату, сохраните сцену, нажмите Play и полюбуйтесь своим творением пару минут. Хорошие гейм-дизайнеры называют это тестированием. Чего-то не хватает, да? Хмм. Возможно, если я полюбуюсь ещё немного, то…

Скрипты и физика

Нам нужно больше движения и цвета! Хотя, если ваше суровое детство было наполнено бетонными игрушками, то можно оставить всё как есть. Для всех остальных пришло время скриптов. Я буду приводить примеры на C#, но можно писать и на JS или Boo. На самом деле выбирать последние два смысла не имеет, они были добавлены в Юнити скорее как довесок, меньше поддерживаются, хуже расширяются и для них сложнее найти примеры. Особенно ужасен Boo, который по сути является unpythonic Python. Мерзость. Виват, питонисты!

Создаём C# Script, называем его «PlayerController», перетаскиваем на префаб Player и открываем с помощью Visual Studio любимого редактора. Сперва нужно потереть лишний мусор, оставим только нужное.

Using UnityEngine; public class PlayerController: MonoBehaviour { void Update() { } }
Функция Update вызывается в каждом кадре, что очень удобно для реализации движения, внутри неё мы и будем размещать код. Нажатия кнопок игроком можно получить с помощью класса Input . В комплекте с Unity идут замечательные настройки ввода, достаточно написать Input.GetAxis («Horizontal») и мы уже знаем нажал ли игрок на клавиатуре стрелку вправо или влево. Если у игрока подключён геймпад, то он может управлять и с него, нам даже не надо писать лишний код.


Такой нехитрой строчкой мы получаем информацию о действиях пользователя и создаём вектор движения. Для того, чтобы вектор куда-нибудь приложить, нам понадобится Rigidbody . Выделяем префаб Player и через меню Component -> Physics -> Rigidbody добавляем нужный компонент. Теперь мы можем на него ссылаться в нашём скрипте.

Rigidbody.AddForce(direction);
Функция AddForce имеет несколько интересных вариантов приложения силы , но для начала нам хватит и значений по умолчанию.

Готово! Сохраняем, жмём Play, тестируем.

Эээм, знаете, мне это напомнило тот момент из фильма Inception, где мужик бешено вращал глазами и катился кубарем то по стене, то по потолку. Наверное так он себя чувствовал.

Нам нужно запретить вращение и передвижение по оси Z. Выделяем префаб, смотрим на компонент Rigidbody и видим раздел Constraints. Оставляем неотмеченными только первые две галочки X и Y, остальные четыре включаем. Чуть выше снимаем галочку Use Gravity и прописываем Drag равный четырём (в разделе об эстетике я расскажу зачем это было сделано). Тестируем ещё раз.

Оно шевелится и не вертится! Ура! Но делает это слишком медленно. Добавим одну переменную к нашему скрипту и задействуем её в формуле нашего движения. Весь код будет в итоге выглядеть так:

Using UnityEngine; public class PlayerController: MonoBehaviour { public int acceleration; void Update() { var direction = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0); rigidbody.AddForce(direction * acceleration); } }
Заметили как в инспекторе появилось новое поле Acceleration у нашего скрипта? Эффектно, да? Вбиваем в поле тридцатку или что-нибудь на ваш вкус и проверяем в действии.

Материалы и коллайдеры

Пора уже сделать какую-нибудь кнопку, чтобы было на что нажимать. Дублируем префаб Wall и переименовываем его в «Button». В инспекторе у коллайдера ставим галочку Is Trigger. Это развоплотит нашу кнопку и заставит другие объекты проходить сквозь неё. Создаём скрипт «Button» и вешаем на кнопку.

У коллайдеров-триггеров есть события OnTriggerEnter и OnTriggerExit , которые вызываются всякий раз, когда что-то пересекает область триггера. На самом деле это не совсем так, ибо есть множество разных объектов и физический движок обрабатывает не все столкновения, подробнее читайте .

Для начала просто проверим как работают триггеры. Напишем что-нибудь в консольку Unity. Функция Debug.Log очень полезная, кроме текста она также умеет печатать разные игровые объекты.

Using UnityEngine; public class Button: MonoBehaviour { void OnTriggerEnter(Collider other) { Debug.Log("Hello"); } void OnTriggerExit(Collider other) { Debug.Log("Habr!"); } }
Кинули кнопку на сцену. Потестировали? Идём дальше. Было бы нагляднее, если бы наша кнопка меняла цвет при нажатии. Для цвета нам нужно прикрепить к кнопке материал . Create -> Material, назовём его «Button Mat» и накинем на кнопку. В свойствах материала выберем для Main Color зелёненький. Теперь в скрипте мы можем обращаться к цвету материала с помощью renderer.material.color и менять его как вздумается. Заставим кнопку краснеть от вхождения в неё нашего колобка. Как-то пошло вышло.

Using UnityEngine; public class Button: MonoBehaviour { void OnTriggerEnter(Collider other) { renderer.material.color = new Color(1, 0, 0); } void OnTriggerExit(Collider other) { renderer.material.color = new Color(0, 1, 0); } }
Класс Color может принимать кроме тройки RGB ещё и альфу, но у нас стоит обычный диффузный шейдер, поэтому она для нас не важна. Тестируем!

Если вы ещё не сделали это, то настала пора прибраться в нашем проекте, иначе мы заблудимся в мешанине префабов и скриптов. Например создадим папку «Levels» для хранения сцен, «Prefabs» для складирования заготовок, «Materials» для материалов и «Scripts» для скриптов, а потом рассортируем накопившееся богатство по папочкам.

Знаете, а ведь наша кнопка до сих пор не похожа на кнопку! Давайте её сплющим и заставим продавливаться под колобком. Выберите кнопку в иерархии, сделайте её толщиной в 0.3 единицы и положите на пол, т. е. выставьте координату Z в 0.35. Видите в инспекторе наверху три удобных кнопочки «Select», «Revert» и «Apply»? С помощью них можно взаимодействовать с префабом прямо на месте. Нажмите Apply и все кнопки отныне будут плоские и лежачие.

Для реализации программной анимации мы будет использовать класс Transform . У него есть свойство localPosition , которое позволит нам двигать кнопку:

Transform.localPosition += new Vector3(0, 0, 0.3f);
Этот код нажмёт кнопку. В целом это выглядит так:

Using UnityEngine; public class Button: MonoBehaviour { void OnTriggerEnter(Collider other) { transform.localPosition += new Vector3(0, 0, 0.3f); renderer.material.color = new Color(1, 0, 0); } void OnTriggerExit(Collider other) { transform.localPosition -= new Vector3(0, 0, 0.3f); renderer.material.color = new Color(0, 1, 0); } }
Протестировали. Наезд на кнопку вынуждает её нехило колбасить из-за сферического коллайдера колобка, который не всегда будет соприкасаться в утопленной кнопкой. Как это решить? Вспоминаем, что игры наполовину состоят из лжи, а значит размеры коллайдера совсем не обязательно должны совпадать с моделькой. Смотрим в инпекторе свойства коллайдера, учетверяем его размер по оси Z и смещаем его на -1.5 в том же направлении. Тестируем! Так гораздо лучше.

Двери, ящики и магниты

Теперь, когда у нас есть полнофункциональная кнопка, можно заставить её что-нибудь делать. Склонируем префаб стенки, назовём его «Door», создадим красненький материал «Door Mat», повесим его куда нужно и закинем свежеиспечённую дверь на сцену. Для того, чтобы как-то воздействовать на дверь, нам нужно иметь ссылку на её объект, поэтому создадим у кнопки новую переменную.

Public GameObject door;
GameObject это класс, в который заворачиваются все-все-все объекты на сцене, а значит у них у всех есть функция SetActive , которая представлена в инспекторе галочкой в левом верхнем углу. Если вы ещё пользуетесь Unity третьей версии, то вам придётся воспользоваться альтернативами . С помощью свойства активности можно прятать объекты не удаляя их. Они как бы пропадают со сцены и их коллайдеры перестают участвовать в расчётах. Прям то, что надо для двери. Приводим код к следующему виду:

Using UnityEngine; public class Button: MonoBehaviour { public GameObject door; void OnTriggerEnter(Collider other) { door.SetActive(false); transform.localPosition += new Vector3(0, 0, 0.3f); renderer.material.color = new Color(1, 0, 0); } void OnTriggerExit(Collider other) { door.SetActive(true); transform.localPosition -= new Vector3(0, 0, 0.3f); renderer.material.color = new Color(0, 1, 0); } }
Выбираем кнопку на сцене, перетаскиваем дверь из иерархии на появившееся поле в свойствах скрипта кнопки. Проверяем код в действии.

Наезд колобком на кнопку автомагически растворяет дверь и возвращает её на место после разъезда. Но какой толк нам от кнопки, которая постоянно выключается и запирает нам дверь? Настал час ящиков! Копируем префаб стенки, называем его «Box», добавляем к нему Rigidbody, не забываем проделать те же самые операции, что и с Player"ом, а затем кидаем его на сцену.

Как вы наверное заметили, толкать ящик не очень-то удобно. Кроме того, если он застрянет в углу комнаты, то достать его будет невозможно. Как вариант, мы можем сделать зоны-телепорты по углам комнаты, которые будут перемещать все попавшие в них ящики, но это немного мудрёно. Добавим в PlayerController магнит, который будет притягивать все близлежащие ящики. Функция Input.GetButton в отличие от Input.GetButtonDown будет возвращать true до тех пор, пока нажата запрашиваемая кнопка. То, что нам нужно.

If (Input.GetButton("Jump"))
Как мы будем находить ящики? Вариантов множество, например, мы можем прицепить к Player"у ещё один коллайдер и регистрировать OnTriggerEnter или OnTriggerStay , но тогда нужно будет решать проблему раннего реагирования триггера кнопки. Помните ту ссылку на матрицу с разными коллайдерами? Вот-вот. К тому же магнит должен работать только по нажатию кнопки, в остальное время он не нужен. Поэтому мы будем вручную проверять столкновения с помощью . Transform.position даст нам координаты центра колобка. Поищем объекты поблизости:

Var big = Physics.OverlapSphere(transform.position, 2.1f);
Поищем объекты, практически касающиеся колобка.

Var small = Physics.OverlapSphere(transform.position, 0.6f);
Две полученные сферы захватят все объекты, в том числе стены и кнопки. Чтобы отсеять лишнее, воспользуемся метками , они нам ещё не раз пригодятся. Идём в Edit -> Project Settings -> Tags и создаём метки на все случаи жизни: «Box», «Wall», «Button», «Door». «Player» уже есть. Выбираем префабы и метим их с помощью выпадающего списка вверху инспектора. Теперь мы можем отсеять нужные нам коробки:

Foreach (var body in big) if (System.Array.IndexOf(small, body) == -1 && body.tag == "Box") body.rigidbody.AddForce((transform.position - body.transform.position) * 20);
Нашли объект в большой сфере, проверили его наличие в малой сфере, проверили метку, двинули к себе. Немного математики с векторами, ничего сложного для тех, кто не прогуливал школу.

Ящик всё равно нас немного толкает, не не будем сейчас на этом заморачиваться, в движении всё равно это не мешает.

Внимательные читатели давно уже заметили один баг кнопки. Внимательные и усердные его уже исправили. Какой же это баг?

Если в поле коллайдера попадает два объекта, а потом один объект уходит по своим делам, то срабатывает лишнее выключение кнопки. Что делать? Нам нужно считать количество колобков и ящиков в области коллайдера и выключать кнопку, когда рядом никого нет, а включать при любом количестве тех и других. К сожалению, в Unity нет списка текущих столкновений. Очень жаль. Возможно, у разработчиков ещё не дошли до этого руки. В любом случае это решается парой строчек кода. Мы можем сделать свой список и складывать в него все приходящие объекты, вынимать все уходящие, а состояние кнопки менять в Update.

Плохой вариант

using UnityEngine; using System.Collections.Generic; public class Button: MonoBehaviour { public GameObject door; public bool pressed = false; private List colliders = new List(); void Update() { if (colliders.Count > 0 && !pressed) { door.SetActive(false); transform.localPosition += new Vector3(0, 0, 0.3f); renderer.material.color = new Color(1, 0, 0); pressed = true; } else if (colliders.Count == 0 && pressed) { door.SetActive(true); transform.localPosition -= new Vector3(0, 0, 0.3f); renderer.material.color = new Color(0, 1, 0); pressed = false; } } void OnTriggerEnter(Collider other) { colliders.Add(other); } void OnTriggerExit(Collider other) { colliders.Remove(other); } }


предложил вместо списка использовать счётчик. Этот способ однозначно изящнее, но старый код оставляю выше для примера, как не надо делать.

Using UnityEngine; public class Button: MonoBehaviour { public GameObject door; private int colliderCount = 0; void OnTriggerEnter(Collider other) { if (colliderCount == 0) { door.SetActive(false); transform.localPosition += new Vector3(0, 0, 0.3f); renderer.material.color = new Color(1, 0, 0); } colliderCount++; } void OnTriggerExit(Collider other) { colliderCount--; if (colliderCount == 0) { door.SetActive(true); transform.localPosition -= new Vector3(0, 0, 0.3f); renderer.material.color = new Color(0, 1, 0); } } }

Сцены, частицы и шейдеры

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

Дублируем префаб стенки, переименовываем в «Finish», меняем метку на одноимённую, превращаем коллайдер в триггер. Создадим материал «Finish Mat» с манящим голубеньким цветом и повесим на финиш.

Вся семья в сборе. Но как-то не очень маняще и слишком похоже на стенку. И на дверь. И на кубик. На помощь приходят шейдеры ! Сейчас у нас для всех материалов используется обычный матовый диффузный шейдер . В свойствах материала выберем для финиша Transparent/Specular . Этот шейдер будет учитывать альфу цвета и отсвечивать вторым цветом, который мы укажем. Поставим у голубенького альфу в половину, а отблеск сделаем белым. Тестируем.

Пока финиш выглядит не очень прозрачным, нужно как-то намекнуть, что он бесплотный. Для этого добавим к финишу систему частиц , которые будут плавать внутри и манить игрока. Component -> Effects -> Particle System. Если выбрать финиш на сцене, то можно смотреть на симуляцию, чтобы было проще создать желаемый эффект. В первую очередь поставим галочку Prewarm, тогда в игре частицы появятся заранее и будут продолжать свою нехитрую жизнь, а не возникнут на глазах игрока. Start Lifetime на единичку. Start Speed сделаем поменьше, например 0.1. Start Size 0.1. Цвет выставим голубенький. На вкладке Emission меняем Rate на две сотни. На вкладке Shape поставим Shape равным Box, это заставит частицы появляться на всём объёме финиша. Потом установим галочку Random Direction, чтобы частицы летали в разные стороны. Активируем вкладку Size over Lifetime, выбираем там какую-нибудь восходящую линию. На вкладке Randerer меняем стандартный Renderer Mode на Mesh. Меняем Mesh на сферу. Готово! Много-много маленьких пузыриков появляются и исчезают, а финиш теперь выглядит гораздо веселее.

Осталось заставить финиш перемещать игрока на следующий уровень. Для управления сценами в Unity есть несколько полезных функций и переменных. Application.loadedLevel покажет нам текущий уровень, Application.levelCount покажет их количество, а Application.LoadLevel загрузит желаемый. Кроме того, нам нужно указать в Build Settings все сцены, в которые мы хотим попасть. Создадим новый скрипт «Finish», повесим на префаб и напишем внутри следующее:

Using UnityEngine; public class Finish: MonoBehaviour { void OnTriggerEnter(Collider other) { if (other.tag == "Player") if (Application.loadedLevel + 1 != Application.levelCount) Application.LoadLevel(Application.loadedLevel + 1); else Application.LoadLevel(0); } }
Мы проверяем, что на финиш попал игрок, а потом перемещаемся на следующий или первый уровень. Дегустируем наш новый полнофункциональный финиш.

Эстетика, динамика и механика

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

Посмотрим на наше творение. Самая часто используемая механика - передвижение. Давайте внимательно посмотрим всё ли у неё в порядке. Открываем код.

Var direction = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0);
Если приглядеться, то видно, что по диагоналям наш вектор длиннее, а значит больше и прилагаемая сила. Можно исправить это

Здравствуйте уважаемые игроделы.
На просторах интернета уроков по созданию игр в Unity3D очень много, но на верхнем слое и направленных новичкам очень мало.
В данном серии уроков, я буду расписывать о создании меню игры при помощи стандартного GUI, не использую нестандартные компоненты.
Урок направлен для новичков, которые желают научится создавать свое меню в игре.

По окончанию первого урока у вас получится меню игры с работающими кнопками:

В уроке будут использоваться функции:
1) public int - назначение переменной числовым значением
2) GUI.BeginGroup - создание GUI группы
3) GUI.Button - Создание GUI кнопки
4) Debug.Log - вывод сообщения в консоли
5) Application.LoadLevel - загрузка уровня
6) Application.Quit - закрытие игры

Итак, начнем:
Шаг 1: Создаем в окне Project C# скрипт и называем его по своему.
Шаг 2: Создаем игровую камеру:
* На верхнем меню программы нажимаем пункт GameObject
* После нажатия появляется выпадающее меню, в нем нажимаем пункт Create Other
* В появившемся списке нажимаем на строку с названием Camera и после этого действия в окне Hierarchy появляется объект Camera

Шаг 3: Назначаем объекту Camera скрипт, который создали в первом шаге. Для этого в окне Project находим ваш скрипт, у меня он называется Menu, и перетягиваем его в окно Hierarchy на объект Camera.
Чтобы удостовериться в правильности хода нужно: нажать на объект Camera в окне Hierarchy. В окне Inspector вы у объекта увидите такое одержимое:

Если же у вас появилась выделенная на изображении строка с названием вашего скрипта, то сделали вы все правильно.

Шаг 4: Откроем скрипт в редакторе. Для этого нажмите на ваш скрипт двойным нажатием левой кнопки мыши в окне Project. У вас откроется редактор скриптов, в моем случае это MonoDevelop. Открыв редактор, перед вами появится вас скрипт, который будет абсолютно пустой, но с базовым содержанием:

200?"200px":""+(this.scrollHeight+5)+"px");">
using UnityEngine;
using System.Collections;
public class Menu1: MonoBehaviour {
// Use this for initialization
void Start () {
}
void Update () {
}
}


Строка

200?"200px":""+(this.scrollHeight+5)+"px");">public class [u]Menu : MonoBehaviour {

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

Шаг 5: Отредактируем скрипт под меню, для этого можно удалить некоторые строки, которые вам не понадобятся в этом уроке:

200?"200px":""+(this.scrollHeight+5)+"px");">
// Use this for initialization - этот комментарий нам не нужен

// Update is called once per frame
void Update () {
} - метод Void нам тоже не понадобится

Шаг 6: Наш скрипт подготовлен для создания на нем меню игры.
Перед методом void Start создадим переменную для нумерации окон в меню игры.
Содержании строки такое:

200?"200px":""+(this.scrollHeight+5)+"px");">public int window;


public int - задаем числовое значение переменной
window - название переменной, которая будет использоваться в скрипте с числовым значением

Шаг 6: Для правильной работы меню, при старте работы скрипта у нас должно отображаться одно содержимое, для этого в метод void Start добавим стартовое значением переменной window . Весь метод будет выглядеть так:

200?"200px":""+(this.scrollHeight+5)+"px");">
void Start () {
window = 1;
}

Метод при старте исполнения скрипта будет назначать переменной window значение 1

[b]Шаг 7:
Начнем саму работу с GUI выводом меню, для этого создадим ниже метода void Start, метод в выводом GUI. Выглядеть он будет так:

200?"200px":""+(this.scrollHeight+5)+"px");">
void OnGUI () {
}

Данный метод в программе Unity3D и в вашем создаваемом приложении вызовет вывод графических элементов.

Шаг 8: Чтобы меню отображалось по центру экрана и не приходилось для каждой кнопки рассчитывать местоположение, создадим группу, которая будет выводить свое содержимое по центру экрана:

200?"200px":""+(this.scrollHeight+5)+"px");">
GUI.EndGroup ();

GUI.BeginGroup - создаем группу
(new Rect - задаем значение, что дальше будут даны данные о расположении самой группы
(Screen.width / 2 - 100, - задаем расположение группы относительно ширины экрана
Screen.height / 2 - 100, - задаем расположение группы относительно высоты экрана
200 - задаем ширину группы
200 - задаем высоту группы

Значения ширины и высоты можно свои ставить, но чтобы все было по центру аккуратно в Screen.width / 2 - 100, Screen.height / 2 - 100 значение 100 заменяем на свое значение. То есть если же ваша группа будет иметь ширину и высоту 300, то в замен 100 вы должны ввести половину от ширины 300. Вводимое значение будет 150.

Шаг 9: Создаем вывод меню, если переменная window = 1. Для этого, между началом и концом группы, созданной в шаге №8, то есть

200?"200px":""+(this.scrollHeight+5)+"px");">
GUI.BeginGroup (new Rect (Screen.width / 2 - 100, Screen.height / 2 - 100, 200, 200));
Сюда!!!
GUI.EndGroup ();


Напишем выдаваемое значение при window = 1:

200?"200px":""+(this.scrollHeight+5)+"px");">
if(window == 1)
{
if(GUI.Button (new Rect (10,30,180,30), "Играть"))
{
window = 2;
}
if(GUI.Button (new Rect (10,70,180,30), "Настройки"))
{
window = 3;
}
if(GUI.Button (new Rect (10,110,180,30), "Об Игре"))
{
window = 4;
}
if(GUI.Button (new Rect (10,150,180,30), "Выход"))
{
window = 5;
}
}

if(window == 1) - если windows равно значению 1, то создадим вывод
if(GUI.Button (new Rect (10,30,180,30), "Играть")) - создаем кнопку "Играть"
{window = 2;} - если нажата кнопка "Играть", то window получит значение 2
С остальными кнопка так же.

Шаг 10: Создадим вывод, если переменная window равно 2

200?"200px":""+(this.scrollHeight+5)+"px");">
if(window == 2)
{

{
Application.LoadLevel(1);
}
{
Application.LoadLevel(2);
}
{
Application.LoadLevel(3);
}
{
window = 1;
}
}

Выводим кнопки, которые доступны при нажатии на кнопку "Играть". Вывод ни чем не отличается о предыдущих кнопок, расшифрую только новые функции:
Debug.Log("Уровень 1 загружен"); -
Application.LoadLevel(1); - вызываем функцию, которая загружает уровень игры. 1 - можно менять на нужный вам уровень. Числовое значение можно брать, если нажать на сочетании клавиш Ctrl + Shift + B.

Шаг 11: Создаем вывод, если window имеет значение 3:

200?"200px":""+(this.scrollHeight+5)+"px");">
if(window == 3)
{

{
}
{
}
{
}
if(GUI.Button (new Rect (10,160,180,30), "Назад"))
{
window = 1;
}
}


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

Шаг 12: Выводим содержимое, если значение у window 4

200?"200px":""+(this.scrollHeight+5)+"px");">
if(window == 4)
{

{
window = 1;
}
}

Шаг 13: Выводим содержимое, если переменная window имеет значение 5 и нажата кнопка "Выход"

200?"200px":""+(this.scrollHeight+5)+"px");">
if(window == 5)
{

{
Application.Quit();
}
{
window = 1;
}
}


В данном выводе из новых функций, только она:
Application.Quit(); - данная функция выключает приложении при нажатии кнопки "Да".
P.S. Функция не работает в редакторе Unity3D, она работает только в скомпилированном проекте.

Скрипт готов, если же вы делали все по шагам, то у вас появится меню, которое было показано на изображении вначале.

If(window == 2)
{
GUI.Label(new Rect(50, 10, 180, 30), "Выберите уровень");
if(GUI.Button (new Rect (10,40,180,30), "Уровень 1"))
{
Debug.Log("Уровень 1 загружен");
Application.LoadLevel(1);
}
if(GUI.Button (new Rect (10,80,180,30), "Уровень 2"))
{
Debug.Log("Уровень 2 загружен");
Application.LoadLevel(2);
}
if(GUI.Button (new Rect (10,120,180,30), "Уровень 3"))
{
Debug.Log("Уровень 3 загружен");
Application.LoadLevel(3);
}
if(GUI.Button (new Rect (10,160,180,30), "Назад"))
{
window = 1;
}
}

If(window == 3)
{
GUI.Label(new Rect(50, 10, 180, 30), "Настройки Игры");
if(GUI.Button (new Rect (10,40,180,30), "Игра"))
{
}
if(GUI.Button (new Rect (10,80,180,30), "Аудио"))
{
}
if(GUI.Button (new Rect (10,120,180,30), "Видео"))
{
}
if(GUI.Button (new Rect (10,160,180,30), "Назад"))
{
window = 1;
}
}

If(window == 4)
{
GUI.Label(new Rect(50, 10, 180, 30), "Об Игре");
GUI.Label(new Rect(10, 40, 180, 40), "Информация об разработчике и об игре");
if(GUI.Button (new Rect (10,90,180,30), "Назад"))
{
window = 1;
}
}

If(window == 5)
{
GUI.Label(new Rect(50, 10, 180, 30), "Вы уже выходите?");
if(GUI.Button (new Rect (10,40,180,30), "Да"))
{
Application.Quit();
}
if(GUI.Button (new Rect (10,80,180,30), "Нет"))
{
window = 1;
}
}
GUI.EndGroup ();
}
}


На данный момент это первый урок, в будущем появятся еще пару, которые научат делать полностью функциональное меню

Бесподобна для быстрого прототипирования игр. Если у вас появилась идея какой-то новой игровой механики, с Unity её можно реализовать и протестировать в кратчайшие сроки. Под катом я расскажу как можно сделать несложную головоломку используя только стандартные компоненты и не залезая в графические редакторы. По ходу статьи я продемонстрирую пару трюков игрового дизайна, а в конце немного расскажу об эстетике, динамике и механике.

Для самых нетерпеливых по ссылкам ниже находится готовый прототип.
Онлайн версия
Скомпилированная версия для Windows [Зеркало ] ~7.5 МБ

Что мы собираемся делать? Двумерную головоломку с колобком в роли главного героя, который может двигать ящики, которые могут нажимать кнопки, которые могут открывать двери, за которыми скрывается выход из уровня, который построил я. Или вы, у нас же здесь туториал как-никак.

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

Грубый набросок

Я соврал, я не буду ждать. Создаём пустой проект без лишних пакетов и выбираем схему расположения окошек на свой вкус, я буду использовать Tall. Добавляем в иерархию сферу, перетаскиваем на неё главную камеру. Теперь камера будет следовать за нашей сферой, если она вдруг захочет погулять. Переименовываем сферу в «Player», перетаскиваем в Project, теперь у нас есть prefab , который мы можем использовать в любых новых сценах, если таковые будут. Не забывайте проверять координаты префабов при создании и использовании, если мы хотим делать игрушку в двух измерениях, то третья ось должна быть выставлена в ноль для всех взаимодействующих объектов.

Теперь добавим источник света, лезем в меню GameObject -> Create Other -> Directional light . Его координаты не имеют значения, он будет освещать наши объекты одинаково из любого места. Однако, имеет смысл поднять его немного над сценой, чтобы не мешался при выделении объектов, поэтому поставим ему координаты (0;0;-10). К слову о сцене, ось X у нас будет расти слева направо, Y - снизу вверх, а Z - от зрителя вглубь экрана. Покликайте по стрелочкам вокруг кубика в правом верхнем углу сцены и поверните её нужным образом.

Добавим на сцену кубик, назовём его «Wall» и перетащим в Assets. Одинокая кубическая стена рядом со сферическим колобком не очень-то впечатляет, да? Три поля Scale в инспекторе позволят нам вытягивать стенку, а комбинация клавиш Ctrl+D создаст её копию. В Unity есть много других полезных горячих клавиш , например зажатый Ctrl ограничивает перемещение объектов единичными интервалами, а клавиша V позволит тягать объект за вершины, и они будут липнуть к вершинам других объектов. Замечательно, не правда ли? И вы всё ещё пишете свой движок? Ну-ну.

Сообразите что-нибудь похожее на комнату, сохраните сцену, нажмите Play и полюбуйтесь своим творением пару минут. Хорошие гейм-дизайнеры называют это тестированием. Чего-то не хватает, да? Хмм. Возможно, если я полюбуюсь ещё немного, то…

Скрипты и физика

Нам нужно больше движения и цвета! Хотя, если ваше суровое детство было наполнено бетонными игрушками, то можно оставить всё как есть. Для всех остальных пришло время скриптов. Я буду приводить примеры на C#, но можно писать и на JS или Boo. На самом деле выбирать последние два смысла не имеет, они были добавлены в Юнити скорее как довесок, меньше поддерживаются, хуже расширяются и для них сложнее найти примеры. Особенно ужасен Boo, который по сути является unpythonic Python. Мерзость. Виват, питонисты!

Создаём C# Script, называем его «PlayerController», перетаскиваем на префаб Player и открываем с помощью Visual Studio любимого редактора. Сперва нужно потереть лишний мусор, оставим только нужное.

Using UnityEngine; public class PlayerController: MonoBehaviour { void Update() { } }
Функция Update вызывается в каждом кадре, что очень удобно для реализации движения, внутри неё мы и будем размещать код. Нажатия кнопок игроком можно получить с помощью класса Input . В комплекте с Unity идут замечательные настройки ввода, достаточно написать Input.GetAxis («Horizontal») и мы уже знаем нажал ли игрок на клавиатуре стрелку вправо или влево. Если у игрока подключён геймпад, то он может управлять и с него, нам даже не надо писать лишний код.


Такой нехитрой строчкой мы получаем информацию о действиях пользователя и создаём вектор движения. Для того, чтобы вектор куда-нибудь приложить, нам понадобится Rigidbody . Выделяем префаб Player и через меню Component -> Physics -> Rigidbody добавляем нужный компонент. Теперь мы можем на него ссылаться в нашём скрипте.

Rigidbody.AddForce(direction);
Функция AddForce имеет несколько интересных вариантов приложения силы , но для начала нам хватит и значений по умолчанию.

Готово! Сохраняем, жмём Play, тестируем.

Эээм, знаете, мне это напомнило тот момент из фильма Inception, где мужик бешено вращал глазами и катился кубарем то по стене, то по потолку. Наверное так он себя чувствовал.

Нам нужно запретить вращение и передвижение по оси Z. Выделяем префаб, смотрим на компонент Rigidbody и видим раздел Constraints. Оставляем неотмеченными только первые две галочки X и Y, остальные четыре включаем. Чуть выше снимаем галочку Use Gravity и прописываем Drag равный четырём (в разделе об эстетике я расскажу зачем это было сделано). Тестируем ещё раз.

Оно шевелится и не вертится! Ура! Но делает это слишком медленно. Добавим одну переменную к нашему скрипту и задействуем её в формуле нашего движения. Весь код будет в итоге выглядеть так:

Using UnityEngine; public class PlayerController: MonoBehaviour { public int acceleration; void Update() { var direction = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0); rigidbody.AddForce(direction * acceleration); } }
Заметили как в инспекторе появилось новое поле Acceleration у нашего скрипта? Эффектно, да? Вбиваем в поле тридцатку или что-нибудь на ваш вкус и проверяем в действии.

Материалы и коллайдеры

Пора уже сделать какую-нибудь кнопку, чтобы было на что нажимать. Дублируем префаб Wall и переименовываем его в «Button». В инспекторе у коллайдера ставим галочку Is Trigger. Это развоплотит нашу кнопку и заставит другие объекты проходить сквозь неё. Создаём скрипт «Button» и вешаем на кнопку.

У коллайдеров-триггеров есть события OnTriggerEnter и OnTriggerExit , которые вызываются всякий раз, когда что-то пересекает область триггера. На самом деле это не совсем так, ибо есть множество разных объектов и физический движок обрабатывает не все столкновения, подробнее читайте .

Для начала просто проверим как работают триггеры. Напишем что-нибудь в консольку Unity. Функция Debug.Log очень полезная, кроме текста она также умеет печатать разные игровые объекты.

Using UnityEngine; public class Button: MonoBehaviour { void OnTriggerEnter(Collider other) { Debug.Log("Hello"); } void OnTriggerExit(Collider other) { Debug.Log("Habr!"); } }
Кинули кнопку на сцену. Потестировали? Идём дальше. Было бы нагляднее, если бы наша кнопка меняла цвет при нажатии. Для цвета нам нужно прикрепить к кнопке материал . Create -> Material, назовём его «Button Mat» и накинем на кнопку. В свойствах материала выберем для Main Color зелёненький. Теперь в скрипте мы можем обращаться к цвету материала с помощью renderer.material.color и менять его как вздумается. Заставим кнопку краснеть от вхождения в неё нашего колобка. Как-то пошло вышло.

Using UnityEngine; public class Button: MonoBehaviour { void OnTriggerEnter(Collider other) { renderer.material.color = new Color(1, 0, 0); } void OnTriggerExit(Collider other) { renderer.material.color = new Color(0, 1, 0); } }
Класс Color может принимать кроме тройки RGB ещё и альфу, но у нас стоит обычный диффузный шейдер, поэтому она для нас не важна. Тестируем!

Если вы ещё не сделали это, то настала пора прибраться в нашем проекте, иначе мы заблудимся в мешанине префабов и скриптов. Например создадим папку «Levels» для хранения сцен, «Prefabs» для складирования заготовок, «Materials» для материалов и «Scripts» для скриптов, а потом рассортируем накопившееся богатство по папочкам.

Знаете, а ведь наша кнопка до сих пор не похожа на кнопку! Давайте её сплющим и заставим продавливаться под колобком. Выберите кнопку в иерархии, сделайте её толщиной в 0.3 единицы и положите на пол, т. е. выставьте координату Z в 0.35. Видите в инспекторе наверху три удобных кнопочки «Select», «Revert» и «Apply»? С помощью них можно взаимодействовать с префабом прямо на месте. Нажмите Apply и все кнопки отныне будут плоские и лежачие.

Для реализации программной анимации мы будет использовать класс Transform . У него есть свойство localPosition , которое позволит нам двигать кнопку:

Transform.localPosition += new Vector3(0, 0, 0.3f);
Этот код нажмёт кнопку. В целом это выглядит так:

Using UnityEngine; public class Button: MonoBehaviour { void OnTriggerEnter(Collider other) { transform.localPosition += new Vector3(0, 0, 0.3f); renderer.material.color = new Color(1, 0, 0); } void OnTriggerExit(Collider other) { transform.localPosition -= new Vector3(0, 0, 0.3f); renderer.material.color = new Color(0, 1, 0); } }
Протестировали. Наезд на кнопку вынуждает её нехило колбасить из-за сферического коллайдера колобка, который не всегда будет соприкасаться в утопленной кнопкой. Как это решить? Вспоминаем, что игры наполовину состоят из лжи, а значит размеры коллайдера совсем не обязательно должны совпадать с моделькой. Смотрим в инпекторе свойства коллайдера, учетверяем его размер по оси Z и смещаем его на -1.5 в том же направлении. Тестируем! Так гораздо лучше.

Двери, ящики и магниты

Теперь, когда у нас есть полнофункциональная кнопка, можно заставить её что-нибудь делать. Склонируем префаб стенки, назовём его «Door», создадим красненький материал «Door Mat», повесим его куда нужно и закинем свежеиспечённую дверь на сцену. Для того, чтобы как-то воздействовать на дверь, нам нужно иметь ссылку на её объект, поэтому создадим у кнопки новую переменную.

Public GameObject door;
GameObject это класс, в который заворачиваются все-все-все объекты на сцене, а значит у них у всех есть функция SetActive , которая представлена в инспекторе галочкой в левом верхнем углу. Если вы ещё пользуетесь Unity третьей версии, то вам придётся воспользоваться альтернативами . С помощью свойства активности можно прятать объекты не удаляя их. Они как бы пропадают со сцены и их коллайдеры перестают участвовать в расчётах. Прям то, что надо для двери. Приводим код к следующему виду:

Using UnityEngine; public class Button: MonoBehaviour { public GameObject door; void OnTriggerEnter(Collider other) { door.SetActive(false); transform.localPosition += new Vector3(0, 0, 0.3f); renderer.material.color = new Color(1, 0, 0); } void OnTriggerExit(Collider other) { door.SetActive(true); transform.localPosition -= new Vector3(0, 0, 0.3f); renderer.material.color = new Color(0, 1, 0); } }
Выбираем кнопку на сцене, перетаскиваем дверь из иерархии на появившееся поле в свойствах скрипта кнопки. Проверяем код в действии.

Наезд колобком на кнопку автомагически растворяет дверь и возвращает её на место после разъезда. Но какой толк нам от кнопки, которая постоянно выключается и запирает нам дверь? Настал час ящиков! Копируем префаб стенки, называем его «Box», добавляем к нему Rigidbody, не забываем проделать те же самые операции, что и с Player"ом, а затем кидаем его на сцену.

Как вы наверное заметили, толкать ящик не очень-то удобно. Кроме того, если он застрянет в углу комнаты, то достать его будет невозможно. Как вариант, мы можем сделать зоны-телепорты по углам комнаты, которые будут перемещать все попавшие в них ящики, но это немного мудрёно. Добавим в PlayerController магнит, который будет притягивать все близлежащие ящики. Функция Input.GetButton в отличие от Input.GetButtonDown будет возвращать true до тех пор, пока нажата запрашиваемая кнопка. То, что нам нужно.

If (Input.GetButton("Jump"))
Как мы будем находить ящики? Вариантов множество, например, мы можем прицепить к Player"у ещё один коллайдер и регистрировать OnTriggerEnter или OnTriggerStay , но тогда нужно будет решать проблему раннего реагирования триггера кнопки. Помните ту ссылку на матрицу с разными коллайдерами? Вот-вот. К тому же магнит должен работать только по нажатию кнопки, в остальное время он не нужен. Поэтому мы будем вручную проверять столкновения с помощью . Transform.position даст нам координаты центра колобка. Поищем объекты поблизости:

Var big = Physics.OverlapSphere(transform.position, 2.1f);
Поищем объекты, практически касающиеся колобка.

Var small = Physics.OverlapSphere(transform.position, 0.6f);
Две полученные сферы захватят все объекты, в том числе стены и кнопки. Чтобы отсеять лишнее, воспользуемся метками , они нам ещё не раз пригодятся. Идём в Edit -> Project Settings -> Tags и создаём метки на все случаи жизни: «Box», «Wall», «Button», «Door». «Player» уже есть. Выбираем префабы и метим их с помощью выпадающего списка вверху инспектора. Теперь мы можем отсеять нужные нам коробки:

Foreach (var body in big) if (System.Array.IndexOf(small, body) == -1 && body.tag == "Box") body.rigidbody.AddForce((transform.position - body.transform.position) * 20);
Нашли объект в большой сфере, проверили его наличие в малой сфере, проверили метку, двинули к себе. Немного математики с векторами, ничего сложного для тех, кто не прогуливал школу.

Ящик всё равно нас немного толкает, не не будем сейчас на этом заморачиваться, в движении всё равно это не мешает.

Внимательные читатели давно уже заметили один баг кнопки. Внимательные и усердные его уже исправили. Какой же это баг?

Если в поле коллайдера попадает два объекта, а потом один объект уходит по своим делам, то срабатывает лишнее выключение кнопки. Что делать? Нам нужно считать количество колобков и ящиков в области коллайдера и выключать кнопку, когда рядом никого нет, а включать при любом количестве тех и других. К сожалению, в Unity нет списка текущих столкновений. Очень жаль. Возможно, у разработчиков ещё не дошли до этого руки. В любом случае это решается парой строчек кода. Мы можем сделать свой список и складывать в него все приходящие объекты, вынимать все уходящие, а состояние кнопки менять в Update.

Плохой вариант

using UnityEngine; using System.Collections.Generic; public class Button: MonoBehaviour { public GameObject door; public bool pressed = false; private List colliders = new List(); void Update() { if (colliders.Count > 0 && !pressed) { door.SetActive(false); transform.localPosition += new Vector3(0, 0, 0.3f); renderer.material.color = new Color(1, 0, 0); pressed = true; } else if (colliders.Count == 0 && pressed) { door.SetActive(true); transform.localPosition -= new Vector3(0, 0, 0.3f); renderer.material.color = new Color(0, 1, 0); pressed = false; } } void OnTriggerEnter(Collider other) { colliders.Add(other); } void OnTriggerExit(Collider other) { colliders.Remove(other); } }


Lertmind предложил вместо списка использовать счётчик. Этот способ однозначно изящнее, но старый код оставляю выше для примера, как не надо делать.

Using UnityEngine; public class Button: MonoBehaviour { public GameObject door; private int colliderCount = 0; void OnTriggerEnter(Collider other) { if (colliderCount == 0) { door.SetActive(false); transform.localPosition += new Vector3(0, 0, 0.3f); renderer.material.color = new Color(1, 0, 0); } colliderCount++; } void OnTriggerExit(Collider other) { colliderCount--; if (colliderCount == 0) { door.SetActive(true); transform.localPosition -= new Vector3(0, 0, 0.3f); renderer.material.color = new Color(0, 1, 0); } } }

Сцены, частицы и шейдеры

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

Дублируем префаб стенки, переименовываем в «Finish», меняем метку на одноимённую, превращаем коллайдер в триггер. Создадим материал «Finish Mat» с манящим голубеньким цветом и повесим на финиш.

Вся семья в сборе. Но как-то не очень маняще и слишком похоже на стенку. И на дверь. И на кубик. На помощь приходят шейдеры ! Сейчас у нас для всех материалов используется обычный матовый диффузный шейдер . В свойствах материала выберем для финиша Transparent/Specular . Этот шейдер будет учитывать альфу цвета и отсвечивать вторым цветом, который мы укажем. Поставим у голубенького альфу в половину, а отблеск сделаем белым. Тестируем.

Пока финиш выглядит не очень прозрачным, нужно как-то намекнуть, что он бесплотный. Для этого добавим к финишу систему частиц , которые будут плавать внутри и манить игрока. Component -> Effects -> Particle System. Если выбрать финиш на сцене, то можно смотреть на симуляцию, чтобы было проще создать желаемый эффект. В первую очередь поставим галочку Prewarm, тогда в игре частицы появятся заранее и будут продолжать свою нехитрую жизнь, а не возникнут на глазах игрока. Start Lifetime на единичку. Start Speed сделаем поменьше, например 0.1. Start Size 0.1. Цвет выставим голубенький. На вкладке Emission меняем Rate на две сотни. На вкладке Shape поставим Shape равным Box, это заставит частицы появляться на всём объёме финиша. Потом установим галочку Random Direction, чтобы частицы летали в разные стороны. Активируем вкладку Size over Lifetime, выбираем там какую-нибудь восходящую линию. На вкладке Randerer меняем стандартный Renderer Mode на Mesh. Меняем Mesh на сферу. Готово! Много-много маленьких пузыриков появляются и исчезают, а финиш теперь выглядит гораздо веселее.

Осталось заставить финиш перемещать игрока на следующий уровень. Для управления сценами в Unity есть несколько полезных функций и переменных. Application.loadedLevel покажет нам текущий уровень, Application.levelCount покажет их количество, а Application.LoadLevel загрузит желаемый. Кроме того, нам нужно указать в Build Settings все сцены, в которые мы хотим попасть. Создадим новый скрипт «Finish», повесим на префаб и напишем внутри следующее:

Using UnityEngine; public class Finish: MonoBehaviour { void OnTriggerEnter(Collider other) { if (other.tag == "Player") if (Application.loadedLevel + 1 != Application.levelCount) Application.LoadLevel(Application.loadedLevel + 1); else Application.LoadLevel(0); } }
Мы проверяем, что на финиш попал игрок, а потом перемещаемся на следующий или первый уровень. Дегустируем наш новый полнофункциональный финиш.

Эстетика, динамика и механика

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

Посмотрим на наше творение. Самая часто используемая механика - передвижение. Давайте внимательно посмотрим всё ли у неё в порядке. Открываем код.

Var direction = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0);
Если приглядеться, то видно, что по диагоналям наш вектор длиннее, а значит больше и прилагаемая сила. Можно исправить это