Counter-Strike

Counter-Strike

64 ratings
ФИЗИКА СТРЕЙФОВ
By piita 🍂
Понимать физику стрейфов мы будем, используя находящийся в открытом доступе код Half-Life 1. А конкретно, нам понадобятся файлы input.cpp, pm_shared.c и pm_math.c

Скорость (velocity) – векторная величина. Мы можем задать её тремя компонентами (проекциями на оси системы координат). Либо можно представить её как пару – модуль скорости (speed) и её направление (единичный вектор).
При 100 fps движок CS 100 раз в секунду обрабатывает движения мыши и нажатия кнопок, после чего, исходя из этих данных, вычисляет, как должна вести себя моделька игрока. Если быть точнее, то движок знает не про нажатия кнопок, а просто получает команды «идти вправо», «присесть», «прыгнуть» и т. д., но для простоты будем говорить о кнопках.

Получение вектора скорости игрока на каждом таком такте происходит в несколько этапов:
Получить состояние кнопок движения (WASD)

На основе полученного состояния получить вектор желаемой скорости (то есть той, которую игрок стремится приобрести) в системе координат, связанной с моделькой игрока

Зная направление, в котором смотрит игрок в данный момент, найти связь (а точнее матрицу перехода) между системой координат модельки и системой координат внешнего мира

В случае наличия трения (к примеру, когда игрок идёт по земле) уменьшить модуль текущей скорости

Используя полученную на этапе 3. матрицу, перевести вектор желаемой скорости в систему координат внешнего мира. Затем определённым образом сложить эту скорость с вектором текущей скорости. Причём способ сложения будет различаться в зависимости от того, находится игрок на земле или в воздухе. Таким образом, получен искомый вектор скорости.


   
Award
Favorite
Favorited
Unfavorite
ЭТАП 1
Прежде всего, взглянем на эту функцию из input.cpp:



Как видите, она возвращает значение val, характеризующее состояние некой клавиши. Например, если кнопка была зажата в каком-то предыдущем фрейме, и всё ещё зажата в текущем фрейме, то val будет равен 1. А если, к примеру, кнопка не была нажата и не нажимается в текущем фрейме, то val равен 0.
ЭТАП 2
Теперь в том же input.cpp рассмотрим следующий участок кода, исходя из нажатых клавиш (а точнее отправленных команд) формирующий вектор желаемой скорости (forwardmove, sidemove, upmove), то есть то направление, в котором мы хотим двигаться в данный фрейм. К концу статьи станет понятно, куда мы будем двигаться в итоге после всех расчётов.



При вычислении компонент использовались значения cl_upspeed (320 по умолчанию), cl_forwardspeed (400), cl_backspeed (400) и cl_sidespeed (400). При зажатой клавише Shift скорость в Half-Life умножается на 0.3, в CS 1.6 на 0.52. Если вектор после этого оказался по длине больше, чем maxspeed (250 с ножом/пистолетом по умолчанию), то масштабируем его так, что он становится равным 250.

ЭТАП 3
Теперь посмотрим на интересующую нас часть функции PM_PlayerMove из файла pm_shared.c



Что это за AngleVectors там стоит? Всё довольно просто. Введём обозначения:
АСК – абсолютная система координат, связанная с внешним миром (картой)
ССК – связанная с моделькой игрока система координат

Тот вектор желаемой скорости, который мы получали в input.cpp, записан в ССК, причём эта система левосторонняя (ось X смотрит прямо вперёд, ось Y вправо, ось Z вверх - именно в этих направлениях мы получаем положительные значения forwardmove, rightmove и upmove соответственно). Мы же хотим использовать этот вектор для вычисления итоговой скорости, а та записана в правосторонней АСК. Функция AngleVectors даст нам матрицу перехода от АСК к ССК.



На вход поступают три угла, которые задают направление, в котором мы смотрим, то есть положение ССК относительно АСК. Эти углы называются углами Крылова и повсеместно используются в навигации:
рыск (yaw) - поворот направо-налево (ось Z)

тангаж (pitch) - наклон вверх-вниз (ось Y)

крен (roll) - наклон вправо-влево (ось X).

Для краткости обозначим их следующим образом:



Для нахождения матрицы перехода от АСК к ССК нужно перемножить матрицы поворота, соответствующие углам Крылова. Плюс ещё одна матрица задаст отражение оси Y, дабы из правосторонней получить левостороннюю систему координат:



После перемножения получатся как раз те формулы, которые вы видели в коде функции AngleVectors. Итоговая матрица перехода от АСК к ССК имеет вид:



Здесь forward, right и up – единичные вектора, задающие оси ССК в проекциях на оси АСК.
ЭТАП 4
Вернёмся в PM_PlayerMove. Там у нас далее вызывается функция PM_Friction (в том случае, если мы находимся на земле). Интересующая нас часть этой функции выглядит так:



При sv_friction 4 и при 100 fps (откуда длительность фрейма будет 0.01 секунды) получаем drop = control * 0.04. Если наша скорость больше значения sv_stopspeed (по умолчанию 100), то control равен нашей скорости, а drop составляет 4 процента от неё. Именно на эти 4 процента и замедлится наша скорость на выходе из PM_Friction.
ЭТАП 5 ч1
После PM_Friction вызывается либо PM_WalkMove (если мы на земле), либо PM_AirMove (если мы не на земле). Рассмотрим эти случаи отдельно.

Движение на земле

Вот что представляет из себя PM_WalkMove :



Здесь мы получаем forwardmove и sidemove из input.cpp, затем используем forward и right из полученной нами выше матрицы для того, чтобы перевести вектор желаемой скорости в АСК, как мы и планировали.



Чтобы исключить влияние клавиш движения на вертикальную скорость игрока (в системе ССК), мы не используем upmove и обнуляем вертикальные компоненты forward и right (что вынуждает нас нормировать эти вектора, дабы они не повлияли на величину желаемой скорости). Далее скорость обрезается до значения maxspeed, и вызывается функция PM_Accelerate, в которую передаются значение и направление (единичный вектор) желаемой скорости, а также значение sv_accelerate (по умолчанию 5).



DotProduct это не что иное, как скалярное произведение векторов velocity и wishdir. Программно оно вычисляется с использованием их компонент, но нас интересует именно физический смысл. Так как wishdir единичный вектор, задающий желаемое направление движения, а velocity это текущий вектор скорости, то мы фактически проецируем velocity на направление wishdir. Чем больше между ними угол, тем меньше будет проекция, то есть переменная currentspeeed.

Далее вычисляется addspeed как разница желаемой скорости и currentspeed, а также accelspeed (accel * pmove->frametime * pmove->friction можно оценить как 5 * 0.01 * 1 = 0.05, тогда accelspeed составит 5 процентов от желаемой скорости).

В зависимости от соотношения между addspeed и accelspeed одна из этих величин становится длиной той самой прибавки в скорости, которую мы так желали получить. К вектору исходной скорости прибавляется вектор, направленный вдоль wishdir и равный по величине accelspeed (мы можем их складывать, так как заведомо перевели их в одну систему координат):



Попробуем теперь прочувствовать, что же всё это значит.

Эксперимент 1. Мы зажали W и просто бежим вперёд с ножом или пистолетом. Функция PM_Friction в данный рассматриваемый фрейм обрежет скорость на 4 процента, теперь вместо 250 мы имеем 240 юнитов/сек. Так как зажата только W, то в input.cpp её состояние равно 1, состояние остальных клавиш движения 0. Мы сначала получаем cmd->forwardmove = 400, cmd->sidemove = 0, а затем после масштабирования (так как мы превысили максимальную скорость 250) cmd->forwardmove = 250, cmd->sidemove = 0. В функции PM_WalkMove вектор (fmove, smove) задаст направление, в котором мы смотрим. Направление wishdir совпадает с направлением нашей текущей скорости, величина желаемой скорости wishspeed равна 250, и мы заходим в PM_Accelerate. Здесь
currentspeed = 240 * cos(0) = 240
addspeed = 250 - 240 = 10
accelspeed = 250 * 0.05 = 12.5 > 10

поэтому accelspeed становится равным 10. Далее мы прибавляем к нашей скорости 240 юнитов/сек ещё 10 юнитов/сек в том же направлении, и скорость вновь становится равной 250. Вот так мы просто бежим вперёд по земле.

Эксперимент 2. Мы, как и раньше, бежим вперёд и вдруг нажимаем A. Скорость после PM_Friction так же 240 юнитов/сек. W была зажата и всё ещё зажата, поэтому её состояние 1, клавиша A зажата только что, её состояние 0.5. Поэтому сначала cmd->forwardmove = 400, cmd->sidemove = 200, а затем после масштабирования cmd->forwardmove = 223.6, cmd->sidemove = 111.8. Внутри PM_WalkMove после получения желаемой скорости мы передаём в PM_Accelerate wishspeed = 250 и единичный вектор желаемого направления wishdir, составляющий с нашей текущей скоростью угол arctg(200/400) = 26.565 градуса. В PM_Accelerate находим:
currentspeed = 240 * cos(26.565) = 214.66
addspeed = 250 - 214.66 = 35.34
accelspeed = 250 * 0.05 = 12.5 < 35.34

поэтому accelspeed остаётся равным 12.5. К нашим 240 юнитам/сек прибавляем 12.5 юнитов/сек в направлении wishdir. По теореме косинусов находим:
x^2 = 240^2 + 12.5^2 + 2* 240 * 12.5 * cos(26.565) = 63122.77
x = 251.24

Итак, наша скорость чуть повернулась влево и увеличилась примерно на 1.24 юнита/сек.
В последующие фреймы состояние клавиши A будет равно 1, поэтому wishdir всё время будет составлять 45 градусов с тем направлением, в котором мы смотрим. Схематично сложение скоростей происходит следующим образом:



Вектор скорости всё время будет поворачиваться влево, а её величина будет меняться в силу наличия трения любопытным образом - несколько фреймов будет уменьшаться примерно до 249, затем где-то за 14 фреймов вырастет до 262, а после будет плавно уменьшаться в течение полусотни фреймов (напомню, при 100 fps каждый фрейм длится 0.01 секунды). Вот мы и получили объяснение такого явления, как fastrun.

Эксперимент 3. До сих пор мы не задействовали поворот самой модельки. Произведём его при помощи "стрелочек" (команд +left и +right). Будем бежать, зажав A, W и левую стрелку. Через несколько секунд вы обнаружите, что бежите по кругу - мы всё время уводим вектор wishdir влево, а текущая скорость будто пытается догнать его. Так как мы установили постоянную скорость поворота модельки при помощи стрелочки, то между wishdir и текущей скоростью будет сохраняться некоторый небольшой угол. При этом PM_Friction будет каждый фрейм уменьшать скорость, а PM_Accelerate поворачивать её и увеличивать ровно настолько же. В итоге мы бежим по кругу с постоянной скоростью.

Как можно догадаться, установившаяся скорость будет меняться в зависимости от скорости поворота на стрелочке, которая определяется кваром cl_yawspeed (по умолчанию 210). Подбирая этот квар, можно добиться некой максимальной скорости (около 278 юнитов в секунду).

ЭТАП 5 ч2
Эксперимент 4. Теперь будем поворачивать мышью. Мы знаем, что существует максимум скорости при прейстрейфе. Что нам делать, чтобы достигнуть его побыстрее, не нарезая круги? Будем разгоняться с нуля. Сначала наберём скорость при помощи W, и далее зажмём A. Вектор скорости потащится вслед за желаемым направлением скорости влево. Начнём поворачивать мышь влево, не давая текущей скорости "догнать" желаемое направление. Скорость будет постепенно расти, поэтому чтобы addspeed не стал слишком маленьким, нам со временем нужно всё больше "остерегаться" чересчур острого угла между текущей скоростью и желаемым направлением. Другими словами, угловую скорость мы в идеале будем постепенно увеличивать, в итоге выйдя на cl_yawspeed 118. Кроме того, если мы хотим осуществить как можно меньший поворот мышью, было бы замечательно после нажатия A не сразу начать поворот мыши, а "опоздать" на 0.1-0.15 сек, пока скорость достигнет 260 юнитов/сек и будет близка в желаемому направлению. А затем мы поведём мышь влево, постепенно увеличивая угловую скорость. Очень быстро мы достигли бы максимума.

Но как происходит на практике? Времени на всё про всё у нас очень мало. Мышь мы начинаем вести примерно в тот же момент, когда нажимаем A. Далее мы подсознательно чувствуем, что проскочить угловую скорость, соответствующую cl_yawspeed 118, невероятно просто, поэтому, как только мы ускорили нашу мышь, мы стараемся вести её примерно с той самой заветной угловой скоростью. Когда мы повернулись почти на 70-80 градусов, мы по опыту знаем, что скорость уже достигнута, и отталкиваемся от земли. Так мы разгоняемся на lj блоках, очерчивая на земле примерно четвертинку от описываемых выше кругов.

А что если у нас нет возможности сделать такой поворот, а скорость очень нужна (к примеру, мы находимся в довольно узком коридоре)? Поступим следующим образом - разгоняемся на W как и раньше, а затем поворачиваем мышь вправо на 30-40 градусов, нажимаем A и ведём мышь влево. Таким образом, мы сразу дел

Движение в воздухе

Ну что ж, оставим землю в покое. Посмотрим, что происходит в воздухе.

PM_AirMove отличается от PM_WalkMove разве лишь тем, что вместо PM_Accelerate вызывается функция PM_AirAccelerate, и передаётся в неё не sv_accelerate, а sv_airaccelerate(по умолчанию 10). Сама же PM_AirAccelerate практически один в один повторяет PM_Accelerate:



Однако здесь есть два важных для физики отличия. Во-первых, в воздухе на нас не действует трение, и во-вторых мы обрезаем wishspeed до 30 (переменная wishspd равна wishspeed). Таким образом, мы не можем использовать те же тактики, что на земле. Действительно, наш currenspeed не должен превышать 30, иначе никакого ускорения мы заведомо не получим. И тут мы идём на хитрость - отказываемся от использования W, будем нажимать только A либо D. А угол между текущей скоростью и желаемым направлением мы будем делать таким, что скалярное произведение DotProduct окажется меньше 30.

Но не будем гнать лошадей, сначала сориентируемся в цифрах.

Разбежимся на W, затем прыгнем, отпустим W и нажмём A. Никакое трение на нас теперь не действует, поэтому текущая скорость равна 250 юнитов/сек и в первый фрейм в воздухе направлена прямо туда, куда мы смотрим. В PM_AirAccelerate мы зайдем, имея направление wishdir, смотрящее влево, и wishspeed 250.
wishspd = 30
currentspeed = 0 (cos(90) = 0)
addspeed = 30 - 0 = 30 > 0
accelspeed = 10 * 250 * 0.01 * 1 = 25 < 30

поэтому accelspeed остаётся равным 25. Итоговая скорость будет чуть отклонена влево и по теореме Пифагора составит
x^2 = 25^2 + 250^2 = 63125
x = 251.25

Таким образом, наша скорость окажется чуть больше, и мы пролетим большую дистанцию. Хотя, по сути, вперёд мы пролетели ровно настолько же, как если бы не нажимали кнопок вообще - дистанция больше только за счёт отклонения влево во время полёта.

Сейчас наш угол между wishdir и текущей скоростью был примерно 90 градусов. Предположим, что наша скорость на протяжении всего полёта равна 250 юнитов/сек. Попробуем найти угол, при котором DotProduct достигнет 30. Из cos(a) = 30/250 получаем a = 83.1 градуса. Значит, при угле больше 83.1 градуса мы будем получать приращение скорости. Поначалу оно будет небольшое, но с увеличением угла DotProduct достигнет значения 5 (это произойдет при arccos(5/250) = 88.854 градуса), addspeed станет равным 25, и дальнейшее увеличение угла уже не повлияет на длину прибавляемого вектора - accelspeed будет всё время равным 25. Мало того, это положение является максимумом в плане прибавляемой скорости - чем больше мы продолжаем увеличивать угол, тем меньше становится вектор результирующей скорости в силу векторного сложения. Когда угол превышает 90 градусов, DotProduct оказывается отрицательным, addspeed гарантированно больше accelspeed, и мы всё ещё имеем прибавку в скорости. Сложение векторов при этом выглядит следующим образом (схематично):



Заметьте, что текущая скорость, как и на земле, будто гонится за нашим вектором wishdir, а мы уводим его то в одну сторону, то в другую.

При угле 92.866 градуса мы получим, что вектор скорости лишь повернётся, а по величине останется прежним. При углах, больших 92.866 градуса мы будем просто-напросто терять скорость, причём чем дальше, тем всё больше.

Подведём итоги вычислений для стрейфов в воздухе. Обозначим угол между текущей скоростью и желаемым направлением u.
u <= 83.1
Никакого изменения скорости не происходит (даже по направлению). Если в воздухе повернёте мышь влево, нажмёте D и продолжите вести мышь влево, ваша скорость никак не изменится. Если попробуете делать стрейфы, зажав W, ваша скорость никак не изменится.

83.1 < u < 92.866
Ваша скорость меняет направление в сторону клавиши стрейфа и растёт по величине, причём максимум прироста происходит при угле 88.854 градуса. Полученный прирост вы видите в столбике Gain в lj статистике.

u > 92.866
Скорость меняет направление и уменьшается по величине. Именно эти потери скорости показывает lj статистика в столбике Loss.


Чувствуете, в какой узкий зазор мы должны попасть, чтобы получить прирост скорости?

А теперь самое интересное - вспомним, что всё это было получено в предположении, что скорость во время полёта сохраняла значение 250 юнитов/сек. Но она же растёт! И вместе с её ростом полученный нами зазор для u неумолимо уменьшается. А если ещё вспомнить, что при lj мы заранее набираем скорость около 275? Ух, непростое это оказывается дело - lj прыгать. Отсюда мы можем сделать следующий вывод - если хотите минимизировать потери скорости и максимизировать её прирост, то либо вы делаете одинаковые по угловой скорости стрейфы, сужающиеся по ходу полёта; либо вы сохраняете амплитуду стрейфов, но по ходу полёта делаете их более плавными. Вот такие пироги.

автор: Kpoluk, специально для KZ-Rush.ru
34 Comments
counter strike 4 Feb, 2020 @ 2:39pm 
етот топ паможыт вам итти в переот на 100 проц лох
ben gann 4 Feb, 2020 @ 11:50am 
Но за статью лукас поставлю
ben gann 4 Feb, 2020 @ 11:50am 
Я как посмотрю, в комментариях одни гуманитарии сидят :lunar2019piginablanket::winter2019cooldog:
vanya_pro2006 2 Feb, 2020 @ 4:25am 
Ебать, питонисту ничего не понятно
Бесячий Кот 31 Jan, 2020 @ 6:47am 
нечего не понятно но очень интересно:lunar2019piginablanket:
🆆🅾🆁🅺🆂🅰🅼🆈 3 Nov, 2018 @ 2:25pm 
Академики в ахуевлении от такой инфы)))))
twi 17 May, 2018 @ 10:23am 
+rep
H0sh1k0 ❀ 26 Apr, 2018 @ 1:16pm 
ты красавчик) вообще найс обьяснил
а то на просторах интернета нету такой инфы
Belgium 17 Feb, 2018 @ 4:22am 
я хуй пойму
ANTOSHIBA007 5 Feb, 2018 @ 5:56am 
Уровень БОГ! Просто красавчик! Таких глубоких разборов я не видел еще. 1 единственный минус, что контингент играющих сегодня, это разобрать не сможет:( к большому сожалению.