Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- Техническое описание: тики, горутины персонажей, расписания и маршруты
- 1. Назначение
- Система должна поддерживать большое количество персонажей с разной степенью детализации симуляции:
- • активные персонажи самостоятельно следят за глобальным счётчиком тиков и выполняют свою логику;
- • неактивные персонажи не обрабатывают каждый тик, а просыпаются по событиям и расписанию;
- • движение в выгруженных областях рассчитывается упрощённо;
- • при активации областей маршрут уточняется, а персонажи при необходимости переходят к подробной симуляции.
- Каждому персонажу соответствует собственная горутина. Пул worker вместо горутин персонажей в этой архитектуре не используется.
- 2. Основные сущности
- СущностьОтветственность
- Координатор времениУвеличивает глобальный счётчик, управляет торможением
- Горутина персонажаВладеет состоянием персонажа, обрабатывает тики и события
- Планировщик расписанийОтправляет уведомления при наступлении заданного игрового тика
- Индекс маршрутов по зонамПозволяет найти маршруты, пересекающие активируемую область
- Кэш путейХранит повторно используемые данные пути и при необходимости его детализацию
- Точная пространственная сеткаХранит фактическое положение объектов, участвующих в подробной симуляции
- Игровое время выражается номером тика. Реальное монотонное время используется координатором для выдерживания частоты.
- 3. Глобальные тики
- 3.1. Счётчик времени
- Глобальный счётчик:
- • имеет тип atomic.Uint64;
- • изменяется только координатором;
- • увеличивается строго на единицу;
- • не пропускает значения;
- • доступен персонажам и системным горутинам для чтения.
- Значение глобального счётчика означает последний выпущенный тик, который активные участники могут обрабатывать.
- Остановка игры передаётся отдельным сигналом, например через context, а не специальным значением счётчика.
- 3.2. Локальный прогресс
- У каждого активного участника имеется локальный номер полностью завершённого тика.
- Участник:
- 1. Читает глобальный счётчик.
- 2. Если локальный счётчик меньше глобального, выполняет следующий локальный тик.
- 3. После полного завершения работы увеличивает локальный счётчик.
- 4. Публикует завершённый тик в атомарном поле.
- 5. Повторяет проверку.
- Отставшие активные участники выполняют тики последовательно. Присваивать глобальное значение локальному счётчику без выполнения работы нельзя.
- Исключение — переход из упрощённой симуляции в активную: он описан отдельно.
- 3.3. Допустимая рассинхронизация
- Участники могут находиться на разных локальных тиках.
- Большинство взаимодействий передаются событиями. Критические изменения состояния, например нанесение урона, используют необходимую синхронизацию.
- Блокировки обеспечивают безопасность изменения данных, но сами по себе не обеспечивают единый хронологический порядок событий.
- 4. Координатор времени
- 4.1. Частота проверок
- Координатор работает отдельной горутиной с целевой частотой 10 проверок за номинальную длительность игрового тика.
- checkInterval = tickDuration / 10
- Пример для 60 Гц:
- Длительность тика: ≈ 16,67 мс
- Целевой интервал проверки: ≈ 1,67 мс
- Эта частота является целевой, а не гарантированной: планировщик Go и ОС могут задерживать выполнение.
- 4.2. Подстройка сна
- Координатор измеряет фактические интервалы между проверками и динамически подбирает сон:
- • учитывает время собственной работы;
- • при устойчивых опозданиях уменьшает сон;
- • использует сглаживание измерений, чтобы не реагировать резко на единичные задержки;
- • имеет настраиваемую нижнюю границу сна, исключающую постоянное активное ожидание.
- Подстройка не должна отнимать у персонажей CPU ради формального достижения частоты проверок.
- Во время торможения координатор продолжает работать с той же целевой частотой.
- 4.3. Итерация координатора
- На каждой итерации координатор:
- 1. Принимает доступные запросы торможения и снятия регистрации.
- 2. Проверяет прогресс зарегистрированных отстающих.
- 3. Удаляет завершённые запросы.
- 4. При отсутствии отстающих и наступлении срока выпускает следующий тик.
- 5. Рассчитывает время следующего пробуждения.
- Обработка входящих сообщений должна быть ограничена по количеству или времени, чтобы непрерывный поток запросов не вытеснял остальную работу.
- 5. Торможение глобального времени
- 5.1. Регистрация отставания
- Порог отставания первоначально принимается равным 10 тикам и задаётся параметром.
- Если:
- globalTick - completedTick > maxLagTicks
- участник отправляет координатору указатель на структуру своего прогресса:
- type ParticipantProgress struct {
- ID ParticipantID
- CompletedTick atomic.Uint64
- }
- Идентификаторы системных участников, включая планировщик, не должны конфликтовать с идентификаторами персонажей.
- Правила:
- • структура имеет стабильный адрес;
- • после начала использования содержащий atomic объект не копируется;
- • участник записывает прогресс через Store;
- • координатор читает через Load;
- • канал регистрации не заменяет атомарный доступ к последующим обновлениям.
- В первой реализации прогресс публикуется после каждого завершённого активного тика.
- 5.2. Набор отстающих
- Координатор единолично владеет набором зарегистрированных отстающих. Mutex для этого набора не нужен.
- При получении запроса координатор:
- 1. Проверяет актуальный прогресс.
- 2. Если участник уже догнал, не добавляет его.
- 3. Иначе добавляет или обновляет его регистрацию.
- Повторные запросы одного участника объединяются. Участник не отправляет запрос на каждом догоняющем тике: достаточно одного запроса на эпизод отставания.
- 5.3. Поведение при торможении
- Пока набор отстающих не пуст:
- • глобальный счётчик не увеличивается;
- • персонажи продолжают догонять текущий глобальный тик;
- • координатор продолжает принимать сообщения и проверять прогресс.
- Участник считается догнавшим, если:
- completedTick >= globalTick
- Один догнавший участник не снимает торможение за остальных.
- 5.4. Возобновление времени
- Когда набор становится пустым:
- 1. Координатор выпускает один следующий тик сразу.
- 2. Следующий выпуск назначается через обычную длительность тика.
- Реальное время, прошедшее в торможении, не создаёт задолженность игровых тиков.
- 5.5. Ограничения протокола
- Порог отставания является мягким: до обработки запроса координатором глобальный счётчик может продвинуться дальше.
- Запрос торможения нельзя молча потерять при переполнении канала.
- При завершении участника или переходе в неактивный режим его регистрация должна сниматься отдельной командой. Запросы регистрации и снятия регистрации от одного участника должны обрабатываться в порядке их отправки.
- При повторном использовании ID нужен номер поколения участника, чтобы старая команда не затронула новую горутину.
- 6. Режимы персонажа
- 6.1. Активный режим
- Персонаж:
- • следит за глобальными тиками;
- • выполняет каждый необходимый локальный тик;
- • обрабатывает входящие события;
- • при отставании участвует в механизме торможения.
- Когда персонаж догнал глобальное время, он ожидает примерно четверть номинального тика перед повторной проверкой.
- Ожидание должно прерываться входящим сообщением и остановкой игры. Реализация может использовать таймер совместно с select, а не безусловный time.Sleep.
- 6.2. Неактивный режим
- Персонаж:
- • не проверяет глобальный счётчик периодически;
- • ждёт сообщения в своём inbox;
- • выполняет пункты расписания;
- • может обрабатывать внешние события;
- • не считается отставшим из-за длительного сна.
- Для неактивных персонажей задержка обработки расписания на несколько секунд допустима.
- Само пробуждение не обязательно означает переход в активный режим: персонаж может выполнить пункт расписания, зарегистрировать следующий и снова уснуть.
- 6.3. Переход в активный режим
- При активации персонаж:
- 1. Определяет актуальный игровой тик.
- 2. Актуализирует состояние по маршруту, расписанию и прошедшему времени.
- 3. Обрабатывает необходимые накопленные переходы.
- 4. Принимает выбранный тик актуализации за исходную точку активной симуляции.
- 5. Начинает последовательно обрабатывать последующие тики.
- Он не выполняет все тики, пропущенные во время штатного сна.
- Если актуализация заняла время и глобальный счётчик уже продвинулся, дальнейшее отставание обрабатывается обычным механизмом активного режима.
- 7. Планировщик расписаний
- 7.1. Структура хранения
- Основная структура:
- map[Tick][]ObjectID
- Ключ — целевой тик пробуждения. Значение — ID персонажей, запросивших уведомление на этот тик.
- Содержимое расписания хранится у персонажа. Планировщик сообщает только:
- «Наступило время проверить расписание».
- 7.2. Регистрация
- Регистрация выполняется через потокобезопасную функцию с mutex.
- Под одним mutex должны быть согласованы:
- • добавление записей;
- • извлечение записей для наступившего тика;
- • состояние обработанного диапазона времени;
- • обработка регистрации на уже пройденный тик.
- Если целевой тик уже обработан, запрос направляется на ближайшую доставку, а не помещается в оставшийся позади элемент карты.
- Отправка в inbox персонажа не выполняется под mutex расписания. Планировщик сначала забирает записи в локальный список, затем освобождает блокировку.
- 7.3. Продвижение планировщика
- Планировщик следит за глобальным счётчиком и обрабатывает тики последовательно.
- Если он увидел переход с 100 на 103, он обрабатывает:
- 101 → 102 → 103
- Сам глобальный счётчик значений не пропускает; планировщик мог лишь не наблюдать промежуточные изменения.
- После передачи уведомлений соответствующего тика в предусмотренную очередь доставки планировщик публикует свой завершённый тик. Завершения работы персонажами он не ждёт.
- При отставании планировщик регистрируется у координатора по тому же протоколу, что и активный персонаж.
- 7.4. Устаревшие уведомления
- Получение уведомления не является безусловной командой завершить старый маршрут.
- Персонаж проверяет собственное актуальное расписание. Если старый пункт отменён или заменён, ненужное уведомление игнорируется.
- Это позволяет начать с хранения только ObjectID, без обязательного EventID.
- 8. Упрощённое движение в выгруженных областях
- 8.1. Начало маршрута
- Неактивный персонаж:
- 1. Получает путь из кэша.
- 2. Определяет общую длительность движения.
- 3. Фиксирует начало и ожидаемое окончание.
- 4. Регистрирует маршрут в пространственном индексе затронутых зон.
- 5. Регистрирует пробуждение на окончание маршрута.
- 6. Переходит в ожидание.
- Потиковое изменение положения не выполняется.
- 8.2. Исходная детализация
- Первоначально достаточно знать:
- • путь или ссылку на него;
- • затронутые зоны;
- • начало движения;
- • общую длительность;
- • время окончания.
- Время входа в каждую зону заранее рассчитывать необязательно.
- При этом кэш должен содержать или позволять получить данные, необходимые для последующей детализации: геометрию, длины участков, параметры движения.
- 9. Пространственный индекс маршрутов
- Маршруты регистрируются в отдельной структуре, параллельной точной сетке шардов.
- Логически:
- ZoneKey → набор ссылок на маршруты
- Индекс не означает, что персонаж одновременно находится во всех пересекаемых зонах. Он позволяет найти маршруты-кандидаты при активации области.
- Размер зоны задаётся параметром. В коде следует задавать длину стороны, чтобы исключить неоднозначность между 10 м² и 10×10 м.
- Запись маршрута должна иметь идентификатор или поколение. Это необходимо, чтобы после отмены или замены пути отличать актуальные пространственные записи от старых.
- Данные маршрута, читаемые несколькими горутинами, должны быть неизменяемыми после публикации либо защищёнными синхронизацией.
- 10. Уточнение маршрута при активации зоны
- 10.1. Условие детализации
- Когда зона становится активной, система получает зарегистрированные в ней маршруты.
- Если у маршрута отсутствует временная разметка по зонам, запускается её расчёт.
- Одновременная активация нескольких зон одного маршрута не должна запускать несколько независимых одинаковых расчётов. Запросы объединяются либо обрабатываются последовательно горутиной персонажа.
- 10.2. Результат детализации
- В первой реализации рассчитываются интервалы прохождения всех зон маршрута:
- type ZonePassage struct {
- Zone ZoneKey
- EnterTick Tick
- ExitTick Tick
- }
- Интервалы рекомендуется трактовать как [EnterTick, ExitTick), чтобы однозначно обрабатывать границы.
- Если маршрут возвращается в одну зону, для неё сохраняются несколько прохождений.
- Разметка должна учитывать фактическое распределение времени движения. Делить общую длительность поровну между зонами нельзя, если это не соответствует модели маршрута.
- Время начала и окончания уточнённого маршрута должно оставаться согласованным с исходным планом. Если оценка прибытия меняется, персонаж обновляет своё расписание.
- 10.3. Действие после детализации
- Для активной зоны определяется:
- • персонаж уже прошёл её — немедленное пробуждение не нужно;
- • персонаж находится в ней сейчас — требуется актуализация положения;
- • персонаж войдёт позже — требуется пробуждение к моменту входа.
- Нужно учитывать оба случая:
- 1. Зона активировалась после регистрации маршрута.
- 2. Новый маршрут зарегистрирован через уже активную зону.
- Проверять только событие первоначальной активации зоны недостаточно.
- 10.4. Актуализация положения
- Сообщение направляется горутине персонажа. Она:
- 1. Проверяет актуальность маршрута.
- 2. Вычисляет положение на текущий игровой момент.
- 3. При необходимости включает точную пространственную представленность.
- 4. Выбирает дальнейший режим: подробное расписание или активная потиковая обработка.
- Отмена, замена и завершение маршрута должны делать старые записи недействительными и обеспечивать их последующее удаление из индекса.
- 11. Доставка сообщений
- Для inbox персонажей и системных каналов необходимо определить ограниченную по памяти политику доставки.
- Обязательные требования:
- • координатор не зависает на отправке сообщения персонажу;
- • отправка не выполняется под пространственными блокировками или mutex расписания;
- • критические сообщения не теряются молча;
- • одинаковые уведомления «проверь актуальное состояние» допускается объединять;
- • завершение персонажа учитывается при доставке;
- • циклические блокирующие ожидания между персонажами исключаются.
- В частности, участник, тормозящий мир, не должен ожидать наступления будущего глобального тика для завершения своего текущего тика.
- 12. Проверки корректности и метрики
- Обязательные тесты
- 1. Глобальный счётчик увеличивается последовательно.
- 2. Локальный прогресс публикуется только после завершения тика.
- 3. Несколько отстающих независимо удерживают торможение.
- 4. Завершение одного участника не оставляет вечную регистрацию.
- 5. После торможения выпускается один тик без наверстывающего скачка.
- 6. Планировщик обрабатывает все наблюдённые с задержкой тики.
- 7. Регистрация на уже обработанный тик не теряется.
- 8. Длительный сон персонажа не вызывает ложного торможения.
- 9. Старое уведомление не завершает новый маршрут.
- 10. Учитывается будущий вход в уже активную зону.
- 11. Повторное пересечение одной зоны сохраняется в разметке.
- 12. Конкурентные сценарии проходят проверку go test -race.
- Основные метрики
- • фактические интервалы проверок координатора;
- • число отстающих и их отставание;
- • длительность и доля времени торможения;
- • задержка включения и снятия тормоза;
- • отставание планировщика;
- • размеры очередей;
- • количество активных и спящих персонажей;
- • число маршрутов и записей пространственного индекса;
- • стоимость и количество детализаций маршрутов.
- 13. Границы текущего решения
- В данное описание не входит очистка шардов при паузе. Торможение глобального счётчика не является безопасной остановкой всех операций мира.
- Параметрами остаются:
- • частота игровых тиков;
- • порог отставания;
- • минимальный сон координатора;
- • размеры каналов и очередей;
- • размер пространственных зон;
- • критерии активации и деактивации персонажей.
- Основной принцип реализации: горутина персонажа остаётся владельцем его поведения; координатор управляет только игровым временем, планировщик — пробуждениями, а пространственный индекс маршрутов — обнаружением необходимости уточнить симуляцию.
Advertisement
Add Comment
Please, Sign In to add comment