一键导入
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 |