274 lines
9.0 KiB
Rust
274 lines
9.0 KiB
Rust
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};
|
|
|
|
use crate::ui::{despawn_main_menu, spawn_main_menu};
|
|
|
|
mod ui;
|
|
|
|
const PADDLE_GAP: f32 = 20.0; // gap between the paddles and the wall (window border)
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.insert_state(GameState::MainMenu)
|
|
.add_systems(Startup, spawn_camera)
|
|
.add_systems(OnEnter(GameState::MainMenu), spawn_main_menu)
|
|
.add_systems(OnExit(GameState::MainMenu), despawn_main_menu)
|
|
.add_observer(ui::button_hover_start)
|
|
.add_observer(ui::button_hover_stop)
|
|
// TODO: System to operate buttons & other UI widgets
|
|
// .add_systems(Update, )
|
|
.add_systems(OnEnter(GameState::Playing), setup_game)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
input_handler,
|
|
setup_connection,
|
|
handle_tasks,
|
|
send_info,
|
|
recv_info,
|
|
)
|
|
.run_if(in_state(GameState::ConnectionDemo)),
|
|
)
|
|
.add_message::<WebSocketConnectionMessage>()
|
|
.insert_resource(ChatTimer {
|
|
timer: Timer::new(Duration::from_secs(1), TimerMode::Repeating),
|
|
})
|
|
.run();
|
|
}
|
|
|
|
/// Main game state indicator
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, States)]
|
|
enum GameState {
|
|
MainMenu,
|
|
Playing,
|
|
ConnectionDemo, // TODO: Remove this state.
|
|
}
|
|
|
|
/// Utility to despawn entities with a given component. Useful for scene
|
|
/// changes.
|
|
///
|
|
/// I'm using this primarily with marker components to delete UI elements when
|
|
/// they are no longer needed. E.g.: Removing the main menu after starting.
|
|
pub fn despawn<T: Component>(mut commands: Commands, targets: Query<Entity, With<T>>) {
|
|
targets
|
|
.iter()
|
|
.for_each(|entt| commands.entity(entt).despawn());
|
|
}
|
|
|
|
fn spawn_camera(mut commands: Commands) {
|
|
commands.spawn(Camera2d);
|
|
}
|
|
|
|
/// Initialize the scene
|
|
fn setup_game(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
|
window: Single<&Window>,
|
|
) {
|
|
// ball
|
|
commands.spawn((
|
|
Ball,
|
|
Mesh2d(meshes.add(Circle::new(10.0))),
|
|
MeshMaterial2d(materials.add(Color::srgb(1.0, 0.0, 0.0))),
|
|
Transform::default(),
|
|
));
|
|
|
|
let paddle_mesh = meshes.add(Rectangle::new(10.0, 100.0));
|
|
let paddle_material = materials.add(Color::WHITE);
|
|
// Player 1
|
|
commands.spawn((
|
|
Paddle,
|
|
Mesh2d(paddle_mesh.clone()),
|
|
MeshMaterial2d(paddle_material.clone()),
|
|
Transform::from_xyz(-window.width() / 2.0 + PADDLE_GAP, 0.0, 1.0),
|
|
));
|
|
// Player 2
|
|
commands.spawn((
|
|
Paddle,
|
|
Mesh2d(paddle_mesh),
|
|
MeshMaterial2d(paddle_material),
|
|
Transform::from_xyz(window.width() / 2.0 - PADDLE_GAP, 0.0, 1.0),
|
|
));
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct Ball;
|
|
|
|
/// Marker component for player paddles
|
|
///
|
|
/// Maybe one for each player?
|
|
/// Maybe it can hold the WebSocket, too.
|
|
/// *Maybe* I can have one struct with an Option<Ws> to know which
|
|
/// player is the local one (the one with a socket).
|
|
#[derive(Component)]
|
|
struct Paddle;
|
|
|
|
/// 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(Debug, Error)]
|
|
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}"),
|
|
}
|
|
}
|
|
}
|