В последнее время я работаю над SIMPLE Physics, набором образовательных тренажеров по физике, призванных помочь преподавать и изучать физику интуитивно без дорогостоящего лабораторного оборудования или личных занятий. Каждый симулятор позволяет пользователям импортировать и экспортировать сцены и потенциально добавлять некоторые дополнительные функции через Lua.

До недавнего времени сценарии Lua были весьма ограничены. Его можно было использовать для добавления/удаления объектов и изменения переменных, например, но, что особенно важно, он не мог влиять на объекты после их создания. Гравитация, а также была функция update(), которая запускалась в каждом кадре.

-- this example instantiates a multicolored grid of circles
for row = 1,HEIGHT do
    for col = 1,WIDTH do
        color = {
            r = (row * col) / (WIDTH * HEIGHT) * 255,
            g = col / WIDTH * 255,
            b = row / HEIGHT * 255
        }
        add_shape{
            shape = "circle", 
            x = col * OFFSET + START_X_OFFSET, 
            y = row * OFFSET, 
            r = RAD, 
            mass = 1, 
            color = color
        }
    end
end

Важно отметить, что не было возможности воздействовать на отдельные объекты напрямую после того, как они были созданы. К настоящему времени вы, вероятно, можете видеть, к чему все идет.

Я связался со своим профессором физики по поводу этого проекта, и он сказал мне, что хотел бы, чтобы студенты могли кодировать уравнения, которые мы изучаем в классе. Например, базовое интегрирование положения с помощью метода Эйлера или уравнений столкновения. При этом я решил реализовать способ, с помощью которого интерфейс Lua мог бы обновлять отдельные объекты.

К чему я пришел, так это к функциям обновления объекта. Указав функцию, которая будет вызываться для объекта в каждом кадре, пользователи теперь могут изменять характеристики объекта, включая положение, скорость, цвет и т. д. Вот MVP для лаборатории интеграции, которая позволяет студентам написать свой собственный метод интеграции:

-- the student should edit this function
local function integrate(x, y, v_x, v_y, dt)
    return {
        new_x = x + v_x * dt,
        new_y = y + v_y * dt,
    }
end
X_VEL = 1
Y_VEL = -1
-- this function is called on the circle every frame
function update_fn(obj)
    local old_x, old_y = obj.x, obj.y
    data = integrate(old_x, old_y, X_VEL, Y_VEL, DT / 100)
    obj.x, obj.y = data.new_x, data.new_y
    return obj
end
add_shape {
    shape="circle",
    x=SCREEN_X/2,
    y=SCREEN_Y/2,
    r=1,
    mass=1,
    update_function="update_fn"
}
GRAVITY = 0

Идея состоит в том, чтобы студенты написали свою собственную функцию integrate(). В идеале должен быть еще один круг, использующий физический движок симулятора, и, сравнив поведение двух фигур, можно будет увидеть ошибку интегрирования по Эйлеру.

Когда я писал примеры, я понял, что функции обновления объекта очень похожи на то, как работает ECS Unity. Функции обновления, по сути, являются компонентом скрипта. Хотя к каждому объекту можно добавить только одну функцию обновления, это ограничение довольно легко обойти.

Осознав это, я решил написать Flappy Bird. Здесь я признаю, что название немного кликбейтно: по-прежнему нет способа обрабатывать пользовательский ввод из Lua-скриптов, поэтому программу все еще нельзя назвать игровым движком. Пользователи могут перетаскивать объекты с помощью мыши, поэтому можно было бы настроить физические кнопки в сцене, а также есть глобальные переменные MOUSE_X и MOUSE_Y Lua, но Lua не может видеть щелчки клавиатуры или мыши. Из-за этого я написал очень простой ИИ для игры во Flappy Bird.

Мой код на Lua немного дилетантский и слишком длинный для фрагмента кода, поэтому я рекомендую вам проверить его на GitHub, если вы хотите его прочитать.

Создавать Flappy Bird поверх такого физического движка довольно интересно. Я не удосужился написать условие конца игры, но если птица врежется в одну из труб, они обе улетят. Вместо того, чтобы вручную обновлять x-позицию труб, я просто инициализировал их отрицательную скорость. Я предполагаю, что это похоже на то, как это будет работать в Unity или Godot. Однако, несмотря на то, что на движке определенно можно писать игры, он не очень эргономичен. Хотя я не против.

Что еще меня удивило, так это скорость взаимодействия с Lua из Rust. В каждом кадре программа загружает многочисленные поля объектов Lua в физический движок и передает их обратно в Lua. В примере с Flappy Bird физический движок почти неактивен, поэтому я ожидал, что система Lua будет более интенсивной, чем физический движок, но она использует примерно десятую часть производительности, если я правильно читаю свой профайлер. У меня возник соблазн переписать Универсальную гравитационную часть SIMple Physics в сценарий Lua.