Правая система координат. X — восток, Y — вверх (высота), Z — север.
Матрица ориентации 3×4 хранится в entity по оффсету +0x150.
Единицы в движке (game units/sec) примерно равны м/с. На спидометре выводится значение, умноженное на
константу 0.2778 (\( \approx 1/3.6 \)).
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) }
Тяга складывается из двух слотов двигателей, умножается на бустер, и делится на общую массу.
20.00
Ускорение (game units / s²)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] }
Главная особенность движка — экспоненциальное затухание скорости с нелинейным коэффициентом. Параметры берутся из двух полей базы данных: RESFRONT (коэффициент) и TURBULENCE (масштаб нелинейности).
// __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 от деления на ноль.
Вычисление происходит один раз при загрузке корпуса из БД:
Важно: из 7+ вызовов ApplyDrag только один использует настоящий divisor
(продольная скорость + RESFRONT). Все остальные передают 1e10 — при таком делителе
|v/1e10| ≈ 0, и drag становится чисто линейным (k = rate).
Подробнее — секция «Пайплайн: БД → Entity» ниже (с полным декомпилированным кодом).
График: Скорость (V) от Времени (t = 8 секунд)
Анимация пролёта (покрываемое расстояние глайдером без тяги)
При высоких скоростях управляемость глайдера падает. Движок вводит поправочный коэффициент к угловому ускорению.
Меньшая маневренность = Больший радиус поворота
// Поворот: 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), или аналитически через дискриминант для спидометра.
0
Теоретический Максимум (Текущие настройки)Основано на настройках Тяги, Массы и Сопротивления (Drag) в панелях выше.
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 шагов }
При постоянной тяге скорость асимптотически стремится к равновесию thrust = drag.
Тяга (синяя) постоянна, а drag (фиолетовый) растёт с ростом скорости. Когда силы
уравновешиваются — это и есть Vmax. Анимация использует
параметры из панелей Тяга и Drag выше.
Слева: баланс сил (Thrust vs Drag). Справа: скорость сходится к Vmax (зелёная пунктирная). Автоматический цикл ~10 с.
При полете над ландшафтом гравитация применяется по-разному в зависимости от того, глайдер это или наземный юнит. В полёте у движка есть система авто-восстановления высоты от поверхности (эффект воздушной подушки).
// ═══ Гравитация ═══ 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 бинарника AIMII.exe. Значения с ? —
runtime, зависят от карты/конфигурации.
| Адрес | Значение | Имя | Где используется |
|---|
Структура физического объекта (entity) в памяти. Оффсеты от начала объекта. Все поля — float
если не указано иное.
| Offset | Тип | Поле | Описание |
|---|
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 выше. Нажми ЗАПУСК и смотри как скорость растёт.
0
Vmax теоретический (u/s)0
Спидометр Vmax0%
% от Vmax сейчас\(v \rightarrow v / 2^{k \cdot dt}\), где \(k\) растёт с \(|v|\). Похоже на квадратичное сопротивление воздуха, но через экспоненту.
Скорость асимптотически стремится к стационарному значению, где \(Thrust = Drag\). Зависит от двигателей, бустера, массы и drag параметров.
Падает с ростом скорости: \(turn\_factor = C / (speed + S)\). Быстрый глайдер = плохо поворачивает.
Простой Euler (не RK4). Физика зависит от fps: при низком fps dt больше, из-за нелинейности drag результат может отличаться.
Летающие (глайдеры) и наземные/водные — разные ветки кода, разные формулы поворота и коллизии с ландшафтом.
Из БД: divisor = 1000 / (TURBULENCE + 1).
Высокая TURBULENCE → малый divisor → сильнее тормозит на высокой скорости.
entity+0x250.
Параметры сопротивления загружаются из игровой базы данных через двухэтапный пайплайн. Ключевые функции декомпилированы из Ghidra:
// Чтение полей из игровой БД 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;
// 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 |
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.5
→ TURBULENCE в БД ≈ 221 (из 1000/4.5 − 1).
Модель vs реальность: R² = 0.971 (торможение),
ошибка Vmax = 0.000%.
Помнишь то чувство в детстве — разогнаться с горы и прыгнуть? И вместо падения камнем... парить. Не набирать высоту, нет — просто лететь, медленно теряя высоту, как настоящий планер. Движок это позволяет. Нужна правильная комбинация параметров.
vert_drag_down — ключ к планированию. Чем он выше, тем медленнее вы теряете высоту. Лёгкий
корпус + слабый двигатель на малом тангаже + высокий vert_drag = планер мечты. А если ещё и бустер...