{"id":15650147,"url":"https://github.com/rj/bevy_timewarp","last_synced_at":"2025-04-18T20:31:36.235Z","repository":{"id":188937413,"uuid":"679725583","full_name":"RJ/bevy_timewarp","owner":"RJ","description":"A rollback library that buffers component state. Useful for netcode.","archived":true,"fork":false,"pushed_at":"2024-03-07T15:17:25.000Z","size":370,"stargazers_count":41,"open_issues_count":4,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-18T01:21:13.822Z","etag":null,"topics":["bevy","multiplayer-game","rollback","rollback-netcode"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RJ.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-08-17T13:31:14.000Z","updated_at":"2025-03-31T15:54:40.000Z","dependencies_parsed_at":"2023-08-17T15:25:01.368Z","dependency_job_id":"774b3cba-be64-4edd-b905-0bc179a69f63","html_url":"https://github.com/RJ/bevy_timewarp","commit_stats":null,"previous_names":["rj/bevy_timewarp"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RJ%2Fbevy_timewarp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RJ%2Fbevy_timewarp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RJ%2Fbevy_timewarp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RJ%2Fbevy_timewarp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RJ","download_url":"https://codeload.github.com/RJ/bevy_timewarp/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249543297,"owners_count":21288703,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bevy","multiplayer-game","rollback","rollback-netcode"],"created_at":"2024-10-03T12:33:36.526Z","updated_at":"2025-04-18T20:31:35.982Z","avatar_url":"https://github.com/RJ.png","language":"Rust","readme":"# bevy_timewarp\n\nBuffer and rollback to states up to a few frames ago, for rollback networking.\n\nDoesn't do any networking, just concerned with buffering old component states in order to\nrevert values to previous frames and quickly fast-forward back to the original frame.\nAssumes game logic uses bevy's `FixedUpdate` schedule.\n\nCurrent status: under development alongside a multiplayer server-authoritative game.\nI am \"RJ\" on bevy's discord, in the networking channel, if you want to discuss.\n\n**STATUS** i built this for bevy 0.11, and have recently upgraded to 0.13 (still testing on 0.13..)\n\n### \"Sports Games\"\n\nThis crate is built for so called \"sports games\" where dynamic entities interact in the same\ntimeframe. That means clients simulate all entities into the future, in the same timeframe as\nthe local player. This necessitates predicting player inputs.\n\nThis is quite different to traditional Quake style FPS netcode, which is for a mostly static\nworld, with players running around shooting eachother. In Quake style, you are always seeing\na delayed version of other players, interpolated between two snapshots. The server does\nbackwards reconcilliation / lag compensation to verify hits.\n\n#### Example rollback scenario:\n\nIn your client/server game:\n\n- client is simulating frame 10\n- server snapshot for frame 6 arrives, including values for an entity's component T\n- client updates entity's ServerSnapshot\u003cT\u003e value at frame 6 (ie, the past)\n- Timewarp triggers a rollback to frame 6 if snapshot != our stored value for frame 6.\n- - winds back frame counter to 6\n  - copies the server snapshot value to the component\n  - resimulates frames 7,8,9,10 as fast as possible\n  - during this process, your systems would apply player inputs you've stored for each frame\n- Rollback ends and frame 11 continues normally.\n\n### Quickstart\n\nAdd the plugin, then register the components you wish to be rollback capable:\n\n```rust\n// Store 10 frames of rollback, run timewarp systems after our GameLogic set.\napp.add_plugins(TimewarpPlugin::new(10, MySets::GameLogic));\n// register components that should be buffered and rolled back as needed:\napp.register_rollback::\u003cMyComponent\u003e();\napp.register_rollback::\u003cPosition\u003e();\n// etc..\n```\n\nAny entity that has a `T` Component will automatically be given a [`ComponentHistory\u003cT\u003e`] and\n[`ServerSnapshot\u003cT\u003e`] component.\n\n`ComponentHistory` is a circular buffer of the last N frames of component values.\nThis is logged every frame automatically, so is mostly your client predicted values.\nYou typically won't need to interact with this.\n\n`ServerSnapshot` is a buffer of the last few authoritative component values, typically what\nyou received from the game server. Your network system will need to add new values to this.\n\nWhen you receive authoritative updates, add them to the ServerSnapshot\u003cMyComponent\u003e like so:\n\n```rust\nfn process_position_updates_from_server(\n    mut q: Query\u003c(Entity, \u0026mut ServerSnapshot\u003cPosition\u003e)\u003e,\n    update: Res\u003cUpdateFromServer\u003e, // however you get your updates, not our business.\n){\n    // typically update.frame would be in the past compared to current client frame\n    for (entity, mut ss_position) in q.iter_mut() {\n        let component_val = update.get_position_at_frame_for_entity(update.frame, entity); // whatever\n        ss_position.insert(update.frame, component_val);\n    }\n}\n```\n\nAlternatively, and especially if you are inserting a component your entity has never had before,\nmeaning there will be no `ServerSnapshot\u003cT\u003e` component, insert components in the past like this:\n\n```rust\nlet add_at_frame = 123;\nlet historical_component = InsertComponentAtFrame::new(add_at_frame, MyComponent);\ncommands.entity(e1).insert(historical_component);\n```\n\n#### Systems configuration\n\nDivide up your game systems so that during a rollback you still apply stored player input,\nbut ignore stuff like sending network messages etc.\n\nDuring a rollback, the [`Rollback`] resource will exist. Use this in a `run_if` condition.\n\n```rust\n// Normal game loop when not doing a rollback/fast-forward\napp.add_systems(FixedUpdate,\n    (\n        frame_inc,\n        process_server_messages,\n        process_position_updates_from_server,\n        spawn_stuff,\n        read_player_inputs,\n        apply_all_player_inputs_to_simulation_for_this_frame,\n        do_physics,\n        render,\n        etc,\n    )\n    .chain()\n    .in_set(MySets::GameLogic)\n    .run_if(not(resource_exists::\u003cRollback\u003e())) // NOT in rollback\n);\n// Abridged game loop for replaying frames during rollback/fast-forward\napp.add_systems(FixedUpdate,\n    (\n        frame_inc,\n        apply_all_player_inputs_to_simulation_for_this_frame,\n        do_physics,\n    )\n    .chain()\n    .in_set(MySets::GameLogic)\n    .run_if(resource_exists::\u003cRollback\u003e()) // Rollback is happening.\n);\n```\n\n## Visual smoothing of errors\n\nTimewarp snaps the simulation state – ie. the value of a component at a specific frame simulated\nlocally vs the value after rollback and resimulate might differ.\nIf you register your components like this:\n\n```rust\n    app.register_rollback_with_correction_logging::\u003cPosition\u003e();\n```\n\nthen timewarp will capture the before and after versions of components when doing a rollback,\nand put it into a [`TimewarpCorrection\u003cPosition\u003e`] component for your game to examine.\nTypically this would be useful for some visual smoothing - you might gradually blend over the\nerror distance with your sprite, even though the underlying physical simulation snapped correct.\n\n### Testing various edge cases\n\nTODO: I don't know how to link rustdocs to integration tests..\n\nSee the `basic_rollback` test for the most\nstraightforward scenario: a long lived entity with components that haven't been added/removed,\nand an authoritative server update arrives. Apply value to past frame, rollback and continue.\n\nThe `despawn_markers` test illustrates how to despawn –\nrather than doing `commands.entity(id).despawn()`, which would remove all trace and thus\nmean the entity couldn't be revived if we needed to rollback to a frame when it was alive,\nyou insert a [`DespawnMarker(frame_number)`][DespawnMarker] component, which cleans up\nthe entity immediately by removing all its registered components, then does the actual despawn\nafter `rollback_window` frames have elapsed.\n\nThe `despawn_revival_during_rollback` test\ndoes something similar, but triggers a rollback which will restore components to an entity\ntagged with a `DespawnMarker` in order to resimulate after a server update arrives, and then\nremove the components again.\n\nThe `component_add_and_remove` test\ntests how a server can add a component to an entity in the past, in this case a Shield, which\nprevents our enemy from taking damage.\n\nTODO describe the various other tests.\n\n### Quick explanation of entity death in our rollback world\n\nIn order to preserve the `Entity` id, removing an entity using the `DespawnMarker` in fact\nremoves all its registered components, but leaves the bare entity alive for rollback_frames.\n\nSuch entities retain their ComponentHistory buffers so they can be revived if needed because of\na rollback. Finally, after rollback_frames has elapsed, they are despawn_recursived.\n\nRemoving components is hopefully a sufficient substitute for immediately despawning, however\nbe aware the entity id will still exist until finally despawned.\n\n### Caveats:\n\n- Developing this alongside a simple game, so this is based on what I need for my attempt at\n  a server-authoritative multiplayer game.\n- Currently requires you to use [`GameClock`] struct from this crate as frame counter.\n- Littered with a variety of debug logging, set your log level accordingly\n- Unoptimized: clones components each frame without checking if they've changed.\n- Doesn't rollback resources or other things, just (registered) component data.\n- Registered components must impl `PartialEq`\n- I'm using a patched version of `bevy_xpbd` at the mo, to make `Collider` impl `PartialEq`\n  (PRs sent..)\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frj%2Fbevy_timewarp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frj%2Fbevy_timewarp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frj%2Fbevy_timewarp/lists"}