МЕХАНОИДЫ 2

Интерактивная Визуализация Движка Физики Глайдеров

Система координат движка

Правая система координат. X — восток, Y — вверх (высота), Z — север. Матрица ориентации 3×4 хранится в entity по оффсету +0x150.

X (восток)
Y (высота)
Z (север)

Скорость: Внутренняя в Отображаемую

Единицы в движке (game units/sec) примерно равны м/с. На спидометре выводится значение, умноженное на константу 0.2778 (\( \approx 1/3.6 \)).

\[ DisplaySpeed = InternalSpeed \times 0.2778 \]
100
СПИДОМЕТР
C / Pseudocode CRenderer_DrawCockpitSpeedometer @ 0x418C00
void DrawSpeedometer() {
    float theoretical_max = GetMiddleSpeed(g_pPlayerGlider, 1.0, true);

    // Текущая скорость — длина вектора
    float current_speed = sqrt(vx*vx + vy*vy + vz*vz);

    // Конвертация: internal → display
    float display_speed = current_speed * *(float*)0x5C4884;  // × 0.2778

    // Отрисовка: sprintf("%4d", (int)display_speed)
}
          

Тяга и Ускорение

Тяга складывается из двух слотов двигателей, умножается на бустер, и делится на общую массу.

\[ Thrust_{total} = (E_0 + E_1) \times \begin{cases} 1.25, & Booster \\ 1.0, & Normal \end{cases} \] \[ Acceleration = \frac{Thrust_{total}}{Mass} \]
Бустер Активен (x1.25)

20.00

Ускорение (game units / s²)
C / Pseudocode CalcThrust @ 0x4F1B10
float CalcThrust(CGliderObject* glider, float modifier) {
    CMechanoid* mech = glider->mechanoid;  // [+0x1184]
    if (!mech) return 0.0;

    float thrust = 0.0;

    // Двигатель в слоте 0 (mech+0xA8)
    if (mech->engine_slot_0 != 0)
        thrust += GetEquipmentThrust(mech + 0xA8) * modifier;

    // Двигатель в слоте 1 (mech+0x150)
    if (mech->engine_slot_1 != 0)
        thrust += GetEquipmentThrust(mech + 0x150) * modifier;

    // Бустер: ×1.25
    if (mech->booster_active)
        thrust *= 1.25;         // [0x5C3BA8] = 1.25

    return thrust / glider->mass;  // mass @ [+0x248]
}
          

Нелинейное Сопротивление (Drag)

Главная особенность движка — экспоненциальное затухание скорости с нелинейным коэффициентом. Параметры берутся из двух полей базы данных: RESFRONT (коэффициент) и TURBULENCE (масштаб нелинейности).

C / Decompiled ApplyDrag @ 0x410D50
// __cdecl, декомпилировано из Ghidra
void ApplyDrag(float* velocity, float dt, float drag_rate, float divisor) {
    float k;
    if (divisor == 0.0)        // без турбулентности
        k = drag_rate;
    else
        k = (fabs(*velocity / divisor) + 1.0) * drag_rate;

    // x87 FPU: log2(e) × ln(2) × k × dt = 1.0 × k × dt
    // f2xm1 + fscale → вычисляет 2^(k·dt)
    float decay = pow(2, k * dt);
    *velocity = *velocity / decay;
}

// Вызовы в FUN_0050daf0:
ApplyDrag(&v_forward,  dt, entity.drag_coeff,   entity.drag_divisor);  // нелинейный!
ApplyDrag(&v_lateral,  dt, entity.lateral_drag,  1e10);              // линейный
ApplyDrag(&v_vertical, dt, entity.vert_drag_*,  1e10);              // линейный
ApplyDrag(&turn_yaw,   dt, 1e8,              1e10);              // быстрое затух.
ApplyDrag(&turn_pitch, dt, 1e10,             1e10);              // мгновенное
          

❓ Откуда берётся divisor?

В самой ApplyDrag divisor не вычисляется — он приходит как параметр (param_4). Сравнение с нулём — это guard от деления на ноль.
Вычисление происходит один раз при загрузке корпуса из БД:

DB «TURBULENCE» = T
    ↓ FUN_004cf790 (загрузчик body_config)
body_config+0xBC = 1000.0 / (T + 1.0)
    ↓ CGliderObject_ChangeID (простая копия)
entity+0x250 = drag_divisor
    ↓ FUN_0050daf0 (физика глайдера, вызов)
ApplyDrag(..., divisor=entity+0x250) ← только для RESFRONT!

Важно: из 7+ вызовов ApplyDrag только один использует настоящий divisor (продольная скорость + RESFRONT). Все остальные передают 1e10 — при таком делителе |v/1e10| ≈ 0, и drag становится чисто линейным (k = rate).
Подробнее — секция «Пайплайн: БД → Entity» ниже (с полным декомпилированным кодом).

\[ k = \left( \left| \frac{v}{divisor} \right| + 1.0 \right) \cdot rate \] \[ v_{new} = v_{old} \cdot 2^{-k \cdot dt} \]

График: Скорость (V) от Времени (t = 8 секунд)

Анимация пролёта (покрываемое расстояние глайдером без тяги)

Поворот и Маневренность

При высоких скоростях управляемость глайдера падает. Движок вводит поправочный коэффициент к угловому ускорению.

\[ TurnFactor = \begin{cases} 1.0 & v < Threshold \\ \frac{Limit}{v + Scale} & v \ge Threshold \end{cases} \]
Маневренность: 100%

Меньшая маневренность = Больший радиус поворота

C / Pseudocode FUN_0050daf0 — steering branch
// Поворот: entity+0x262 (has_steering)
float turn_accel = turn_input * max_speed * dt * TURN_RATE_CONST;
// turn_input   @ entity+0x2B8 (yaw), +0x2BC (pitch)
// max_speed    @ entity+0x27C
// TURN_RATE_CONST = [0x5C382C]

turn_rate_yaw   += turn_accel;   // entity+0x2C4
turn_rate_pitch += turn_accel;   // entity+0x2C8

// Ограничение: |turn_rate| <= max_speed × dt
if (fabs(turn_rate_yaw) > max_speed * dt)
    turn_rate_yaw = sign(turn_rate_yaw) * max_speed * dt * 2;

// Скоростная коррекция: на высокой скорости поворот медленнее
if (speed >= SPEED_THRESHOLD) {        // [0x5C384C]
    turn_factor = TURN_LIMIT / (speed + TURN_SCALE);
    // [0x5C3860] / (speed + [0x5C375C])
} else {
    turn_factor = 1.0;
}
turn_rate_yaw *= turn_factor;
              

Максимальная Стационарная Скорость

Максимальная скорость достигается когда Thrust = Drag. Это вычисляется итеративно за 100 шагов симуляции в движке игры (GetMiddleSpeed), или аналитически через дискриминант для спидометра.

\[ \text{Равновесие:} \quad \frac{Thrust}{Mass} \cdot dt = v \cdot \left(1 - 2^{-k \cdot dt}\right) \] \[ \Rightarrow\; V_{max} = \frac{-b + \sqrt{b^2 + 4a \cdot accel}}{2a}, \quad a = \frac{rate \cdot \ln 2}{div},\; b = rate \cdot \ln 2 \]

0

Теоретический Максимум (Текущие настройки)

Основано на настройках Тяги, Массы и Сопротивления (Drag) в панелях выше.

C / Pseudocode GetMiddleSpeed @ 0x4F1BA0 — итерационный метод
float GetMiddleSpeed_Iterative(CGliderObject* glider, float thrust_mod) {
    float thrust = CalcThrust(glider, thrust_mod);
    float v = 10.0;     // начальная скорость
    float dt = 0.025;   // фиксированный шаг

    for (int i = 0; i < 100; i++) {
        // Тот же drag что и в основной физике
        float k = (v / glider->drag_divisor + 1.0) * glider->drag_coeff;
        float decay = pow(2, k * dt);
        v = thrust * dt + v / decay;
    }
    return v;  // сходится к V_max за ~100 шагов
}
            

Набор скорости → Vmax

При постоянной тяге скорость асимптотически стремится к равновесию thrust = drag. Тяга (синяя) постоянна, а drag (фиолетовый) растёт с ростом скорости. Когда силы уравновешиваются — это и есть Vmax. Анимация использует параметры из панелей Тяга и Drag выше.

\[ \text{Равновесие:} \quad \frac{Thrust}{Mass} \cdot dt = v \cdot \left(1 - 2^{-k \cdot dt}\right) \] \[ v < V_{max} \;\Rightarrow\; Thrust > Drag \;\Rightarrow\; \text{ускорение} \] \[ v = V_{max} \;\Rightarrow\; Thrust = Drag \;\Rightarrow\; \text{стационарный режим} \]

Слева: баланс сил (Thrust vs Drag). Справа: скорость сходится к Vmax (зелёная пунктирная). Автоматический цикл ~10 с.

Гравитация и Взаимодействие с Ландшафтом

При полете над ландшафтом гравитация применяется по-разному в зависимости от того, глайдер это или наземный юнит. В полёте у движка есть система авто-восстановления высоты от поверхности (эффект воздушной подушки).

Гравитация: \[ gravity = 9.8 \times gravity\_scale \] \[ gravity_{ground} = gravity \times 0.5 \]
Аэротормоз у земли: \[ alt = pos_y - terrain\_h \] \[ \text{if } alt < Thresh \;\wedge\; v_y < 0\text{:} \] \[ brake = \left(Brake_F - \frac{alt}{RefH}\right) \cdot Mult \]
C / Pseudocode FUN_0050daf0 — gravity & terrain branch
// ═══ Гравитация ═══
float gravity = DAT_005d4f6c * entity.gravity_scale;  // [entity+0x288]

if (!entity.has_flight_flag)
    gravity *= 0.5;  // [0x5C2F70] — наземные падают медленнее

vertical_accel -= gravity;

// ═══ Аэротормоз у земли (для летающих) ═══
float terrain_h = SampleTerrain(entity.position);
float altitude  = entity.pos_y - terrain_h;

if (altitude < DAT_005d4f64 * ALTITUDE_THRESHOLD
    && velocity.y < 0) {
    float brake = (BRAKE_FACTOR - altitude / DAT_005d4f64)
                * BRAKE_MULT;
    ApplyDrag(&velocity.y, dt, brake, 1e10);
}

// ═══ Потолок для ракет (entity_type 0x0E) ═══
if (vertical_accel > 0 && entity.type == 0x0E)
    vertical_accel *= 0.5;
            

Таблица Констант (.rdata)

Все найденные константы из секции .rdata бинарника AIMII.exe. Значения с ? — runtime, зависят от карты/конфигурации.

Адрес Значение Имя Где используется

Карта Памяти: CGliderObject

Структура физического объекта (entity) в памяти. Оффсеты от начала объекта. Все поля — float если не указано иное.

Offset Тип Поле Описание
C / Struct CGliderObject — физические поля
struct CGliderObject {
    // ... base entity fields ...
    void*    body_config;            // +0x00C  → Body Config (DB params)
    // ... gap ...
    float    rotation_matrix[12];  // +0x150  3x4 row-major
    float    pos_x, pos_y, pos_z;   // +0x190  мировая позиция
    float    fwd_x, fwd_y, fwd_z;   // +0x19C  ось «вперёд»
    float    up_x,  up_y,  up_z;    // +0x1A8  ось «вверх»
    float    rt_x,  rt_y,  rt_z;    // +0x1B4  ось «вправо»
    // ... gap ...
    float    mass;                  // +0x248
    float    drag_coeff;             // +0x24C  лобовое сопротивление (DB: RESFRONT)
    float    drag_divisor;            // +0x250  1000/(TURBULENCE+1) из БД
    float    vert_drag_up;            // +0x254  верт. drag (вверх)
    float    vert_drag_down;          // +0x258  верт. drag (вниз)
    float    lateral_drag;            // +0x25C  боковой drag
    bool     has_gravity;             // +0x260
    bool     has_flight;              // +0x261  может летать
    bool     has_steering;            // +0x262  есть управление
    bool     is_ground_unit;          // +0x263
    // ... gap ...
    float    vel_x, vel_y, vel_z;   // +0x2A0  мировая скорость
    float    turn_input_yaw;          // +0x2B8  ввод поворота
    float    turn_input_pitch;        // +0x2BC
    float    turn_rate_yaw;           // +0x2C4  угловая скорость
    float    turn_rate_pitch;         // +0x2C8
    // ... gap ...
    void*    mechanoid;              // +0x1184 → CMechanoid*
};
            

Полная Симуляция Физики

Глайдер стартует с нулевой скорости и разгоняется под тягой. Drag тормозит — скорость асимптотически выходит на Vmax. Все параметры берутся из панелей Тяга и Drag выше. Нажми ЗАПУСК и смотри как скорость растёт.

Параметры: Thrust=30000, Mass=1500, Drag=0.10, Div=500
0 u/s
DSP 0
DST 0 m
TIME 0.0 s
Горизонт: Время (сек)  |  Вертикаль: Скорость (u/s)

0

Vmax теоретический (u/s)

0

Спидометр Vmax

0%

% от Vmax сейчас

Выводы

Нелинейный Drag

\(v \rightarrow v / 2^{k \cdot dt}\), где \(k\) растёт с \(|v|\). Похоже на квадратичное сопротивление воздуха, но через экспоненту.

Набор скорости

Скорость асимптотически стремится к стационарному значению, где \(Thrust = Drag\). Зависит от двигателей, бустера, массы и drag параметров.

Маневренность

Падает с ростом скорости: \(turn\_factor = C / (speed + S)\). Быстрый глайдер = плохо поворачивает.

Euler Integration

Простой Euler (не RK4). Физика зависит от fps: при низком fps dt больше, из-за нелинейности drag результат может отличаться.

Две системы

Летающие (глайдеры) и наземные/водные — разные ветки кода, разные формулы поворота и коллизии с ландшафтом.

TURBULENCE → divisor

Из БД: divisor = 1000 / (TURBULENCE + 1). Высокая TURBULENCE → малый divisor → сильнее тормозит на высокой скорости. entity+0x250.

Пайплайн: БД → Body Config → Entity

Параметры сопротивления загружаются из игровой базы данных через двухэтапный пайплайн. Ключевые функции декомпилированы из Ghidra:

1. Загрузка из БД → Body Config

C / Decompiled FUN_004cf790
// Чтение полей из игровой БД
body->weight     = ReadFloat("WEIGHT");
body->max_weight = ReadFloat("MAXWEIGHT") * 1.25;
body->resfront   = ReadFloat("RESFRONT");
body->resside    = ReadFloat("RESSIDE");
body->restop     = ReadFloat("RESTOP");

// TURBULENCE — единственное поле с трансформацией!
float turb_db = ReadFloat("TURBULENCE");
body->turbulence = 1000.0 / (turb_db + 1.0);

body->stabfront  = ReadFloat("STABFRONT");
body->stabside   = ReadFloat("STABSIDE");
body->rotspeed   = ReadFloat("ROTATESPEED");
body->careen     = ReadFloat("CAREEN") * 0.075;
body->deltat     = ReadFloat("DELTAT") * 0.1;
                

2. Копирование в Entity

C / Decompiled CGliderObject_ChangeID
// body_config* cfg = entity->body_config;
entity->drag_coeff   = cfg->resfront;    // +0x24C
entity->drag_divisor  = cfg->turbulence;  // +0x250
entity->vert_drag_up  = cfg->restop;      // +0x254
entity->vert_drag_dn  = cfg->restop;      // +0x258
entity->lateral_drag  = cfg->resside;     // +0x25C

// Стабилизация: инверсия + epsilon
entity->stab_front  =
    1.0 / (cfg->stabfront + 0.0001);  // +0x274
entity->stab_side   =
    1.0 / (cfg->stabside  + 0.0001);  // +0x278

// Скорость поворота: градусы → радианы
entity->turn_speed  =
    cfg->rotspeed * 0.01745;  // +0x27C
                

Маппинг полей БД

DB поле Body Config Entity Трансформация Описание
RESFRONT +0xB0 +0x24C value (as-is) Лобовой drag (rate)
TURBULENCE +0xBC +0x250 1000 / (val + 1) Делитель нелинейности (divisor)
RESSIDE +0xB4 +0x25C value (as-is) Боковой drag
RESTOP +0xB8 +0x254, +0x258 value (as-is) Верт. drag (вверх и вниз)
STABFRONT +0xC0 +0x274 1 / (val + 0.0001) Стабилизация (лоб)
STABSIDE +0xC4 +0x278 1 / (val + 0.0001) Стабилизация (бок)
ROTATESPEED +0xC8 +0x27C val × π/180 Скорость поворота (рад)
CAREEN +0xCC +0x280 val × 0.075 Крен
DELTAT +0xD0 +0x284 val × 0.1 Дельта T

Штраф перегруза

C / Decompiled CGliderObject_AssignMechanoid @ 0x4F8804
float ratio = total_weight / max_carry_weight;
if (ratio > 0.9) {                            // [0x5C3C54]
    float penalty = (ratio - 0.9) * 9.0 + 1.0;  // [0x5C47BC]
    entity->drag_coeff  = (RESFRONT - 1.0) * penalty + 1.0;
    entity->lateral_drag = (RESSIDE  - 1.0) * penalty + 1.0;
    // TURBULENCE (drag_divisor) НЕ меняется!
}
              

При 100% загрузки: penalty = 1.9. При 110%: penalty = 2.8. Турбулентность (entity+0x250) не зависит от массы.

Верификация по телеметрии

Реальный полёт глайдера (~270 точек CSV, dt ≈ 62ms, цикл разгон→Vmax→торможение):
Fitted RESFRONT ≈ 0.0414, fitted drag_divisor ≈ 4.5TURBULENCE в БД ≈ 221 (из 1000/4.5 − 1).
Модель vs реальность: R² = 0.971 (торможение), ошибка Vmax = 0.000%.

Секрет мечты: Парящий глайдер

Помнишь то чувство в детстве — разогнаться с горы и прыгнуть? И вместо падения камнем... парить. Не набирать высоту, нет — просто лететь, медленно теряя высоту, как настоящий планер. Движок это позволяет. Нужна правильная комбинация параметров.

Условие парения:
\[ \text{Скорость падения} \approx 0 \quad \Longleftrightarrow \quad \underbrace{g \cdot gravity\_scale}_{\text{сила гравитации}} \leq \underbrace{ApplyDrag(v_y,\; vert\_drag\_down)}_{\text{вертикальное сопротивление}} \]
Плюс тяга вверх через тангаж:
\[ v_y^{+} = \frac{Thrust}{Mass} \cdot \sin(\theta_{pitch}) \cdot dt \] Чуть задрать нос — и вертикальная компонента тяги компенсирует гравитацию

Конструктор планера

Бустер (x1.25)
ALT 200 m
SPD 0 u/s
DST 0 m
TIME 0 s
Vy 0
?
Нажми «Прыжок!» чтобы проверить
Подсказка из дизасма: Движок проверяет высоту над ландшафтом. Если entity близко к земле и падает вниз — включается дополнительный drag (эффект воздушной подушки). Параметр vert_drag_down — ключ к планированию. Чем он выше, тем медленнее вы теряете высоту. Лёгкий корпус + слабый двигатель на малом тангаже + высокий vert_drag = планер мечты. А если ещё и бустер...