Sinkronisasi state real-time pada arsitektur server game headless menuntut efisiensi komputasi ekstrem untuk mengejar tick rate tinggi (misalnya 60–128 Hz). Pendekatan arsitektur di Rust umumnya terbagi menjadi dua paradigma: Entity Component System (ECS) berbasis pola archetypal (seperti Bevy ECS atau Hecs) dan model konkurensi berbasis Actor (seperti Tokio Actor loop atau Actix). Pilihan arsitektur ini menentukan bagaimana memori dialokasikan, seberapa sering CPU cache terutilisasi, dan seberapa besar overhead lock contention saat memproses mutasi state.

Karakteristik Memori: ECS Cache Locality vs Actor Heap Allocation

ECS archetypal mengorganisir data dalam format Structure of Arrays (SoA) atau array contiguous per arketipe. Komponen dari entitas yang memiliki komposisi tipe sama disimpan bersebelahan di memori. Desain ini memaksimalkan efisiensi instruksi CPU melalui pemanfaatan L1/L2 cache prefetching ketika sistem melakukan iterasi batch pada ratusan hingga ribuan entitas.

Sebaliknya, Actor pattern membungkus state entitas atau sesi di balik boundary isolasi (task independen). Setiap actor biasanya memiliki alokasi heap terpisah untuk state internalnya. Akses data antar-entitas memerlukan passing message melalui channel (seperti tokio::sync::mpsc), yang menghasilkan overhead alokasi ring buffer, pointer indirection, dan cache miss signifikan saat iterasi massal.

  • ECS Archetype: Data contiguous, no indirection, SIMD-friendly, payload snapshot mudah di-slice langsung dari memori.
  • Actor Pattern: Terisolasi, state terfragmentasi di heap, mutasi aman secara modular, namun lambat saat agregasi batch snapshot massal.

Analisis Trade-Off: Latency Budget, Mutability, dan Biaya Komputasi

1. Mutasi State dan Concurrency Control

Pada model Actor, mutasi state bebas dari lock contention eksplisit karena state dieksekusi secara strictly single-threaded di dalam event loop masing-masing actor. Namun, kelemahannya muncul saat sistem spasial (misalnya deteksi tabrakan atau authority area) membutuhkan query data dari beberapa entitas sekaligus. Hal ini memicu request-response berantai antar-channel yang rentan deadlocking atau menumpuk latency tail.

ECS menangani konkurensi pada level query system menggunakan peminjaman tipe data Rust (borrow checker compile-time atau runtime borrow checker bertipe RWLock striped seperti UnsafeCell yang divalidasi per-archetype). Sistem dapat berjalan paralel pada core CPU berbeda selama komponen yang diakses saling disjoint (misalnya &mut Transform vs &Velocity).

2. Latency Budget dan Compute Footprint

Server beroperasi dengan batas waktu eksekusi tetap (latency budget), misalnya 16.6ms untuk 60 Hz tick rate. Proyek seperti CluvexStudio/Aether memprioritaskan determinisme dan throughput tinggi.

Penggunaan ECS mengurangi compute footprint per core CPU secara drastis karena pemrosesan data linier. Akibatnya, satu instans server dapat menampung kapasitas entitas lebih besar dengan utilisasi memori yang lebih padat, menekan biaya infrastruktur cloud. Actor pattern cenderung memakan footprint memori lebih tinggi akibat overhead channel queues, context switching antar-task, dan alokasi stack per task.

Implementasi Snapshot Serialisasi Rendah Lock Contention

Pola ideal untuk server game real-time adalah hybrid architecture: gunakan ECS untuk simulasi core tick dan state store, lalu gunakan Tokio task murni untuk handling network I/O. Snapshot diekstrak dari ECS secara contiguous, disalin ke ring buffer atau broadcast channel, meminimalisir durasi lock pada state utama.

use std::sync::Arc;
use tokio::sync::watch;

#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TransformComponent {
    pub position: [f32; 3],
    pub rotation: [f32; 4],
}

#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EntitySnapshot {
    pub entity_id: u32,
    pub transform: TransformComponent,
}

#[derive(Clone, Debug)]
pub struct WorldSnapshot {
    pub tick: u64,
    pub entities: Vec<EntitySnapshot>,
}

// Simulasi ekstraksi snapshot contiguous dari Archetype ECS
pub fn extract_snapshot_system(
    tick: u64,
    entities: &[u32],
    transforms: &[TransformComponent],
) -> WorldSnapshot {
    assert_eq!(entities.len(), transforms.len());
    
    // Alokasi memori contiguous tanpa indirection
    let mut snapshot_entities = Vec::with_capacity(entities.len());
    for (&id, &tf) in entities.iter().zip(transforms.iter()) {
        snapshot_entities.push(EntitySnapshot {
            entity_id: id,
            transform: tf,
        });
    }

    WorldSnapshot {
        tick,
        entities: snapshot_entities,
    }
}

// Pola transmisi broadcast non-blocking ke layer network
pub struct NetworkBroadcaster {
    sender: watch::Sender<Arc<WorldSnapshot>>,
}

impl NetworkBroadcaster {
    pub fn new() -> (Self, watch::Receiver<Arc<WorldSnapshot>>) {
        let dummy = Arc::new(WorldSnapshot { tick: 0, entities: Vec::new() });
        let (sender, receiver) = watch::channel(dummy);
        (Self { sender }, receiver)
    }

    pub fn publish_tick(&self, snapshot: WorldSnapshot) {
        // Shared reference atomic tanpa lock contention berkepanjangan
        let _ = self.sender.send(Arc::new(snapshot));
    }
}
Catatan: Penggunaan tokio::sync::watch channel menyediakan pola read-heavy yang ideal. Network worker hanya mengambil snapshot terbaru jika client lagging, membuang frame lama secara deterministik tanpa memblokir simulation tick.

Panduan Pemilihan: Kapan Memilih ECS atau Actor?

  • Pilih Archetypal ECS jika: Game membutuhkan densitas entitas tinggi, simulasi fisika intensif, dependensi mutasi spasial yang rapat, dan serialisasi data massal pada tick rate tinggi (≥60 Hz).
  • Pilih Actor Pattern jika: Arsitektur berfokus pada isolasi failure (misalnya sistem chat, billing, matchmaking, inventory transactional) di mana interaksi antar-entitas jarang terjadi dan keamanan status stateful per sesi lebih penting daripada throughput iterasi batch.