Skip to content

Physics

Владислав edited this page Sep 5, 2026 · 1 revision

Физика мяча

Весь шаг физики - Player_Step() в jb_player.c, прямой перенос тела WM_TIMER из WndProc 0x000208f8..0x00021268. Три координаты, три скорости, никаких коллизий кроме одной проверки байта тайла.

Координаты

Поле Глобал оригинала Смысл
ball_x g_ballX 0x00064b0c поперёк трассы, в тайлах
ball_row g_ballRow 0x00064b08 вдоль трассы, в строках сетки
ball_y g_ballY 0x00064b10 высота над трассой
ball_prev_x g_ballPrevX 0x0006118c ball_x предыдущего кадра
vel_x, vel_z, vel_y 0x000649d4 / 0x000649cc / 0x000649d8 скорости
spin g_ballSpin 0x00064994 фаза вращения

ball_y - настоящая высота: пока она больше нуля, мяч в воздухе. ball_row одновременно и позиция, и камера (см. Track-and-camera).

Ускорения

Базовая величина - accel = 7.0f * dt (kAccelX 0x000262ac). Дальше асимметрия, которую легко потерять при «аккуратной» переписи:

if (left_down)          vel_x += accel * 2.0f;
else if (left_released) { vel_x -= accel; if (vel_x <= 0) { vel_x = 0; left_released = 0; } }

if (down_down)  vel_z -= (vel_z >= 0.0f) ? accel * 2.0f : accel;
  • Нажатие даёт двойное ускорение, отпускание - одинарное торможение. Разгон вдвое быстрее, чем остановка.
  • Торможение живёт на отдельном флаге *_released, который ставится в WM_KEYUP и снимается сам, когда скорость дошла до нуля. Просто «нет нажатия» ничего не тормозит.
  • Тормоз (down) даёт двойную величину, только если мяч ещё едет вперёд; при движении назад - одинарную.

Есть и ловушка, унаследованная целиком: гашение по оси Z заперто на флаге z_axis_damped (DAT_00064940). В оригинале у этого глобала нет ни одной записи, в порте он выставлен в 0 и не меняется, поэтому ветки затухания up/down не выполняются никогда - vel_z держится, пока его не сбросят явно. Порт хранит эту логику как есть, а не «исправляет».

Интеграция и границы

ball_row += vel_z * dt;
ball_x   += vel_x * dt;
ball_y   += vel_y * dt;

if (ball_x > (float)map_cols) ball_x = (float)map_cols;   /* 0x00020ecc */
if (ball_x <= -16.0f)         ball_x = -16.0f;            /* 0x00020ef4 */

ball_row < 0 обнуляется вместе с vel_z - назад за старт уехать нельзя.

Опрос тайла

Одна проверка на кадр, и та с двумя странностями:

row = (int)((double)ball_row + 0.58);
col = (int)(ball_prev_x + (ball_prev_x > 0.0f ? 1.0f : 0.0f));
tile = tile_grid[8 + row * map_cols - col];
  • 0.58 - не «примерно половина». Это точный double из литерального пула (0x3fe28f5c / 0x28f5c28f по адресам 0x0021119c / 0x000211a4); мяч «стоит» чуть впереди своей строки.
  • Индекс идёт вычитанием столбца от базы +8: сетка читается зеркально относительно центра (WndProc 0x00020f74 индексирует &DAT_00031180, то есть g_tileGrid + 8).
  • Столбец вне 1..map_cols даёт тот же результат, что пустой тайл (0x00020fbc / 0x00020fe0 ведут в общий LAB_00020fa4).

Пустой тайл под мячом при ball_y <= 0 означает одно из двух:

if (auto_jump == 1 && ball_y == 0.0f) vel_y = JB_JUMP_IMPULSE;  /* автопрыжок */
else                                  fall_flag = 1;            /* падение */

auto_jump (g_autoJump 0x00026430) по умолчанию 1 и переключается в меню - это тот самый пункт настроек, а не отладочный флаг.

Прыжок и гравитация

case JB_KEY_JUMP:
    if (vel_y == 0.0f) vel_y = JB_JUMP_IMPULSE;  /* 10.0, 0x000262b4 */
    else               jump_buffered = 1;

Сравнение с нулём точное: прыжок в воздухе не работает, но нажатие не теряется - оно запоминается и тратится в момент касания:

if (ball_y < 0.0f) {
    landed = 1; ball_y = 0.0f; vel_y = 0.0f;
    if (jump_buffered) { jump_buffered = 0; vel_y = JB_JUMP_IMPULSE; }
}

Гравитация - vel_y -= 20.0f * dt (kGravity 0x00026434), и только при vel_y != 0.0: лежащий на трассе мяч не «набирает» отрицательную скорость.

Флаг landed порт поднимает здесь, а звук отбоя играет jb_main.c - ровно там, где оригинал звал hssSpeaker::playSound перед записью 0.0 в g_ballY.

Падение и респавн

if (fall_flag == 1) {
    vel_y = -11.0f;
    ball_y -= dt * 11.0f;
    if (ball_y < -20.0f) Player_Respawn(st);
}

Player_Respawn (0x00012f18) ставит ball_y = 5.0, ball_x = 0, ball_row = 0, vel_y = -1.0, гасит остальные скорости и сбрасывает все клавиши. Потом - поиск чекпоинта (PlaceAtCheckpoint):

  • работает только при cam_row > 0x46 (cmp r0,#0x46);
  • граница - cam_row - 0x14, перебираются чекпоинты с индекса 1, пока их строка меньше границы, и берётся последний подходящий;
  • ball_x = (8 - checkpoint_x[k]) - 0.5f - снова зеркальное «8 минус столбец», плюс полтайла к центру;
  • ball_row = checkpoint_y[k] - 0.2f - чуть перед линией.

Камеру Player_Respawn не трогает: оригинал выводит её из g_ballRow на следующем WM_TIMER, порт делает то же в конце функции.

Спин и кадр мяча

spin = (int)(vel_z * dt * 350.0f + (float)spin);   /* 0x00021094 */

и заворачивается в 0..0x240 двумя беззнаковыми делениями - вверх на 0x000210f4, вниз на 0x000210cc. Дальше jb_ball.c превращает фазу в кадр:

frame = (int)((double)spin * 0.03125);            /* 1/32 */
src_x = (frame * 70) % (9 * 70);                  /* 0x276 = 630 */

0x240 = 576, 576 / 32 = 18, полоса шириной 0x276 = 630, поэтому за один цикл спина полоса из девяти кадров прокручивается дважды.

Мяч рисуется не как картинка, а как маска: Blit_Glyph с цветом hud_r/g/b (250, g_hudR 0x00026280) - в 331.bmp лежит только покрытие. Тень - отдельный спрайт 74x48, рисуется лишь при ball_y >= 0.0, со своим смещением 40.0 + ball_y * 6.0 (0x0001ec38, 0x0001ec80), поэтому «отрыв» тени от мяча в прыжке - оригинальное поведение, а не артефакт порта.

Оба блита сдвинуты влево от центра: -0x23 для мяча, -0x25 для тени (0x0001ef08, 0x0001ecac).

Clone this wiki locally