├── wasm_client_example
├── index.scss
├── index.html
├── README.MD
├── Cargo.toml
└── src
│ ├── shared.rs
│ └── main.rs
├── .gitignore
├── README.md
├── Cargo.toml
├── examples
├── shared.rs
├── server.rs
└── client.rs
└── src
└── lib.rs
/wasm_client_example/index.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /Cargo.lock
3 | /wasm_client_example/target
4 | /wasm_client_example/dist
5 |
--------------------------------------------------------------------------------
/wasm_client_example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/wasm_client_example/README.MD:
--------------------------------------------------------------------------------
1 | # `BEMW Wasm Client Example`
2 |
3 | This client is a wasm build version of the BEMW client example. It runs in your browser and proves that BEMW and Eventwork are able to communicate between browser and native apps. You can build and run it like any other Bevy WASM app.
4 |
5 | I use [Trunk](https://trunkrs.dev/) for testing. See the [Bevy Cheatbook WASM guide](https://bevy-cheatbook.github.io/platforms/wasm.html) for more information on running Bevy in the browser.
6 |
--------------------------------------------------------------------------------
/wasm_client_example/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "wasm_client_example"
3 | version = "0.8.0"
4 | authors = ["James ", "Neikos "]
5 | edition = "2021"
6 | description = "Wasm Client Example project"
7 | readme = "README.md"
8 | repository = "https://github.com/jamescarterbell/bevy_eventwork"
9 | license = "MIT"
10 | categories = ["game-development", "network-programming"]
11 | resolver = "2"
12 |
13 |
14 | [badges]
15 | maintenance = { status = "actively-developed" }
16 |
17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
18 |
19 | [dependencies]
20 | # This is a bevy plugin
21 | bevy = "0.15.0"
22 | bevy_eventwork = { version = "0.10", default-features = false }
23 | bevy_eventwork_mod_websockets = { path = "../" }
24 | serde = { version = "1.0.190", features = ["derive"] }
25 | url = { version = "2.0.0" }
26 |
--------------------------------------------------------------------------------
/wasm_client_example/src/shared.rs:
--------------------------------------------------------------------------------
1 | use bevy::prelude::*;
2 | use bevy_eventwork::NetworkMessage;
3 | use bevy_eventwork_mod_websockets::WebSocketProvider;
4 | use serde::{Deserialize, Serialize};
5 |
6 | #[derive(Serialize, Deserialize, Clone, Debug)]
7 | pub struct UserChatMessage {
8 | pub message: String,
9 | }
10 |
11 | impl NetworkMessage for UserChatMessage {
12 | const NAME: &'static str = "example:UserChatMessage";
13 | }
14 |
15 | #[derive(Serialize, Deserialize, Clone, Debug)]
16 | pub struct NewChatMessage {
17 | pub name: String,
18 | pub message: String,
19 | }
20 |
21 | impl NetworkMessage for NewChatMessage {
22 | const NAME: &'static str = "example:NewChatMessage";
23 | }
24 |
25 | #[allow(unused)]
26 | pub fn client_register_network_messages(app: &mut App) {
27 | use bevy_eventwork::AppNetworkMessage;
28 |
29 | // The client registers messages that arrives from the server, so that
30 | // it is prepared to handle them. Otherwise, an error occurs.
31 | app.listen_for_message::();
32 | }
33 |
34 | #[allow(unused)]
35 | pub fn server_register_network_messages(app: &mut App) {
36 | use bevy_eventwork::AppNetworkMessage;
37 |
38 | // The server registers messages that arrives from a client, so that
39 | // it is prepared to handle them. Otherwise, an error occurs.
40 | app.listen_for_message::();
41 | }
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `bevy_eventwork_mod_websockets` (BEMW)
2 |
3 | [](https://bevyengine.org/learn/quick-start/plugin-development/#main-branch-tracking)
4 | [](https://crates.io/crates/bevy_eventwork_mod_websockets)
5 | [](https://docs.rs/bevy_eventwork_mod_websockets)
6 |
7 | A crate that provides a websocket networking transport layer for [Bevy_eventwork](https://github.com/jamescarterbell/bevy_eventwork) that supports WASM and Native.
8 |
9 | ## Supported Platforms
10 |
11 | - WASM
12 | - Windows
13 | - Linux
14 | - Mac
15 |
16 | ## Getting Started
17 |
18 | See [Bevy_eventwork](https://github.com/jamescarterbell/bevy_eventwork) for details on how to use `bevy_eventwork`.
19 |
20 | The only difference from `bevy_eventworks` getting started directions is to disable `bevy_eventworks` default features and use this crates `WebSocketProvider` and `NetworkSettings`.
21 | Other than that the crate functions identically to stock bevy_eventworks. No features, changes, or manual shenanigans are needed to compile for WASM.
22 | It just works.
23 |
24 | ```rust
25 | app.add_plugins(bevy_eventwork::EventworkPlugin::<
26 | WebSocketProvider,
27 | bevy::tasks::TaskPool,
28 | >::default());
29 |
30 | app.insert_resource(NetworkSettings::default());
31 |
32 | ```
33 |
34 | ## Supported Eventwork + Bevy Version
35 |
36 | | EventWork Version | BEMW Version | Bevy Version |
37 | | :---------------: | :----------: | :----------: |
38 | | 0.10 | 0.3 | 0.15 |
39 | | 0.9 | 0.2 | 0.14 |
40 | | 0.8 | 0.1 | 0.13 |
41 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "bevy_eventwork_mod_websockets"
3 | version = "0.3.1"
4 | edition = "2021"
5 | resolver = "2"
6 | description = "A Websocket NetworkProvider for Bevy_eventwork"
7 | readme = "README.md"
8 | repository = "https://github.com/NoahShomette/bevy_eventwork_mod_websockets"
9 | license = "MIT"
10 | categories = ["game-development", "network-programming"]
11 | autoexamples = false
12 | exclude = ["wasm_client_example"]
13 |
14 | [badges]
15 | maintenance = { status = "actively-developed" }
16 |
17 | [[example]]
18 | name = "client"
19 |
20 | [[example]]
21 | name = "server"
22 |
23 | [workspace]
24 | members = ["wasm_client_example"]
25 |
26 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
27 |
28 | [dependencies]
29 | bevy_eventwork = { version = "0.10", default-features = false }
30 | # This is a bevy plugin
31 | bevy = { version = "0.15.0", features = [], default-features = false }
32 | # Used for on wire serialization
33 | bincode = "1.3.3"
34 | # Used for non-tokio dependent threaded message passing
35 | async-channel = "2.3.1"
36 | # Used for providers, which are async in nature
37 | async-trait = "0.1.74"
38 | # Websocket
39 | url = { version = "2.5.4" }
40 | futures = { version = "0.3.29" }
41 |
42 | # Used 1.33.0or Stream type and other ext
43 | futures-lite = "2.5.0"
44 |
45 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
46 | async-tungstenite = { version = "0.28.0", features = [
47 | "async-std-runtime",
48 | "url",
49 | ] }
50 | async-std = { version = "1.12.0" }
51 |
52 | [target.'cfg(target_arch = "wasm32")'.dependencies]
53 | tokio-tungstenite-wasm = { version = "0.3.1" }
54 | send_wrapper = "^0.6"
55 |
56 | [dev-dependencies]
57 | bevy = { version = "0.15.0", features = ["default_font"] }
58 | serde = { version = "1.0.215", features = ["derive"] }
59 | serde_json = { version = "1.0.133" }
60 |
--------------------------------------------------------------------------------
/examples/shared.rs:
--------------------------------------------------------------------------------
1 | use bevy::prelude::*;
2 | use bevy_eventwork::NetworkMessage;
3 | use bevy_eventwork_mod_websockets::WebSocketProvider;
4 | use serde::{Deserialize, Serialize};
5 |
6 | /////////////////////////////////////////////////////////////////////
7 | // In this example the client sends `UserChatMessage`s to the server,
8 | // the server then broadcasts to all connected clients.
9 | //
10 | // We use two different types here, because only the server should
11 | // decide the identity of a given connection and thus also sends a
12 | // name.
13 | //
14 | // You can have a single message be sent both ways, it simply needs
15 | // to implement both `NetworkMessage" and both client and server can
16 | // send and recieve
17 | /////////////////////////////////////////////////////////////////////
18 |
19 | #[derive(Serialize, Deserialize, Clone, Debug)]
20 | pub struct UserChatMessage {
21 | pub message: String,
22 | }
23 |
24 | impl NetworkMessage for UserChatMessage {
25 | const NAME: &'static str = "example:UserChatMessage";
26 | }
27 |
28 | #[derive(Serialize, Deserialize, Clone, Debug)]
29 | pub struct NewChatMessage {
30 | pub name: String,
31 | pub message: String,
32 | }
33 |
34 | impl NetworkMessage for NewChatMessage {
35 | const NAME: &'static str = "example:NewChatMessage";
36 | }
37 |
38 | #[allow(unused)]
39 | pub fn client_register_network_messages(app: &mut App) {
40 | use bevy_eventwork::AppNetworkMessage;
41 |
42 | // The client registers messages that arrives from the server, so that
43 | // it is prepared to handle them. Otherwise, an error occurs.
44 | app.listen_for_message::();
45 | }
46 |
47 | #[allow(unused)]
48 | pub fn server_register_network_messages(app: &mut App) {
49 | use bevy_eventwork::AppNetworkMessage;
50 |
51 | // The server registers messages that arrives from a client, so that
52 | // it is prepared to handle them. Otherwise, an error occurs.
53 | app.listen_for_message::();
54 | }
55 |
--------------------------------------------------------------------------------
/examples/server.rs:
--------------------------------------------------------------------------------
1 | use bevy::tasks::TaskPool;
2 | use bevy::{prelude::*, tasks::TaskPoolBuilder};
3 | use bevy_eventwork::{ConnectionId, EventworkRuntime, Network, NetworkData, NetworkEvent};
4 | use bevy_eventwork_mod_websockets::{NetworkSettings, WebSocketProvider};
5 | use std::net::{IpAddr, Ipv4Addr, SocketAddr};
6 |
7 | mod shared;
8 |
9 | fn main() {
10 | let mut app = App::new();
11 | app.add_plugins((MinimalPlugins, bevy::log::LogPlugin::default()));
12 |
13 | // Before we can register the potential message types, we
14 | // need to add the plugin
15 | app.add_plugins(bevy_eventwork::EventworkPlugin::<
16 | WebSocketProvider,
17 | bevy::tasks::TaskPool,
18 | >::default());
19 |
20 | // Make sure you insert the EventworkRuntime resource with your chosen Runtime
21 | app.insert_resource(EventworkRuntime(
22 | TaskPoolBuilder::new().num_threads(2).build(),
23 | ));
24 |
25 | // A good way to ensure that you are not forgetting to register
26 | // any messages is to register them where they are defined!
27 | shared::server_register_network_messages(&mut app);
28 |
29 | app.add_systems(Startup, setup_networking);
30 | app.add_systems(Update, (handle_connection_events, handle_messages));
31 |
32 | // We have to insert the WS [`NetworkSettings`] with our chosen settings.
33 | app.insert_resource(NetworkSettings::default());
34 |
35 | app.run();
36 | }
37 |
38 | // On the server side, you need to setup networking. You do not need to do so at startup, and can start listening
39 | // at any time.
40 | fn setup_networking(
41 | mut net: ResMut>,
42 | settings: Res,
43 | task_pool: Res>,
44 | ) {
45 | let ip_address = "127.0.0.1".parse().expect("Could not parse ip address");
46 |
47 | info!("Address of the server: {}", ip_address);
48 |
49 | let _socket_address = SocketAddr::new(ip_address, 8080);
50 |
51 | match net.listen(
52 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8081),
53 | &task_pool.0,
54 | &settings,
55 | ) {
56 | Ok(_) => (),
57 | Err(err) => {
58 | error!("Could not start listening: {}", err);
59 | panic!();
60 | }
61 | }
62 |
63 | info!("Started listening for new connections!");
64 | }
65 |
66 | #[derive(Component)]
67 | struct Player(ConnectionId);
68 |
69 | fn handle_connection_events(
70 | mut commands: Commands,
71 | net: Res>,
72 | mut network_events: EventReader,
73 | ) {
74 | for event in network_events.read() {
75 | if let NetworkEvent::Connected(conn_id) = event {
76 | commands.spawn((Player(*conn_id),));
77 |
78 | // Broadcasting sends the message to all connected players! (Including the just connected one in this case)
79 | net.broadcast(shared::NewChatMessage {
80 | name: String::from("SERVER"),
81 | message: format!("New user connected; {}", conn_id),
82 | });
83 | info!("New player connected: {}", conn_id);
84 | }
85 | }
86 | }
87 |
88 | // Receiving a new message is as simple as listening for events of `NetworkData`
89 | fn handle_messages(
90 | mut new_messages: EventReader>,
91 | net: Res>,
92 | ) {
93 | for message in new_messages.read() {
94 | let user = message.source();
95 |
96 | info!("Received message from user: {}", message.message);
97 |
98 | net.broadcast(shared::NewChatMessage {
99 | name: format!("{}", user),
100 | message: message.message.clone(),
101 | });
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/examples/client.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::type_complexity)]
2 |
3 | use bevy::{
4 | color::palettes,
5 | prelude::*,
6 | tasks::{TaskPool, TaskPoolBuilder},
7 | };
8 | use bevy_eventwork::{ConnectionId, EventworkRuntime, Network, NetworkData, NetworkEvent};
9 | use bevy_eventwork_mod_websockets::{NetworkSettings, WebSocketProvider};
10 |
11 | mod shared;
12 |
13 | fn main() {
14 | let mut app = App::new();
15 |
16 | app.add_plugins(DefaultPlugins);
17 |
18 | // You need to add the `EventworkPlugin` first before you can register
19 | // `ClientMessage`s
20 | app.add_plugins(bevy_eventwork::EventworkPlugin::<
21 | WebSocketProvider,
22 | bevy::tasks::TaskPool,
23 | >::default());
24 |
25 | // Make sure you insert the EventworkRuntime resource with your chosen Runtime
26 | app.insert_resource(EventworkRuntime(
27 | TaskPoolBuilder::new().num_threads(2).build(),
28 | ));
29 |
30 | // A good way to ensure that you are not forgetting to register
31 | // any messages is to register them where they are defined!
32 | shared::client_register_network_messages(&mut app);
33 |
34 | app.add_systems(Startup, setup_ui);
35 |
36 | app.add_systems(
37 | Update,
38 | (
39 | handle_connect_button,
40 | handle_message_button,
41 | handle_incoming_messages,
42 | handle_network_events,
43 | ),
44 | );
45 |
46 | // We have to insert the WS [`NetworkSettings`] with our chosen settings.
47 | app.insert_resource(NetworkSettings::default());
48 |
49 | app.init_resource::();
50 |
51 | app.add_systems(PostUpdate, handle_chat_area);
52 |
53 | app.run();
54 | }
55 |
56 | ///////////////////////////////////////////////////////////////
57 | ////////////// Incoming Message Handler ///////////////////////
58 | ///////////////////////////////////////////////////////////////
59 |
60 | fn handle_incoming_messages(
61 | mut messages: Query<&mut GameChatMessages>,
62 | mut new_messages: EventReader>,
63 | ) {
64 | let mut messages = messages.get_single_mut().unwrap();
65 |
66 | for new_message in new_messages.read() {
67 | messages.add(UserMessage::new(&new_message.name, &new_message.message));
68 | }
69 | }
70 |
71 | fn handle_network_events(
72 | mut new_network_events: EventReader,
73 | connect_query: Query<&Children, With>,
74 | mut text_query: Query<&mut Text>,
75 | mut messages: Query<&mut GameChatMessages>,
76 | ) {
77 | let connect_children = connect_query.get_single().unwrap();
78 | let mut text = text_query.get_mut(connect_children[0]).unwrap();
79 | let mut messages = messages.get_single_mut().unwrap();
80 |
81 | for event in new_network_events.read() {
82 | info!("Received event");
83 | match event {
84 | NetworkEvent::Connected(_) => {
85 | messages.add(SystemMessage::new(
86 | "Succesfully connected to server!".to_string(),
87 | ));
88 | text.0 = String::from("Disconnect");
89 | }
90 |
91 | NetworkEvent::Disconnected(_) => {
92 | messages.add(SystemMessage::new("Disconnected from server!".to_string()));
93 | text.0 = String::from("Connect to server");
94 | }
95 | NetworkEvent::Error(err) => {
96 | messages.add(UserMessage::new(String::from("SYSTEM"), err.to_string()));
97 | }
98 | }
99 | }
100 | }
101 |
102 | ///////////////////////////////////////////////////////////////
103 | ////////////// Data Definitions ///////////////////////////////
104 | ///////////////////////////////////////////////////////////////
105 |
106 | #[derive(Resource)]
107 | struct GlobalChatSettings {
108 | chat_style: (TextFont, TextColor),
109 | author_style: (TextFont, TextColor),
110 | }
111 |
112 | impl FromWorld for GlobalChatSettings {
113 | fn from_world(_world: &mut World) -> Self {
114 | GlobalChatSettings {
115 | chat_style: (
116 | TextFont::from_font_size(20.0),
117 | TextColor::from(Color::BLACK),
118 | ),
119 | author_style: (
120 | TextFont::from_font_size(20.0),
121 | TextColor::from(palettes::css::RED),
122 | ),
123 | }
124 | }
125 | }
126 |
127 | enum ChatMessage {
128 | SystemMessage(SystemMessage),
129 | UserMessage(UserMessage),
130 | }
131 |
132 | impl ChatMessage {
133 | fn get_author(&self) -> String {
134 | match self {
135 | ChatMessage::SystemMessage(_) => "SYSTEM".to_string(),
136 | ChatMessage::UserMessage(UserMessage { user, .. }) => user.clone(),
137 | }
138 | }
139 |
140 | fn get_text(&self) -> String {
141 | match self {
142 | ChatMessage::SystemMessage(SystemMessage(msg)) => msg.clone(),
143 | ChatMessage::UserMessage(UserMessage { message, .. }) => message.clone(),
144 | }
145 | }
146 | }
147 |
148 | impl From for ChatMessage {
149 | fn from(other: SystemMessage) -> ChatMessage {
150 | ChatMessage::SystemMessage(other)
151 | }
152 | }
153 |
154 | impl From for ChatMessage {
155 | fn from(other: UserMessage) -> ChatMessage {
156 | ChatMessage::UserMessage(other)
157 | }
158 | }
159 |
160 | struct SystemMessage(String);
161 |
162 | impl SystemMessage {
163 | fn new>(msg: T) -> SystemMessage {
164 | Self(msg.into())
165 | }
166 | }
167 |
168 | #[derive(Component)]
169 | struct UserMessage {
170 | user: String,
171 | message: String,
172 | }
173 |
174 | impl UserMessage {
175 | fn new, M: Into>(user: U, message: M) -> Self {
176 | UserMessage {
177 | user: user.into(),
178 | message: message.into(),
179 | }
180 | }
181 | }
182 |
183 | #[derive(Component)]
184 | struct ChatMessages {
185 | messages: Vec,
186 | }
187 |
188 | impl ChatMessages {
189 | fn new() -> Self {
190 | ChatMessages { messages: vec![] }
191 | }
192 |
193 | fn add>(&mut self, msg: K) {
194 | let msg = msg.into();
195 | self.messages.push(msg);
196 | }
197 | }
198 |
199 | type GameChatMessages = ChatMessages;
200 |
201 | ///////////////////////////////////////////////////////////////
202 | ////////////// UI Definitions/Handlers ////////////////////////
203 | ///////////////////////////////////////////////////////////////
204 |
205 | #[derive(Component)]
206 | struct ConnectButton;
207 |
208 | fn handle_connect_button(
209 | net: ResMut>,
210 | settings: Res,
211 | interaction_query: Query<
212 | (&Interaction, &Children),
213 | (Changed, With),
214 | >,
215 | mut text_query: Query<&mut Text>,
216 | mut messages: Query<&mut GameChatMessages>,
217 | task_pool: Res>,
218 | ) {
219 | let mut messages = if let Ok(messages) = messages.get_single_mut() {
220 | messages
221 | } else {
222 | return;
223 | };
224 |
225 | for (interaction, children) in interaction_query.iter() {
226 | let mut text = text_query.get_mut(children[0]).unwrap();
227 | if let Interaction::Pressed = interaction {
228 | if net.has_connections() {
229 | net.disconnect(ConnectionId { id: 0 })
230 | .expect("Couldn't disconnect from server!");
231 | } else {
232 | text.0 = String::from("Connecting...");
233 | messages.add(SystemMessage::new("Connecting to server..."));
234 |
235 | net.connect(
236 | url::Url::parse("ws://127.0.0.1:8081").unwrap(),
237 | &task_pool.0,
238 | &settings,
239 | );
240 | }
241 | }
242 | }
243 | }
244 |
245 | #[derive(Component)]
246 | struct MessageButton;
247 |
248 | fn handle_message_button(
249 | net: Res>,
250 | interaction_query: Query<&Interaction, (Changed, With)>,
251 | mut messages: Query<&mut GameChatMessages>,
252 | ) {
253 | let mut messages = if let Ok(messages) = messages.get_single_mut() {
254 | messages
255 | } else {
256 | return;
257 | };
258 |
259 | for interaction in interaction_query.iter() {
260 | if let Interaction::Pressed = interaction {
261 | match net.send_message(
262 | ConnectionId { id: 0 },
263 | shared::UserChatMessage {
264 | message: String::from("Hello there!"),
265 | },
266 | ) {
267 | Ok(()) => (),
268 | Err(err) => messages.add(SystemMessage::new(format!(
269 | "Could not send message: {}",
270 | err
271 | ))),
272 | }
273 | }
274 | }
275 | }
276 |
277 | #[derive(Component)]
278 | struct ChatArea;
279 |
280 | fn handle_chat_area(
281 | chat_settings: Res,
282 | messages: Query<&GameChatMessages, Changed>,
283 | mut chat_text_query: Query<(Entity, &mut Text), With>,
284 | mut read_messages_index: Local,
285 | mut commands: Commands,
286 | ) {
287 | let messages = if let Ok(messages) = messages.get_single() {
288 | messages
289 | } else {
290 | return;
291 | };
292 | let (text_entity, _) = chat_text_query.get_single_mut().unwrap();
293 |
294 | for message_index in *read_messages_index..messages.messages.len() {
295 | let message = &messages.messages[message_index];
296 | let new_message = commands
297 | .spawn((
298 | Text::new(format!("{}:", message.get_author())),
299 | chat_settings.author_style.clone(),
300 | ))
301 | .with_child((
302 | TextSpan::new(format!("{}\n", message.get_text())),
303 | chat_settings.chat_style.clone(),
304 | ))
305 | .id();
306 | commands.entity(text_entity).add_children(&[new_message]);
307 | }
308 |
309 | *read_messages_index = messages.messages.len();
310 | }
311 |
312 | fn setup_ui(mut commands: Commands, _materials: ResMut>) {
313 | commands.spawn(Camera2d);
314 |
315 | commands.spawn((GameChatMessages::new(),));
316 |
317 | commands
318 | .spawn((
319 | Node {
320 | width: Val::Percent(100.0),
321 | height: Val::Percent(100.0),
322 | justify_content: JustifyContent::SpaceBetween,
323 | flex_direction: FlexDirection::ColumnReverse,
324 | ..default()
325 | },
326 | Into::::into(Color::NONE),
327 | ))
328 | .with_children(|parent| {
329 | parent
330 | .spawn(Node {
331 | width: Val::Percent(100.0),
332 | height: Val::Percent(90.0),
333 | ..default()
334 | })
335 | .with_children(|parent| {
336 | parent
337 | .spawn((
338 | Text::default(),
339 | Node {
340 | flex_direction: FlexDirection::Column,
341 | ..default()
342 | },
343 | ))
344 | .insert(ChatArea);
345 | });
346 | parent
347 | .spawn((
348 | Node {
349 | width: Val::Percent(100.0),
350 | height: Val::Percent(10.0),
351 | ..default()
352 | },
353 | Into::::into(palettes::css::GRAY),
354 | ))
355 | .with_children(|parent_button_bar| {
356 | parent_button_bar
357 | .spawn((
358 | Button,
359 | Node {
360 | width: Val::Percent(50.0),
361 | height: Val::Percent(100.0),
362 | align_items: AlignItems::Center,
363 | justify_content: JustifyContent::Center,
364 | ..default()
365 | },
366 | ))
367 | .insert(MessageButton)
368 | .with_children(|button| {
369 | button.spawn((
370 | Text::new("Send Message!"),
371 | TextFont::from_font_size(40.0),
372 | TextColor::from(Color::BLACK),
373 | TextLayout::new_with_justify(JustifyText::Center),
374 | ));
375 | });
376 |
377 | parent_button_bar
378 | .spawn((
379 | Button,
380 | Node {
381 | width: Val::Percent(50.0),
382 | height: Val::Percent(100.0),
383 | align_items: AlignItems::Center,
384 | justify_content: JustifyContent::Center,
385 | ..default()
386 | },
387 | ))
388 | .insert(ConnectButton)
389 | .with_children(|button| {
390 | button.spawn((
391 | Text::new("Connect to server"),
392 | TextFont::from_font_size(40.0),
393 | TextColor::from(Color::BLACK),
394 | TextLayout::new_with_justify(JustifyText::Center),
395 | ));
396 | });
397 | });
398 | });
399 | }
400 |
--------------------------------------------------------------------------------
/wasm_client_example/src/main.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::type_complexity)]
2 |
3 | use bevy::{
4 | color::palettes,
5 | prelude::*,
6 | tasks::{TaskPool, TaskPoolBuilder},
7 | };
8 | use bevy_eventwork::{ConnectionId, EventworkRuntime, Network, NetworkData, NetworkEvent};
9 |
10 | use bevy_eventwork_mod_websockets::{NetworkSettings, WebSocketProvider};
11 |
12 | mod shared;
13 |
14 | fn main() {
15 | let mut app = App::new();
16 |
17 | app.add_plugins(DefaultPlugins);
18 |
19 | // You need to add the `ClientPlugin` first before you can register
20 | // `ClientMessage`s
21 | app.add_plugins(bevy_eventwork::EventworkPlugin::<
22 | WebSocketProvider,
23 | bevy::tasks::TaskPool,
24 | >::default());
25 |
26 | // Make sure you insert the EventworkRuntime resource with your chosen Runtime
27 | app.insert_resource(EventworkRuntime(
28 | TaskPoolBuilder::new().num_threads(2).build(),
29 | ));
30 |
31 | // A good way to ensure that you are not forgetting to register
32 | // any messages is to register them where they are defined!
33 | shared::client_register_network_messages(&mut app);
34 |
35 | app.add_systems(Startup, setup_ui);
36 |
37 | app.add_systems(
38 | Update,
39 | (
40 | handle_connect_button,
41 | handle_message_button,
42 | handle_incoming_messages,
43 | handle_network_events,
44 | ),
45 | );
46 |
47 | // We have to insert the TCP [`NetworkSettings`] with our chosen settings.
48 | app.insert_resource(NetworkSettings::default());
49 |
50 | app.init_resource::();
51 |
52 | app.add_systems(PostUpdate, handle_chat_area);
53 |
54 | app.run();
55 | }
56 |
57 | ///////////////////////////////////////////////////////////////
58 | ////////////// Incoming Message Handler ///////////////////////
59 | ///////////////////////////////////////////////////////////////
60 |
61 | fn handle_incoming_messages(
62 | mut messages: Query<&mut GameChatMessages>,
63 | mut new_messages: EventReader>,
64 | ) {
65 | let mut messages = messages.get_single_mut().unwrap();
66 |
67 | for new_message in new_messages.read() {
68 | messages.add(UserMessage::new(&new_message.name, &new_message.message));
69 | }
70 | }
71 |
72 | fn handle_network_events(
73 | mut new_network_events: EventReader,
74 | connect_query: Query<&Children, With>,
75 | mut text_query: Query<&mut Text>,
76 | mut messages: Query<&mut GameChatMessages>,
77 | ) {
78 | let connect_children = connect_query.get_single().unwrap();
79 | let mut text = text_query.get_mut(connect_children[0]).unwrap();
80 | let mut messages = messages.get_single_mut().unwrap();
81 |
82 | for event in new_network_events.read() {
83 | info!("Received event");
84 | match event {
85 | NetworkEvent::Connected(_) => {
86 | messages.add(SystemMessage::new(
87 | "Succesfully connected to server!".to_string(),
88 | ));
89 | text.0 = String::from("Disconnect");
90 | }
91 |
92 | NetworkEvent::Disconnected(_) => {
93 | messages.add(SystemMessage::new("Disconnected from server!".to_string()));
94 | text.0 = String::from("Connect to server");
95 | }
96 | NetworkEvent::Error(err) => {
97 | messages.add(UserMessage::new(String::from("SYSTEM"), err.to_string()));
98 | }
99 | }
100 | }
101 | }
102 |
103 | ///////////////////////////////////////////////////////////////
104 | ////////////// Data Definitions ///////////////////////////////
105 | ///////////////////////////////////////////////////////////////
106 |
107 | #[derive(Resource)]
108 | struct GlobalChatSettings {
109 | chat_style: (TextFont, TextColor),
110 | author_style: (TextFont, TextColor),
111 | }
112 |
113 | impl FromWorld for GlobalChatSettings {
114 | fn from_world(_world: &mut World) -> Self {
115 | GlobalChatSettings {
116 | chat_style: (
117 | TextFont::from_font_size(20.0),
118 | TextColor::from(Color::BLACK),
119 | ),
120 | author_style: (
121 | TextFont::from_font_size(20.0),
122 | TextColor::from(palettes::css::RED),
123 | ),
124 | }
125 | }
126 | }
127 |
128 | enum ChatMessage {
129 | SystemMessage(SystemMessage),
130 | UserMessage(UserMessage),
131 | }
132 |
133 | impl ChatMessage {
134 | fn get_author(&self) -> String {
135 | match self {
136 | ChatMessage::SystemMessage(_) => "SYSTEM".to_string(),
137 | ChatMessage::UserMessage(UserMessage { user, .. }) => user.clone(),
138 | }
139 | }
140 |
141 | fn get_text(&self) -> String {
142 | match self {
143 | ChatMessage::SystemMessage(SystemMessage(msg)) => msg.clone(),
144 | ChatMessage::UserMessage(UserMessage { message, .. }) => message.clone(),
145 | }
146 | }
147 | }
148 |
149 | impl From for ChatMessage {
150 | fn from(other: SystemMessage) -> ChatMessage {
151 | ChatMessage::SystemMessage(other)
152 | }
153 | }
154 |
155 | impl From for ChatMessage {
156 | fn from(other: UserMessage) -> ChatMessage {
157 | ChatMessage::UserMessage(other)
158 | }
159 | }
160 |
161 | struct SystemMessage(String);
162 |
163 | impl SystemMessage {
164 | fn new>(msg: T) -> SystemMessage {
165 | Self(msg.into())
166 | }
167 | }
168 |
169 | #[derive(Component)]
170 | struct UserMessage {
171 | user: String,
172 | message: String,
173 | }
174 |
175 | impl UserMessage {
176 | fn new, M: Into>(user: U, message: M) -> Self {
177 | UserMessage {
178 | user: user.into(),
179 | message: message.into(),
180 | }
181 | }
182 | }
183 |
184 | #[derive(Component)]
185 | struct ChatMessages {
186 | messages: Vec,
187 | }
188 |
189 | impl ChatMessages {
190 | fn new() -> Self {
191 | ChatMessages { messages: vec![] }
192 | }
193 |
194 | fn add>(&mut self, msg: K) {
195 | let msg = msg.into();
196 | self.messages.push(msg);
197 | }
198 | }
199 |
200 | type GameChatMessages = ChatMessages;
201 |
202 | ///////////////////////////////////////////////////////////////
203 | ////////////// UI Definitions/Handlers ////////////////////////
204 | ///////////////////////////////////////////////////////////////
205 |
206 | #[derive(Component)]
207 | struct ConnectButton;
208 |
209 | fn handle_connect_button(
210 | net: ResMut>,
211 | settings: Res,
212 | interaction_query: Query<
213 | (&Interaction, &Children),
214 | (Changed, With),
215 | >,
216 | mut text_query: Query<&mut Text>,
217 | mut messages: Query<&mut GameChatMessages>,
218 | task_pool: Res>,
219 | ) {
220 | let mut messages = if let Ok(messages) = messages.get_single_mut() {
221 | messages
222 | } else {
223 | return;
224 | };
225 |
226 | for (interaction, children) in interaction_query.iter() {
227 | let mut text = text_query.get_mut(children[0]).unwrap();
228 | if let Interaction::Pressed = interaction {
229 | if net.has_connections() {
230 | net.disconnect(ConnectionId { id: 0 })
231 | .expect("Couldn't disconnect from server!");
232 | } else {
233 | text.0 = String::from("Connecting...");
234 | messages.add(SystemMessage::new("Connecting to server..."));
235 |
236 | net.connect(
237 | url::Url::parse("ws://127.0.0.1:8081").unwrap(),
238 | &task_pool.0,
239 | &settings,
240 | );
241 | }
242 | }
243 | }
244 | }
245 |
246 | #[derive(Component)]
247 | struct MessageButton;
248 |
249 | fn handle_message_button(
250 | net: Res>,
251 | interaction_query: Query<&Interaction, (Changed, With)>,
252 | mut messages: Query<&mut GameChatMessages>,
253 | ) {
254 | let mut messages = if let Ok(messages) = messages.get_single_mut() {
255 | messages
256 | } else {
257 | return;
258 | };
259 |
260 | for interaction in interaction_query.iter() {
261 | if let Interaction::Pressed = interaction {
262 | match net.send_message(
263 | ConnectionId { id: 0 },
264 | shared::UserChatMessage {
265 | message: String::from("Hello there!"),
266 | },
267 | ) {
268 | Ok(()) => (),
269 | Err(err) => messages.add(SystemMessage::new(format!(
270 | "Could not send message: {}",
271 | err
272 | ))),
273 | }
274 | }
275 | }
276 | }
277 |
278 | #[derive(Component)]
279 | struct ChatArea;
280 |
281 | fn handle_chat_area(
282 | chat_settings: Res,
283 | messages: Query<&GameChatMessages, Changed>,
284 | mut chat_text_query: Query<(Entity, &mut Text), With>,
285 | mut read_messages_index: Local,
286 | mut commands: Commands,
287 | ) {
288 | let messages = if let Ok(messages) = messages.get_single() {
289 | messages
290 | } else {
291 | return;
292 | };
293 | let (text_entity, _) = chat_text_query.get_single_mut().unwrap();
294 |
295 | for message_index in *read_messages_index..messages.messages.len() {
296 | let message = &messages.messages[message_index];
297 | let new_message = commands
298 | .spawn((
299 | Text::new(format!("{}:", message.get_author())),
300 | chat_settings.author_style.clone(),
301 | ))
302 | .with_child((
303 | TextSpan::new(format!("{}\n", message.get_text())),
304 | chat_settings.chat_style.clone(),
305 | ))
306 | .id();
307 | commands.entity(text_entity).add_children(&[new_message]);
308 | }
309 |
310 | *read_messages_index = messages.messages.len();
311 | }
312 |
313 | fn setup_ui(mut commands: Commands, _materials: ResMut>) {
314 | commands.spawn(Camera2d);
315 |
316 | commands.spawn((GameChatMessages::new(),));
317 |
318 | commands
319 | .spawn((
320 | Node {
321 | width: Val::Percent(100.0),
322 | height: Val::Percent(100.0),
323 | justify_content: JustifyContent::SpaceBetween,
324 | flex_direction: FlexDirection::ColumnReverse,
325 | ..default()
326 | },
327 | Into::::into(Color::NONE),
328 | ))
329 | .with_children(|parent| {
330 | parent
331 | .spawn(Node {
332 | width: Val::Percent(100.0),
333 | height: Val::Percent(90.0),
334 | ..default()
335 | })
336 | .with_children(|parent| {
337 | parent
338 | .spawn((
339 | Text::default(),
340 | Node {
341 | flex_direction: FlexDirection::Column,
342 | ..default()
343 | },
344 | ))
345 | .insert(ChatArea);
346 | });
347 | parent
348 | .spawn((
349 | Node {
350 | width: Val::Percent(100.0),
351 | height: Val::Percent(10.0),
352 | ..default()
353 | },
354 | Into::::into(palettes::css::GRAY),
355 | ))
356 | .with_children(|parent_button_bar| {
357 | parent_button_bar
358 | .spawn((
359 | Button,
360 | Node {
361 | width: Val::Percent(50.0),
362 | height: Val::Percent(100.0),
363 | align_items: AlignItems::Center,
364 | justify_content: JustifyContent::Center,
365 | ..default()
366 | },
367 | ))
368 | .insert(MessageButton)
369 | .with_children(|button| {
370 | button.spawn((
371 | Text::new("Send Message!"),
372 | TextFont::from_font_size(40.0),
373 | TextColor::from(Color::BLACK),
374 | TextLayout::new_with_justify(JustifyText::Center),
375 | ));
376 | });
377 |
378 | parent_button_bar
379 | .spawn((
380 | Button,
381 | Node {
382 | width: Val::Percent(50.0),
383 | height: Val::Percent(100.0),
384 | align_items: AlignItems::Center,
385 | justify_content: JustifyContent::Center,
386 | ..default()
387 | },
388 | ))
389 | .insert(ConnectButton)
390 | .with_children(|button| {
391 | button.spawn((
392 | Text::new("Connect to server"),
393 | TextFont::from_font_size(40.0),
394 | TextColor::from(Color::BLACK),
395 | TextLayout::new_with_justify(JustifyText::Center),
396 | ));
397 | });
398 | });
399 | });
400 | }
401 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | /// A provider for WebSockets
2 | #[cfg(not(target_arch = "wasm32"))]
3 | pub type WebSocketProvider = native_websocket::NativeWesocketProvider;
4 |
5 | /// A provider for WebSockets
6 | #[cfg(target_arch = "wasm32")]
7 | pub type WebSocketProvider = wasm_websocket::WasmWebSocketProvider;
8 |
9 | #[cfg(not(target_arch = "wasm32"))]
10 | pub use native_websocket::NetworkSettings;
11 |
12 | #[cfg(target_arch = "wasm32")]
13 | pub use wasm_websocket::NetworkSettings;
14 |
15 | #[cfg(not(target_arch = "wasm32"))]
16 | mod native_websocket {
17 | use std::{net::SocketAddr, pin::Pin};
18 |
19 | use async_channel::{Receiver, Sender};
20 | use async_std::net::{TcpListener, TcpStream};
21 | use async_trait::async_trait;
22 | use async_tungstenite::{
23 | tungstenite::{protocol::WebSocketConfig, Message},
24 | WebSocketStream,
25 | };
26 | use bevy::prelude::{error, info, trace, Deref, DerefMut, Resource};
27 | use bevy_eventwork::{error::NetworkError, managers::NetworkProvider, NetworkPacket};
28 | use futures::{
29 | stream::{SplitSink, SplitStream},
30 | SinkExt, StreamExt,
31 | };
32 | use futures_lite::{Future, FutureExt, Stream};
33 |
34 | /// A provider for WebSockets
35 | #[derive(Default, Debug)]
36 | pub struct NativeWesocketProvider;
37 |
38 | #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
39 | #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
40 | impl NetworkProvider for NativeWesocketProvider {
41 | type NetworkSettings = NetworkSettings;
42 |
43 | type Socket = WebSocketStream;
44 |
45 | type ReadHalf = SplitStream>;
46 |
47 | type WriteHalf = SplitSink, Message>;
48 |
49 | type ConnectInfo = url::Url;
50 |
51 | type AcceptInfo = SocketAddr;
52 |
53 | type AcceptStream = OwnedIncoming;
54 |
55 | async fn accept_loop(
56 | accept_info: Self::AcceptInfo,
57 | _: Self::NetworkSettings,
58 | ) -> Result {
59 | let listener = TcpListener::bind(accept_info)
60 | .await
61 | .map_err(NetworkError::Listen)?;
62 | Ok(OwnedIncoming::new(listener))
63 | }
64 |
65 | async fn connect_task(
66 | connect_info: Self::ConnectInfo,
67 | network_settings: Self::NetworkSettings,
68 | ) -> Result {
69 | info!("Beginning connection");
70 | let (stream, _response) = async_tungstenite::async_std::connect_async_with_config(
71 | connect_info,
72 | Some(*network_settings),
73 | )
74 | .await
75 | .map_err(|error| match error {
76 | async_tungstenite::tungstenite::Error::ConnectionClosed => {
77 | NetworkError::Error(String::from("Connection closed"))
78 | }
79 | async_tungstenite::tungstenite::Error::AlreadyClosed => {
80 | NetworkError::Error(String::from("Connection was already closed"))
81 | }
82 | async_tungstenite::tungstenite::Error::Io(io_error) => {
83 | NetworkError::Error(format!("Io Error: {}", io_error))
84 | }
85 | async_tungstenite::tungstenite::Error::Tls(tls_error) => {
86 | NetworkError::Error(format!("Tls Error: {}", tls_error))
87 | }
88 | async_tungstenite::tungstenite::Error::Capacity(cap) => {
89 | NetworkError::Error(format!("Capacity Error: {}", cap))
90 | }
91 | async_tungstenite::tungstenite::Error::Protocol(proto) => {
92 | NetworkError::Error(format!("Protocol Error: {}", proto))
93 | }
94 | async_tungstenite::tungstenite::Error::WriteBufferFull(buf) => {
95 | NetworkError::Error(format!("Write Buffer Full Error: {}", buf))
96 | }
97 | async_tungstenite::tungstenite::Error::Utf8 => {
98 | NetworkError::Error("Utf8 Error".to_string())
99 | }
100 | async_tungstenite::tungstenite::Error::AttackAttempt => {
101 | NetworkError::Error("Attack Attempt".to_string())
102 | }
103 | async_tungstenite::tungstenite::Error::Url(url) => {
104 | NetworkError::Error(format!("Url Error: {}", url))
105 | }
106 | async_tungstenite::tungstenite::Error::Http(http) => {
107 | NetworkError::Error(format!("HTTP Error: {:?}", http))
108 | }
109 | async_tungstenite::tungstenite::Error::HttpFormat(http_format) => {
110 | NetworkError::Error(format!("HTTP Format Error: {}", http_format))
111 | }
112 | })?;
113 | info!("Connected!");
114 | return Ok(stream);
115 | }
116 |
117 | async fn recv_loop(
118 | mut read_half: Self::ReadHalf,
119 | messages: Sender,
120 | _settings: Self::NetworkSettings,
121 | ) {
122 | loop {
123 | let message = match read_half.next().await {
124 | Some(message) => match message {
125 | Ok(message) => message,
126 | Err(err) => match err {
127 | async_tungstenite::tungstenite::Error::ConnectionClosed
128 | | async_tungstenite::tungstenite::Error::AlreadyClosed => {
129 | error!("Connection Closed");
130 | break;
131 | }
132 | _ => {
133 | error!("Nonfatal error detected: {}", err);
134 | continue;
135 | }
136 | },
137 | },
138 | None => {
139 | continue;
140 | }
141 | };
142 |
143 | let packet = match message {
144 | Message::Text(_) => {
145 | error!("Text Message Received");
146 | break;
147 | }
148 | Message::Binary(binary) => match bincode::deserialize(&binary) {
149 | Ok(packet) => packet,
150 | Err(err) => {
151 | error!("Failed to decode network packet from: {}", err);
152 | break;
153 | }
154 | },
155 | Message::Ping(_) => {
156 | error!("Ping Message Received");
157 | break;
158 | }
159 | Message::Pong(_) => {
160 | error!("Pong Message Received");
161 | break;
162 | }
163 | Message::Close(_) => {
164 | error!("Connection Closed");
165 | break;
166 | }
167 | Message::Frame(_) => todo!(),
168 | };
169 |
170 | if messages.send(packet).await.is_err() {
171 | error!("Failed to send decoded message to eventwork");
172 | break;
173 | }
174 | info!("Message deserialized and sent to eventwork");
175 | }
176 | }
177 |
178 | async fn send_loop(
179 | mut write_half: Self::WriteHalf,
180 | messages: Receiver,
181 | _settings: Self::NetworkSettings,
182 | ) {
183 | while let Ok(message) = messages.recv().await {
184 | let encoded = match bincode::serialize(&message) {
185 | Ok(encoded) => encoded,
186 | Err(err) => {
187 | error!("Could not encode packet {:?}: {}", message, err);
188 | continue;
189 | }
190 | };
191 |
192 | trace!("Sending the content of the message!");
193 |
194 | match write_half
195 | .send(async_tungstenite::tungstenite::Message::Binary(encoded))
196 | .await
197 | {
198 | Ok(_) => (),
199 | Err(err) => {
200 | error!("Could not send packet: {:?}: {}", message, err);
201 | break;
202 | }
203 | }
204 |
205 | trace!("Succesfully written all!");
206 | }
207 | }
208 |
209 | fn split(combined: Self::Socket) -> (Self::ReadHalf, Self::WriteHalf) {
210 | let (write, read) = combined.split();
211 | (read, write)
212 | }
213 | }
214 |
215 | #[derive(Clone, Debug, Resource, Default, Deref, DerefMut)]
216 | #[allow(missing_copy_implementations)]
217 | /// Settings to configure the network, both client and server
218 | pub struct NetworkSettings(WebSocketConfig);
219 |
220 | /// A special stream for recieving ws connections
221 | #[allow(clippy::type_complexity)]
222 | pub struct OwnedIncoming {
223 | inner: TcpListener,
224 | stream: Option>>>>>,
225 | }
226 |
227 | impl OwnedIncoming {
228 | fn new(listener: TcpListener) -> Self {
229 | Self {
230 | inner: listener,
231 | stream: None,
232 | }
233 | }
234 | }
235 |
236 | impl Stream for OwnedIncoming {
237 | type Item = WebSocketStream;
238 |
239 | fn poll_next(
240 | self: Pin<&mut Self>,
241 | cx: &mut std::task::Context<'_>,
242 | ) -> std::task::Poll