Математика в Gamedev по-простому
443 subscribers
34 photos
1 video
17 links
Как на самом деле работают стрельба, толпа NPC, графика, физика тканей.

Канал про то, что ИИ не заменит: понимание.

Разборы на пальцах, рабочий код, интерактивы. dev-math.ru

Сотрудничество: @it_bizdev
Download Telegram
Загадка недели

Ты написал платформер. Нажимаешь стрелку — персонаж идёт.
Отпускаешь — он продолжает скользить.

Код выглядит вот так:

velocity.x += Input.GetAxis("Horizontal") * speed;
transform.position += velocity * Time.deltaTime;


Почему персонаж не останавливается мгновенно?

Ответ: velocity.x никогда не обнуляется.
При input = 0 скорость не уменьшается, а накапливается сессиями нажатий.
Фикс — добавить трение: velocity.x *= (1 - friction * Time.deltaTime)
или просто обнулять при input == 0:
```csharp
float input = Input.GetAxis("Horizontal");
if (Mathf.Abs(input) > 0.01f)
velocity.x = Mathf.MoveTowards(velocity.x, input * speed, accel * Time.deltaTime);
else
velocity.x = Mathf.MoveTowards(velocity.x, 0f, friction * Time.deltaTime);
```


#мат_геймдев #МатРазбор #физика
6👍3🔥2
🕳️ Sweep test и CCD: когда объект «проваливается» сквозь стену

Снаряд летит быстро. Кадр 1 — перед стеной. Кадр 2 — за стеной.
Физика не поймала пересечение — проверяла только конечную позицию.
Это туннелирование (tunneling).

Решение 1: SphereCast / RaycastAll
Двигай не точку, а луч или объём вдоль вектора движения.

RaycastHit hit;
if (Physics.SphereCast(
transform.position,
col.radius,
velocity.normalized,
out hit,
velocity.magnitude * Time.fixedDeltaTime))
{
transform.position = hit.point + hit.normal * col.radius;
velocity = Vector3.Reflect(velocity, hit.normal) * bounciness;
}
else transform.position += velocity * Time.fixedDeltaTime;


Решение 2: CCD в Rigidbody
Rigidbody → Collision Detection → Continuous (для объекта)
или Continuous Dynamic (если сталкивается с другими быстрыми объектами).

Когда какой режим:
→ Discrete — медленные объекты, декор
→ Continuous — быстрые объекты (снаряды) vs статичные стены
→ Continuous Dynamic — быстрые vs быстрые

CCD дороже по CPU. Включай только там, где нужно.

#мат_геймдев #МатРазбор #физика #туннелирование
👍4🔥32
📡 Raycast, SphereCast, BoxCast — собственно, какой кастер выбрать?

Сталкивались с тем, что выпущенный снаряд пролетает сквозь угол колонны, а персонаж намертво застревает в дверном проёме шириной с него самого? Корень обеих бед — неправильно выбранный кастер. Давайте разберём, чем Raycast, SphereCast и BoxCast отличаются на практике.

По сути все три делают одно: пускают зонд из точки в направлении и сообщают, во что он упёрся. Разница только в форме зонда, и именно от неё зависит, что вы поймаете, а что молча пропустите.

Physics.Raycast — математический луч, сечение нулевое. Берите, когда важна именно точка попадания: курсор прицела, проверка прямой видимости от NPC до игрока, лазер, хитскан в стиле какого-нибудь раннего шутера. Самый дешёвый из тройки.

Где он ломается? Банально на всём, что имеет ширину. Снаряд проходит сквозь угол колонны, потому что луч прошёл мимо на пару сантиметров, а сам-то снаряд не точка.

Physics.SphereCast — то же самое, только вдоль луча движется не точка, а сфера заданного радиуса. Объём проверки получается капсульный (не цилиндр, как иногда пишут — торцы скруглённые). Ставьте на снаряды с физическим размером, проверку тоннелирования у быстрых объектов, swept-коллизии для камеры, чтобы она не клиппилась в стену, как в каком-нибудь старом Skyrim до патчей. Дороже Raycast, но ловит граничные случаи, где тот промолчит.

Physics.BoxCast — та же история, только вдоль луча движется коробка. Инструмент для всего, что само по себе «ящик»: проверки CharacterController, ground check у платформера, тест «пролезет ли персонаж в узкий проход». Ну и не забывайте про ориентацию — Quaternion.identity суют по инерции слишком часто, а коробка-то поворачивается вместе с персонажем.


// Ground check через BoxCast — точнее, чем одиночный Raycast,
// и не ловит ложные срабатывания на краях ступенек.
bool IsGrounded()
{
Vector3 origin = transform.position + Vector3.up * 0.1f;
Vector3 halfSize = new Vector3(0.4f, 0.05f, 0.4f);
return Physics.BoxCast(
origin, halfSize, Vector3.down,
transform.rotation, 0.15f, groundMask);
}


Чтож, итог простой: представьте объём, который реально движется по сцене. У снаряда — капсула, у персонажа — коробка, у взгляда — линия. Под этот объём и берите кастер. Конечно же, правило не единственно верное, нюансов в физике Unity хватает, но для 90% случаев работает.

#мат_геймдев #КодПодСкопой #физика
6🔥4👍2
Кватернионы через зеркала: как одно геометрическое наблюдение убивает магию формулы q·v·q⁻¹
https://dev-math.ru/articles/rotations/

12 интерактивных демо в браузере, гимбал-лок, plate trick, формулы выводятся честно из геометрии. Если q · v · q⁻¹ когда-нибудь заставляла вас захлопнуть учебник со словами «и тут, конечно же, всё интуитивно понятно» — статья ровно про это. Жду 🔥 и репостов друзьям-геймдевам 😆

В большинстве материалов кватернионы вводятся как магия: «вот вам четыре числа с условием нормировки, применяйте по формуле сэндвича, не задавайте вопросов». Я предлагаю обратный заход: показываю, что любой 3D-поворот можно получить как композицию двух отражений в плоскостях (угол между зеркалами — половина угла поворота, линия пересечения — ось), и кватернион — это просто компактная запись пары зеркал. Из этого естественно вырастает и половинный угол в формулах, и сэндвич q · v · q⁻¹ как «два полуповорота, компенсирующие искажение длины», и дважды-покрытие SO(3) — то самое, из-за чего поворот на 720° возвращает в исходное состояние, а на 360° — нет (в браузере есть честный Dirac belt trick с распутыванием лент). По дороге разбираем зоопарк альтернатив: углы Эйлера с гимбал-локом, матрицы 3×3 как «куда уехали базисные векторы», ось-угол и SLERP — каждое со своими плюсами и минусами, без объявления любимчика заранее.

#мат_геймдев #МатРазбор #кватернионы #графика
🔥11👍4👏2
Брейк-радары в процедурной генерации — выбрасываем плохой вариант раньше, чем запустить BFS

Сталкивались с тем, что процедурный генератор карт подвисает на пару секунд прямо посреди загрузки? Вроде всё логично: набросали вариант, прогнали BFS, убедились что карта проходима, повторили. Но итераций таких — сотни, и каждая тащит полный обход графа. Давайте разберём, как брейк-радары позволяют выбрасывать провальные кандидаты ещё до старта тяжёлого обхода.

По сути, процедурный генератор — это машина перебора. Вы накидываете вариант, проверяете условия (карта связна, путь существует, длина маршрута не меньше N шагов), если не подходит — выбрасываете и пробуете снова. Банальный подход: BFS на каждый вариант. На карте 100×100 с тысячей итераций вы быстро упрётесь в потолок.

Брейк-радар — это дешёвая проверка *до* тяжёлого алгоритма. Если она говорит «этот вариант точно плохой» — прерываете итерацию сразу, BFS даже не стартует. Хороший радар: O(1) или O(маленькое), зато отсекает значительную долю провалов.

Итак, три примера на пальцах.

Трасса с кольцом

Представьте генератор трасс как у какого-нибудь Mini Motor Racing — тайлы с прямыми, поворотами влево и вправо. Чтобы трасса замкнулась, сумма поворотов должна быть ровно ±360°. Это геометрический факт, BFS здесь вообще не нужен:


int netRotation = 0;
foreach (var tile in generatedTrack)
netRotation += tile.rotationDelta; // +90 или -90

if (Mathf.Abs(netRotation) != 360)
return false; // выбрасываем без BFS


Сумма не сходится — трасса физически не замкнётся. Ноль смысла проверять связность.

Данж с изолированными комнатами

Генерируем подземелье, нужно убедиться что все комнаты достижимы. Полный BFS — O(W×H). Но перед ним работает грубый инвариант: если комнат N, то минимальное число соединений для связного графа — N−1 (дерево). Дверей меньше — данж гарантированно несвязен:


int rooms = CountRooms();
int doors = CountDoorTiles();

if (doors < rooms - 1)
return false; // математически несвязно, BFS не нужен


Конечно же, это нижняя оценка — двери могут вести в тупики, и BFS всё равно понадобится для финальной проверки. Но очевидные провалы убираем бесплатно.

Минимальная длина пути

Классика головоломок и платформеров: путь от старта до финиша должен быть не короче K шагов. BFS даст точный ответ, но Манхэттенское расстояние даёт бесплатную нижнюю оценку:


int manhattan = Mathf.Abs(start.x - end.x) + Mathf.Abs(start.y - end.y);

if (manhattan >= minPathLength)
return false; // реальный путь не короче манхэттена — а нам нужно именно короче


Ну и тут важно не перепутать направление неравенства. Манхэттен — *нижняя* граница для кратчайшего пути на сетке без диагоналей. Реальный путь будет длиннее или равен. Так что если манхэттен уже не дотягивает до минимума — дальше незачем смотреть.

Чтож, итог простой: брейк-радар — не замена BFS, а фильтр перед ним. Хорошая система генерации выстраивает радары по возрастанию стоимости — сначала O(1) по счётчикам, потом что-то дешёвое по периметру, и только потом полный обход. Моё решение тут точно не единственное, у каждой задачи свои инварианты, которые можно поймать заранее. Но принцип один: чем раньше выбросили плохой вариант — тем быстрее нашли хороший.

Буду рад обсудить в комментариях, какие радары используете вы.

#мат_геймдев #КодПодСкопой #процедурнаягенерация
🔥7👍2👏21
Hitscan vs projectile: почему выбор механики стрельбы — это архитектурное решение

Рейлган в Quake попадает в момент нажатия — это raycast, один вызов, результат за кадр. Ракета — честный физический объект со своей скоростью: пока летит, противник уже не там, и целиться нужно в точку, где он окажется через ~400 мс.

Первый подход стал стандартом для тактических шутеров — CS, Valorant. Второй живёт в аркадных — Quake, UT, частично Apex. И это не вкусовщина: за выбором тянется целый хвост — lag compensation на сервере, recoil-паттерны, вся ощущаемая отзывчивость стрельбы.

На следующей неделе давайте разберём, когда какой выбирать и как это реализуется под капотом.

#мат_геймдев #шутеры #алгоритмы
🔥122
Квадратный спред: почему углы «едят» ваши пули

Наивная реализация: dx = Random(-spread, spread), dy — то же самое. Распределение равномерное, но форма — квадрат. А диагональ квадрата длиннее стороны в √2 раз, так что в угловых направлениях пуля улетает на spread * 1.41 вместо spread. Конус разброса физически не круглый — и это чувствуется.

Правильный вариант — полярные координаты:


const angle = Math.random() * Math.PI * 2;
const radius = spread * Math.sqrt(Math.random());
const dx = Math.cos(angle) * radius;
const dy = Math.sin(angle) * radius;


Почему sqrt? Без него точки кучкуются к центру — площадь кольца растёт с радиусом, а вероятность нет. sqrt это компенсирует и даёт равномерную плотность по всему диску.

В конце недели — полный разбор: cone, gaussian, детерминированные паттерны и когда равномерный диск вообще не нужен.

#мат_геймдев #шутеры #алгоритмы
10🤔5🔥2
Cone-uniform vs Gaussian

Равномерный диск — это честно, но не реалистично. В жизни большинство пуль летит ближе к центру прицела, а не равномерно по всему конусу. Gaussian-распределение это отражает: пули кучкуются вокруг центра, хвосты редкие.

На практике разница такая: в CS-подобных играх первый выстрел — почти точный, разброс нарастает с темпом стрельбы. Это не равномерный диск и не чистый Gaussian — это детерминированный паттерн поверх него. Но базовый случайный спред лежащий в основе чаще всего именно Gaussian.


function gaussianRandom(mean = 0, std = 1): number {
const u = 1 - Math.random();
const v = Math.random();
return mean + std * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}

const dx = gaussianRandom(0, spread);
const dy = gaussianRandom(0, spread);


Собственно, выбор между uniform и Gaussian — это выбор ощущения. Uniform читается как «аркада», Gaussian — как «оружие с характером». Когда что уместно — разберём в конце недели, там же детерминированные паттерны и recoil-кривые.

#мат_геймдев #шутеры #алгоритмы
👍14🔥32
О стрельбе в играх по-простому: hitscan, projectile, recoil, lag compensation — шесть пластов под кнопкой выстрела
https://dev-math.ru/articles/shooting/

Шесть параграфов с интерактивами, формулами и кодом на C#. Если когда-нибудь в команде звучало «ну стрельбу-то сделаем за выходные», а потом неделями отлавливали «попал — не засчиталось», статья ровно про это. Жду 🔥 и репостов друзьям-геймдевам 😆

В статье о том, что под одним «нажал курок» лежит шесть независимых задач, и каждая решается по-своему:
▸ Hitscan vs projectile — архитектурный выбор: мгновенный луч (CS, Valorant) или физический снаряд (Quake, Kraber в Apex). За выбором тянется хвост: lag compensation, recoil, ощущаемая отзывчивость.
▸ Хитбоксы — почему индустрия стандартизировалась на капсулах, а не на полигональном меше. Valve в сентябре 2015 целиком переписали систему именно ради этого.
▸ Спред пули — почему два независимых random дают квадрат вместо круга, и как полярные координаты с √ это лечат.
▸ Recoil-паттерны — детерминированная таблица в духе AK-47, которую pro-игроки заучивают как стихи.
▸ Упреждение — банальное квадратное уравнение для AI-снайперов, решаемое за десять строк кода.
▸ Lag compensation — зачем сервер откатывает мир на 80–200 мс назад на каждом выстреле.

По дороге — интерактивы, в которых можно покрутить параметры руками, и сниппеты на C#.

Если соберём 200 🔥 — пишу продолжение по теме, которая больше всего зайдёт в комментариях (баллистика снайперок, penetration через стены, ricochet, damage falloff…).

#мат_геймдев #МатРазбор #шутеры #алгоритмы
🔥44
This media is not supported in your browser
VIEW IN TELEGRAM
В Dreams на PS4 нет ни одного полигона. Совсем.

Взгляните на знаменитый Shadertoy «Happy Jumping» от Inigo Quilez — по экрану прыгает живая капля с глазами, мягкими тенями, AO. Никаких треугольников. Только SDF.

Это signed distance field и raymarching. Геометрия описана математически: для любой точки в пространстве функция возвращает то, как далеко до ближайшей поверхности.

Академическая штука для красивых демок? Конечно же нет. Всё это можно использовать в продакшене. Dreams от Media Molecule целиком построена на SDF — вся игра, все пользовательские ассеты. Doom Eternal считает через SDF-brushes свою gore-систему (есть отдельный Siggraph 2020 talk). Claybook делает на этом пластилиновую физику.

Шейдер персонажа Inigo Quilez выглядит круто, помещается в один небольшой файл, работает в браузере. Но у этого есть своя цена — и именно поэтому в играх raymarching чаще локальный эффект, чем основной рендер.

В статье на этой неделе разберём: что такое distance field, как работает sphere tracing, почему две сферы сливаются в каплю через одну функцию smin и откуда берутся "почти бесплатные" soft shadows. Плюс попробую сделать демки на WebGL чтобы можно было покрутить параметры самостоятельно 🔥

#мат_геймдев #МатРазбор #шейдеры #VFX
🔥12👍6
Как лепят миры из пластилина

В SDF-мире вся геометрия — это функции расстояния. Объединение двух объектов? min(a, b). Ставите две сферы рядом, берёте min — и получаете не каплю, а два камня, лежащих друг на друге. Жёсткий стык, шов на пиксель.

А хочется ртути. Слизи. Жидкого золота. Капли воды, которая стекает и сливается с другой. Нужно мягкое объединение.

Собственно, вот оно — polynomial smin:


float smin(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}

k — ширина шва. k=0 — жёсткое объединение, обычный min. k=0.3 — между сферами протягивается перешеек. k=1 — две сферы превращаются почти в каплю.

Эта формула везде, где в кадре что-то текучее: лава и слизь во многих играх, любая «жидкая» геометрия в шейдерных демосценах. Конечно же, в больших AAA-катсценах жидкость чаще пекут в alembic-кэш — но это уже не процедурка, а проигрывание заранее посчитанной симуляции. У smin своя ниша.

smin выигрывает у alembic-кэшей везде где что-то меняется от ввода игрока в реальном времени. Боссы-слизни, которые делятся и сливаются. Кровь и слизь, динамически стекающиеся в лужи под ногами. Терраморфинг от взрывов, налипание снега. Жгуты энергии из руки игрока к цели. Запечённую симуляцию тут не подсунешь — форма зависит от того, что игрок делает прямо сейчас. А smin честно считает её на лету.

Основная проблема SDF и Raymarching — это производительность. И это главная причина почему SDF не вытеснили полигоны. Но с нынешними мощностями устройств эти техники применимы «локально». Когда полученная ими визуализация композится в сцену. Например, VFX заклинания или какие-то отдельные эффекты.

#мат_геймдев #МатРазбор #шейдеры
🔥12
Raymarch на 5 fps — это не GPU виноват. Это вы «шагаете» неправильно

У вас есть SDF и луч. Как найти, где луч упирается в поверхность? Самый очевидный ответ — идём вдоль луча маленькими шагами и проверяем, попали ли.


// плохо: фиксированный шаг
for (int i = 0; i < 1000; i++) {
if (sdf(p) < EPS) return p;
p += dir * 0.01;
}


И вот ловушка. Шаг маленький — тысячи итераций на пиксель, шейдер выдает 5 fps. Шаг большой — луч проскакивает сквозь тонкие объекты, артефакты по всей сцене. Компромисс «либо медленно, либо неточно». А хочется оба.

Собственно, в чём идея. SDF возвращает расстояние до ближайшей поверхности. Значит, можно шагнуть ровно на это расстояние — и гарантированно не пройти насквозь.


// хорошо: sphere tracing
for (int i = 0; i < 64; i++) {
float d = sdf(p);
if (d < EPS) return p;
p += dir * d; // ← шаг ровно SDF
}


Близко к поверхности шаги мелкие (поле говорит «осторожно, рядом»), в пустоте — большие («лети спокойно»). Обычно 30–60 итераций на всю сцену.

По сути, вся магия SDF держится именно на этой идее. Без sphere tracing формулы расстояний были бы просто красивой математикой без практического смысла.

#мат_геймдев #МатРазбор #шейдеры
🔥111
SDF и raymarching по-простому: от smin до мешей и обратно
https://dev-math.ru/articles/sdf/

Как из двух сфер собирается одна капля через smin, как sphere tracing рендерит её без единого треугольника, и почему в реальных играх SDF чаще локальный VFX, чем основной рендер. Шесть разделов, шесть интерактивов в браузере, плюс ответы на два вопроса: «А цена sdf(p) какая?» и «А что с мешами в SDF?».

Завершим цикл того о чём я писал на неделе статьёй с ответами на вопросы в комментах, и я пошел думать что буду рассказывать на следующей неделе. 🔥 как всегда приветствуются. Это классическая валюта собираемая мной за классные статьи. Работаю за "классы" :) Ну и делимся статьей с друзьями, лучшая благодарность автору :)

#мат_геймдев #МатРазбор #шейдеры #VFX
🔥12
Толпа без AI: сотни юнитов на трёх правилах

По голосованию победитель очевиден. Так что начнём. У парижан в Assassin's Creed Unity нет интеллекта. У каждого крысака в Vermintide и зомби из World War Z — тоже. Когда в кадре одновременно сотни персонажей, если бы у каждого крутилось полноценное behavior tree, FPS умер бы.

Так что там — не AI, а несколько локальных правил: «не наступай на ноги соседям», «иди примерно туда же, куда соседи», «не отставай». Собственно, это и есть толпа в играх — эмерджентность. Крейг Рейнольдс описал это в 1986 году и назвал boids.

Дальше начинается инженерия. Days Gone крутит орду из сотен Freakers. World War Z — Swarm Engine с зомби, текущими по уровню как жидкость. Под капотом везде одно и то же: boids, spatial hashing, ORCA, плюс LOD AI на дальних юнитах.

На неделе разберём: три формулы Рейнольдса, почему наивная реализация умирает на 500 юнитах, как ORCA не даёт юнитам сталкиваться в узких местах. 🔥

#мат_геймдев #МатРазбор #AI #algorithms
❤‍🔥18🔥13👍1
Boids: три правила стаи

Когда вы смотрите на стаю скворцов на закате, кажется, что это один организм с общей волей. На самом деле там нет лидера: каждая птица следит примерно за семью ближайшими соседями — независимо от того, как далеко они оказались. Это измерили Ballerini и соавторы в 2008 году по стереосъёмкам стай над Римом (PNAS).

Сам алгоритм старше. Крейг Рейнольдс собрал три правила ещё в 1987-м (SIGGRAPH '87). Само слово boids — от bird-oid object:

separation — отталкивайся от того, кто оказался слишком близко.
alignment — подстраивай свою скорость под среднюю скорость соседей.
cohesion — тянись к их центру масс.

Собственно, в Unity это делается так:


Vector3 sep = Vector3.zero, ali = Vector3.zero, coh = Vector3.zero;
int count = neighbors.Count;

foreach (var other in neighbors) {
Vector3 d = transform.position - other.position;
if (d.sqrMagnitude < sepRadius * sepRadius) sep += d.normalized;
ali += other.velocity;
coh += other.position;
}

velocity += wSep * sep
+ wAli * (ali / count - velocity)
+ wCoh * (coh / count - transform.position);
velocity = Vector3.ClampMagnitude(velocity, maxSpeed);
transform.position += velocity * Time.deltaTime;


Магия — в балансе весов. Большой wSep — юниты разлетаются в пыль. Большой wCoh — слипаются в шарик и стоят. Всё интересное — где-то между.

В играх это стаи в Subnautica, орда Freakers в Days Gone, Swarm Engine в World War Z. Конечно же, в боевой реализации есть ещё seek к цели и avoidance препятствий — но базовая стая держится на этих трёх правилах. 🔥

#мат_геймдев #МатРазбор #AI #biomimicry
🔥12❤‍🔥7👍4
Толпа на сетке: несколько клеток, а не миллион проверок

В boids каждому юниту нужны его ближайшие соседи — без них не сработают ни separation, ни alignment, ни cohesion. Наивный ответ — пройтись по всем и проверить расстояние:


// плохо: O(N²) на каждом кадре
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (i == j) continue;
if ((boid[i].position - boid[j].position).sqrMagnitude < r * r) {
// учесть соседа
}
}
}


Считаем по головам. 100 юнитов — 10 000 проверок за кадр. 500 — 250 000. 1000 — миллион. И всё это 60 раз в секунду.

Итак, spatial hash. Соседи по определению рядом — значит, проверять надо только тех, кто в той же зоне пространства. Разбиваем сцену на клетки фиксированного размера, раскладываем юнитов по клеткам и смотрим рядом:


// каждый кадр пересобираем
var grid = new Dictionary<Vector2Int, List<Boid>>();
foreach (var b in boids) {
var cell = new Vector2Int(
(int)(b.position.x / cellSize),
(int)(b.position.z / cellSize));
if (!grid.TryGetValue(cell, out var list))
grid[cell] = list = new List<Boid>();
list.Add(b);
}


Размер клетки чаще всего берут равными радиусу видимости соседей.

По сути алгоритм boids — это часть задачи. Ещё часть — найти соседей достаточно быстро. Без этого игры с сотнями юнитов на экране просто не существовали бы. 🔥 Кроме того их надо отрендерить, а об этом уже поговорим позже.


#мат_геймдев #МатРазбор #algorithms #optimization
🔥12
Толпа без AI: как три правила, хеш-сетка и ORCA делают массовку живой
https://dev-math.ru/articles/crowd/

Вы просили про толпу. Ну держитесь 🙂 Семь разделов, шесть интерактивов и тысяча юнитов в одном экране браузера. Я заморочился — получилось так объёмно, что статьи скоро можно сшивать в книжку (Поэтому немного задержался. Хотел в пятницу, а получилось в субботу). Жду огоньков и репостов друзьям 🔥

Симуляция массовки упирается в две проблемы: общий behavior tree «толпа делает X» не масштабируется, а наивный поиск соседей даёт O(N²) и кладёт сцену уже на пятистах юнитах. В статье разбираю архитектуру по слоям: три правила Рейнольдса для группового движения, uniform spatial hash для поиска соседей за O(N), упрощённый ORCA для встречных потоков в коридоре, steering behaviors для целей и обхода препятствий, плюс отдельная часть про GPU instancing для рендера тысячи юнитов одним draw call'ом. В финале — разбор под капотом AC Unity, World War Z, They Are Billions, Vermintide 2 и RTS-обобщённо.

#мат_геймдев #МатРазбор #algorithms #AI
🔥22❤‍🔥11
Please open Telegram to view this post
VIEW IN TELEGRAM
Тысяча юнитов в кадре — это, конечно, эффектно. Но в AAA это только начало.

Давайте пройдемся до конца по толпе тогда (по результатам опроса). А потом уже пойдем либо к лутбоксам, либо к ELO и MMR.

В прошлой статье мы разобрали, как оживляют тысячу юнитов через boids + spatial hash на CPU. При этом в Assassin's Creed Unity на улицах Парижа — до 10 000 NPC одновременно. В массовых битвах Total War: Warhammer 2 — тоже десять тысяч, у каждого свой коллайдер и анимация. В Helldivers 2 — плотные волны термидов на одного игрока. They Are Billions — название говорит за себя.

По сути, любая CPU-архитектура решения из прошлой статьи на этом масштабе отвалится. У проблемы два слоя, которые мы не разобрали — глобальная навигация через flow fields (много готовых реализаций, но для такой задачи уже лучше написать свою) и перенос симуляции на GPU (compute shaders + indirect rendering).

Разберем на этой неделе. Всё как обычно со статьей в конце недели. Данная тема прикольна ещё тем, что если углублятся в детали реализации (особенно рендера), то станет понятнее как работает та же Nvidia Flex и подобные технологии.

Ну и делимся постами, ставим реакции, пишем чего хотелось бы ещё в комменты. Короче статьи, больше каких-то примеров. Хочется понимать, что интересно, а что нет. А то по SDF статью посмотрело меньше народу, чем даже по толпе, а вышла она уже неделю как.

#мат_геймдев #МатРазбор #algorithms #performance
👍16🔥4
Flow fields: один расчёт вместо тысячи A*

Тысяча юнитов идёт к одной точке. Если для каждого считать A* — тысяча независимых поисков за кадр. Дороговато.

Как нам сделать меньше расчётов? По своей сути, идея проста. Можно сделать один расчёт вместо тысячи. Посчитали поле — все юниты просто читают из него «куда идти из своей клетки».

Разбиваем карту на сетку. От цели обратной волной (Dijkstra) в каждой клетке считаем дистанцию до цели и записываем вектор на соседа — это и есть flow field. Юниту достаточно прочитать вектор из своей клетки. O(1) на запрос, без зависимости от длины пути и количества юнитов.

«А если цель движется?» В RTS — почти никогда. Клик в точку, поход к зданию — цель статична, поле живёт минутами. При движущейся цели перерсчитать получается в любом случае не так дорого. Одну дейкстру пустить по карте не так долго, как для каждого юнита считать А*. Тем более с шумом от boids там не обязательно моментальная реакция на пользовательский ввод или триггеры.

Проблема возникает если много юнитов и у каждого своя цель — A* снова в выигрыше. Flow fields — это про движение толпы к общей цели. Канонический разбор — Elijah Emerson, AI Game Programming Wisdom 5 (2008).

#мат_геймдев #МатРазбор #algorithms #AI
👍11🔥3