Skip to content

Getting Started with Streams

This guide demonstrates serialization and deserialization of game data using the Astralix streams module.

Basic Serialization

Write data to a JSON file:

cpp
#include "streams/serialization-context.hpp"
#include "streams/adapters/file/file-stream-writer.hpp"

// Create serialization context
auto ctx = SerializationContext::create(SerializationFormat::Json);

// Write data using operator[]
ctx["config"]["windowWidth"] = 1920;
ctx["config"]["windowHeight"] = 1080;
ctx["config"]["fullscreen"] = false;
ctx["config"]["volume"] = 0.8f;

// Serialize to file
ElasticArena arena(1024);
auto* block = ctx->to_buffer(arena);
auto buffer = std::make_unique<StreamBuffer>(1024);
buffer->write(block->ptr, block->size);

FileStreamWriter writer("config.json", std::move(buffer));
writer.write();
writer.flush();

Basic Deserialization

Read data from a JSON file:

cpp
#include "streams/serialization-context.hpp"
#include "streams/adapters/file/file-stream-reader.hpp"

// Read file into buffer
FileStreamReader reader("config.json");
reader.read();
auto buffer = reader.get_buffer();

// Deserialize
auto ctx = SerializationContext::create(SerializationFormat::Json, std::move(buffer));

// Read data using operator[] and as<T>()
int width = ctx["config"]["windowWidth"].as<int>();
int height = ctx["config"]["windowHeight"].as<int>();
bool fullscreen = ctx["config"]["fullscreen"].as<bool>();
float volume = ctx["config"]["volume"].as<float>();

Serializing Custom Types

Make your types serializable:

cpp
struct Player {
    std::string name;
    int level;
    float position_x, position_y, position_z;
    std::vector<int> inventory;

    void Serialize(Ref<SerializationContext> ctx) const {
        (*ctx)["player"]["name"] = name;
        (*ctx)["player"]["level"] = level;
        (*ctx)["player"]["position_x"] = position_x;
        (*ctx)["player"]["position_y"] = position_y;
        (*ctx)["player"]["position_z"] = position_z;

        // Serialize array items
        for (size_t i = 0; i < inventory.size(); i++) {
            (*ctx)["player"]["inventory"][static_cast<int>(i)] = inventory[i];
        }
    }

    void Deserialize(Ref<SerializationContext> ctx) {
        name = (*ctx)["player"]["name"].as<std::string>();
        level = (*ctx)["player"]["level"].as<int>();
        position_x = (*ctx)["player"]["position_x"].as<float>();
        position_y = (*ctx)["player"]["position_y"].as<float>();
        position_z = (*ctx)["player"]["position_z"].as<float>();

        // Deserialize array
        auto inv_ctx = (*ctx)["player"]["inventory"];
        size_t count = inv_ctx.size();
        inventory.clear();
        for (size_t i = 0; i < count; i++) {
            inventory.push_back(inv_ctx[static_cast<int>(i)].as<int>());
        }
    }
};

Note: The streams module currently supports only basic types (int, float, std::string, bool) as SerializableValue. Vector types like glm::vec3 must be decomposed into individual float components.

Nested Objects

Serialize complex hierarchies:

cpp
void SaveGameState(const GameState& state) {
    auto ctx = SerializationContext::create(SerializationFormat::Json);

    // Player data
    state.player.Serialize(ctx);

    // World data
    (*ctx)["gameState"]["world"]["name"] = state.worldName;
    (*ctx)["gameState"]["world"]["seed"] = state.seed;
    (*ctx)["gameState"]["world"]["time"] = state.worldTime;

    // Entities
    for (size_t i = 0; i < state.entities.size(); i++) {
        auto entity_ctx = SerializationContext::create(SerializationFormat::Json);
        state.entities[i].Serialize(entity_ctx);
        (*ctx)["gameState"]["entities"][static_cast<int>(i)] = entity_ctx;
    }

    // Write to file
    ElasticArena arena(4096);
    auto* block = ctx->to_buffer(arena);
    auto buffer = std::make_unique<StreamBuffer>(4096);
    buffer->write(block->ptr, block->size);

    FileStreamWriter writer("save.json", std::move(buffer));
    writer.write();
    writer.flush();
}

Memory Streams

Serialize to memory for network transmission:

cpp
// Serialize to buffer
auto ctx = SerializationContext::create(SerializationFormat::Json);

(*ctx)["messageType"] = std::string("PlayerUpdate");
(*ctx)["playerId"] = 42;
(*ctx)["position_x"] = 10.0f;
(*ctx)["position_y"] = 0.0f;
(*ctx)["position_z"] = 5.0f;

// Convert to buffer
ElasticArena arena(512);
auto* block = ctx->to_buffer(arena);

// Send buffer over network
SendToServer(block->ptr, block->size);

// Deserialize from buffer
auto recv_buffer = std::make_unique<StreamBuffer>(512);
recv_buffer->write(static_cast<char*>(block->ptr), block->size);

auto read_ctx = SerializationContext::create(SerializationFormat::Json, std::move(recv_buffer));

std::string msgType = (*read_ctx)["messageType"].as<std::string>();
int playerId = (*read_ctx)["playerId"].as<int>();
float pos_x = (*read_ctx)["position_x"].as<float>();
float pos_y = (*read_ctx)["position_y"].as<float>();
float pos_z = (*read_ctx)["position_z"].as<float>();

Note: MemoryStreamReader is currently abstract and has no concrete implementation. MemoryStreamWriter exists but is missing its write() override. For now, use the arena-based buffer approach shown above.

Versioning

Support multiple data versions:

cpp
void Serialize(Ref<SerializationContext> ctx) const {
    (*ctx)["save"]["version"] = 2; // Current version

    // Version 2 data
    (*ctx)["save"]["newField"] = newValue;

    // Original data
    (*ctx)["save"]["oldField"] = oldValue;
}

void Deserialize(Ref<SerializationContext> ctx) {
    int version = 1; // Default to v1

    // Check if version field exists using kind()
    if ((*ctx)["save"]["version"].kind() != SerializationTypeKind::Unknown) {
        version = (*ctx)["save"]["version"].as<int>();
    }

    if (version >= 2) {
        newValue = (*ctx)["save"]["newField"].as<int>();
    } else {
        newValue = defaultValue; // Upgrade from v1
    }

    oldValue = (*ctx)["save"]["oldField"].as<int>();
}

Note: The streams module does not have a HasKey() method. Use kind() to check if a field exists - it will return SerializationTypeKind::Unknown for missing keys.

Optional Fields

Handle missing fields gracefully:

cpp
void Deserialize(Ref<SerializationContext> ctx) {
    // Required field
    requiredValue = (*ctx)["config"]["required"].as<int>();

    // Optional field with default
    auto optional_proxy = (*ctx)["config"]["optional"];
    if (optional_proxy.kind() != SerializationTypeKind::Unknown) {
        optionalValue = optional_proxy.as<int>();
    } else {
        optionalValue = defaultValue;
    }
}

Next Steps

  • Learn about Project for configuration management
  • Explore Engine for scene serialization
  • Review scene file formats in the documentation