docs: изучен CGameMovement из Source SDK 2013 — расхождения с нашей физикой

Доки Valve закрыты Anubis, читал первоисточник
ValveSoftware/source-sdk-2013, src/game/shared/gamemovement.cpp.

Записаны точные причины двух симптомов пользователя и ещё трёх расхождений:

1. StepMove у оригинала пробует оба варианта (вниз и вверх на flStepSize) и
   берёт тот, что ушёл дальше по горизонтали; вертикальную скорость берёт из
   движения ВНИЗ (m_vecVelocity.z = vecDownVel.z). У нас подшаг всегда идёт
   путём «поднять 18 и уронить», режет путь на рампе и добавляет паразитную
   вертикаль — отсюда и потеря скорости вверх, и «прыжки с рампы» вниз.
2. Friction() вызывается каждый кадр на земле безусловно, формула
   control = max(speed, sv_stopspeed), drop = control*friction*frametime.
   У нас трение только при отсутствии ввода — по рампе на бегу его нет.
3. Гравитация делится 0.5 до и 0.5 после движения (StartGravity/FinishGravity).
4. Ускорение — Quake-формула accelspeed = accel*frametime*wishspeed, а в
   воздухе ещё и ограничение sv_air_max_wishspeed = 30. Отсюда «вязкое»
   управление в воздухе, которого у нас нет.
5. Прыжок по фронту: в оригинале при выходе из воды ставится
   m_nOldButtons |= IN_JUMP с комментарием «don't jump again until released» —
   наша защёлка воспроизводит штатный приём движка.

Код не менял: сначала фиксирую расхождения, чтобы не подгонять тесты.
This commit is contained in:
Niko Marmeladkov 2026-10-13 15:20:08 +03:00
parent 2e906e034b
commit 570f4bb9d0

View file

@ -783,3 +783,114 @@ Source делает наоборот: при `FL_ONGROUND` вертикальн
в Source движение просто останавливается.
- `sv_maxspeed = 320`, `sv_maxvelocity = 350` как отдельные ограничения.
- Ускорение/торможение поворота камеры, инерция при стрейфе.
## Часть B6. Изучение `CGameMovement` (Source SDK 2013)
Источник: `src/game/shared/gamemovement.cpp` из `ValveSoftware/source-sdk-2013`
(доки Valve под Anubis, читался исходник). Ниже — расхождения нашей реализации
с оригиналом, найденные **чтением первоисточника**, а не догадками.
### B6.1. Почему теряется скорость на рампе вверх
`CGameMovement::StepMove` устроен принципиально иначе, чем наш подшаг:
```cpp
// Try sliding forward both on ground and up 16 pixels
// take the move that goes farthest
TryPlayerMove( &vecEndPos, &trace ); // сначала ВНИЗ — получаем vecDownPos/vecDownVel
...подъём на flStepSize...
TryPlayerMove(); // потом ВВЕРХ — получаем vecUpPos
// decide which one went farther
float flDownDist = ( vecDownPos.x - vecPos.x )^2 + ( vecDownPos.y - vecPos.y )^2;
float flUpDist = ( vecUpPos.x - vecPos.x )^2 + ( vecUpPos.y - vecPos.y )^2;
if ( flDownDist > flUpDist ) { едем вниз; }
else { /* copy z value from slide move */ mv->m_vecVelocity.z = vecDownVel.z; }
```
Три отличия от нашего кода, каждое даёт симптом:
1. **Пробуются оба варианта, побеждает тот, что ушёл дальше по горизонтали.**
Мы всегда идём путём «поднять на 18 и уронить». На рампе вверх путь вниз
(без подшага) уходит дальше — значит оригинал его и берёт, а подшаг не
выполняется вовсе. У нас он выполняется всегда и режет путь.
2. **Вертикальная скорость берётся из движения ВНИЗ**
(`m_vecVelocity.z = vecDownVel.z`), а не из движения вверх. Иначе на
рампе появляется паразитная вертикальная составляющая.
3. Отказ от подшага при `trace.plane.normal[2] < 0.7` — то есть на крутых
поверхностях подшаг не применяется вовсе.
### B6.2. Почему при спуске «прыгает с рампы»
Наш подшаг выполняется по признаку «движение заблокировано» и всегда с
подъёмом на `STEP_HEIGHT = 18`. На рампе вниз игрок кадр за кадром
подскакивает на 18 юнитов и падает обратно — визуально это «прыжки с
рампы». Оригинал при спуске идёт по пути `flDownDist`, который дальше, и
подшага не делает.
### B6.3. Трение: у нас не так
`CGameMovement::Friction()` вызывается **каждый кадр на земле** безусловно
(проверка только `GetGroundEntity() != NULL`), а не «если нет ввода».
Формула оригинала:
```cpp
control = (speed < sv_stopspeed) ? sv_stopspeed : speed; // 100 по умолчанию
drop += control * friction * frametime; // sv_friction = 4
newspeed = max(0, speed - drop);
```
У нас трение срабатывает только при `wish.length_squared() < 1.0`, то есть
при беге по рампе его нет вовсе.
### B6.4. Гравитация делится пополам до и после движения
```cpp
void CGameMovement::StartGravity( void ) {
mv->m_vecVelocity[2] -= ent_gravity * GetCurrentGravity() * 0.5 * frametime;
}
void CGameMovement::FinishGravity( void ) {
mv->m_vecVelocity[2] -= ent_gravity * GetCurrentGravity() * frametime * 0.5;
}
```
В комментарии Valve: «yes, this 0.5 looks wrong, but it's not». Суммарно
гравитация та же, но момент приложения другой — это и даёт правильное
ощущение веса на дуге.
### B6.5. Ускорение: Quake-алгоритм, а не линейная интерполяция
```cpp
currentspeed = velocity.Dot(wishdir);
addspeed = wishspeed - currentspeed;
if (addspeed <= 0) return;
accelspeed = accel * frametime * wishspeed * friction; // НЕ accel*dt!
if (accelspeed > addspeed) accelspeed = addspeed;
velocity += accelspeed * wishdir;
```
`AirAccelerate` отличается одной строкой — ограничением желаемой скорости:
```cpp
if ( wishspd > GetAirSpeedCap() ) wishspd = GetAirSpeedCap(); // sv_air_max_wishspeed = 30
```
Вот почему в воздухе управление «вязкое»: прирост скорости за кадр
ограничен, а ускорение не накапливается. У нас `AIR_ACCEL = 12` при
`GROUND_ACCEL = 10` и линейная интерполяция к целевой скорости — в воздухе
у нас **быстрее**, чем на земле.
### B6.6. Прыжок по фронту
В чистом Source `IN_JUMP` проверяется по уровню, но при выходе из воды
ставится `mv->m_nOldButtons |= IN_JUMP; // Don't jump again until released`.
То есть «не прыгать, пока не отпустят» — штатный приём движка, а не
изобретение: наша защёлка `jump_latched` воспроизводит именно его.
### B6.7. Порядок работы на будущее
1. `StepMove` по оригиналу: оба варианта, побеждает дальний, `z` из
вниз. Чинит и потерю скорости, и «прыжки с рампы».
2. `Friction()` безусловно на земле.
3. `StartGravity` / `FinishGravity` пополам.
4. `Accelerate` / `AirAccelerate` квото-формулой с `sv_air_max_wishspeed`.
5. `CheckVelocity` — ограничиватель `sv_maxvelocity = 350`.