Listening For Websockets From Bevy Example?

Hey all!

Man, I'm having a SUPER difficult time properly connecting to a websocket server from my Bevy app.

My gut feelings is that there should be a Resource for the socket, maybe a system to set up the connection, and then another system (function handler) when a message comes in.

I even tried using ai to help me, but the issue is that it "moves" all my stuff to a different thread so then I am unable to use the Commands to spawn anything or send the message anywhere else...

Sometimes it does run and compile but then just prints out "Receiver has been dropped" when I should be getting a message. :thinking:

Does anyone have any simple working example code that I can reference?

Thanks!

1 Like

So I went through this, and it's a bit of a hassle, but here's a workable minimal-ish example:

You can press space to establish a connection to an echo server.

It stores the connection in a Component that can be attached to any entity.

It uses non-blocking I/O so there should be no need for async or separate threads. But I'm also not super confident in this implementation at the moment.

Did you look into existing libraries for this like GitHub - foxzool/bevy_octopus: A Low leveling ECS driven network plugin for Bevy. ?

2 Likes

Wow, it works! Amazing!!

Thanks!!

Well, I can connect and receive websocket messages, at least. :+1:

When I paste the send_info function in my editor though it immediately underlines in red the variable "transforms" in line 11 when calling send...

fn send_info(
some_data: Query<(&Transform,)>,
mut entities_with_client: Query<(&mut WebSocketClient,)>,
) {
for (mut client,) in entities_with_client.iter_mut() {
let transforms = &some_data.iter().map(|x| x.0.clone()).collect::<Vec<>>();
info!("Sending data: {transforms:?}");
match client
.0
.0
.send(Message::Binary(bincode::serialize(transforms).unwrap()))
{
Ok(
) => info!("Data successfully sent!"),
Err(tungstenite::Error::Io(e)) if e.kind() == ErrorKind::WouldBlock => { /* ignore */ }
Err(e) => {
warn!("Could not send the message: {e:?}");
}
}
}
}

Here's the error I'm seeing:

the trait bound `bevy::prelude::Transform: other_player::_::_serde::Serialize` is not satisfied
for local types consider adding `#[derive(serde::Serialize)]` to your `bevy::prelude::Transform` type
for types from other crates check whether the crate offers a `serde` feature flag
the following other types implement trait

I'm using bevy { version = "0.14", features = ["wayland"] }, is that maybe why I'm seeing this error? Also, how would you invoke this "send_info" from other places in the app when you do want to send messages to the server?

Thanks! :+1: :bowing_man:

I'm not defining "Transform" in my code anywhere so idk how I can add #[derive(serde::Serialize)] to it.

I'm not sure what it means by "check whether the crate offers a serde feature flag
the following other types implement trait" :thinking:

Ah, you need to enable bevy's serialize feature for this to work:

You can see the feature flags on docs.rs/bevy (specifically here: bevy 0.14.2 - Docs.rs ).

1 Like

You could either:

  1. invoke .send directly on the websocket client. Since the underlying TcpStream is non-blocking this shouldn't be too costly. However since it would need exclusive access only one system can have access to a websocket at a time, so this could limit parallelism.
  2. add an event that another system reads that contains messages to send (look at e.g. Events - Unofficial Bevy Cheat Book )
1 Like

Hey, I just tried copmiling my app, and it seems like these libraries are not compatible with wasm... :skull:

Is there any way to make this work for a wasm build?

These are the only libs you are using?

How are you calling "connect" and "MaybeTlsStream" is a way that works on wasm?

I added slightly cursed and convoluted wasm support to the example repo.

See mainly this commit: add wasm support · ambiso/bevy_websocket_example@90e72f7 · GitHub (and also the following 2 commits).

In wasm you can't use rustls or tungsten, you need to use the WebSocket API provided by the browser. See e.g. WebSocket - Web APIs | MDN .

I'm basically recreating this example from wasm using the web-sys crate which provides bindings to the browser's APIs. I can then set callback functions that modify the state inside the wasm context - specifically when a message is received it appends the message content to the recv_queue.

The callbacks are stored in the Client so they are not dropped (It appears setting event listeners only creates weak references? I'm not 100% sure.)

SendWrapper is required because bevy Components need to be Send, since bevy is designed to run in a multi-threaded environment. However, the Component is never actually sent across threads, since you can't currently use multiple threads in the browser's wasm context and bevy hasn't been modified to work with Web Workers or something similar (see WebAssembly multithreading tracking issue · Issue #4078 · bevyengine/bevy · GitHub and Ensure that `JsValue` isn't considered `Send` by alexcrichton · Pull Request #955 · rustwasm/wasm-bindgen · GitHub ). Therefore, we can just use SendWrapper to safely make our Client struct Send, and we never run into runtime errors caused by accessing the Component from different threads since there's no other threads anyway.

I also added a timer so messages are only sent once a second, and updated the Cargo.toml to enable optimization for dependencies in the dev profile, since otherwise I'm only getting 20 fps in the browser. With optimizations of the dependencies it reaches my monitor's refresh rate.

1 Like

Probably the implementation clarity could be significantly improved by fully abstracting away the client and putting each implementation into its own module.
Probably should make a crate out of this at some point...

1 Like

Thanks!

I'm not exactly following you here, but I did find this crate ewebsock (GitHub - rerun-io/ewebsock: A Rust Websocket client that compiles to both native and web) that claims to support both native and web (wasm)... do you think this would be a good option for me?

1 Like

You pushed these updates just for me? Thanks! :face_holding_back_tears:

I don't understand how to run it though.

When I run cargo run I get this error:

cargo run

Compiling bevy_websocket v0.1.0 (/Users/jim/g/bevy_websocket_example)

error[E0432]****: unresolved import web_sys

--> src/main.rs:126:5

|

126 | use web_sys::MessageEvent;

| ^^^^^^^ use of undeclared crate or module web_sys

warning**: unused imports: Arc and Mutex**

--> src/main.rs:4:12

|

4 | sync::{Arc, Mutex},

| ^^^ ^^^^^

|

= note: #[warn(unused_imports)] on by default

For more information about this error, try rustc --explain E0432.

warning**:** bevy_websocket (bin "bevy_websocket") generated 1 warning

error**:** could not compile bevy_websocket (bin "bevy_websocket") due to 1 previous error; 1 warning emitted

When I try to run the wasm version (is it this command?) trunk serve --no-default-features

I then get this error:

2024-10-22T17:53:45.620915Z INFO :rocket: Starting trunk 0.21.1
2024-10-22T17:53:46.043381Z ERROR error getting the canonical path to the build target HTML file "/Users/jim/g/bevy_websocket_example/index.html"
2024-10-22T17:53:46.043418Z INFO 1: No such file or directory (os error 2)

Looks good!

1 Like

Looks like I didn't test the native version again. You can run it with cargo run --target wasm32-unknown-unknown. It assumes you have wasm-server-runner installed (cargo install wasm-server-runner).

1 Like

This is awesome. Thanks!! :+1:

I think there might be a small bug in this recv_info function though:

fn recv_info(mut q: Query<(&mut WebSocketClient,)>) {
    for (mut client,) in q.iter_mut() {
        #[cfg(not(target_arch = "wasm32"))]
        {
            match client.0 .0.read() {
                Ok(m) => info!("Received message {m:?}"),
                Err(tungstenite::Error::Io(e)) if e.kind() == ErrorKind::WouldBlock => { /* ignore */
                }
                Err(e) => warn!("error receiving: {e}"),
            }
        }
        #[cfg(target_arch = "wasm32")]
        {
            while let Some(m) = client.0.recv_queue.borrow_mut().pop_front() {
                info!("Received message {m:?}")
            }
        }
    }
}

When I run it natively I can see "Received message" printed. everything good :white_check_mark:

When I run it on wasm though I don't see "Received message"... I can see logs in the browser console that I am connected and sending the messages, and I can see "Got message" be printed from the Client impl code, but the message is for some reason not making it to making it to recv_info in the wasm case. Do you know why this could be? :thinking:

1 Like

I ran your project, and I am seeing "Received message" even on wasm!! :exploding_head:

So, I think the issue is that my server is sending my a json string, in the console I see got message like this:

Got message: JsValue("{"action_type":"YouJoined","data":{"player_uuid":"c318ba7a1f774fa1b1f18e2451d01480","player_friendly_name":"Zoomy","color":"teal","x_position":0.0,"y_position":0.0,"cracker_x":-979.62286,"cracker_y":-98.00482,"cracker_points":16,"player_points":0,"all_other_players":[]}}")

I should be able to change this to pass strings around instead of the Vec right? or is it "JsValue"s that I want to through the recv_queue?

1 Like

hmm I'm trying to set this on Client:

pub recv_queue: Rc<RefCell<VecDeque<String>>>,

and then push it like this:

recv_queue
      .borrow_mut()
      .push_back(event.data().as_string());

But when I get the message on the other side it just looks like an empty array...

while let Some(m) = client.0.recv_queue.borrow_mut().pop_front() {
    web_sys::console::log_1(&format!("Received message {:?}", m).into());
}

Console is printing, "Received message: "

hmmm

1 Like

ok, got it now using JsValue from wasm_bindgen!! :+1: :pray: :rocket: :sob: :face_holding_back_tears: :dancing_women: :beers:

1 Like