원클릭으로
rollback
Rollback Netcode guidance for Fortress Rollback. Use when Converting games to rollback netcode, engine integration, state management.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Rollback Netcode guidance for Fortress Rollback. Use when Converting games to rollback netcode, engine integration, state management.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Crate Publishing guidance for Fortress Rollback. Use when Publishing to crates.io, version bumps, release checklist.
Changelog Practices guidance for Fortress Rollback. Use when Writing CHANGELOG entries, deciding what to document.
Design Decision Log Pattern guidance for Fortress Rollback. Use when Architectural decisions, design alternatives, superseding prior choices.
Repository-wide engineering policy and project context for Fortress Rollback. Use when implementing, diagnosing, reviewing, testing, documenting, or releasing changes in this repository.
GitHub Actions Best Practices guidance for Fortress Rollback. Use when Writing GitHub Actions workflows, CI debugging, actionlint, caching.
Workspace Organization guidance for Fortress Rollback. Use when Organizing workspace, splitting crates, module structure decisions.
| name | rollback |
| description | Rollback Netcode guidance for Fortress Rollback. Use when Converting games to rollback netcode, engine integration, state management. |
Rollback netcode reduces perceived latency by predicting remote inputs, then rolling back and re-simulating when predictions are wrong.
HashMap with BTreeMap (or sort before iteration)libm or use fixed-point mathrand_pcg for RNG (saved with state)Instant::now() with frame countersRollback IDframes_ahead())pub enum RollbackRequest<T: Config> {
SaveGameState { cell: GameStateCell<T>, frame: Frame },
LoadGameState { cell: GameStateCell<T>, frame: Frame },
AdvanceFrame { inputs: PlayerInputs<T> },
}
fn game_loop(session: &mut Session<Config>, game: &mut Game) -> Result<(), Error> {
session.poll_remote_clients();
session.add_local_input(local_handle, game.read_local_input())?;
for request in session.advance_frame()? {
match request {
RollbackRequest::SaveGameState { cell, frame } => {
cell.save(frame, Some(game.state.clone()), Some(game.checksum()));
}
RollbackRequest::LoadGameState { cell, frame } => {
if let Some(loaded) = cell.load() {
game.state = loaded;
}
}
RollbackRequest::AdvanceFrame { inputs } => {
game.advance_frame(&inputs);
}
}
}
Ok(())
}
struct SimulationState {
players: Vec<Player>,
entities: Vec<Entity>,
frame_number: u32,
rng_state: Pcg64,
// DO NOT include: render state, audio, UI, debug info
}
struct Game {
sim: SimulationState, // Saved/restored during rollback
presentation: PresentationState, // Never rolled back (animations, particles)
}
const MAX_ROLLBACK_FRAMES: usize = 20;
struct StateBuffer {
states: [Option<GameState>; MAX_ROLLBACK_FRAMES],
}
impl StateBuffer {
fn save(&mut self, state: GameState, frame: u32) {
self.states[(frame as usize) % MAX_ROLLBACK_FRAMES] = Some(state);
}
fn load(&self, frame: u32) -> Option<&GameState> {
self.states[(frame as usize) % MAX_ROLLBACK_FRAMES].as_ref()
}
}
fn main() {
App::new()
.add_plugins(GgrsPlugin::<MyConfig>::default())
.rollback_component_with_clone::<Transform>()
.rollback_component_with_clone::<Velocity>()
.rollback_resource_with_clone::<FrameCount>()
.add_systems(GgrsSchedule, (
movement_system,
collision_system,
).chain())
.run();
}
fn movement_system(mut query: Query<(&Rollback, &mut Transform, &Velocity)>) {
let mut items: Vec<_> = query.iter_mut().collect();
items.sort_by_key(|(rb, _, _)| rb.id());
for (_, mut transform, velocity) in items {
transform.translation += velocity.0;
}
}
| Feature | Problem | Alternative |
|---|---|---|
Events<T> | Not snapshot/restored | Use components |
Local<T> | Not snapshot/restored | Use resources |
Added<T> / Changed<T> | Triggers on every restore | Avoid in game logic |
Time | Uses wall clock | Use frame counter resource |
use bitflags::bitflags;
bitflags! {
#[derive(Clone, Copy, PartialEq, Eq, Default)]
#[repr(transparent)]
pub struct PlayerInput: u16 {
const UP = 0b0000_0001;
const DOWN = 0b0000_0010;
const LEFT = 0b0000_0100;
const RIGHT = 0b0000_1000;
const A = 0b0001_0000;
const B = 0b0010_0000;
}
}
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub struct FrameInput {
pub buttons: PlayerInput,
pub stick_x: i8,
pub stick_y: i8,
}
// Default prediction: repeat last known input
fn predict_input(last_known: FrameInput) -> FrameInput { last_known }
#[repr(C)]
struct InputPacket {
sequence: u32,
start_frame: u32,
ack_frame: u32,
inputs: [FrameInput; 8], // Redundant: last 8 inputs
}
Use Glenn Fiedler's reliable UDP pattern: sequence number + ack + ack_bits bitfield.
fn compute_checksum(state: &GameState) -> u64 {
let mut hasher = seahash::SeaHasher::new();
state.frame.hash(&mut hasher);
for player in &state.players {
player.position.x.to_bits().hash(&mut hasher);
player.position.y.to_bits().hash(&mut hasher);
}
hasher.finish()
}
Exchange checksums periodically between peers. Mismatch = desync.
pub trait RollbackEngine {
type State: Clone;
type Input: Copy + Default;
fn save_state(&self) -> Self::State;
fn load_state(&mut self, state: &Self::State);
fn advance(&mut self, inputs: &[Self::Input]);
fn checksum(&self) -> u64;
}
SessionBuilder::start_synctest_session() forces rollback every frame| Pitfall | Fix |
|---|---|
| Forgot to register state for rollback | Audit all simulation-affecting fields |
| Using Bevy Events for game state | Use components instead |
| Non-deterministic query order | Sort by Rollback ID |
| Wall clock time in simulation | Use TICK_RATE constant |
| Float accumulation | Reconstruct from integers |