Отложив на пол-дороге другие механизмы - взялся, наконец, за основы лагокомпенсируемой архитектуры.
Умных указателей решил не городить - ибо и так голова пухнет при взгляде на движок БД.
Все механизмы для построения лагокомпенсации в БД уже есть.
Краткое содержание предыдущий серий: Лагокомпенсация - за счёт "слоёной" реальности, где каждый слой может тикать независимо, и на основе этого - классический локстеп (благо, Паскаль умеет в воспроизводимость, благодаря стандартизованной до бита математике с 32-битными флоатами), где базовый слой тикает на 500мс в прошлом, получая финализованные инпуты всех игроков (не успел дойти - отбросить), а всплывающие слои - тикают ускоренно, собирая доходящие с задержкой инпуты других игроков и донося их влияние на игровую реальность до текущего момента. Слои - краткоживущие, всплывают - и самоликвидируются. "Слой настоящего", в котором шурует локальный игрок - на деле преходящая времянка, верхний из вплывающих слоёв, который ликвидируется, когда его догоняет всплывающий снизу.
"Всплывающий" означает, что он тикает с таким ускорением, на какое у проца лошадиных сил хватит, пока все слои между собой не выжрут целое ядро на 100%. Чем мощнее проц - тем менее дёрганая лагокомпенсация и тем более бессмысленная и беспощадная эта гонка всплывающих. Перегрелся? Вентилятор надо было вовремя чистить.
Система неэпически понтовая и экономичная, и вот как выходит ея имплементация:
1. порождение нового слоя
ничего не стоит. Буквально ничего. Все задействованные механизмы - ленивые, выполнить аллокацию ускоренного поля и аллокацию индекса в пространстве из восьми индексов (индекс запихан в три бита маски) - и всё.
2. Слои образуют древовидную структуру. Ориентировочно, у базового Зеро на глубине -500мс - два дитя, глубокий всплывающий А и ближнее дно Б, которое висит на глубине -150мс и спамит мелким всплывающим В (второй элемент цепочки), который, всплывая на поверхность настоящего, заменяет собой настоящее Г, отправляя на свалку истории. Итого: у Зеро два дитяти, А и Б, у Б - тоже два дитяти, В и Г. Это кроме тех случаев, когда А выпинывает Б вверх, заменяя его и становясь Б, а бывший Б становится В. В таком случае, у Зеро - три дитяти: новый А, Б и В, а Г - дитя В. Когда этот редкий всплывающий-всю-дорогу дойдёт доверху и станет Г, случается редкая ситуация: у Зеро, три дитяти, А, Б и Г, а у Б - один, мелкий всплывающий В. Поскольку ближнее дно спамит мелкими всплывающими не по детски. Первым - Г, на скорости 1.0, Потом - задание Какану на рендер, на основе Г. Затем - Зеро и Б, на скорости 1.0, потом В, на скорости 2.0, потом А на скорости 2.0, а потом снова В - на всё оставшееся время кадра, до железки - или пока не упрётся в настоящее, выпнув Г.
3. обращение одним объектом к другому на чтение его свойств - относительно легковесное, но не бесплатное. Надо при *каждом* обращении к объекту вызывать его метод PeekForRead, который вернёт клон, актуальный именно для текущего слоя. Ну, и в идеале, весь код засран ассертами Assert(not WrongLayer, 'Wrong layer, d00d!'); Потому что, за отсутствием умных указателей, ссылка на объект как была, так и остаётся на экземпляр, актуальный для того слоя, в котором был создан ссылающийся. А объект с тех пор мог расслоиться-расклонироваться. И получение текущего - подняться по всей цепочке наследования слоёв, проверяя "А если найду?". Но не всё так плохо: планируемая длина этой цепочки - ровно два дочерних, а проверка заполненности ускоренного поля - один бит на инстанс.
4. обращение одним объектом к другому на его модификацию - относительно легковесное, *только* посредством передачи события через вызов WakeUp, который сначала склонирует объект в текущий слой, потом прервёт сон раньше запланированного (а спать они у нас могут по 500 тиков, для фаербола, летящего в пустоте между точками А и Б), и, наконец, вернёт актуальный клон. В идеале, у лагокомпенсированной энтити наружу не должны торчать никакие методы, способные её модифицировать, кроме виртуального Подъём(ЖриЭтоСобытие). А если таки торчиа - то уляпаны ассертами, как фонарный столб в девяностые - рекламными объявлениями.
4а. дополнительная цена - события это классы. И каждое взаимодействие - вызов конструктора, а потом освобождение инстанса, задалбывающие штатный менеджер памяти. Классы - потомки TObject, не приндадлежат БД, не сериализуемые.
4б. спящий объект просыпается в нужный тик за счёт отправленного самому себе черед шедулер события-будильника. И вот эти уже - сериализуемые и лагокомпенсируемые. У него ещё ссылка на это событие, отменяет, если разбудили раньше другим способом.
5. налог на всю эту воздушность и легковесность (дарзанебы, бро!): инстанс не может быть освобождён, пока не протухнет последний всплывающий слой, пересекавшийся с ним во времени. Потому что там на него может быть ссылка, по которой кто-нибудь полезет из его ускоренных полей выковыривать ссылки на клоны. То есть, на 500 милисекундных тиков,
каждый слой тащит за собой цепочку из 500 кладбищ.
Наклает суровые ограничения на полёт эскадрона мыслей шальных: да не намусоришь ты короткоживущими инстансами. Иначе пямять засрётся только в путь.
Добавлено спустя 11 часов 10 минут 55 секунд:Терминология
- Код: Выделить всё
type
TLayerRole = (
lro_Bottom, { Дно.
In multiplayer, runs at -500ms using perfect inputs finalized by the server. }
lro_DeepUpwell, { Глубинный восходящий.
Propagates changes from the bottom to the thermocline, thus lazily correcting for late inputs }
lro_Thermocline, { Термоклин.
Holds steady at -150ms, assuming most inputs arrive *above* it }
lro_FastSurfacing, { Быстрый приповерхностный.
Bubbles the changes from the thermocline to the surface thus doing the bulk of lag compensation }
lro_PresentSurface { Поверхность настоящего.
Runs on local player inputs }
);