Да. Когда-то надо остановиться.
Сегодня обдумывал устройство многослойного мира - и части головоломки щёлкнули наконец, складываясь в единое - и главное, простое! - целое.
1. Логика - однопоточная во веки веков (оставляя возможность распараллеливать операции, результат которых можно применить после их завершения - т.е. сами игровые объекты из других потоков не модифицируются, применение результатов происходит в логическом потоке).
2. Никаких оптимизированных массивов для индексации объектов по индексу типа longint. Все связи обычными ссылками (поля типа TObject, упрощённо говоря)
3. Во все поля эксплуатируется механизм "ускоренных" полей класса, которые я сейчас активно строгаю - и для реализации которых, собственно, потребовался менеджер памяти. Один из главных плюсов - все поля одного типа стираются командой менеджеру памяти, и операция эта практически не загрязняет кеш.
Ускоренные поля НЕ сериализуются в поток, но я просто решил, что в сохранёнку идёт только базовый слой (когда сохраняешь сетевую игру, например, или сервер пакует снапшот мира для нового игрока).
4. Перед использованием любого объекта надо убедиться, что ссылка ведёт на объект текущего слоя посредством метода ThisLayer.
например:
//BearIAmRunningFrom - поле класса
var bear: TBear; //локальная переменная метода
bear:= TBear(BearIAmRunningFrom.ThisLayer);
???:= bear.Hunger;
или
???:= TBear(BearIAmRunningFrom.ThisLayer).Hunger;
5. Перед любым изменением, объект надо получить из его же метода BeforeModifying, который склонирует объект, если он принадлежит не текущему слою.
пример: bear:= TBear(bear.BeforeModifying); //может вернуть себя, а может и клона
Для отлова мест, где забыл - специальный режим отладки (люто тормозной), который каждый тик обходит всё дерево и составляет CRC для всех объектов всех слоёв (хранятся, опять же, в ускоренных полях).
6. Клоны объекта для других слоёв хранятся в ускоренных полях, из которых их берут методы ThisLayer и BeforeModifying, так что ссылка на изначальный объект продолжает работать - и
не требует ни модификации, ни проверок в момент клонирования!
7. Объект, имеющий клонов в других слоях, вместо удаления кладётся на кладбище и лежит там, пока клоны не удалятся. Таким образом, ссылка на изначальный объект продолжает работать.
Зачем нам нужно, чтобы ссылка на изначальный объект не нашего слоя продолжала работать? Чтобы обеспечить clone-on-write (посредством костыля BeforeModifying) самым тупым и пошлым образом: копированием куска памяти со всем содержимым (с дополнительным проходом чтобы подправить счётчики ссылок строк и массивов, правда). Проход по этим самым массивам (на случай, если содержат объекты) не нужен: ведь старые ссылки продолжают работать, надо лишь не забывать использовать ThisLayer.
Кажется ужасным костылём, но на деле даёт эпическую эффективность расслоения и эпическую дружественность к кешу: ничего не трогается и не клонируется, пока не вызван BeforeModifying.
А учитывая планируемую парадигму "ленивой" физики (просчитать линейное движение на дохрена тиков вперёд и заснуть, разбудите по таймеру или когда в меня ткнётся кто-нибудь) подавляющее большинство объектов просто не успеет расслоиться, поскольку просто не успеют тикнуть за то время, что живут слои лагокомпенсации (500 мс, чаще гораздо меньше)
8. Ещё одно больное место - механизм "я сплю, разбудите через N тиков" будет также реализован на уровне менеджера памяти и ускоренных полей.
Но цена всего этого, повторюсь - логика однопоточная навеки.
Добавлено спустя 3 часа 50 минут 26 секунд:..в деталях, пока что вырисовывается такое:
(когда, наконец, соберётся - предстоит
радость отладки всего сваянного)
- Код: Выделить всё
procedure TChepersyObject.ChangeMask(values, maskmask: dword);
var
i, cidx: integer;
chunk: TChepersyMemoryManagerChunk;
val, newval: dword;
begin
chunk:= GetMemoryManagerChunk;
cidx:= chunk.GetChunkInd(Self);
val:= chunk.AcceleratedField[CPS_AFK_MASK][cidx];
newval:= (val and (not maskmask)) or (values and maskmask);
if newval <> val then chunk.AcceleratedField[CPS_AFK_MASK][cidx]:= newval;
end;
где
const
CpsMMChunkSizePoT = 17;
CpsMMChunkAddrMask = (ptruint(ptrint(-1)) shr CpsMMChunkSizePoT) shl CpsMMChunkSizePoT;
function TChepersyObject.GetMemoryManagerChunk: TChepersyMemoryManagerChunk;
begin
ptruint(pointer(Result)):= ptruint(pointer(Self)) and CpsMMChunkAddrMask;
end;
где
TChepersyMemoryManagerChunk = class
protected
...
function _GetAccField(kind: TCpsAccFieldKind; idx:integer): dword;
procedure _SetAccField(kind: TCpsAccFieldKind; idx:integer; value: dword);
public
...
property AcceleratedField[kind: TCpsAccFieldKind; idx: integer]: dword
read _GetAccField write _SetAccField;
end;
где
function TChepersyMemoryManagerChunk._GetAccField(kind: TCpsAccFieldKind; idx:integer): dword;
begin
Assert((idx >= 0) and (idx <= f_IdxHigh), 'index out of bounds in TChepersyMemoryManagerChunk._GetAccField()');
if not Assigned(f_AccField[kind]) then Exit(0);
if 0 = (f_AccFieldMask[kind][idx div 32] and (dword(1) shl (idx mod 32))) then Exit(0);
Result:= f_AccField[kind][idx];
end;
const ptruint_bits = sizeof(ptruint) * 8;
procedure TChepersyMemoryManagerChunk._SetAccField(kind: TCpsAccFieldKind; idx:integer; value: dword);
begin
Assert((idx >= 0) and (idx <= f_IdxHigh), 'index out of bounds in TChepersyMemoryManagerChunk._GetAccField()');
if value = 0 then begin
if not Assigned(f_AccField[kind]) then Exit;
if 0 = (f_AccFieldMask[kind][idx div ptruint_bits]
and (ptruint_bits(1) shl (idx mod ptruint_bits))) then Exit;
dec(f_AccFieldCount[kind]);
if 0 = f_AccFieldCount[kind] then begin
SetLength(f_AccField[kind], 0);
SetLength(f_AccFieldMask[kind], 0);
end;
end
else begin
if not Assigned(f_AccFieldMask[kind])
then SetLength(f_AccFieldMask[kind], DivRoundUp(f_IdxHigh, ptruint_bits));
if (High(f_AccField[kind]) < idx)
then SetLength(f_AccField[kind], max(Length(f_AccField[kind]),
1 + min(f_IdxHigh, DivRoundUp(idx, 16) * 16)));
if 0 = (f_AccFieldMask[kind][idx div ptruint_bits]
and (ptruint_bits(1) shl (idx mod ptruint_bits)))
then begin
inc(f_AccFieldCount[kind]);
f_AccFieldMask[kind][idx div ptruint_bits]:= f_AccFieldMask[kind][idx div ptruint_bits]
or (ptruint_bits(1) shl (idx mod ptruint_bits));
end;
f_AccField[kind][idx]:= value;
end;
end;