[Из песочницы] Простейший физический движок
Вас интересуют игры? Хотите создать игру, но не знаете с чего начать? Тогда вам сюда. В этой статье я рассмотрю простейший физический движок, с построения которого можно начать свой путь в GameDev’e. И да, движок будем писать с нуля.
Несколько раз мои друзья интересовались, как же я пишу игры / игровые движки. После очередного такого вопроса и ответа я решил сделать статью, раз эта тема так интересна.
В качестве языка программирования был выбран javascript, потому что возможности скачать IDE и компилятор у подопытного знакомого не было. Рисовать будем на canvas.
Постановка задачи
Необходимо для нескольких объектов на плоскости реализовать взаимодействие с помощью фундаментальной силы гравитации.
Т.е. сделать что-то подобное притяжению звёзд в космосе.
Алгоритм
Для начала нужно уяснить отличие компьютерной физики от реальной. Реальная физика действует непрерывно (во всяком случае обратное не доказать на текущий момент). Компьютерная физика, как и компьютер действуют дискретно, т.е. мы не можем вычислять её непрерывно, поэтому разбиваем её вычисление на шаги с определённым интервалом (я предпочитаю интервал 25 мс). Координаты объектов меняются после каждого шага и объекты выводятся на экран.
Теперь приступим к самой гравитации.
Закон всемирного тяготения (Ньютонова гравитация) гласит:
F = G * m1 * m2 / R^2 (1)
где:
F [Н]- сила притяжения между двумя объектами
G = 6.67*10^-11 [м^3/(кг * с^2)]- гравитационная постоянная
m1, m2 [кг] - массы 1 и 2 объектов
R [м] - расстояние между центрами масс объектов
Как это нам поможет в определении новых координат? А мы эту силу будем прикладывать к этим объектам, используя второй закон Ньютона:
F = m * a (2)
где:
F [Н] - сила, приложенная к текущему объекту
m [кг] - масса текущего объекта
a [м/с^2] - ускорение текущего объекта
Забудем на время то, что в (1) сила — скаляр, а в (2) сила — вектор. И во 2 случае будем считать силу и ускорение скалярами.
Вот и получили изменение ускорения:
a = F / m (3)
Изменение скорости и координат следует из следующего:
a = v' → a = dv / dt → dv = a * dt
v = s' → v = ds / dt → ds = v * dt
v += dv
Pos += ds
где:
d - дифференциал (производная)
v - скорость
s - расстояние
Pos - точка, текущие координаты объекта
переходим от векторов к скалярам:
a.x = a * cos(α)
a.y = a * sin(α)
dv.x = a.x * dt
dv.y = a.y * dt
v.x += dv.x
v.y += dv.y
ds.x = v.x * dt
ds.y = v.y * dt
Pos.x += ds.x
Pos.y += ds.y
где:
cos(α) = dx / R
sin(α) = dy / R
dx = Pos2.x - Pos.x
dy = Pos2.y - Pos.y
R^2 = dx^2 + dy^2
Так как другого вида силы в проекте пока нет, то используем (1) в таком виде и немножко облегчим вычисления:
F = G * m * m2 / R^2
a = G * m2 / R^2
Код
Запускаемую страничку index.html создадим сразу и подключим код:
Physics
Основное внимание уйдёт на файл с кодом программы script.js. Код для отрисовки откомментирован достаточно и он не касается темы:
var canvas, context;
var HEIGHT = window.innerHeight, WIDTH = window.innerWidth;
document.addEventListener("DOMContentLoaded", main, true);
function main(){
// создаём холст на весь экран и прикрепляем его на страницу
canvas = document.createElement('canvas');
canvas.height = HEIGHT;
canvas.width = WIDTH;
canvas.id = 'canvas';
canvas.style.position = 'absolute';
canvas.style.top = '0';
canvas.style.left = '0';
document.body.appendChild(canvas);
context = canvas.getContext("2d");
/*******
другой код
*******/
}
function Draw(){
// очищение экрана
context.fillStyle = "#000000";
context.fillRect(0, 0, WIDTH, HEIGHT);
// рисование кругов
context.fillStyle = "#ffffff";
for(var i = 0; i < star.length; i++){
context.beginPath();
context.arc(
star[i].x - star[i].r,
star[i].y - star[i].r,
star[i].r,
0,
Math.PI * 2
);
context.closePath();
context.fill();
}
}
Теперь самое вкусное: код, который просчитывает физику.
На каждый объект мы будем хранить только массу, координаты и скорость. Ах да, ещё надо радиус — он нам понадобится для рассчёта столкновений, но об этом в следующей статье.
Итак, «класс» объекта будет таким:
function Star(){
this.x = 0;
this.y = 0;
this.vx = 0;
this.vy = 0;
this.r = 2; // Radius
this.m = 1;
}
var star = new Array(); // в этом массиве будут храниться все объекты
var count = 50; // начальное количество объектов
var G = 1; // задаём константу методом подбора
Генерация случайных объектов в самом начале:
var aStar;
for(var i = 0; i < count; i++){
aStar = new Star();
aStar.x = Math.random() * WIDTH;
aStar.y = Math.random() * HEIGHT;
star.push(aStar);
}
Шаг вычисляться будет в следующей функции:
function Step(){
var a, ax, ay, dx, dy, r;
// важно провести вычисление каждый с каждым
for(var i = 0; i < star.length; i++) // считаем текущей
for(var j = 0; j < star.length; j++) // считаем второй
{
if(i == j) continue;
dx = star[j].x - star[i].x;
dy = star[j].y - star[i].y;
r = dx * dx + dy * dy;// тут R^2
if(r < 0.1) r = 0.1; // избегаем деления на очень маленькое число
a = G * star[j].m / r;
r = Math.sqrt(r); // тут R
ax = a * dx / r; // a * cos
ay = a * dy / r; // a * sin
star[i].vx += ax;
star[i].vy += ay;
}
// координаты меняем позже, потому что они влияют на вычисление ускорения
for(var i = 0; i < star.length; i++){
star[i].x += star[i].vx;
star[i].y += star[i].vy;
}
// выводим на экран
Draw();
}
Здесь уже проведены небольшие оптимизации, и dt принял за 1, поэтому исключил из операций умножения.
Ну и долгожданный запуск таймера:
timer = setInterval(Step, 20);
Посмотреть работу можно здесь, а код здесь.
Минусы
Сложность алгоритма растёт экспоненциально, поэтому увеличение объектов влечёт заметное проседание FPS. Решение с помощью Quad tree или других алгоритмов не поможет, но в реальных играх не объекты взаимодействуют по принципу каждый с каждым.
Тестирование производилось на машине с процессором Intel Pentium с частотой 2.4 GHz. При 1000 объектов с интервал вычисления уже превышал 20 мс.
Использование
В качестве силы можно использовать суперпозицию разных сил в (3). Например, тягу двигателя, силу сопротивления грунта и воздуха, а также соударения с другими объектами. Алгоритм можно легко расширить на три измерения, достаточно ввести z аналогично x и y.
Этот алгоритм был написан мною ещё в 9 классе на паскале, а до текущего момента переложен на все языки, которые я знаю просто потому, что могу в качестве личного Hello World’a. Даже в терминале.
Также данный алгоритм можно использовать для другого фундаментального взаимодействия — электромагнитного (G → k, m → q). Я использовал этот алгоритм для построения линий магнитной индукции системы зарядов, но об этом в другой статье.
Всем спасибо за прочтение. Надеюсь данная статья Вам немного поможет в создании собственных игр.