3 min read
Strafe-running

В целях обучения делаю прототип игры вместе с Claude. Такой подход мне нравится больше: сперва мы создаем план того как должна выглядеть фича, и затем поэтапно реализовываем. По ходу могу задавать любые вопросы и уходить вглубь по интересующим темам. Управление персонажем тоже реализовывается с нуля: ходьба и поворот камеры. Конечно, есть уже готовые инструменты от Unity, но в них очень много абстракций, которые могут сбивать и усложнять понимание, плюс это быстро становится no fun.

И вот при реализации движения был замечен интересный эффект.

Это часть кода, которая была в изначальной версии:

void HandleMovement()
{
    float x = Input.GetAxisRaw("Horizontal");
    float z = Input.GetAxisRaw("Vertical");

    Vector3 move = transform.right * x + transform.forward * z;

    if (characterController.isGrounded && verticalVelocity < 0)
        verticalVelocity = -1f;
    else
        verticalVelocity += gravity * Time.deltaTime;

    Vector3 finalMove = move * speed + Vector3.up * verticalVelocity;
    characterController.Move(finalMove * Time.deltaTime);
}

Главное здесь это первые три строки, именно там собирается вектор движения. С виду вроде бы ничего необычного: для того, чтобы персонаж двигался, мы получаем значения по осям ввода (x, z), перемножаем с векторами направления самого объекта GameObject (transform.right, transform.forward) и складываем, получая вектор движения.

Но в этой реализации есть “баг”. Если зажать одновременно клавиши движения вперед/назад и влево/вправо, игрок будет двигаться быстрее.

Чтобы понять, что происходит нужно вспомнить формулу того как считается длина вектора:

a(x,y,z):a=x2+y2+z2\vec{a}(x, y, z): |\vec{a}| = \sqrt{x^2 + y^2 + z^2}

Когда мы жмём только вперед:

Vector3 move = transform.forward * 1 = (0, 0, 1)  // длина вектора = 1

12=1=1\sqrt{1^2} = \sqrt{1} = 1

Когда жмём только вправо:

Vector3 move = transform.right * 1 = (1, 0, 0)  // длина вектора = 1

12=1=1\sqrt{1^2} = \sqrt{1} = 1

А когда жмём вперед+вправо одновременно:

Vector3 move = transform.right * 1 + transform.forward * 1 = (1, 0, 1)

То длина вектора по формуле будет следующей:

12+12=21.41\sqrt{1^2 + 1^2} = \sqrt{2} \approx 1.41

Что почти в полтора раза быстрее.

Это не баг Unity, а чистая геометрия: диагональ квадрата длиннее его стороны.

Этот эффект известен как strafe-running. Именно на нём построена легендарная техника strafe-jumping из Quake и Doom, где движение по диагонали (плюс прыжки) позволяло ускорять персонажа эксплуатируя механику для спидранов или в PvP.

Фиксится это в Unity супер просто. Нам необходимо нормализовать вектор, т.е. нужно каждую компоненту поделить на длину вектора. В формуле это будет выглядеть так:

a^=(12)2+(12)2=12+12=1=1|\hat{a}| = \sqrt{\left(\frac{1}{\sqrt{2}}\right)^2 + \left(\frac{1}{\sqrt{2}}\right)^2} = \sqrt{\frac{1}{2} + \frac{1}{2}} = \sqrt{1} = 1

Тут мы видим, что нормализация сводит длину вектора по диагонали к 1, как раз то, что нам нужно.

Но писать нормализацию самим не нужно, ведь в Unity в Vector3 есть метод .normalized, который делает всю магию за нас. Поэтому конечная формула для движения персонажа будет следующей:

float x = Input.GetAxisRaw("Horizontal");
float z = Input.GetAxisRaw("Vertical");

Vector3 move = (transform.right * x + transform.forward * z).normalized;

После этого простейшего фикса наш персонаж будет двигаться во всех направлениях с одинаковой скоростью.