UE5+Voxel Pro: Host-Local Save, RPC/Chunk Edit Guide

Answered using AI by Vibe Coding

Code Question:

Какая именно версия Unreal Engine и Voxel Plugin (Free/Pro)? Это - pro Нужно ли сохранять мир между сессиями? Где хранить (локально на хосте / на бэкенде)? да нужно сохранять - локально на хосте На каких платформах - на ПК Требуется ли поддержка автономного редактирования чанков (редактор карт) - нет Если хотите — могу: привести пример сигнатур RPC на C++ и сериализации правок (скелет ServerRequestEdit / MulticastApplyEdit); набросать структуру данных для хранения - да нужно

Code Solution:

Отлично — на основе ваших ответов (Unreal Engine + Voxel Plugin Pro, PC, сохранять локально на хосте, без автономного редактора чанков) ниже даю:

  1. Общая архитектура (ключевые принципы)
  • Сервер — авторитет: все правки идут на сервер (ServerRequestEdit), сервер валидирует и применяет на своей копии мира, затем рассылает всем клиентам подтверждённую правку через Multicast (MulticastApplyEdit).
  • Правки должны передаваться компактно (дельты, сжатие) — не слать весь чанк целиком в RPC.
  • Хранение — локально на хосте (папка Save/Worlds/<WorldName>), чанки сохраняются как отдельные файлы или в регион-файлах для уменьшения количества мелких файлов.
  • Асинхронное сохранение/загрузка (чтобы не блокировать игровой поток).
  • Версионирование формата, контроль целостности (checksum), атомарные записи (темповая запись + переименование).
  1. Формат правки (рекомендуемая логика)
  • Самая мелкая единица: EditOperation = {локальная позиция в чанке (FIntVector3 или индекс), тип операции (set-value / set-material / smooth / etc), значение/материал/параметры}.
  • Чанк-правка (FChunkEdit) = {ChunkCoords, ChunkVersion (server-side), Timestamp, TArray<EditOperation> Ops}.
  • Для передачи по сети: сериализовать FChunkEdit в байты, сжать (LZ4/ZLIB), добавить checksum и подпись/игрок-id, и отправить как TArray<uint8> payload в RPC.
  1. Пример структур C++ (UE-style) — логика + сериализация Ниже — упрощённые структуры и сериализация через FMemoryWriter/Reader. Это skeleton, который вы можете расширить под конкретные операции Voxel Plugin.

a) структуры:

// Псевдо-код / UE C++

UENUM()
enum class EEditOpType : uint8
{
    SetValue,
    SetMaterial,
    // ... другие типы
};

struct FEditOperation
{
    FIntVector LocalPos; // локальная координата внутри чанка (x,y,z)
    EEditOpType OpType;
    float Value; // для SetValue
    uint16 MaterialIndex; // для SetMaterial

    bool NetSerialize(FArchive& Ar)
    {
        Ar << LocalPos.X << LocalPos.Y << LocalPos.Z;
        Ar << *(uint8*)&OpType;
        Ar << Value;
        Ar << MaterialIndex;
        return true;
    }
};

struct FChunkEdit
{
    FIntVector ChunkCoords;
    uint32 ServerVersion;
    TArray<FEditOperation> Ops;

    void SerializeToBytes(TArray<uint8>& OutBytes)
    {
        FMemoryWriter Writer(OutBytes, true);
        Writer << ChunkCoords.X << ChunkCoords.Y << ChunkCoords.Z;
        Writer << ServerVersion;
        int32 OpsCount = Ops.Num();
        Writer << OpsCount;
        for (auto& Op : Ops)
        {
            Op.NetSerialize(Writer);
        }
    }

    bool DeserializeFromBytes(const TArray<uint8>& InBytes)
    {
        FMemoryReader Reader(InBytes, true);
        Reader << ChunkCoords.X << ChunkCoords.Y << ChunkCoords.Z;
        Reader << ServerVersion;
        int32 OpsCount = 0;
        Reader << OpsCount;
        Ops.Reset();
        Ops.Reserve(OpsCount);
        for (int32 i = 0; i < OpsCount; ++i)
        {
            FEditOperation Op;
            Op.NetSerialize(Reader);
            Ops.Add(Op);
        }
        return true;
    }
};

b) сжатие / контроль целостности (пример):

// Сжать OutBytes -> CompressedBytes
int32 UncompressedSize = OutBytes.Num();
TArray<uint8> Compressed;
Compressed.SetNumUninitialized(FCompression::CompressMemoryBound(NAME_LZ4, UncompressedSize));
int32 CompressedSize = 0;
FCompression::CompressMemory(NAME_LZ4, Compressed.GetData(), Compressed.Num(), OutBytes.GetData(), UncompressedSize, COMPRESS_BiasSpeed);
Compressed.SetNum(CompressedSize);

// можно хранить перед сжатием размер и CRC32
uint32 CRC = FCrc::MemCrc32(OutBytes.GetData(), OutBytes.Num());
  1. RPC-сигнатуры в UE (пример C++)
  • Используем компактный сетевой payload (TArray<uint8>) — чтобы не путаться с пределами RPC-параметров и сериализации UE.
UCLASS()
class AMyVoxelServerManager : public AActor
{
    GENERATED_BODY()

public:
    // Клиент -> Сервер: запрос на правку
    UFUNCTION(Server, Reliable, WithValidation)
    void ServerRequestEdit(APlayerController* RequestingPC, const TArray<uint8>& CompressedEditPayload);
    void ServerRequestEdit_Implementation(APlayerController* RequestingPC, const TArray<uint8>& CompressedEditPayload);
    bool ServerRequestEdit_Validate(APlayerController* RequestingPC, const TArray<uint8>& CompressedEditPayload);

    // Сервер -> все клиенты: применять правку
    UFUNCTION(NetMulticast, Reliable)
    void MulticastApplyEdit(const TArray<uint8>& CompressedEditPayload);
    void MulticastApplyEdit_Implementation(const TArray<uint8>& CompressedEditPayload);
};
  • Валидация (ServerRequestEdit_Validate) должна:

    • проверить подпись/авторство (если используется),
    • ограничить размер payload,
    • проверить, что игрок имеет право редактировать этот чанк,
    • базовая sanity-check полей после декомпрессии.
  • Реализация (ServerRequestEdit_Implementation) — сервер декомпрессирует, проверит и применяет правку в атомарной операции (лод/блокировка чанка), увеличит версию чанка, сохранит в очередь на диск и вызовет MulticastApplyEdit для распространения.

  1. Пример skeleton реализации в коде (упрощённо)
void AMyVoxelServerManager::ServerRequestEdit_Implementation(APlayerController* RequestingPC, const TArray<uint8>& CompressedEditPayload)
{
    // 1) Проверки размера/подписи (omitted)
    // 2) Распаковка
    TArray<uint8> Decompressed;
    // узнаем UncompressedSize каким-то образом (можно положить в header)
    FCompression::UncompressMemory(NAME_LZ4, Decompressed.GetData(), UncompressedSize, CompressedEditPayload.GetData(), CompressedEditPayload.Num());

    FChunkEdit Edit;
    Edit.DeserializeFromBytes(Decompressed);

    // 3) Авторитетная валидация: пересечение дальности игрока, права, sane coords и т.д.
    if (!IsEditAllowed(RequestingPC, Edit)) return;

    // 4) Применение к серверной структуре (нужна блокировка/синхронизация)
    ApplyEditToServerWorld(Edit);

    // 5) Сохранение на диск (асинхронно) — MarkChunkDirtyAndScheduleSave(Edit.ChunkCoords)

    // 6) Рассылка всем клиентам (можно отправить тот же сжатый payload)
    MulticastApplyEdit(CompressedEditPayload);
}

void AMyVoxelServerManager::MulticastApplyEdit_Implementation(const TArray<uint8>& CompressedEditPayload)
{
    // Клиенты распаковывают и применяют локально (без проверки авторизации)
    TArray<uint8> Decompressed;
    // Uncompress...
    FChunkEdit Edit;
    Edit.DeserializeFromBytes(Decompressed);

    // Здесь инициировать Voxel Plugin API apply: e.g. FVoxelData->Set* или использовать транзакции Voxel Plugin
    ApplyEditLocally(Edit);
}

Замечание: при больших payload'ах RPC может не подойти — в этом случае лучше использовать replication graph/streaming или кастомный сетевой канал (UDP/TCP) / chunked upload.

  1. Формат хранения на диске (рекомендуемый)
  • Путь: Saved/Worlds/<WorldName>/
    • world.meta.json (версия формата, seed, pluginVersion, settings)
    • region/ (опционально)
      • r.<rx>.<rz>.mreg (границы регионов, например 32x32 чанка) — уменьшает число файлов
    • chunks/
      • c_x_y_z.dat или лучше индексировать в region-file для меньшего количества файлов
  • Чанк-файл (бинарный):
    • Magic (4 байта) e.g. "VCHK"
    • FormatVersion (uint16)
    • ChunkCoords (3 * int32)
    • ServerVersion (uint32)
    • Timestamp (int64)
    • UncompressedSize (int32)
    • CompressedSize (int32)
    • CRC32 (uint32) // CRC от несжатых данных
    • CompressedPayload[CompressedSize]
  • Region-файл: содержит индекс записей (offset/size) и блоки чанков внутри.
  1. Рекомендации по реализации сохранения/загрузки
  • Сохранять чанки асинхронно (IAsyncTask / FPooledThread) — на больших мирах запись синхронно вызовет фризы.
  • Сохранять чанки при:
    • модификации чанка (mark dirty -> schedule save через N секунд),
    • выгрузке чанка,
    • периодический autosave (каждые N минут),
    • выключение сервера (flush).
  • Использовать батчинг записей — несколько чанков в один region-file.
  • При записи — писать temp-файл + MoveFile (atomically replace) или использовать файловую транзакцию.
  • Хранить метаданные в world.meta.json для быстрой загрузки.
  1. Конфликты и версия/ревизии
  • Каждый чанк хранит ServerVersion (инкремент при каждой правке). Клиент может держать локальные ожидающие правки, и когда на сервере изменилось, сервер рассылает актуальную версию.
  • Если два игрока редактируют одно и то же местоположение одновременно, сервер разрешает по порядку получения/по приоритету, или применяет предопределённую политику (last-write-wins / merge rules).
  1. Компрессия и сети
  • Рекомендуемые: LZ4 (быстро) или Zlib (лучше сжатие, медленнее). В UE есть FCompression API с алгоритмами.
  • Стараться отправлять диффы, а не полные массивы: например, отправлять только изменённые воксели (sparse ops), run-length encoding, или данные в формате octree/meshes для крупных правок.
  1. Пример manifest/набор файлов (структура каталога)
  • Saved/Worlds/MyWorld/
    • world.meta.json
    • regions/
      • r.-1.0.mreg
      • r.0.0.mreg
    • backups/
      • ...timestamped backups...
    • chunk_cache/ (временная кэш папка)
  1. Безопасность и ограничения
  • Ограничьте максимальный размер правки за RPC.
  • Валидация авторизации (PlayerController -> PlayerID).
  • Логирование/rate limiting правок от одного клиента.
  • Бекап: держать старую копию region при перезаписи несколько версий назад.
  1. Что ещё могу сделать для вас
  • Подготовить полный примерный компонент AActor с реализацией сериализации, сжатия и RPC (полный C++ код ready-to-compile, адаптированный под UE4/UE5).
  • Сгенерировать код записи/чтения region-файла (region index + блоки).
  • Подготовить адаптацию конкретно под Voxel Plugin Pro API (напр. как превратить FChunkEdit -> вызовы Voxel Plugin для изменения данных и перекомпаирования месшей).

Если хотите, могу сейчас:

  1. написать полный пример класса AMyVoxelServerManager.cpp/.h с реализацией ServerRequestEdit/MulticastApplyEdit + сериализацией/сжатием и простым on-disk форматом; или
  2. описать на диске структуру region-файла и дать код RegionReader/RegionWriter.

Что предпочитаете?