Dummy client to learn the other half of WebSockets

This is mostly a copy of ambiso/bevy_websocket_example:
https://github.com/ambiso/bevy_websocket_example/tree/main

It demonstrates how to hold and use a (Tungstenite) WebSocket in a Bevy
app. I've altered it slightly (and skipped impl'ing the send function)
to fit my own chatroom dummy target.

This is completely useless as a chatroom app, but it's not supposed to
be one. Now I can move on to building the actual Pong game!
This commit is contained in:
2025-10-18 10:57:22 -05:00
parent 0ebc137369
commit 7128377924
3 changed files with 212 additions and 1 deletions

View File

@@ -1,3 +1,9 @@
[workspace]
members = ["client", "server"]
resolver = "3"
[profile.dev]
opt-level = 1
[profile.dev.package."*"]
opt-level = 3

View File

@@ -4,3 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { version = "0.17.2", features = ["serialize"] }
rustls = "0.23.33"
thiserror = "2.0.17"
tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots", "rustls"] }

View File

@@ -1,3 +1,204 @@
use std::{io::ErrorKind, net::TcpStream, time::Duration};
use bevy::{
ecs::world::CommandQueue,
prelude::*,
tasks::{AsyncComputeTaskPool, Task, block_on, futures_lite::future},
};
use thiserror::Error;
use tungstenite::{WebSocket, http::Response, stream::MaybeTlsStream};
fn main() {
println!("Hello, world!");
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(
Update,
(
input_handler,
setup_connection,
handle_tasks,
send_info,
recv_info,
),
)
.add_message::<WebSocketConnectionMessage>()
.insert_resource(ChatTimer {
timer: Timer::new(Duration::from_secs(1), TimerMode::Repeating),
})
.run();
}
/// Initialize the scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
commands.spawn(Camera2d);
commands.spawn((
Mesh2d(meshes.add(Circle::new(10.0))),
MeshMaterial2d(materials.add(Color::srgb(1.0, 0.0, 0.0))),
Transform::default(),
));
}
/// ECS Component to hold the WebSocket. I guess there's going to be a magic
/// entity that controls the networking.
#[derive(Component)]
struct WsClient(
(
WebSocket<MaybeTlsStream<TcpStream>>,
Response<Option<Vec<u8>>>,
),
);
/// Container component for a bevy task. Specifically, the async Websocket
/// setup.
#[derive(Component)]
struct WsSetupTask(Task<Result<CommandQueue, ConnSetupError>>);
/// Used to signal user input to other systems -- in particular, the notice to
/// open a connection to the server.
#[derive(Message)]
enum WebSocketConnectionMessage {
SetupConnection,
// TODO: Presumably a TeardownConnection, right?
}
#[derive(Error, Debug)]
enum ConnSetupError {
#[error("IO")]
Io(#[from] std::io::Error),
#[error("WebSocket")]
WebSocket(#[from] tungstenite::Error),
}
/// The keyboard input handler (mainly to trigger WS connection)
fn input_handler(
input: Res<ButtonInput<KeyCode>>,
mut messages: MessageWriter<WebSocketConnectionMessage>,
) {
if input.just_pressed(KeyCode::Space) {
messages.write(WebSocketConnectionMessage::SetupConnection);
}
}
/// The system which establishes the WS connection. It responds to [`WebSocketConnectionMessage`]s.
///
/// When a [`WebSocketConnectionMessage::SetupConnection`] message is emitted,
/// this system responds by creating a new (bevy) [`Task`]. This task is stored
/// in the component [`WsSetupTask`] which is attached to a new entity.
///
/// The closure executed by this task attempts to reach out to a WebSocket URL.
/// Upon successful connection, the socket is placed in a [`WsClient`]
/// component and added to the same entity. At the same time, the [`WsSetupTask`]
/// is removed since the task has now finished.
///
/// Monitoring for completion of the task is done by the system [`handle_tasks()`].
fn setup_connection(
mut commands: Commands,
mut messages: MessageReader<WebSocketConnectionMessage>,
) {
for msg in messages.read() {
match msg {
WebSocketConnectionMessage::SetupConnection => {
info!("Connecting to server...");
let url = "ws://localhost:4000/websocket";
let entity = commands.spawn_empty().id();
{
let pool = AsyncComputeTaskPool::get();
let task = pool.spawn(async move {
let mut client = tungstenite::connect(url)?;
match client.0.get_mut() {
MaybeTlsStream::Plain(p) => p.set_nonblocking(true)?,
MaybeTlsStream::Rustls(stream_owned) => {
stream_owned.get_mut().set_nonblocking(true)?
}
_ => todo!(),
};
info!("Connected!");
// First message is this client's username. I'll hard-code
// a value for this PoC.
client.0.send(tungstenite::Message::Text("Dingus".into()))?;
let mut command_queue = CommandQueue::default();
command_queue.push(move |world: &mut World| {
world
.entity_mut(entity)
.insert(WsClient(client))
.remove::<WsSetupTask>();
});
Ok(command_queue)
});
commands.entity(entity).insert(WsSetupTask(task));
}
} // _ => { there are no other variants right now, so this is pretty silly }
}
}
}
/// Polls the websocket setup task(s) to check for progress.
///
/// If the task has succeeded, it will give back a [`CommandQueue`]. This is
/// appended to the current [`Commands`] so they are acted upon this tick.
///
/// If it has failed, an error is printed to the console.
///
/// The task is self-removing, so we don't need to delete the [`WsSetupTask`]
/// component here.
fn handle_tasks(mut commands: Commands, mut transform_tasks: Query<&mut WsSetupTask>) {
for mut task in &mut transform_tasks {
if let Some(result) = block_on(future::poll_once(&mut task.0)) {
match result {
Ok(mut commands_queue) => {
commands.append(&mut commands_queue);
}
Err(e) => info!("Connection failed. Err: {e:?}"),
}
}
}
}
#[derive(Resource)]
struct ChatTimer {
timer: Timer,
}
/// Unused, left over from example code.
///
/// It *would* tick a timer, sending a message to the echo server each time the
/// timer expires. But I have a chatroom not an echo server, so this bit
/// doesn't make sense.
///
/// If I were to make a full chat client, I would replace this with some kind
/// of event handler.
fn send_info(
some_data: Query<(&Transform,)>,
time: Res<Time>,
mut client_entt: Query<(&mut WsClient,)>,
mut cfg: ResMut<ChatTimer>,
) {
// cfg.timer.tick(time.delta());
// if cfg.timer.just_finished() {
// info!("Sending username to server.");
// for
// }
}
/// Reads & prints all messages from all websockets in [`WsClient`]s.
///
/// For a real chat app, this would need to print to the screen somehow. I'd
/// need a lot of other things, too, and this is supposed to be Pong. So I'm
/// not going to fix any of that.
fn recv_info(mut q: Query<&mut WsClient>) {
for mut client in q.iter_mut() {
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}"),
}
}
}