A rollback library that buffers component state. Useful for netcode.

Overview

bevy_timewarp

Buffer and rollback to states up to a few frames ago, for rollback networking.

Doesn't do any networking, just concerned with buffering old component states in order to revert values to previous frames and quickly fast-forward back to the original frame. Assumes game logic uses bevy's FixedUpdate schedule.

Current status: under development alongside a multiplayer server-authoritative game. I am "RJ" on bevy's discord, in the networking channel, if you want to discuss.

"Sports Games"

This crate is built for so called "sports games" where dynamic entities interact in the same timeframe. That means clients simulate all entities into the future, in the same timeframe as the local player. This necessitates predicting player inputs.

This is quite different to traditional Quake style FPS netcode, which is for a mostly static world, with players running around shooting eachother. In Quake style, you are always seeing a delayed version of other players, interpolated between two snapshots. The server does backwards reconcilliation / lag compensation to verify hits.

Example rollback scenario:

In your client/server game:

  • client is simulating frame 10
  • server snapshot for frame 6 arrives, including values for an entity's component T
  • client updates entity's ServerSnapshot value at frame 6 (ie, the past)
  • Timewarp triggers a rollback to frame 6 if snapshot != our stored value for frame 6.
    • winds back frame counter to 6
    • copies the server snapshot value to the component
    • resimulates frames 7,8,9,10 as fast as possible
    • during this process, your systems would apply player inputs you've stored for each frame
  • Rollback ends and frame 11 continues normally.

Quickstart

Add the plugin, then register the components you wish to be rollback capable:

// Store 10 frames of rollback, run timewarp systems after our GameLogic set.
app.add_plugins(TimewarpPlugin::new(10, MySets::GameLogic));
// register components that should be buffered and rolled back as needed:
app.register_rollback::<MyComponent>();
app.register_rollback::<Position>();
// etc..

Any entity that has a T Component will automatically be given a [ComponentHistory<T>] and [ServerSnapshot<T>] component.

ComponentHistory is a circular buffer of the last N frames of component values. This is logged every frame automatically, so is mostly your client predicted values. You typically won't need to interact with this.

ServerSnapshot is a buffer of the last few authoritative component values, typically what you received from the game server. Your network system will need to add new values to this.

When you receive authoritative updates, add them to the ServerSnapshot like so:

fn process_position_updates_from_server(
    mut q: Query<(Entity, &mut ServerSnapshot<Position>)>,
    update: Res<UpdateFromServer>, // however you get your updates, not our business.
){
    // typically update.frame would be in the past compared to current client frame
    for (entity, mut ss_position) in q.iter_mut() {
        let component_val = update.get_position_at_frame_for_entity(update.frame, entity); // whatever
        ss_position.insert(update.frame, component_val);
    }
}

Alternatively, and especially if you are inserting a component your entity has never had before, meaning there will be no ServerSnapshot<T> component, insert components in the past like this:

let add_at_frame = 123;
let historical_component = InsertComponentAtFrame::new(add_at_frame, MyComponent);
commands.entity(e1).insert(historical_component);

Systems configuration

Divide up your game systems so that during a rollback you still apply stored player input, but ignore stuff like sending network messages etc.

During a rollback, the [Rollback] resource will exist. Use this in a run_if condition.

// Normal game loop when not doing a rollback/fast-forward
app.add_systems(FixedUpdate,
    (
        frame_inc,
        process_server_messages,
        process_position_updates_from_server,
        spawn_stuff,
        read_player_inputs,
        apply_all_player_inputs_to_simulation_for_this_frame,
        do_physics,
        render,
        etc,
    )
    .chain()
    .in_set(MySets::GameLogic)
    .run_if(not(resource_exists::<Rollback>())) // NOT in rollback
);
// Abridged game loop for replaying frames during rollback/fast-forward
app.add_systems(FixedUpdate,
    (
        frame_inc,
        apply_all_player_inputs_to_simulation_for_this_frame,
        do_physics,
    )
    .chain()
    .in_set(MySets::GameLogic)
    .run_if(resource_exists::<Rollback>()) // Rollback is happening.
);

Visual smoothing of errors

Timewarp snaps the simulation state – ie. the value of a component at a specific frame simulated locally vs the value after rollback and resimulate might differ. If you register your components like this:

    app.register_rollback_with_correction_logging::<Position>();

then timewarp will capture the before and after versions of components when doing a rollback, and put it into a [TimewarpCorrection<Position>] component for your game to examine. Typically this would be useful for some visual smoothing - you might gradually blend over the error distance with your sprite, even though the underlying physical simulation snapped correct.

Testing various edge cases

TODO: I don't know how to link rustdocs to integration tests..

See the basic_rollback test for the most straightforward scenario: a long lived entity with components that haven't been added/removed, and an authoritative server update arrives. Apply value to past frame, rollback and continue.

The despawn_markers test illustrates how to despawn – rather than doing commands.entity(id).despawn(), which would remove all trace and thus mean the entity couldn't be revived if we needed to rollback to a frame when it was alive, you insert a [DespawnMarker(frame_number)][DespawnMarker] component, which cleans up the entity immediately by removing all its registered components, then does the actual despawn after rollback_window frames have elapsed.

The despawn_revival_during_rollback test does something similar, but triggers a rollback which will restore components to an entity tagged with a DespawnMarker in order to resimulate after a server update arrives, and then remove the components again.

The component_add_and_remove test tests how a server can add a component to an entity in the past, in this case a Shield, which prevents our enemy from taking damage.

TODO describe the various other tests.

Quick explanation of entity death in our rollback world

In order to preserve the Entity id, removing an entity using the DespawnMarker in fact removes all its registered components, but leaves the bare entity alive for rollback_frames.

Such entities retain their ComponentHistory buffers so they can be revived if needed because of a rollback. Finally, after rollback_frames has elapsed, they are despawn_recursived.

Removing components is hopefully a sufficient substitute for immediately despawning, however be aware the entity id will still exist until finally despawned.

Caveats:

  • Developing this alongside a simple game, so this is based on what I need for my attempt at a server-authoritative multiplayer game.
  • Currently requires you to use [GameClock] struct from this crate as frame counter.
  • Littered with a variety of debug logging, set your log level accordingly
  • Unoptimized: clones components each frame without checking if they've changed.
  • Doesn't rollback resources or other things, just (registered) component data.
  • Registered components must impl PartialEq
  • I'm using a patched version of bevy_xpbd at the mo, to make Collider impl PartialEq (PRs sent..)
You might also like...
A framework for saving and loading game state in Bevy.

Bevy_save A framework for saving and loading game state in Bevy. Features bevy_save is primarily built around extension traits to Bevy's World. Serial

Minimalistic state machine for Bevy Game Engine.

🎚️ Moonshine Behavior Minimalistic state machine for Bevy game engine. Overview This crates is designed to provide a simple, stack-based, lightweight

A Rust wrapper and bindings of Allegro 5 game programming library

RustAllegro A thin Rust wrapper of Allegro 5. Game loop example extern crate allegro; extern crate allegro_font; use allegro::*; use allegro_font::*;

High performance Rust ECS library
High performance Rust ECS library

Legion aims to be a feature rich high performance Entity component system (ECS) library for Rust game projects with minimal boilerplate. Getting Start

Rust library to create a Good Game Easily

ggez What is this? ggez is a Rust library to create a Good Game Easily. The current version is 0.6.0-rc0. This is a RELEASE CANDIDATE version, which m

Rust bindings for libtcod 1.6.3 (the Doryen library/roguelike toolkit)

Warning: Not Maintained This project is no longer actively developed or maintained. Please accept our apologies. Open pull requests may still get merg

A dependency-free chess engine library built to run anywhere.
A dependency-free chess engine library built to run anywhere.

♔chess-engine♚ A dependency-free chess engine library built to run anywhere. Demo | Docs | Contact Me Written in Rust 🦀 💖 Why write a Chess engine?

Scion is a tiny 2D game library built on top of wgpu, winit and legion.
Scion is a tiny 2D game library built on top of wgpu, winit and legion.

Scion is a 2D game library made in rust. Please note that this project is in its first milestones and is subject to change according to convience need

Tiny cross-platform webview library for C/C++/Golang. Uses WebKit (Gtk/Cocoa) and Edge (Windows)

webview A tiny cross-platform webview library for C/C++/Golang to build modern cross-platform GUIs. Also, there are Rust bindings, Python bindings, Ni

Comments
  • Anachronous entity snapping should create a TimewarpCorrection

    Anachronous entity snapping should create a TimewarpCorrection

    in apply_snapshots_and_snap_for_anachronous when snapping anachronous entities to a server snapshot, we should generate a TimewarpCorrection if needed, just like we do for non-anachronous rollbacks.

    opened by RJ 0
  • Decide on the PartialEq issue for TimewarpComponent

    Decide on the PartialEq issue for TimewarpComponent

    You register components for rollback like this:

    app.register_component::<Position>()
    

    and that is defined something like this:

    fn register_component<T: Component>() { ... }
    

    It would be useful to add + PartialEq to the trait bounds, because being able to compare components is useful for avoiding storing duplicates, detecting mispredictions after rollback, etc.

    However, not all components from 3rd-party crates I want to rollback are PartialEq.

    Easy enough to make my own components PartialEq, and the physics components are almost all PartialEq, with the exception of Collider, which wraps up parry::SharedShape, see:

    https://github.com/dimforge/parry/issues/51

    If i can get SharedShape to be PartialEq (and all the physics components) that would probably be my favourite option.

    If it was possible to implement different versions of certain systems like this:

    fn record_component_history<T: TimewarpComponent + PartialEq>(...) {
       // emit TimewarpCorrection only if oldval != newval
    }
    fn record_component_history<T: TimewarpComponent>(...) {
       // emit TimewarpCorrection regardless, make the game compare them
    }
    

    that would also be fine. Maybe possible with rust's min_specialization feature?

    Another work-around would be to require PartialEq in the general case but allow registering non-partialeq components with a different function, on the basis they would always emit a TimewarpCorrection when resimulated. That would probably be a big mess of duplicated generic functions though, without specialization. Worth some investigation.

    opened by RJ 1
Owner
Richard Jones
Richard Jones
Rollback-safe implementations and utilities for Bevy Engine

bevy_roll_safe Rollback-safe implementations and utilities for Bevy Engine. Motivation Some of Bevy's features can't be used in a rollback context (wi

Johan Klokkhammer Helsing 3 Sep 17, 2023
Provides a mechanism to lay out data into GPU buffers according to WGSL's memory layout rules

Provides a mechanism to lay out data into GPU buffers ensuring WGSL's memory layout requirements are met. Features supports all WGSL host-shareable ty

Teodor Tanasoaia 69 Dec 30, 2022
Creative Coding Framework based on Entity Component System (ECS) written in Rust

creativity creativity is Creative Coding Framework based on Entity Component System (ECS) written in Rust. Key Features TBA Quick Start TBA How To Con

Chris Ohk 9 Nov 6, 2021
Entity Component System focused on usability and speed.

Shipyard ⚓ Shipyard is an Entity Component System focused on usability and speed. If you have any question or want to follow the development more clos

Dylan Ancel 524 Jan 1, 2023
A safe, fast and cross-platform 2D component-based game framework written in rust

shura shura is a safe, fast and cross-platform 2D component-based game framework written in rust. shura helps you to manage big games with a component

Andri 28 Jan 17, 2023
A performant, small and versatile entity component system written in Rust

A performant, zero-dependency ECS library with a nice API written in Rust. Usage # Cargo.toml [dependecies] kiwi-ecs = "1.3" // lib.rs use kiwi_ecs::

Jonas Everaert 9 Nov 18, 2022
A generated entity component system 🦎

gecs ?? A generated entity component system. The gecs crate provides a compile-time generated, zero-overhead ECS for simulations on a budget. Unlike o

null 61 Jun 16, 2023
A nine slice/patch plugin for bevy ui nodes as single component using a fragment shader.

Bevy nine slice/patch Material Plugin Quick and easy auto-scaling nine slice/patch material for bevy ui nodes implemented as Fragment Shader. Features

Lorenz 13 Dec 14, 2023
State of the art "build your own engine" kit powered by gfx-hal

A rendering engine based on gfx-hal, which mimics the Vulkan API. Building This library requires standard build tools for the target platforms, except

Amethyst Foundation 801 Dec 28, 2022
State machine engine

orga Deterministic state machine engine written in Rust Orga is a stack for building blockchain applications powered by Tendermint consensus. Status:

Nomic 103 Dec 21, 2022