4

Часть 4: проблема с дальними атаками

Серия Как мы разрабатывали свой автобаттлер

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

Проблема: дДо этого дальники (range ≥ 2) в симуляции работали как ближники: в тик выстрела сразу летели Attack + Damage. В реплее это выглядело странно — лучник делает замах, а урон появляется мгновенно, без полёта болта.

При этом в контенте и дизайне уже была ось melee / ranged, дальность влияла на таргетинг и движение, но не на тайминг урона. Получался разрыв между тем, что игрок ожидает от подобного боя, и тем, что реально происходило в логе.

Модель снаряда

In-flight состояние — простой immutable record:

internal sealed class SimProjectile

{

public int ProjectileId { get; init; }

public int SourceId { get; init; }

public int TargetId { get; init; }

public int LaunchTick { get; init; }

public int HitTick { get; init; }

public int OutgoingDamage { get; init; }

public bool Crit { get; init; }

public DamageKind DamageKind { get; init; }

public Hex FromHex { get; init; }

public Hex ToHex { get; init; }

}

FromHex / ToHex — снимок гексов на момент выстрела (задел под отладку и будущий VFX). В фазе 1 попадание привязано к id цели, а не к координатам. OutgoingDamage записывается при запуске, но на HitTick не используется. Урон пересчитывается заново через ResolveAutoAttackDamage по текущим статам атакующего и цели. Из выстрела «переносится» только флаг крита и тип урона.

ProjectileSystem: два метода, одна ответственность

Вся логика сосредоточена в ProjectileSystem.cs.

LaunchAutoAttack — только enqueue

Метод не пишет в лог и не наносит урон. Создаёт id, кладёт снаряд в список, возвращает projectileId:

int id = nextProjectileId++;

projectiles.Add(new SimProjectile

{

ProjectileId = id,

SourceId = source.Id,

TargetId = target.Id,

LaunchTick = tick,

HitTick = tick + travelTicks,

OutgoingDamage = outgoingDamage,

Crit = crit,

DamageKind = damageKind,

FromHex = source.Pos,

ToHex = target.Pos,

});

return id;

Событие Attack в лог пишет вызывающий CombatSimulator, не ProjectileSystem.

TickProjectiles — resolve на HitTick

В начале каждого тика симуляции (до шагов юнитов) проходим список с конца:

for (int i = projectiles.Count - 1; i >= 0; i--)

{

var p = projectiles[i];

if (tick < p.HitTick) continue;

projectiles.RemoveAt(i);

// цель мертва → ProjectileFizzle

// стрелок удалён из списка → ProjectileFizzle

// иначе → ResolveAutoAttackDamage → ApplyProjectileHit → Damage

}

Где это вшито в CombatSimulator

В Simulate() появляется состояние:

int nextProjectileId = 1;

var projectiles = new List<SimProjectile>();

И порядок в тике:

FinalizeArrivals

→ TickProjectiles // сначала долетают старые болты

→ регены, дебаффы, щиты

→ StepUnit // потом новые выстрелы

Ветка Attack() разделена по u.Range:

Melee (range ≤ 1): как раньше — Attack + Damage в одном тике, durationTicks не ставится.

Ranged (range ≥ 2): в тик выстрела — крит, OnAutoAttack-пассивки, мана за атаку, кулдаун, LaunchAutoAttack, в лог Attack с durationTicks и instanceId. Урон — только когда TickProjectiles дойдёт до HitTick.

private static void Attack(SimUnit u, SimUnit target, ...)

{

bool crit = rng.Chance(u.Stats.CritChance);

PassiveRuntimeSystem.OnAutoAttack(u, crit, tick, events, ...);

u.BusyUntilTick = tick + effAttacker.AttackCooldownTicks;

if (u.Range <= 1) { /* мгновенный урон */ return; }

int travel = UnitStats.TravelTicksFromDistance(HexMath.Distance(u.Pos, target.Pos), speed, TicksPerSecond);

int projectileId = ProjectileSystem.LaunchAutoAttack(...);

events.Add(CombatEvent.Attack(tick, u.Id, target.Id, travel, projectileId));

}

Формула полёта

Целочисленная, 30 Гц — тот же паттерн, что у скорости атаки и движения: travelTicks = max(1, hexDistance × 30 × 100 / projectileSpeed). projectileSpeed = 600 означает 6.0 гекса в секунду.

Контракт лога: один Attack, два смысла

Мы не ввели отдельное событие ProjectileLaunch в сим-логе. Переиспользовали Attack:

public static CombatEvent Attack(int tick, int unitId, int targetId, int? travelTicks = null, int? projectileId = null) => new()

{

Tick = tick,

Kind = CombatEventKind.Attack,

UnitId = unitId,

TargetId = targetId,

DurationTicks = travelTicks,

InstanceId = projectileId,

};

  • Melee: durationTicks = null, instanceId = null

  • Ranged: durationTicks = travel, instanceId = projectileId

Для промаха — отдельный kind:

ProjectileFizzle(tick, projectileId, sourceId, targetId)

Что мы сознательно не сделали (и почему)

Крит на выстреле, митигация на попадании. Между тиками выстрела и попадания на танка могут наложить щит или shred armor — финальный урон отразит состояние на момент попадания, не выстрела.

Один generic-болт для всех дальников. Быстрее довели до playable. Per-hero спрайты, rotation, trail оставили на потом.

Нет rebuild in-flight при seekTo. Перемотка реплея не пересобирает летящие болты из состояния сима — только из событий с текущего eventIndex.

Нет line-of-sight. Снаряд не проверяет препятствия на луче.

Ну и как обычно ссылка на наш проект: тут

Правила сообщества

ОБЩИЕ ПРАВИЛА:

- Уважайте чужой труд и используйте конструктивную критику

- Не занимайтесь саморекламой, пишите качественные и интересные посты

- Никакой политики


СТОИТ ПУБЛИКОВАТЬ:

- Посты о Вашей игре с историей её разработки и описанием полученного опыта

- Обучающие материалы, туториалы

- Интервью с опытными разработчиками

- Анонсы бесплатных мероприятий для разработчиков и истории их посещения;
- Ваши работы, если Вы художник/композитор и хотите поделиться ими на безвозмездной основе

НЕ СТОИТ ПУБЛИКОВАТЬ:

- Посты, содержащие только вопрос или просьбу помочь
- Посты, содержащие только идею игры

- Посты, единственная цель которых - набор команды для разработки игры

- Посты, не относящиеся к тематике сообщества

Подобные посты по решению администрации могут быть перемещены из сообщества в общую ленту.

ЗАПРЕЩЕНО:

- Публиковать бессодержательные посты с рекламой Вашего проекта (см. следующий пункт), а также все прочие посты, содержащие рекламу/рекламные интеграции

- Выдавать чужой труд за свой

Подобные посты будут перемещены из сообщества в общую ленту, а их авторы по решению администрации могут быть внесены в игнор-лист сообщества.


О РАЗМЕЩЕНИИ ССЫЛОК:

Ссылка на сторонний ресурс, связанный с игрой, допускается только при следующих условиях:

- Пост должен быть содержательным и интересным для пользователей, нести пользу для сообщества

- Ссылка должна размещаться непосредственно в начале или конце поста и только один раз

- Cсылка размещается в формате: "Страница игры в Steam: URL"

0
Автор поста оценил этот комментарий

Картинку хоть приложили бы, вообще не понятно о чем речь.


Заходишь по ссылке, какая то регистрация.


Текст непонятно о чем.

Без негатива, но подача поста максимально душная


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

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества

Недвижимость и ремонт

Теги

Популярные авторы

Сообщества