[Из песочницы] Простейший физический движок

Вас интересуют игры? Хотите создать игру, но не знаете с чего начать? Тогда вам сюда. В этой статье я рассмотрю простейший физический движок, с построения которого можно начать свой путь в 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). Я использовал этот алгоритм для построения линий магнитной индукции системы зарядов, но об этом в другой статье.

Всем спасибо за прочтение. Надеюсь данная статья Вам немного поможет в создании собственных игр.

© Habrahabr.ru