Я решил, что хочу научиться делать игру. Не просто посмотреть ролики и отложить, а реально начать делать своими руками. Формат я выбрал амбициозный - рогалик, скорее всего 3D, с перспективой собрать потом уже что-то полноценное. Для старта я выбрал Unity, потому что если меня тянет именно в сторону 3D, то логичнее сразу заходить в тот инструмент, в котором потом можно будет развивать проект дальше.
Первый этап был максимально базовый, но именно он оказался самым важным. Я создал пустую 3D-сцену, добавил пол, поставил игрока в виде обычного куба и сделал так, чтобы он двигался. Звучит просто, но в этот момент игра впервые перестала быть только идеей в голове. У меня появился объект, который реагирует на клавиши и двигается по сцене. Для новичка это вообще отдельное ощущение - ты впервые видишь, что из набора пустых объектов начинает рождаться что-то живое.
Но самое полезное началось не тогда, когда куб поехал вперед, а тогда, когда я начал разбирать код и понимать, что именно происходит внутри Unity. Мне было важно не просто вставить готовый скрипт, а понять фундамент. Что такое класс, что такое объект, почему в Unity все построено на компонентах, зачем нужен MonoBehaviour, чем отличаются public и private, почему есть разные методы вроде Start(), Update() и FixedUpdate(). В какой-то момент движок перестал выглядеть магией и начал складываться в нормальную систему.
По сути, в первый день я столкнулся сразу с несколькими важными вещами из программирования.
Во-первых, с ООП - объектно-ориентированным подходом. Если объяснять по-человечески, то в Unity почти все строится вокруг объектов. Есть объект игрока, есть объект камеры, есть объект пола. А поведение этим объектам дается через компоненты. Скрипт в Unity - это и есть описание поведения. Когда я создавал PlayerMovement, я по сути делал отдельный класс, который отвечает за движение игрока. Этот класс потом вешается на объект Player, и объект начинает вести себя так, как я описал в коде.
Во-вторых, я начал понимать, как устроена структура C#-скрипта. Например, вот такие строки:
public class PlayerMovement : MonoBehaviour
означают, что я создаю класс PlayerMovement, а : MonoBehaviour говорит Unity, что это не просто обычный класс, а скрипт, который можно повесить на объект сцены. Именно благодаря этому Unity понимает, что в этом скрипте есть специальные методы жизненного цикла, которые надо вызывать автоматически.
Дальше идут переменные:
public float moveSpeed = 5f;
private Rigidbody rb;
private float moveX;
private float moveZ;
Здесь я использовал несколько базовых вещей. float - это тип данных для чисел с дробной частью. public значит, что переменная видна в Inspector внутри Unity, и я могу менять ее без переписывания кода. Это удобно, потому что скорость движения можно подкрутить прямо в редакторе. private значит, что переменная нужна только внутри самого скрипта. Так я храню служебные данные, которые не надо показывать снаружи. Rigidbody - это компонент физики, через который объект двигается корректно с точки зрения Unity.
Потом я разобрался с методами. Это вообще одна из ключевых тем первого дня.
Start() - это метод, который Unity вызывает один раз, когда объект запускается в сцене. В нем я получил ссылку на компонент физики:
rb = GetComponent<Rigidbody>();
Метод GetComponent<Rigidbody>() ищет на этом же объекте компонент Rigidbody и возвращает его в переменную rb. Это очень важная механика Unity - скрипты постоянно получают ссылки на другие компоненты и потом работают через них.
Update() - это метод, который вызывается каждый кадр. В нем я считывал ввод с клавиатуры:
moveX = Input.GetAxis("Horizontal");
moveZ = Input.GetAxis("Vertical");
Метод Input.GetAxis() возвращает значение оси управления. Для Horizontal это обычно A/D или стрелки влево-вправо, для Vertical - W/S или стрелки вверх-вниз. То есть здесь я не двигаю игрока напрямую, а только читаю, что нажимает игрок, и сохраняю эти значения в переменные.
А вот само движение происходило уже в FixedUpdate(). Это тоже важный момент. FixedUpdate() используется для физики и вызывается с фиксированным шагом по времени. Если движение идет через Rigidbody, то логичнее делать его именно там. Внутри я собирал направление движения через Vector3:
Vector3 movement = new Vector3(moveX, 0f, moveZ);
Vector3 - это тип данных для трехмерных координат и направлений. Здесь moveX отвечает за движение влево-вправо, 0f значит, что по оси Y мы не двигаемся, а moveZ отвечает за движение вперед-назад. То есть я создавал вектор направления, в котором должен двигаться игрок.
Потом этот вектор использовался в методе:
rb.MovePosition(rb.position + movement * moveSpeed * Time.fixedDeltaTime);
Вот здесь уже произошло настоящее движение. MovePosition() - метод Rigidbody, который перемещает объект. rb.position - текущая позиция игрока. movement * moveSpeed задает скорость и направление. Time.fixedDeltaTime нужен для того, чтобы движение было стабильным и не зависело от частоты кадров. Это вообще одна из первых важных вещей, которую нужно понять в геймдеве: время между кадрами бывает разным, и если его не учитывать, объект может двигаться по-разному на разных компьютерах.
Потом я пошел дальше и начал улучшать этот прототип. Следующим шагом стала камера. Я сделал отдельный скрипт CameraFollow, в котором камера просто следовала за игроком. Там уже появились новые штуки:
public Transform target;
public Vector3 offset = new Vector3(0f, 8f, -8f);
Transform - это компонент, который есть у каждого объекта в Unity. Он хранит позицию, поворот и масштаб. Когда я писал target, я хранил ссылку на объект игрока, за которым должна следовать камера. offset - это смещение камеры относительно игрока, то есть насколько выше и дальше она находится.
Для камеры я использовал метод LateUpdate(). Он вызывается после обычного Update(), и это удобно, потому что сначала игрок успевает обновить свою позицию, а потом камера уже подстраивается под его новое положение. Внутри был вот такой код:
transform.position = target.position + offset;
transform.LookAt(target);
transform.position меняет позицию камеры. target.position + offset ставит ее в нужную точку относительно игрока. А transform.LookAt(target) заставляет камеру смотреть на игрока. Это уже создало ощущение нормальной третьеличной сцены, а не просто теста с кубом на плоскости.
После этого я сделал камеру плавнее. Для этого использовался метод Vector3.Lerp():
Vector3 desiredPosition = target.position + offset;
Vector3 smoothedPosition = Vector3.Lerp(transform.position, desiredPosition, smoothSpeed * Time.deltaTime);
Lerp - это плавный переход из одной позиции в другую. То есть камера перестала телепортироваться за игроком и начала мягко догонять его. Time.deltaTime здесь снова нужен для плавности, независимой от FPS. Это уже добавило ощущение нормального движения, а не дерганого прототипа.
Дальше я добавил поворот игрока в сторону движения. Вот тут появились еще две важные штуки: Quaternion.LookRotation() и Quaternion.Slerp().
Когда игрок движется, я получаю направление его движения и на основе него создаю нужный поворот:
Quaternion targetRotation = Quaternion.LookRotation(movement);
Quaternion - это способ хранить поворот в 3D. Да, тема непростая, но базово можно понимать так: через него Unity задает, куда должен смотреть объект. LookRotation(movement) создает поворот в сторону вектора движения.
А чтобы поворот был не резким, а плавным, использовался:
Quaternion smoothRotation = Quaternion.Slerp(rb.rotation, targetRotation, rotationSpeed * Time.fixedDeltaTime);
rb.MoveRotation(smoothRotation);
Quaternion.Slerp() плавно интерполирует один поворот в другой. То есть объект не щелкает моментально в новую сторону, а красиво разворачивается. MoveRotation() уже применяет этот поворот к Rigidbody. За счет этого игрок начал не просто ездить по сцене, а ощущаться как управляемый персонаж.
Еще один важный момент первого дня - я начал видеть разницу между "код работает" и "игра ощущается нормально". Сначала у меня был просто двигающийся куб. Потом я добавил камеру. Потом сглаживание. Потом поворот. И каждое из этих маленьких улучшений меняло ощущение от проекта. Именно в такие моменты начинаешь понимать, что игра собирается не из одной большой идеи, а из десятков маленьких решений, каждое из которых делает ее чуть живее.
Если коротко, то за первый день я не просто создал первую сцену, а потрогал сразу несколько фундаментальных основ разработки: ООП, классы, компоненты, переменные, методы жизненного цикла Unity, работу с физикой, векторами, камерой и поворотом объекта. Для человека, который начал с полного нуля, это уже очень мощный сдвиг. Самое главное - Unity перестал быть чем-то непонятным и начал раскладываться на простые части. Есть объект. Есть компонент. Есть скрипт. Есть метод. Есть результат на экране.
И вот это, наверное, мой главный итог первого дня. У меня пока не рогалик. У меня пока даже не полноценный персонаж. Но у меня уже есть первый рабочий кусок игры, и я уже начинаю понимать, из чего он состоит. А это намного важнее, чем просто бездумно копировать код из урока.
Во второй статье уже можно показать, как из этого базового прототипа начала рождаться настоящая игра: первый враг, здоровье, урон, атака, несколько противников и волны.