В этом вводном туториале мы разберём, как устроено движение в Unity 2D. Не через копирование кода, а через понимание: как пространство, время и ритм кадров влияют на то, как объект перемещается в игре.
Частая ошибка на старте — копировать готовый код движения, не понимая, почему он работает. В результате игра начинает вести себя странно: персонаж ускоряется на диагонали, по-разному движется на разных ПК, проваливается сквозь объекты или «дёргается» без видимой причины. Такие баги почти невозможно исправить, если не понимать основу.
Мы постепенно пройдём путь от простых перемещений к осознанному выбору между разными подходами к движению, чтобы ты понимал не только как что-то работает, но и почему.
Этот туториал также помогает увидеть связь между внутренней логикой движка и тем, что ты наблюдаешь на экране. Числа, векторы и время перестают быть абстракцией — они становятся частью одного пространства, в котором живёт объект и которое ты начинаешь чувствовать, а не просто рассчитывать.
После того как ты создашь и откроешь 2D-проект в Unity, сцена сначала будет выглядеть пустой. Но даже «пустота» в Unity — это уже пространство. Любой игровой объект, который ты добавишь, всегда будет иметь позицию, заданную координатами.
В 2D-играх нас в первую очередь интересуют две оси: X и Y. Ось X отвечает за движение влево и вправо, ось Y — вверх и вниз. Позиция объекта — это просто точка в этом пространстве.
💡 Важно помнить, что Unity — это 3D-движок, даже если ты делаешь 2D-игру. На самом деле у каждого объекта есть ещё и координата Z. Эта ось направлена вдоль взгляда камеры, и чаще всего в 2D она остаётся равной 0. Но иногда именно значение Z может стать причиной того, что объект «исчезает» или оказывается не там, где ты ожидал.
Ниже показана обычная координатная сетка. Это не абстрактная математика — именно в таком пространстве живёт твоя сцена в Unity. Когда ты двигаешь объект в окне Scene, ты на самом деле меняешь его координаты в этой системе.
Пока не нужно ничего считать или запоминать формулы. Достаточно почувствовать, что сцена Unity и эта сетка — один и тот же мир, просто показанный разными способами.
Мы уже говорили, что местоположение объекта в системе координат, а
значит и в сцене Unity, определяется его позицией. Позиция
записывается в виде набора чисел (x, y, z), где
x отвечает за движение влево и вправо,
y — вверх и вниз, а z — ближе или
дальше от камеры.
В 2D-играх координата Z чаще всего не используется или остаётся равной
0. При этом она всё равно существует и иногда может
влиять на отображение объектов.
Ниже показаны несколько примеров позиций в этом пространстве. Красный
круг находится в точке (1, 1), зелёный — в
(3, -2), а синий — в точке (-3.5, -2.5). Это
просто разные координаты в одном и том же мире.
Пока важно воспринимать позицию как место в пространстве — точку, где сейчас находится объект. Мы не говорим о движении или направлении, а лишь фиксируем, где объект расположен.
💡 Позиция объекта не обязана быть целым числом. Для точного
размещения Unity использует вещественные числа — значения с дробной
частью, например -3.5 или 0.25.
Теперь можно ввести важное понятие, с которым Unity работает постоянно, — вектор. Пока без формул и без сложности. В самом простом смысле вектор — это направленный отрезок.
У вектора есть направление и длина. Его можно представить как стрелку: она начинается в одной точке и указывает в другую. Такая стрелка не просто показывает где что-то находится, а описывает связь между двумя точками в пространстве.
Если говорить о позиции объекта, вектор можно представить как отрезок,
который начинается в точке отсчёта (0, 0) и заканчивается
в точке, где расположен объект. Именно так Unity «видит» положение в
пространстве — не как отдельные числа, а как направление и расстояние
от начала координат.
На картинке ниже позиции объектов показаны не только как точки, но и как стрелки, идущие от начала координат. Эти стрелки и есть векторы, описывающие положение каждого объекта в 2D-пространстве.
💡 Те же самые точки на координатной сетке в Unity будут описываться так:
// Красная точка (1, 1)
redCircle.transform.position = new Vector2(1f, 1f);
// Зелёная точка (3, -2)
greenCircle.transform.position = new Vector2(3f, -2f);
// Синяя точка (-3.5, -2.5)
blueCircle.transform.position = new Vector2(-3.5f, -2.5f);
Это просто разные позиции в одном и том же пространстве, записанные языком Unity.
Важно запомнить простую идею: вектор — это способ описать положение и направление в пространстве.
До этого момента мы молча использовали одну важную вещь — точку
отсчёта. На координатной сетке это точка (0, 0), от
которой откладываются все позиции и направления.
Такое пространство называют глобальным. В нём все объекты существуют в одном общем мире и используют одну и ту же систему координат.
Но в играх часто бывает полезно смотреть на объект не из всего мира, а относительно другого объекта или даже относительно себя. В этом случае появляется локальное пространство — со своей собственной точкой отсчёта.
Представьте простой пример: перед вами проезжает машина слева направо. Для вас направление её движения — направо, но для водителя, сидящего в машине, он едет прямо. Это показывает, что направления осей в глобальной и локальной системах координат не обязаны совпадать.
Другой пример — иерархия объектов. Если объект является дочерним, его локальная позиция отсчитывается не от центра мира, а от позиции родителя. С точки зрения мира объект может находиться в одном месте, а с точки зрения родителя — в другом.
Представьте, что на столе стоит чашка, и вы хотите передвинуть её из центра стола в один из углов. Вам не нужно считать положение чашки относительно всей кухни — вы просто двигаете её относительно поверхности стола.
На этой картинке видно, что координаты позиции дочернего зелёного
объекта зависят от точки отсчёта. Относительно центра мира его позиция
равна (2, 3), а относительно родителя —
(-1, -2). При этом реальное положение объекта на экране
не меняется — меняется только система координат, в которой мы его
описываем.
Главное: позиция всегда измеряется от какой-то точки отсчёта. Какая именно это точка — зависит от того, в каком пространстве мы смотрим на объект.
У вектора есть две ключевые характеристики: направление и длина. Если представить вектор как стрелку, то направление показывает, куда «смотрит» стрелка, а длина — насколько далеко она смещает.
Именно поэтому векторы удобно использовать как смещение. Мы берём текущую позицию объекта (как точку отсчёта) и добавляем к ней вектор смещения. В результате получаем новую позицию.
💡 Это можно воспринимать очень просто: позиция + смещение = новая позиция. Пока не про движение во времени — просто про перенос точки в пространстве.
Например, если мы хотим сместить объект вправо, мы добавляем вектор, у которого X положительный. Если хотим сместить объект влево — добавляем вектор с отрицательным X. То же самое по Y: положительный Y поднимает вверх, отрицательный — опускает вниз.
// текущая позиция объекта (точка). Пример: (1, -1)
Vector2 p = transform.position;
// смещение на 2 вправо → (1, -1) + (2, 0)
p = p + new Vector2(2f, 0f);
// смещение на 1 влево и на 3 вверх → (3, -1) + (-1, 3)
p = p + new Vector2(-1f, 3f);
// применяем новую позицию. Итоговая позиция: (2, 2)
transform.position = p;
Ещё одна полезная идея: смещения можно складывать. Если ты добавил одно смещение, а потом второе — это то же самое, что один раз добавить их сумму. Например, когда игрок одновременно жмёт «вправо» и «вверх», Unity просто объединяет оба смещения в один диагональный вектор.
Горизонтальные и вертикальные направления используются настолько часто, что в Unity для них уже есть готовые обозначения. Это не что-то новое — просто удобные имена для знакомых векторов.
// стандартные направления в Unity 2D
Vector2.right // (1, 0)
Vector2.left // (-1, 0)
Vector2.up // (0, 1)
Vector2.down // (0, -1)
Эти векторы не зависят от позиции объекта. Они всегда указывают направление и имеют длину 1. По сути, это просто заранее подготовленные стрелки, которыми удобно пользоваться снова и снова.
Кроме глобальных направлений, у каждого объекта есть и свои локальные направления. Они зависят от того, как объект повернут и ориентирован в пространстве.
// локальные направления объекта
transform.right // вправо относительно объекта
transform.left // влево относительно объекта
transform.up // вверх относительно объекта
transform.down // вниз относительно объекта
transform.right и
transform.up поворачиваются вместе с объектом.
В отличие от Vector2.right или Vector2.up,
эти направления могут меняться. Если объект повернуть, его локальные
оси повернутся вместе с ним. Не забывай: направление может быть общим
для всего мира или относительным — зависящим от конкретного объекта.
До этого момента мы говорили о простых и понятных направлениях — вверх, вниз, влево, вправо. Но в играх часто нужно другое: получить направление не «в целом», а на конкретную цель.
Для этого используется вычитание векторов. Мы берём позицию цели и вычитаем из неё текущую позицию объекта. В результате получаем вектор направления — стрелку, указывающую от объекта прямо к цели.
// позиция объекта и позиция цели
Vector2 current = transform.position;
Vector2 target = targetPosition;
// вектор направления к цели
Vector2 direction = target - current;
// длина вектора (расстояние до цели)
float distance = direction.magnitude;
// квадрат длины вектора (квадрат расстояния)
float distanceSqr = direction.sqrMagnitude;
💡 magnitude возвращает реальное расстояние до цели, но
требует вычисления квадратного корня, а это достаточно затратная
операция. Поэтому в ситуациях, где таких проверок очень много, часто
используют квадрат расстояния, сравнивая его с квадратом
нужного порога. sqrMagnitude работает быстрее и часто
используется, когда нужно просто сравнить расстояния между собой.
Такой вектор особенно важен в играх. Он сразу содержит две вещи: направление к цели и расстояние до неё, выраженное длиной этого вектора.
Пока достаточно понимать саму идею: вычитание позиций даёт вектор, который отвечает на вопрос «в какую сторону и насколько далеко».
Движение в игре не существует само по себе. Оно возникает как результат последовательной смены состояний, которые отображаются на экране одно за другим.
Движок Unity работает циклом. На каждом шаге он сначала выполняет код и вычисляет новое состояние сцены: позиции объектов, их повороты, значения переменных. Затем это состояние выводится на экран в виде одного кадра. После этого цикл повторяется.
// вызывается один раз за кадр
void Update()
{
// текущая позиция объекта
Vector2 p = transform.position;
// небольшое смещение вправо
p = p + new Vector2(1f, 0f);
// применение новой позиции
transform.position = p;
}
Этот код выполняется каждый кадр. Каждый раз позиция объекта немного меняется, и Unity отображает обновлённое состояние сцены.
Если изменить позицию всего один раз, объект просто окажется в новом месте. Но когда такие небольшие изменения происходят от кадра к кадру, глаз начинает воспринимать их как непрерывное движение.
Таким образом, движение — это не отдельная сущность, а результат последовательного изменения позиции объекта между кадрами относительно наблюдателя (камеры).
Мы уже увидели, что движение появляется как последовательная смена кадров. Но здесь возникает важный вопрос: а что будет, если количество кадров в секунду на разных компьютерах отличается?
Представь два компьютера. Один отображает 50 кадров в секунду, другой — 100. На более мощной системе картинка будет выглядеть плавнее, но если мы будем сдвигать объект на одно и то же расстояние каждый кадр, возникнет проблема.
На компьютере с 100 FPS код выполнится в два раза больше раз за секунду, чем на компьютере с 50 FPS. В результате объект на более быстром компьютере пройдёт в два раза большее расстояние за то же самое время.
Чтобы этого избежать, движение нужно согласовывать не с количеством
кадров, а с реальным временем. В Unity для этого используется значение
Time.deltaTime.
Time.deltaTime показывает, сколько времени прошло с
момента предыдущего кадра. При высоком FPS это значение меньше, при
низком FPS — больше. Используя его, мы можем корректировать величину
смещения в зависимости от скорости обновления кадров.
// вызывается один раз за кадр
void Update()
{
// текущая позиция объекта
Vector2 p = transform.position;
// смещение, согласованное с реальным временем
p = p + new Vector2(1f, 0f) * Time.deltaTime;
// применение новой позиции
transform.position = p;
}
Теперь смещение зависит не от количества кадров, а от того, сколько времени прошло между ними. При высоком FPS шаги будут меньше, при низком — больше, но за одну и ту же секунду объект пройдёт одинаковое расстояние.
В итоге на быстрых компьютерах движение выглядит более плавным, на медленных — может быть более рваным, но пройденное расстояние за одну и ту же секунду остаётся одинаковым на всех системах.
До этого момента мы говорили о принципах движения в целом: кадрах, позициях, смещениях и времени. Теперь можно связать эти идеи напрямую с тем, как движение реализуется в Unity.
В большинстве случаев в Unity используется один из двух подходов к движению: позиционная геометрия и физическая симуляция. Здесь нет «хорошего» или «плохого» варианта — есть только подходящий для конкретной задачи.
При позиционном движении мы работаем напрямую с положением объекта,
изменяя его координаты в пространстве. Мы уже использовали этот
подход, меняя глобальную позицию через
transform.position, а также локальное смещение через
transform.Translate.
Основные плюсы позиционного движения — простота, полный контроль и предсказуемость. Мы точно знаем, где окажется объект в следующий момент времени.
Однако у этого подхода есть и ограничение. Изменяя позицию напрямую,
мы игнорируем физическую симуляцию. Если у объекта есть коллайдер и
Rigidbody, прямое изменение позиции может приводить к
неестественному поведению или пропуску столкновений.
В тех случаях, где важна физика и взаимодействие с окружением,
движение обычно реализуется через Rigidbody. Такой подход
позволяет объекту двигаться более естественно, но при этом уменьшает
прямой контроль со стороны кода.
Вместо прямого перемещения объекта мы воздействуем на его состояние: скорость, импульс или другие физические параметры.
Дальше мы будем рассматривать оба подхода отдельно и разбираться, в каких ситуациях каждый из них оказывается наиболее уместным.
| Критерий | Transform (позиционно) | Rigidbody (физика) |
|---|---|---|
| Смысл | Прямое изменение позиции | Движение как физического тела |
| Контроль | Полный и предсказуемый | Частично передан физике |
| Столкновения | Могут игнорироваться | Обрабатываются корректно |
| Инерция и масса | Отсутствуют | Есть по умолчанию |
| Типичные задачи | UI, логика, простые объекты | Персонажи, экшн, платформеры |
Если объект не участвует в физической симуляции и у него нет физического тела, движение обычно реализуется напрямую через позицию.
Пример: изменение позиции напрямую
// движение вправо через изменение позиции
void Update()
{
transform.position += Vector3.right * 2f * Time.deltaTime;
}
Пример: локальное смещение через Translate
// движение вправо в локальном пространстве объекта
void Update()
{
transform.Translate(Vector3.right * 2f * Time.deltaTime);
}
Метод Translate удобен тем, что по умолчанию двигает
объект в его локальном пространстве — относительно собственного
направления, а не центра мира.
Если объект должен честно взаимодействовать с окружением, иметь
столкновения, массу и инерцию, ему требуется физическое тело —
Collider и Rigidbody.
Пример: движение через силу
Важно понимать, что при переходе к физическому движению векторы никуда не исчезают. Они по-прежнему используются для задания направления, скорости и силы, просто теперь не напрямую меняют позицию объекта, а описывают, как физическое тело должно двигаться.
Вместо «поставить объект в точку» мы говорим физике, в каком направлении и с какой интенсивностью воздействовать на тело. Позиция становится результатом этой работы, а не прямым значением, которое мы перезаписываем сами.
// воздействие силой на физическое тело
void FixedUpdate()
{
rb.AddForce(Vector2.right * 10f);
}
В некоторых случаях требуется более точный контроль, даже при использовании физики. Тогда можно напрямую работать с состоянием тела, например со скоростью.
Пример: управление скоростью тела
// прямое задание скорости (Unity 6+)
void FixedUpdate()
{
rb.linearVelocity = new Vector2(2f, 0f);
// в более старых версиях Unity используется rb.velocity
}
💡 Обрати внимание: физика в Unity работает в собственном ритме,
привязанном к реальному времени, а не к частоте кадров. Поэтому
движение через Rigidbody выполняется в методе
FixedUpdate, и дополнительная корректировка через
Time.deltaTime обычно не требуется.
Оба подхода имеют свои сильные стороны. Важно не выбирать «правильный» способ, а понимать, какой из них подходит для текущей задачи.
Более подробно каждый из этих подходов будет разбираться в отдельных туториалах на сайте.
До этого момента мы рассматривали движение как результат изменения позиции, времени и физических свойств. Осталось связать это с тем, кто и как управляет движением объекта.
В целом здесь тоже существуют два основных подхода: внешнее управление и внутреннее управление.
Внутреннее управление — это движение, заданное алгоритмами, анимацией или логикой игры. Мы уже использовали такие примеры, когда объект двигался сам по себе без участия игрока. Этот подход зависит от задач игры и не имеет единственного «правильного» решения.
Внешнее управление — это управление со стороны игрока. Источником такого управления может быть клавиатура, геймпад, сенсорный экран или любой другой контроллер. Задача игры — получить этот ввод, обработать его и использовать в нужном виде.
Пример: управление через Transform
using UnityEngine;
using UnityEngine.InputSystem;
void Update()
{
float x = 0f;
float y = 0f;
if (Keyboard.current != null)
{
if (Keyboard.current.leftArrowKey.isPressed)
x -= 1f;
if (Keyboard.current.rightArrowKey.isPressed)
x += 1f;
if (Keyboard.current.downArrowKey.isPressed)
y -= 1f;
if (Keyboard.current.upArrowKey.isPressed)
y += 1f;
}
// управление движением через позицию
Vector2 direction = new Vector2(x, y);
transform.Translate(direction * 2f * Time.deltaTime);
}
В этом случае мы напрямую смещаем объект, используя ввод игрока как направление движения. Такой подход прост и хорошо подходит для объектов, не участвующих в физической симуляции.
Пример: управление через Rigidbody
using UnityEngine;
using UnityEngine.InputSystem;
void FixedUpdate()
{
float x = 0f;
float y = 0f;
if (Keyboard.current != null)
{
if (Keyboard.current.leftArrowKey.isPressed)
x -= 1f;
if (Keyboard.current.rightArrowKey.isPressed)
x += 1f;
if (Keyboard.current.downArrowKey.isPressed)
y -= 1f;
if (Keyboard.current.upArrowKey.isPressed)
y += 1f;
}
Vector2 direction = new Vector2(x, y);
// управление движением через физическое тело
rb.linearVelocity = direction * 2f;
}
Здесь ввод игрока используется для управления состоянием физического тела. Объект реагирует на столкновения, инерцию и другие физические свойства сцены.
Главное, что стоит вынести из этого шага: управление — это не движение само по себе. Это лишь источник данных, которые затем преобразуются в направление, скорость или другие параметры движения.
Более подробно разные способы ввода и управления будут рассматриваться в отдельных туториалах.
В этом туториале мы не учили «правильный код». Мы шаг за шагом разбирались, из чего вообще складывается движение в игре: пространство, позиции, векторы, кадры, время и способы воздействия на объект.
Ты увидел, что движение — это не магия и не готовая функция, а последовательность простых решений. Меняя позицию, мы меняем состояние. Отображая состояния по кадрам, мы получаем движение. Учитывая время и контекст, мы делаем его одинаковым на разных системах.
Unity не навязывает единственный путь. Можно двигать объект напрямую, можно доверить это физике, можно управлять им от игрока или алгоритма. Важно не выбрать «лучший» способ, а понимать, почему ты выбираешь именно этот.
Если после этого туториала код стал выглядеть не как набор строк, а как описание происходящего в мире — значит цель достигнута.
Дальше мы будем углубляться в отдельные темы: физику, ввод, камеры и игровые ощущения. Но фундамент уже заложен.