diff --git a/examples/auth/assets/settings.ron b/examples/auth/assets/settings.ron index f3a7913f2..a712669d8 100644 --- a/examples/auth/assets/settings.ron +++ b/examples/auth/assets/settings.ron @@ -15,7 +15,7 @@ MySettings( // transport: WebTransport( // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks // the server will print the certificate digest on startup - // certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", + // certificate_digest: "1a:83:aa:a3:ec:d4:5f:8f:1c:41:e7:70:8f:78:e3:a8:27:f4:db:4e:73:35:e6:a3:ea:f4:95:82:f1:6f:06:4b", // ), server_port: 5001, transport: Udp, diff --git a/examples/bullet_prespawn/assets/settings.ron b/examples/bullet_prespawn/assets/settings.ron index 258391b62..16f39ce17 100644 --- a/examples/bullet_prespawn/assets/settings.ron +++ b/examples/bullet_prespawn/assets/settings.ron @@ -17,7 +17,7 @@ MySettings( transport: WebTransport( // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks // the server will print the certificate digest on startup - certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", + certificate_digest: "1a:83:aa:a3:ec:d4:5f:8f:1c:41:e7:70:8f:78:e3:a8:27:f4:db:4e:73:35:e6:a3:ea:f4:95:82:f1:6f:06:4b", ), // server_port: 5001, // transport: Udp, diff --git a/examples/certificates/cert.pem b/examples/certificates/cert.pem index 946ada686..be14d738e 100644 --- a/examples/certificates/cert.pem +++ b/examples/certificates/cert.pem @@ -1,11 +1,11 @@ -----BEGIN CERTIFICATE----- -MIIBfTCCASOgAwIBAgIUdbC/g5dOvo7rlblM9+BRicaVJAMwCgYIKoZIzj0EAwIw -FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDUwNzE0MTEzNVoXDTI0MDUyMTE0 -MTEzNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D -AQcDQgAE3BeCmoE1gzCuMBDnqMdTeWz8nvD9T2otuWlnZOtc6zL/tqv2qL38yRvg -20X7/t7RE8tsMGlc41tEej/Lr11XcaNTMFEwHQYDVR0OBBYEFK7q4bBLqu3219yE -W50GnETHqNY2MB8GA1UdIwQYMBaAFK7q4bBLqu3219yEW50GnETHqNY2MA8GA1Ud -EwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAJ6w/WKnpfObuk3XVmn3yaIL -SVCRgsAJVdv1cnAbmlUUAiAng/XpUyJRcdQkVvnXsw6gEUmdAUHZaZd6UhLUGiqu -3A== +MIIBfTCCASOgAwIBAgIUKI27VMWcE8NjR3naSwPmvjYn9lQwCgYIKoZIzj0EAwIw +FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDUyMjIyMzE0MFoXDTI0MDYwNTIy +MzE0MFowFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAEqL2JMPpxNZnNu/GGwFCnISMBcBnHQ/aBavlt7cyVnNaa2OwhkIRYTSHw +vlD6CE6thpL/EVXWcbOVw9VXWN0Y8KNTMFEwHQYDVR0OBBYEFMmrC4dORxoH7ivv +qSQcVMeWOKSYMB8GA1UdIwQYMBaAFMmrC4dORxoH7ivvqSQcVMeWOKSYMA8GA1Ud +EwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAJptNIhxm05Nz5bUexxT1g0r +XTwdtq+vkYODcX9KBokPAiBFcLJmsRZ8sKTHDeRNDFw3kLGDrbNqLSkBLjaTmD0G +4w== -----END CERTIFICATE----- diff --git a/examples/certificates/key.pem b/examples/certificates/key.pem index d0f1f6260..820736de6 100644 --- a/examples/certificates/key.pem +++ b/examples/certificates/key.pem @@ -1,5 +1,5 @@ -----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVp1Kz4Vto1yWQwbt -7XhmfX4UbMDcNbrybKffjEc1mCmhRANCAATcF4KagTWDMK4wEOeox1N5bPye8P1P -ai25aWdk61zrMv+2q/aovfzJG+DbRfv+3tETy2wwaVzjW0R6P8uvXVdx +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg0s/XIM5aTi33W9po +uOAz0JBqR9+dd0JikOb8ZHYOn8uhRANCAASovYkw+nE1mc278YbAUKchIwFwGcdD +9oFq+W3tzJWc1prY7CGQhFhNIfC+UPoITq2Gkv8RVdZxs5XD1VdY3Rjw -----END PRIVATE KEY----- diff --git a/examples/client_replication/assets/settings.ron b/examples/client_replication/assets/settings.ron index f1f71978a..11127693a 100644 --- a/examples/client_replication/assets/settings.ron +++ b/examples/client_replication/assets/settings.ron @@ -13,7 +13,7 @@ Settings( transport: WebTransport( // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks // the server will print the certificate digest on startup - certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", + certificate_digest: "1a:83:aa:a3:ec:d4:5f:8f:1c:41:e7:70:8f:78:e3:a8:27:f4:db:4e:73:35:e6:a3:ea:f4:95:82:f1:6f:06:4b", ), // server_port: 5001, // transport: Udp, diff --git a/examples/interest_management/assets/settings.ron b/examples/interest_management/assets/settings.ron index 2d7f41269..5f0101f9f 100644 --- a/examples/interest_management/assets/settings.ron +++ b/examples/interest_management/assets/settings.ron @@ -13,7 +13,7 @@ Settings( transport: WebTransport( // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks // the server will print the certificate digest on startup - certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", + certificate_digest: "1a:83:aa:a3:ec:d4:5f:8f:1c:41:e7:70:8f:78:e3:a8:27:f4:db:4e:73:35:e6:a3:ea:f4:95:82:f1:6f:06:4b", ), // server_port: 5001, // transport: Udp, diff --git a/examples/leafwing_inputs/assets/settings.ron b/examples/leafwing_inputs/assets/settings.ron index 69f08f1c1..7a03770a3 100644 --- a/examples/leafwing_inputs/assets/settings.ron +++ b/examples/leafwing_inputs/assets/settings.ron @@ -17,7 +17,7 @@ MySettings( transport: WebTransport( // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks // the server will print the certificate digest on startup - certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", + certificate_digest: "1a:83:aa:a3:ec:d4:5f:8f:1c:41:e7:70:8f:78:e3:a8:27:f4:db:4e:73:35:e6:a3:ea:f4:95:82:f1:6f:06:4b", ), // server_port: 5001, // transport: Udp, diff --git a/examples/lobby/assets/settings.ron b/examples/lobby/assets/settings.ron index a31ee1699..08075290c 100644 --- a/examples/lobby/assets/settings.ron +++ b/examples/lobby/assets/settings.ron @@ -13,7 +13,7 @@ Settings( // transport: WebTransport( // // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks // // the server will print the certificate digest on startup - // certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", + // certificate_digest: "1a:83:aa:a3:ec:d4:5f:8f:1c:41:e7:70:8f:78:e3:a8:27:f4:db:4e:73:35:e6:a3:ea:f4:95:82:f1:6f:06:4b", // ), server_port: 5001, transport: Udp, diff --git a/examples/priority/assets/settings.ron b/examples/priority/assets/settings.ron index d9530de4d..045098534 100644 --- a/examples/priority/assets/settings.ron +++ b/examples/priority/assets/settings.ron @@ -13,7 +13,7 @@ Settings( transport: WebTransport( // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks // the server will print the certificate digest on startup - certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", + certificate_digest: "1a:83:aa:a3:ec:d4:5f:8f:1c:41:e7:70:8f:78:e3:a8:27:f4:db:4e:73:35:e6:a3:ea:f4:95:82:f1:6f:06:4b", ), // server_port: 5001, // transport: Udp, diff --git a/examples/replication_groups/assets/settings.ron b/examples/replication_groups/assets/settings.ron index 2d0fda66e..4494e6941 100644 --- a/examples/replication_groups/assets/settings.ron +++ b/examples/replication_groups/assets/settings.ron @@ -13,7 +13,7 @@ Settings( transport: WebTransport( // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks // the server will print the certificate digest on startup - certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", + certificate_digest: "1a:83:aa:a3:ec:d4:5f:8f:1c:41:e7:70:8f:78:e3:a8:27:f4:db:4e:73:35:e6:a3:ea:f4:95:82:f1:6f:06:4b", ), // server_port: 5001, // transport: Udp, diff --git a/examples/simple_box/assets/settings.ron b/examples/simple_box/assets/settings.ron index 3c9e9a630..bc1eb9128 100644 --- a/examples/simple_box/assets/settings.ron +++ b/examples/simple_box/assets/settings.ron @@ -13,7 +13,7 @@ Settings( transport: WebTransport( // this is only needed for wasm, the self-signed certificates are only valid for 2 weeks // the server will print the certificate digest on startup - certificate_digest: "24:48:ea:6f:13:a4:4f:2f:42:b9:f3:71:3f:79:c5:7a:d1:1d:29:ab:de:b0:03:4d:94:92:7b:84:69:01:85:1d", + certificate_digest: "1a:83:aa:a3:ec:d4:5f:8f:1c:41:e7:70:8f:78:e3:a8:27:f4:db:4e:73:35:e6:a3:ea:f4:95:82:f1:6f:06:4b", ), // server_port: 5001, // transport: Udp, diff --git a/lightyear/Cargo.toml b/lightyear/Cargo.toml index 97fe81591..ae324c7fc 100644 --- a/lightyear/Cargo.toml +++ b/lightyear/Cargo.toml @@ -14,27 +14,27 @@ exclude = ["/tests"] [features] metrics = [ - "dep:metrics", - "metrics-util", - "metrics-tracing-context", - "metrics-exporter-prometheus", + "dep:metrics", + "metrics-util", + "metrics-tracing-context", + "metrics-exporter-prometheus", ] mock_time = ["dep:mock_instant"] webtransport = [ - "dep:wtransport", - "dep:xwt-core", - "dep:xwt-web-sys", - "dep:web-sys", - "dep:ring", - "dep:wasm-bindgen-futures", + "dep:wtransport", + "dep:xwt-core", + "dep:xwt-web-sys", + "dep:web-sys", + "dep:ring", + "dep:wasm-bindgen-futures", ] leafwing = ["dep:leafwing-input-manager"] xpbd_2d = ["dep:bevy_xpbd_2d"] websocket = [ - "dep:tokio-tungstenite", - "dep:futures-util", - "dep:web-sys", - "dep:wasm-bindgen", + "dep:tokio-tungstenite", + "dep:futures-util", + "dep:web-sys", + "dep:wasm-bindgen", ] steam = ["dep:steamworks"] zstd = ["dep:zstd"] @@ -70,7 +70,7 @@ bevy_xpbd_2d = { version = "0.4", optional = true, default-features = false } # serialization bitcode = { version = "0.5.1", package = "bitcode_lightyear_patch", path = "../vendor/bitcode", features = [ - "serde", + "serde", ] } bytes = { version = "1.5", features = ["serde"] } self_cell = "1.0" @@ -87,8 +87,8 @@ lightyear_macros = { version = "0.15.1", path = "../macros" } tracing = "0.1.40" tracing-log = "0.2.0" tracing-subscriber = { version = "0.3.17", features = [ - "registry", - "env-filter", + "registry", + "env-filter", ] } # server @@ -99,12 +99,12 @@ metrics = { version = "0.22", optional = true } metrics-util = { version = "0.15", optional = true } metrics-tracing-context = { version = "0.15", optional = true } metrics-exporter-prometheus = { version = "0.13.0", optional = true, default-features = false, features = [ - "http-listener", + "http-listener", ] } # bevy bevy = { version = "0.13", default-features = false, features = [ - "multi-threaded", + "multi-threaded", ] } @@ -114,8 +114,8 @@ futures-util = { version = "0.3.30", optional = true } # transport # we don't need any tokio features, we use only use the tokio channels tokio = { version = "1.36", features = [ - "sync", - "macros", + "sync", + "macros", ], default-features = false } futures = "0.3.30" async-compat = "0.2.3" @@ -127,13 +127,13 @@ async-channel = "2.2.0" steamworks = { version = "0.11", optional = true } # webtransport wtransport = { version = "=0.1.13", optional = true, features = [ - "self-signed", - "dangerous-configuration", + "self-signed", + "dangerous-configuration", ] } # websocket tokio-tungstenite = { version = "0.21.0", optional = true, features = [ - "connect", - "handshake", + "connect", + "handshake", ] } # compression zstd = { version = "0.13.1", optional = true } @@ -142,24 +142,28 @@ zstd = { version = "0.13.1", optional = true } console_error_panic_hook = { version = "0.1.7" } ring = { version = "0.17.8", optional = true, default-features = false } web-sys = { version = "0.3", optional = true, features = [ - "WebTransport", - "WebTransportHash", - "WebTransportOptions", - "WebTransportBidirectionalStream", - "WebTransportSendStream", - "WebTransportReceiveStream", - "ReadableStreamDefaultReader", - "WritableStreamDefaultWriter", - "WebTransportDatagramDuplexStream", - "WebSocket", - "CloseEvent", - "ErrorEvent", - "MessageEvent", - "BinaryType", + "Document", + "WebTransport", + "WebTransportHash", + "WebTransportOptions", + "WebTransportBidirectionalStream", + "WebTransportSendStream", + "WebTransportReceiveStream", + "ReadableStreamDefaultReader", + "WritableStreamDefaultWriter", + "WebTransportDatagramDuplexStream", + "WebSocket", + "CloseEvent", + "ErrorEvent", + "MessageEvent", + "BinaryType", + "Blob", + "Url", + "Worker", ] } futures-lite = { version = "2.1.0", optional = true } getrandom = { version = "0.2.11", features = [ - "js", # feature 'js' is required for wasm + "js", # feature 'js' is required for wasm ] } xwt-core = { version = "0.4", optional = true } xwt-web-sys = { version = "0.11", optional = true } @@ -182,14 +186,14 @@ approx = "0.5.1" # we cannot use all-features = true, because we need to provide additional features for bevy_xpbd_2d # when building the docs features = [ - "metrics", - "webtransport", - "leafwing", - "xpbd_2d", - "websocket", - "steam", - "zstd", - "bevy_xpbd_2d/2d", - "bevy_xpbd_2d/f32", + "metrics", + "webtransport", + "leafwing", + "xpbd_2d", + "websocket", + "steam", + "zstd", + "bevy_xpbd_2d/2d", + "bevy_xpbd_2d/f32", ] rustdoc-args = ["--cfg", "docsrs"] diff --git a/lightyear/src/client/mod.rs b/lightyear/src/client/mod.rs index 52f46ccba..da628cad5 100644 --- a/lightyear/src/client/mod.rs +++ b/lightyear/src/client/mod.rs @@ -28,3 +28,6 @@ pub(crate) mod io; pub(crate) mod message; pub(crate) mod networking; pub mod replication; + +#[cfg(target_family = "wasm")] +mod web; diff --git a/lightyear/src/client/plugin.rs b/lightyear/src/client/plugin.rs index 978553147..dcfee73a3 100644 --- a/lightyear/src/client/plugin.rs +++ b/lightyear/src/client/plugin.rs @@ -53,7 +53,7 @@ impl PluginGroup for ClientPlugins { let builder = PluginGroupBuilder::start::(); let tick_interval = self.config.shared.tick.tick_duration; let interpolation_config = self.config.interpolation.clone(); - builder + let builder = builder .add(SetupPlugin { config: self.config, }) @@ -63,7 +63,12 @@ impl PluginGroup for ClientPlugins { .add(ClientReplicationReceivePlugin { tick_interval }) .add(ClientReplicationSendPlugin { tick_interval }) .add(PredictionPlugin) - .add(InterpolationPlugin::new(interpolation_config)) + .add(InterpolationPlugin::new(interpolation_config)); + + #[cfg(target_family = "wasm")] + let builder = builder.add(crate::client::web::WebPlugin); + + builder } } diff --git a/lightyear/src/client/web.rs b/lightyear/src/client/web.rs new file mode 100644 index 000000000..fdb97ef66 --- /dev/null +++ b/lightyear/src/client/web.rs @@ -0,0 +1,54 @@ +//! Module containing extra behaviour that we need when running in wasm + +use bevy::prelude::*; +use std::sync::{Arc, RwLock}; +use wasm_bindgen::{closure::Closure, JsCast, JsValue}; +use web_sys::{js_sys::Array, window, Blob, Url, Worker}; + +pub struct WebPlugin; + +impl Plugin for WebPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, spawn_background_worker); + } +} + +/// In wasm, the main thread gets quickly throttled by the browser when it is hidden (e.g. when the user switches tabs). +/// This means that the app.update() function will not be called, because bevy's scheduler only runs `app.update()` when +/// the browser's requestAnimationFrame is called. (and that happens only when the tab is visible) +/// +/// This is problematic because: +/// - we stop sending packets so the server disconnects the client because it doesn't receive keep-alives +/// - when the client reconnects, it also disconnects because it hasn't been receiving server packets +/// - the internal transport buffers can overflow because they are not being emptied +/// +/// This solution spawns a WebWorker (a background thread) which is not throttled, and which runs +/// `app.update()` at a fixed interval. This way, the client can keep sending and receiving packets, +/// and updating the local World. +fn spawn_background_worker(world: &mut World) { + let world_ptr = Arc::new(RwLock::new(world as *mut World)); + + // The interval is in milliseconds. We can run app.update() infrequently when in the background + let blob = Blob::new_with_str_sequence( + &Array::of1(&JsValue::from_str( + "setInterval(() => self.postMessage(0), 1000);", + )) + .unchecked_into(), + ) + .unwrap(); + + let worker = Worker::new(&Url::create_object_url_with_blob(&blob).unwrap()).unwrap(); + + let closure = Closure::::new(move || { + if window().unwrap().document().unwrap().hidden() { + // Imitate app.update() + let world = unsafe { world_ptr.write().unwrap().as_mut().unwrap() }; + world.run_schedule(Main); + world.clear_trackers(); + } + }); + + worker.set_onmessage(Some(closure.as_ref().unchecked_ref())); + + closure.forget(); +}