Coder Social home page Coder Social logo

ernwong / crystalorb Goto Github PK

View Code? Open in Web Editor NEW
210.0 5.0 13.0 10.15 MB

Network-agnostic, high-level game networking library for client-side prediction and server reconciliation (unconditional rollback).

Home Page: https://ernestwong.nz/crystalorb/demo

License: Other

Rust 99.74% Nix 0.26%
game-networking client-side-prediction crystalorb rollback-netcode rollback-networking

crystalorb's Introduction

WARNING: This crate currently depends on nightly rust unstable and incomplete features.

crystalorb

Network-agnostic, high-level game networking library
for client-side prediction and server reconciliation.

crates.io docs.rs ci cd Coverage



Quick start

You may copy the standalone example to use as a starting template, and build off from there. You may also want to check out crystalorb-mock-network and crystalorb-bevy-networking-turbulence, either to use directly in your projects, or as examples for you to integrate your own choice of networking layer.

If you prefer a more visual and interactive example, there is the demo shown above that uses the Rapier physics engine. Feel free to use the demo's source code as a starting template for your next project.

For more information about how to implement the required traits, refer to the docs.

About

Game networking is hard, because we usually want to give the illusion that the physics simulation is responsive, fair, and consistent between multiple game clients despite having significant network latency between the clients and the server. There are many different ways to solve the game networking problem. CrystalOrb tries implementing one of such ways.

CrystalOrb is a young networking library that implements:

  • Client-side prediction. Clients immediately apply their local input to their simulation before waiting for the server, so that the player's inputs feel responsive.
  • Server reconciliation. Server runs a delayed, authoritative version of the simulation, and periodically sends authoritative snapshots to each client. Since the server's snapshots represent an earlier simulation frame, each client fast-forwards the snapshot they receive until it matches the same timestamp as what's being shown on screen. Once the timestamps match, clients smoothly blend their states to the snapshot states.
  • Display state interpolation. The simulation can run at a different time-step from the render framerate, and the client will automatically interpolate between the two simulation frames to get the render display state.

CrystalOrb does not (yet) implement "lag compensation" (depending on your definition of "lag compensation"), because CrystalOrb clients currently simulate all entities (including other players) in the present anyway. Some netcode clients would rather simulate entities in the past except for the local player. There are pros and cons to both methods:

  • By simulating everything in the present, collision timings will be consistent across all clients provided that no player input significantly changes the course of the simulation. This might be beneficial for physics-heavy games like... Rocket League? This is what CrystalOrb does. If you have two clients side-by-side, and you issue a "jump" command for the player using the left client, then the right client will see a jump after a small delay, but both clients will see the player land at the exact same time.

  • By simulating most remote entities in the past, remote entities require little correction in the client, so other players' movement will look more natural in response to their player inputs. If you have two clients side-by-side, and issue a "jump" command for the player using the left client, then the right client will not see any jerk in the movement, but the jump and the landing will occur slightly later after the left client.

Caveat: You need to bring your own physics engine and "mid-level" networking layer. CrystalOrb is only a "sequencer" of some sort, a library that encapsulates the high-level algorithm for synchronizing multiplayer physics games. CrystalOrb is physics-engine agnostic and networking agnostic (as long as you can implement the requested traits).

Is CrystalOrb any good?

Doubt it. This is my first time doing game networking, so expect it to be all glitter and no actual gold. For more information on game networking, you might have better luck checking out the following:

(Yes, those were where I absorbed most of my small game-networking knowledge from. Yes, their designs are probably much better than CrystalOrb)

Similar and related projects

CrystalOrb is a client-server rollback library, and there are other kinds of rollback/networking libraries out there you should check out! Here are some other rollback libraries by other authors in the rust community you might be interested in:

Your project's not listed? Feel free to make a PR or submit an issue, and I'd be happy to promote it.

More examples

  • Demo by @ErnWong is hard-coded for only two clients (source).
  • orbgame by @vilcans is a Bevy demo that accepts more than two clients.
  • Dango Tribute by @ErnWong is a Bevy interactive experience with multiplayer blob physics. CrystalOrb was the networking code that was eventually extracted out from that project.

Your project's not listed? Feel free to make a PR or submit an issue, and I'd be happy to promote it.

Unstable Rust Features

CrystalOrb currently uses the following unstable features:

#![feature(const_fn_floating_point_arithmetic)]
#![feature(map_first_last)]
#![feature(adt_const_params)]
#![feature(generic_associated_types)]

crystalorb's People

Contributors

ernwong avatar jamescarterbell avatar shatur avatar vilcans avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

crystalorb's Issues

Different code on github and crates.io.

Hello,

I was trying to use this library for a project I am working on. The crates.io version doesn't have the same code base, and there are several compile errors. If it were possible to update the crates.io code to the newest version in the GitHub repo, that would be awesome.

Thanks

Consider supporting other kinds of synchronization strategies

For example, to reduce jerking movement and teleportation so that shooter games can be somewhat playable, it might be nice to display other players in the past (i.e. only render them at positions that have been confirmed by the server), potentially at the expense of lower performance or higher memory footprint.

Some initial exploratory work has started on the abstract-simulator branch (which may be renamed or deleted by the time you're reading this, but don't worry, I'll link to the corresponding PR when it's available).

Update readme with examples and related libraries

As a rust game developer interested in netcode programming,
I want the readme to have updated links to other related rust netcode projects
so I get a better awareness of the community, and can better understand which project is most suitable for my next game.


De-generalize Connection::recv

When I tried to integrate Crystalorb with custom networking, I found that it's not as simple as it can be.

The function Connection::recv is declared as follows:

    /// Interface for CrystalOrb to request the next message received, if there is one.
    ///
    /// CrystalOrb will invoke this method with the three message types as specified in
    /// [`NetworkResource`].
    fn recv<MessageType>(&mut self) -> Option<MessageType>
    where
        MessageType: Debug + Clone + Serialize + DeserializeOwned + Send + Sync + 'static;

It is generic on the MessageType, but it actually only supports three messages, as the documentation says. Having it generic on MessageType is as far as I can tell an unnecessary complication. I found it hard to follow the code in crystalorb_mock_network because of the code required to manage this.

If I check the callers of the recv function in the crystalorb code base, it always looks like one of the following statements:

connection.recv::<Timestamped<WorldType::CommandType>>()
connection.recv::<Timestamped<WorldType::SnapshotType>>()
connection.recv::<ClockSyncMessage>()

I.e. the type of the message is known at the call-site and does not have to be generic.

Wouldn't it be better if Connection instead declared those specific messages? I.e. something like this:

fn recv_command(&mut self) -> Option<Timestamped<WorldType::CommandType>>;
fn recv_snapshot(&mut self) -> Option<Timestamped<WorldType::SnapshotType>>;
fn recv_clock_sync(&mut self) -> Option<ClockSyncMessage>;

Then the code would not have to rely on run-time type information (TypeId) and would be more straight-forward.

Let me know what you think. I may have missed the reasoning behind the design.

Fix test-coverage checks for PRs

I think it currently fails for PRs because GitHub actions don't send secrets to PR-triggered actions, and so it couldn't get the codecov key to publish the results. Either

  • we disable the codecov upload step for PRs, or
  • we disable the entire check for PRs altogether, or
  • we try and fix the GitHub workflow so that publishing to codecov works for PRs, if it is possible.

Priority: medium-low since it would be confusing for new contributors to see their checks failing.

Be able to hot reload server/host.

During a match/game/connection, it should be possible to switch the host for someone else. For example, if Player A in an authoritative host model is the host, they should be able to leave and Player B should become the new host. A basic pseudocode for how this would work is as follows.

(1) Spawn up the new server and set a flag to disable sending out snapshots at first, and sync the time and state to the old server. (will be a bit fiddly)
(2) Have the clients add this new server as a new connection (I think CrystalOrb can already support this step), so that any new player commands would get sent to both the old and the new server
(3) Tell the old server to stop sending out its snapshots at time T into the future, and tell the new server to start sending its snapshot also at time T.
(4) Remove the old server from the client's connections

Note:
In the case of picking a new host, there might be a need to have a default algorithm for picking the new host (second person to join the match/lobby), but the user should be able to set their own (the closest to everyone/someone who had the highest score/etc)

Crash report - Unexpected status change into: Blending(0.0)

Full logs: https://gist.github.com/ErnWong/b63fa650bb013b572e1ba89d193a5c9c

Relevant part of the log:

...
wasm.js:384 Channel Incoming Error: packet channel is full
imports.wbg.__wbg_error_712bad4931fcf046 @ wasm.js:384
wasm.js:384 Channel Incoming Error: packet channel is full
imports.wbg.__wbg_error_712bad4931fcf046 @ wasm.js:384
wasm.js:384 Channel Incoming Error: packet channel is full
imports.wbg.__wbg_error_712bad4931fcf046 @ wasm.js:384
wasm.js:430 WARN /home/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/crystalorb-0.2.1/src/fixed_timestepper.rs:195 Attempted to advance more than the allowed delta seconds (48.8829). This should not happen too often.
wasm.js:430 WARN /home/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/crystalorb-0.2.1/src/fixed_timestepper.rs:237 TimeKeeper is too far behind. Skipping timestamp from Timestamp(14192) to Timestamp(17110) with overshoot from 0.0018708340833272566 to 0.0023041674166355127
wasm.js:430 WARN /home/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/crystalorb-0.2.1/src/fixed_timestepper.rs:195 Attempted to advance more than the allowed delta seconds (0.6154). This should not happen too often.
wasm.js:430 WARN /home/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/crystalorb-0.2.1/src/fixed_timestepper.rs:184 Timestamp has drifted by -0.36539999999998446 seconds. This should not happen too often.
wasm.js:430 WARN /home/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/crystalorb-0.2.1/src/fixed_timestepper.rs:195 Attempted to advance more than the allowed delta seconds (0.5785999999999845). This should not happen too often.
wasm.js:430 WARN /home/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/crystalorb-0.2.1/src/client.rs:698 Abandoning previous snapshot for newer shapshot! Couldn't fastforward the previous snapshot in time.
wasm.js:451 panicked at 'Unexpected status change into: Blending(0.0)', /home/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/crystalorb-0.2.1/src/client.rs:704:17

Stack:

Error
    at imports.wbg.__wbg_new_59cb74e423758ede (http://ernestwong.nz/dango-tribute/client/target/wasm.js:439:19)
    at console_error_panic_hook::hook::hbfe68726ffa79411 (http://ernestwong.nz/dango-tribute/client/target/wasm_bg.wasm:wasm-function[4500]:0x576a8e)
    at core::ops::function::Fn::call::hb6c015bf4f9077a4 (http://ernestwong.nz/dango-tribute/client/target/wasm_bg.wasm:wasm-function[20732]:0x7d482a)
    at std::panicking::rust_panic_with_hook::h894f2bdeea4d0ce8 (http://ernestwong.nz/dango-tribute/client/target/wasm_bg.wasm:wasm-function[8899]:0x6ebce6)
    at std::panicking::begin_panic_handler::{{closure}}::he30feeb51ae9f99b (http://ernestwong.nz/dango-tribute/client/target/wasm_bg.wasm:wasm-function[10701]:0x7396a5)
    at std::sys_common::backtrace::__rust_end_short_backtrace::h83a4032c6b997446 (http://ernestwong.nz/dango-tribute/client/target/wasm_bg.wasm:wasm-function[15727]:0x7ba229)
    at rust_begin_unwind (http://ernestwong.nz/dango-tribute/client/target/wasm_bg.wasm:wasm-function[15055]:0x7b11d5)
    at std::panicking::begin_panic_fmt::h9c5d99ded08e9f1a (http://ernestwong.nz/dango-tribute/client/target/wasm_bg.wasm:wasm-function[15627]:0x7b8edc)
    at <crystalorb::client::ClientWorldSimulations<WorldType> as crystalorb::fixed_timestepper::Stepper>::step::hc045531ebe8a0dc6 (http://ernestwong.nz/dango-tribute/client/target/wasm_bg.wasm:wasm-function[406]:0x10d539)
    at crystalorb::fixed_timestepper::TimeKeeper<T,_>::update::h658dbcd5fc9bfe5e (http://ernestwong.nz/dango-tribute/client/target/wasm_bg.wasm:wasm-function[1440]:0x32b3d0)


imports.wbg.__wbg_error_4bb6c2a97407129a @ wasm.js:451
wasm_bg.wasm:0x7d63c4 Uncaught RuntimeError: unreachable
    at __rust_start_panic (wasm_bg.wasm:0x7d63c4)
    at rust_panic (wasm_bg.wasm:0x7bcc33)
    at std::panicking::rust_panic_with_hook::h894f2bdeea4d0ce8 (wasm_bg.wasm:0x6ebd0a)
    at std::panicking::begin_panic_handler::{{closure}}::he30feeb51ae9f99b (wasm_bg.wasm:0x7396a5)
    at std::sys_common::backtrace::__rust_end_short_backtrace::h83a4032c6b997446 (wasm_bg.wasm:0x7ba229)
    at rust_begin_unwind (wasm_bg.wasm:0x7b11d5)
    at std::panicking::begin_panic_fmt::h9c5d99ded08e9f1a (wasm_bg.wasm:0x7b8edc)
    at <crystalorb::client::ClientWorldSimulations<WorldType> as crystalorb::fixed_timestepper::Stepper>::step::hc045531ebe8a0dc6 (wasm_bg.wasm:0x10d539)
    at crystalorb::fixed_timestepper::TimeKeeper<T,_>::update::h658dbcd5fc9bfe5e (wasm_bg.wasm:0x32b3d0)
    at crystalorb::client::ActiveClient<WorldType>::update::h8deb65d7204b2c76 (wasm_bg.wasm:0x2b1b10)

Unable to compile with latest nightly

I have the following error (I removed duplicated errors to reduce log):

error[E0557]: feature has been removed
 --> /home/gena/.cargo/registry/src/github.com-1ecc6299db9ec823/crystalorb-0.2.1/src/lib.rs:4:12
  |
4 | #![feature(const_generics)]
  |            ^^^^^^^^^^^^^^ feature has been removed
  |
  = note: removed in favor of `#![feature(adt_const_params)]` and `#![feature(generic_const_exprs)]`

   Compiling time v0.2.27
error: `TerminationCondition` is forbidden as the type of a const generic parameter
  --> /home/gena/.cargo/registry/src/github.com-1ecc6299db9ec823/crystalorb-0.2.1/src/fixed_timestepper.rs:92:56
   |
92 | impl<T: FixedTimestepper, const TERMINATION_CONDITION: TerminationCondition>
   |                                                        ^^^^^^^^^^^^^^^^^^^^
   |
   = note: the only supported types are integers, `bool` and `char`
   = help: more complex types are supported with `#![feature(adt_const_params)]`

For more information about this error, try `rustc --explain E0557`.
error: could not compile `crystalorb` due to 10 previous errors
warning: build failed, waiting for other jobs to finish...
error: build failed

I use rustc 1.57.0-nightly (11491938f 2021-09-29)

Bevy 0.7

As a game developer using the bevy engine,
I want to continue using crystalorb with bevy 0.7,
so I can have access to new bevy 0.7 features.

Out of scope: Improving bevy integration and ergonomics. That is handled in #14 .

To investigate: what needs to change to use, for example, the existing crystalorb-bevy-networking-turbulence crate with bevy 0.7?

Avoid making unnecessary copies of the displayable state

This repo started out as a simple state syncing implementation, and to make things easier to implement and more generalisable, the design did not focus on memory footprint and the performance penalties due to unnecessary copying of data.

For example, the client code results in having roughly 6 copies of the entire simulation state at any time! This is due to needing:

  • Two complete worlds: One without the latest snapshot and one with the latest snapshot
  • The next queued snapshot to be applied next
  • The undershot and overshot display states used for deriving the tweened display state
  • The tweened display state itself to be rendered on screen.

On discord, @TheRawMeetball suggested something like the following to be added to the World trait, in place of the DisplayState trait.

type DisplayTarget;
fn write_to_display(&self, target: &mut Self::DisplayTarget, tween: f32);

We could similarly extend this concept to allow for "blending" between the old and new worlds. Things to account for:

  • Generalisability and composability. DisplayState can currently be composed together in different ways to support different kinds of synchronization strategies. Do we still want some kind of composability?
  • It will need to support both client's and server's use case. Client uses the tweened version for rendering, and possibly either the tweened version, undershot version or overshot version of the display state for external game logic. The server might also use the undershot version of the display state to perform external game logic, or to render a diagnostic view of the world.
  • The server may need to know what display state the client observed at a particular timestamp to validate a command (e.g. hit validation with lag compensation in shooter games).

To verify the changes and explore new designs, it might be good to first setup some benchmarking.

Schedule Github Actions to run on latest rust nightly builds

Detect when the library could be broken on someone else's local rust nightly.

Possible options:

  • Run like a dependabot update, but instead of making a PR that tries updating cargo dependencies, make a PR that tries updating ./rust-toolchain and run tests on the result. Downside is that dependabot PRs don't run the checks automatically I think.
  • Use scheduled events

Networked example needed

The example uses a mock connection, so when I tried to implement real networking, that took a while. It would be nice to have an example of a simple but more "real" project.

FWIW, here's what I made: https://github.com/vilcans/orbgame. It's maybe not simple enough to use as an example, and maybe I'm not doing things the right way either, as I'm new to Bevy and hence also crystalorb in Bevy. I haven't figured out how to get events for disconnects yet, so players never disappear.

Remove deref trait abuse

E.g.

crystalorb/src/timestamp.rs

Lines 261 to 272 in 6159018

impl<T> Deref for Timestamped<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> DerefMut for Timestamped<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}

There are probably other cases.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.