Compare commits

..

5 Commits

Author SHA1 Message Date
7837ab49b0 Start button triggers connection & state change
When the start button is pressed, switch to a "connecting" state. This
triggers the spawning of the "connecting" UI message and the connection
startup.

When the connection task finishes, `fn handle_tasks()` collects it and
pushes the CommandQueue into the main world just as before. In addition,
it will change to the "playing" state, which triggers the despawning of
the UI notice.

There is no meaningful connection-error handling path. A failed
connection will print a warning to stdout, and that is all.

There is still no transmitter at all, nor is the receiver hooked up to
one of the paddles.
2025-10-21 13:55:58 -05:00
62c84aceaf Remove stale TODO note 2025-10-21 12:52:53 -05:00
7ac3882e9e Marker components for the ball & paddles 2025-10-21 12:36:47 -05:00
129e9ccc5e Spawn the gameplay elements
Now to start wiring them in for basic operation, then to pipe it all
through the WebSocket.
2025-10-21 08:37:18 -05:00
8b290fdd0d Implement menu despawner 2025-10-20 08:07:58 -05:00
2 changed files with 99 additions and 9 deletions

View File

@@ -8,10 +8,15 @@ use bevy::{
use thiserror::Error;
use tungstenite::{WebSocket, http::Response, stream::MaybeTlsStream};
use crate::ui::{despawn_main_menu, spawn_main_menu};
use crate::ui::{
despawn_connection_wait_screen, despawn_main_menu, spawn_connection_wait_screen,
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)
@@ -21,8 +26,25 @@ fn main() {
.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::Connecting),
(
spawn_connection_wait_screen,
// Closure to immediately dispatch a setup-connection request.
|mut messages: MessageWriter<WebSocketConnectionMessage>| {
messages.write(WebSocketConnectionMessage::SetupConnection);
},
),
)
.add_systems(
OnExit(GameState::Connecting),
despawn_connection_wait_screen,
)
.add_systems(
Update,
(setup_connection, handle_tasks).run_if(in_state(GameState::Connecting)),
)
.add_systems(OnEnter(GameState::Playing), setup_game)
.add_systems(
Update,
(
@@ -45,6 +67,7 @@ fn main() {
#[derive(Clone, Debug, Eq, Hash, PartialEq, States)]
enum GameState {
MainMenu,
Connecting,
Playing,
ConnectionDemo, // TODO: Remove this state.
}
@@ -65,19 +88,50 @@ fn spawn_camera(mut commands: Commands) {
}
/// Initialize the scene
fn setup(
fn setup_game(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
window: Single<&Window>,
) {
commands.spawn(Camera2d);
// 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)]
@@ -184,12 +238,17 @@ fn setup_connection(
///
/// 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>) {
fn handle_tasks(
mut commands: Commands,
mut transform_tasks: Query<&mut WsSetupTask>,
mut states: ResMut<NextState<GameState>>,
) {
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);
states.set(GameState::Playing);
}
Err(e) => info!("Connection failed. Err: {e:?}"),
}

View File

@@ -33,7 +33,7 @@ pub fn spawn_main_menu(mut commands: Commands) {
let mut start_button = cmds.spawn(button_bundle("Start game"));
start_button.observe(
|_trigger: On<Pointer<Click>>, mut game_state: ResMut<NextState<GameState>>| {
game_state.set(GameState::Playing);
game_state.set(GameState::Connecting);
},
);
let mut quit_button = cmds.spawn(button_bundle("Quit Game"));
@@ -46,8 +46,39 @@ pub fn spawn_main_menu(mut commands: Commands) {
});
}
pub fn despawn_main_menu(mut commands: Commands) {
warn!("->> ui.rs: despawn_main_menu() is not implemented.");
/// Despawns the main menu (which is assumed to be the top-most node)
///
/// TODO: Add a marker component, but only in debug builds. A unit test can
/// assert this condition so I don't actually have to check it at runtime
/// on release builds.
pub fn despawn_main_menu(
mut commands: Commands,
top_node: Single<Entity, (With<Node>, Without<ChildOf>)>,
) {
commands.entity(top_node.into_inner()).despawn();
}
pub fn spawn_connection_wait_screen(mut commands: Commands) {
info!("Spawning connecting notice.");
commands.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
flex_direction: FlexDirection::Column,
..Default::default()
},
children![Text::new("Connecting...")],
));
}
pub fn despawn_connection_wait_screen(
mut commands: Commands,
text_nodes: Single<Entity, (With<Node>, With<Text>)>,
) {
info!("Despawning connecting notice.");
commands.entity(text_nodes.into_inner()).despawn();
}
/// The basic bundle for generic buttons.