From 729c74e9327513ca6eae2fe7f909767f501d4810 Mon Sep 17 00:00:00 2001 From: "Elijah C. Voigt" Date: Tue, 7 Nov 2023 19:55:16 -0800 Subject: [PATCH] Select refactors for 2d, sync select/deselects --- assets/models/Martian Chess.glb | 4 +- src/display2d.rs | 225 +++++++++++--------------------- src/display3d.rs | 171 ++++++++++++++++-------- src/game.rs | 80 ++++++++---- src/{hit3d.rs => hit.rs} | 45 +++++-- src/main.rs | 2 +- src/menu.rs | 4 +- 7 files changed, 286 insertions(+), 245 deletions(-) rename src/{hit3d.rs => hit.rs} (78%) diff --git a/assets/models/Martian Chess.glb b/assets/models/Martian Chess.glb index 4f94fb4..60bc757 100644 --- a/assets/models/Martian Chess.glb +++ b/assets/models/Martian Chess.glb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e69424bcc770fe206da12b2539a4a6f0d7dd2649e25e24fdcc5335d70666fa7 -size 29632044 +oid sha256:2288bf563607c4ace5df10ec7c791c64a7f528ceab860c52c448ba8043752262 +size 30428676 diff --git a/src/display2d.rs b/src/display2d.rs index 13493ae..935701c 100644 --- a/src/display2d.rs +++ b/src/display2d.rs @@ -1,15 +1,10 @@ -use bevy::{ - input::{mouse::MouseButtonInput, ButtonState}, - window::{PrimaryWindow, WindowResized}, -}; - -/// /// TODO: Pick up and move pieces! /// TODO: Custom Asset: SpriteSheetAtlas Mapper /// TODO: Handle Cursor! -/// +use bevy::window::{PrimaryWindow, WindowResized}; + use crate::{ - game::{ActiveTile, Board, BoardIndex, Piece, Side, Tile}, + game::{Board, BoardIndex, Piece, Side, Tile}, prelude::*, }; @@ -26,18 +21,15 @@ impl Plugin for Display2dPlugin { .add_systems( Update, ( - active_tile.run_if(in_state(GameState::Display2d)), menu::exit_to_menu.run_if(in_state(GameState::Display2d)), - select_piece.run_if(in_state(GameState::Display2d)), move_piece .run_if(in_state(GameState::Display2d)) .run_if(any_with_component::()), - place_piece - .run_if(in_state(GameState::Display2d)) - .run_if(any_with_component::()), - cancel_place - .run_if(in_state(GameState::Display2d)) - .run_if(on_event::()), + select.run_if(in_state(GameState::Display2d)).run_if( + |buttons: Res>| -> bool { + buttons.just_pressed(MouseButton::Left) + }, + ), snap_back_cancel .run_if(in_state(GameState::Display2d)) .run_if(any_component_removed::()), @@ -157,7 +149,13 @@ fn initialize_board(board: Option>, mut commands: Commands) { .with_children(|parent| { // Spawn tiles game::tiles().for_each(|(index, tile)| { - parent.spawn((tile, index, Display2d, SpriteSheetBundle { ..default() })); + parent.spawn(( + tile, + index, + Display2d, + SpriteSheetBundle { ..default() }, + game::Selectable, + )); }); // Spawn pieces @@ -170,6 +168,7 @@ fn initialize_board(board: Option>, mut commands: Commands) { index.clone(), side, SpriteSheetBundle { ..default() }, + game::Selectable, )); }); }); @@ -249,74 +248,67 @@ fn set_transform( }); } -fn active_tile( - mut events: EventReader, - sprite_q: Query<( - &TextureAtlasSprite, - &Handle, - &GlobalTransform, - &BoardIndex, - )>, - camera_q: Query<(&Camera, &GlobalTransform), With>, +/// Select pieces and tiles in 2d +fn select( + candidates: Query< + ( + Entity, + &TextureAtlasSprite, + &Handle, + &GlobalTransform, + ), + ( + With, + Without, + With, + ), + >, + windows: Query<&Window, With>, + cameras: Query<(&Camera, &GlobalTransform), With>, atlases: Res>, - mut active: ResMut, -) { - events.iter().for_each(|CursorMoved { position, .. }| { - if let Some(position) = camera_q - .iter() - .find_map(|(camera, transform)| camera.viewport_to_world_2d(transform, *position)) - { - let idx = sprite_q.iter().find_map( - |(TextureAtlasSprite { index, anchor, .. }, handle, transform, board_index)| { - // Implementation credit goes to the sprite bevy_mod_picking backend - // TODO: Upstream changes - let pos = transform.translation(); - - let size = { - let sprite_size = atlases - .get(handle) - .map(|atlas| atlas.textures.get(*index).expect("Get this rect texture")) - .expect("get this rect") - .size(); - let (transform_scale, _, _) = transform.to_scale_rotation_translation(); - sprite_size * transform_scale.truncate() - }; - - let center = pos.truncate() - (anchor.as_vec() * size); - let rect = Rect::from_center_half_size(center, size / 2.0); - - rect.contains(position).then_some(board_index) - }, - ); - if active.idx != idx.cloned() { - active.idx = idx.cloned(); - } - } - }); -} - -fn select_piece( - mut events: EventReader, - pieces: Query<(Entity, &BoardIndex), (With, With)>, - active: Res, mut commands: Commands, - mut writer: EventWriter, ) { - events + // For each window (there is only one) + windows .iter() - .filter_map(|MouseButtonInput { button, state, .. }| { - active.idx.as_ref().and_then(|index| { - pieces.iter().find_map(|(entity, board_idx)| { - (board_idx == index).then_some((entity, button, state)) - }) - }) - }) - .filter_map(|(entity, button, state)| { - ((*button, *state) == (MouseButton::Left, ButtonState::Pressed)).then_some(entity) - }) - .for_each(|entity| { - commands.entity(entity).insert(game::Selected); - writer.send(game::GameEvent::SelectPiece); + .filter_map(|window| window.cursor_position()) + .for_each(|position| { + // For each 2d Camera (there is only one) + cameras + .iter() + .filter_map(|(camera, transform)| camera.viewport_to_world_2d(transform, position)) + .for_each(|pos| { + // For each selectable sprite (there are many) + // Filter down to the list of hit objects + candidates + .iter() + .filter_map( + |( + entity, + TextureAtlasSprite { index, anchor, .. }, + handle, + transform, + )| { + let sprite_size = atlases + .get(handle) + .map(|atlas| { + atlas.textures.get(*index).expect("Atlas Sprite Texture") + }) + .expect("Atlas Sprite Rectangle") + .size(); + hit::intersects2d(sprite_size, anchor, transform, pos) + .then_some((entity, transform)) + }, + ) + .max_by(|(_, a), (_, b)| { + a.translation().z.partial_cmp(&b.translation().z).unwrap() + }) + .iter() + .for_each(|(entity, _)| { + info!("Selected entity {:?}", entity); + commands.entity(*entity).insert(game::Selected); + }); + }); }); } @@ -338,75 +330,6 @@ fn move_piece( }) } -/// -/// TODO: Only place piece if spot is valid move for the selected entity -/// -fn place_piece( - mut events: EventReader, - current: Query< - (Entity, &BoardIndex), - (With, With, With), - >, - pieces: Query<&BoardIndex, (Without, With, With)>, - active: Res, - mut board: ResMut, - mut commands: Commands, - mut game_events: EventWriter, - mut move_events: EventWriter, -) { - events - .iter() - .filter_map(|MouseButtonInput { button, state, .. }| { - if (*button, *state) == (MouseButton::Left, ButtonState::Pressed) { - active.idx.as_ref() - } else { - None - } - }) - .filter_map(|idx| { - if !current.is_empty() { - (!pieces.iter().any(|board_index| board_index == idx)).then_some(( - current.single().0, - current.single().1, - idx, - )) - } else { - None - } - }) - .for_each(|(entity, from, to)| match board.move_piece(from, to) { - Ok(moves) => { - commands.entity(entity).remove::(); - moves.iter().for_each(|m| { - move_events.send(m.clone()); - }); - game_events.send(game::GameEvent::PlacePiece); - } - Err(game::GameError::NullMove) => game_events.send(game::GameEvent::PlacePiece), - Err(game::GameError::InvalidMove) => warn!("Invalid move!"), - Err(game::GameError::InvalidIndex) => warn!("Invalid index!"), - }) -} - -fn cancel_place( - mut events: EventReader, - current: Query, With, With)>, - mut commands: Commands, - mut writer: EventWriter, -) { - events - .iter() - .filter(|MouseButtonInput { button, state, .. }| { - (*button, *state) == (MouseButton::Right, ButtonState::Pressed) - }) - .for_each(|_| { - if let Ok(entity) = current.get_single() { - commands.entity(entity).remove::(); - writer.send(game::GameEvent::PlacePiece); - } - }) -} - fn snap_back_cancel( mut events: RemovedComponents, query: Query<&game::BoardIndex, (With, With)>, @@ -415,9 +338,9 @@ fn snap_back_cancel( events.iter().for_each(|entity| { if let Ok(idx) = query.get(entity) { move_events.send(game::Move { - epoch: 0, from: idx.clone(), to: Some(idx.clone()), + ..default() }); } }) diff --git a/src/display3d.rs b/src/display3d.rs index aae1a63..bc5e69d 100644 --- a/src/display3d.rs +++ b/src/display3d.rs @@ -1,15 +1,23 @@ +// TODO: Camera Animations +// Intro animation +// Turn changing animations +// TODO: Pickup/put-down sound in 3d // TODO: Unify the selection logic -// If you select in 2d, it should "be" selected in 3d. -// And if you select in 3d, 2d is selected. +// If you select in 2d, it should "be" selected in 3d. +// And if you select in 3d, 2d is selected. +// TODO: De-select pieces + use crate::{ game::{Board, BoardIndex, Piece, Side}, prelude::*, }; use bevy::{ - core_pipeline::Skybox, - ecs::removal_detection::RemovedComponentReader, - input::mouse::{MouseButtonInput, MouseMotion, MouseWheel}, - render::render_resource::{TextureViewDescriptor, TextureViewDimension}, + core_pipeline::{tonemapping::DebandDither, Skybox}, + input::mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, + render::{ + render_resource::{TextureViewDescriptor, TextureViewDimension}, + view::ColorGrading, + }, window::PrimaryWindow, }; @@ -25,13 +33,14 @@ impl Plugin for Display3dPlugin { .add_systems( Update, ( + hydrate_camera, // TODO: add run_if... menu::exit_to_menu.run_if(in_state(GameState::Display3d)), set_piece_model.run_if(any_component_added::), set_board_model.run_if(any_component_added::), set_tile_hitbox.run_if(any_component_added::), set_piece_position.run_if(any_component_changed::), set_piece_texture.run_if(any_component_changed::), - select_3d + select .run_if(in_state(GameState::Display3d)) .run_if(on_event::()), pick_up.run_if(any_component_added::), @@ -42,15 +51,18 @@ impl Plugin for Display3dPlugin { Update, ( move_camera + .run_if(resource_exists::()) .run_if(in_state(GameState::Display3d)) .run_if(on_event::()), - gizmo_system.run_if(in_state(GameState::Display3d)), + gizmo_system + .run_if(resource_exists::()) + .run_if(in_state(GameState::Display3d)), mouse_zoom + .run_if(resource_exists::()) .run_if(in_state(GameState::Display3d)) .run_if(on_event::()), - selected_gizmo, - ) - .run_if(resource_exists::()), + selected_gizmo.run_if(resource_exists::()), + ), ) .add_systems( OnEnter(GameState::Display3d), @@ -98,27 +110,6 @@ fn load_assets( /// Initialize the 3d board fn initialize(mut commands: Commands, board: Res, assets: Res) { - info!("Initialize 3d camera"); - // let handle = server.load("images/mars.hdr"); - commands.spawn(( - Display3d, - Camera3dBundle { - camera: Camera { - is_active: false, - hdr: true, - ..default() - }, - transform: Transform::from_xyz(0.0, 20.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }, - Skybox(assets.skybox.clone()), - EnvironmentMapLight { - diffuse_map: assets.skybox.clone(), - specular_map: assets.skybox.clone(), - }, - UiCameraConfig { show_ui: true }, - )); - info!("Initializing root"); commands .spawn(( @@ -159,6 +150,7 @@ fn initialize(mut commands: Commands, board: Res, assets: Res, assets: Res>, + assets: Res, + mut commands: Commands, +) { + events + .iter() + .filter(|(name, _)| name.as_str() == "GameCam") + .for_each(|(_, entity)| { + info!("Initialize 3d camera"); + commands.entity(entity).insert(( + Display3d, + Camera3dBundle { + camera: Camera { + is_active: false, + hdr: true, + ..default() + }, + transform: Transform::from_xyz(0.0, 20.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), + dither: DebandDither::Enabled, + color_grading: ColorGrading { ..default() }, + ..default() + }, + Skybox(assets.skybox.clone()), + EnvironmentMapLight { + diffuse_map: assets.skybox.clone(), + specular_map: assets.skybox.clone(), + }, + UiCameraConfig { show_ui: true }, + FogSettings { + color: Color::WHITE, + falloff: FogFalloff::from_visibility_colors( + 100.0, + Color::ORANGE_RED, + Color::NONE, + ), + ..default() + }, + )); + }); +} + fn fix_skybox(mut images: ResMut>, assets: Res) { let image = images.get_mut(&assets.skybox).unwrap(); info!("Loaded skybox image"); @@ -290,9 +325,16 @@ fn mouse_zoom( mut events: EventReader, mut camera: Query<&mut Transform, (With, With)>, ) { - events.iter().for_each(|MouseWheel { y, .. }| { + events.iter().for_each(|MouseWheel { unit, y, .. }| { camera.iter_mut().for_each(|mut t| { - t.translation *= 1.0 - (*y / 4.0); + match unit { + MouseScrollUnit::Line => { + t.translation *= 1.0 - (*y / 4.0); + } + MouseScrollUnit::Pixel => { + t.translation *= 1.0 - (*y / 64.0); + } + } t.look_at(Vec3::ZERO, Vec3::Y); }); }); @@ -413,18 +455,26 @@ fn set_piece_texture( }) } -/// Function for selecting entities based on ray intersection +/// Select tiles and pieces in 3d /// There is a bug where we are selecting multiple entities... -fn select_3d( +/// TODO: Selectable generalize picking pieces **and** hitboxes +fn select( mut events: EventReader, query: Query<(Entity, &Handle, &GlobalTransform)>, meshes: Res>, cameras: Query<(&Camera, &GlobalTransform)>, windows: Query<&Window, With>, - parents: Query, With)>, + selectable: Query, With)>, children: Query<&Children>, mut commands: Commands, - selected: Query, With, With)>, + selected: Query< + Entity, + ( + With, + With, + With, + ), + >, ) { events .iter() @@ -440,8 +490,8 @@ fn select_3d( meshes.get(handle).map(|mesh| (entity, mesh, gt)) }) .for_each(|(entity, mesh, gt)| { - hit3d::intersects(&ray, mesh, >).and_then(|_hit| { - parents + hit::intersects3d(&ray, mesh, >).and_then(|_hit| { + selectable .iter() .find(|&parent| { children @@ -472,7 +522,14 @@ fn select_3d( } fn selected_gizmo( - selected: Query<&Transform, (With, With, With)>, + selected: Query< + &Transform, + ( + With, + With, + With, + ), + >, mut gizmos: Gizmos, ) { selected.iter().for_each(|transform| { @@ -504,15 +561,16 @@ fn pick_up( game::Piece::Drone => gltf.named_animations.get("DroneIdle"), game::Piece::Pawn => gltf.named_animations.get("PawnIdle"), }; - player.play_with_transition( - animation.expect("Pickup Animation").clone(), - Duration::from_secs_f32(0.75), - ); - player.play_with_transition( - idle.expect("Idle animation").clone(), - Duration::from_secs_f32(1.5), - ); - player.repeat(); + player + .start_with_transition( + animation.expect("Pickup Animation").clone(), + Duration::from_secs_f32(0.75), + ) + .play_with_transition( + idle.expect("Idle animation").clone(), + Duration::from_secs_f32(1.5), + ) + .repeat(); } }) }); @@ -520,7 +578,7 @@ fn pick_up( fn put_down( mut events: RemovedComponents, - mut query: Query<&game::Piece, (With, With)>, + mut query: Query<&game::Piece, (With, With, With)>, assets_map: Res, gltfs: Res>, children: Query<&Children>, @@ -536,11 +594,12 @@ fn put_down( game::Piece::Drone => gltf.named_animations.get("DronePutDown"), game::Piece::Pawn => gltf.named_animations.get("PawnPutDown"), }; - player.stop_repeating(); - player.play_with_transition( - animation.expect("PutDown Animation").clone(), - Duration::from_secs_f32(0.75), - ); + player + .play_with_transition( + animation.expect("PutDown Animation").clone(), + Duration::from_secs_f32(0.75), + ) + .stop_repeating(); } }) } diff --git a/src/game.rs b/src/game.rs index 8afa5c6..58d67c4 100644 --- a/src/game.rs +++ b/src/game.rs @@ -6,7 +6,6 @@ impl Plugin for GamePlugin { fn build(&self, app: &mut App) { app.add_event::() .add_event::() - .init_resource::() .add_systems(Startup, setup_board) .add_systems( Update, @@ -14,14 +13,16 @@ impl Plugin for GamePlugin { update_board::.run_if(on_event::()), update_board::.run_if(on_event::()), set_side.run_if(on_event::()), // TODO: correct run_if? + cancel_place.run_if(|buttons: Res>| -> bool { + buttons.just_pressed(MouseButton::Right) + }), + select_sync.run_if(any_component_added::), + deselect_sync.run_if(any_component_removed::()), ), ) .add_systems( PostUpdate, - ( - debug_hovering.run_if(resource_exists::()), - debug_board.run_if(resource_exists::()), - ), + (debug_board.run_if(resource_exists::()),), ); } } @@ -202,14 +203,14 @@ impl std::fmt::Display for Board { } } -#[derive(Debug, Default, Resource)] -pub(crate) struct ActiveTile { - pub idx: Option, -} - +/// Marker component for currently selected entities #[derive(Debug, Default, Component)] pub(crate) struct Selected; +/// Marker component for selectable entities +#[derive(Debug, Default, Component)] +pub(crate) struct Selectable; + fn setup_board(mut commands: Commands) { use Piece::*; commands.insert_resource(Board { @@ -259,21 +260,6 @@ fn setup_board(mut commands: Commands) { }); } -/// TODO: only run_if debug enabled -fn debug_hovering( - selected: Res, - board: Res, - mut debug_info: ResMut, -) { - match &selected.idx { - Some(idx) => debug_info.set( - "hovering".into(), - format!("{:?}@({},{})", board.at(&idx), idx.x, idx.y,), - ), - None => debug_info.set("selected".into(), format!("N/A")), - }; -} - fn debug_board(board: Res, mut debug_info: ResMut) { debug_info.set("board".into(), format!("\n{}", *board)); } @@ -312,3 +298,47 @@ pub(crate) fn set_side(mut events: Query<(&mut Side, &BoardIndex), Changed warn!("{:?}", e), }); } + +fn select_sync( + events: Query>, + pieces: Query<(Entity, &BoardIndex), (With, With)>, + mut commands: Commands, +) { + events.iter().for_each(|entity| { + if let Ok((this_e, this_idx)) = pieces.get(entity) { + if let Some((entangled, _)) = pieces + .iter() + .find(|(e, idx)| *idx == this_idx && *e != this_e) + { + commands.entity(entangled).insert(Selected); + } + } + }) +} + +fn deselect_sync( + mut events: RemovedComponents, + pieces: Query<(Entity, &BoardIndex), (With, With)>, + mut commands: Commands, +) { + events.iter().for_each(|entity| { + if let Ok((this_e, this_idx)) = pieces.get(entity) { + if let Some((entangled, _)) = pieces + .iter() + .find(|(e, idx)| *idx == this_idx && *e != this_e) + { + commands.entity(entangled).remove::(); + } + } + }) +} + +fn cancel_place( + current: Query, With)>, + mut commands: Commands, +) { + current.iter().for_each(|entity| { + info!("De-selecting {:?}", entity); + commands.entity(entity).remove::(); + }); +} diff --git a/src/hit3d.rs b/src/hit.rs similarity index 78% rename from src/hit3d.rs rename to src/hit.rs index 9a480da..063e57d 100644 --- a/src/hit3d.rs +++ b/src/hit.rs @@ -1,14 +1,22 @@ -use bevy::render::{ - mesh::{MeshVertexAttribute, VertexAttributeValues}, - render_resource::VertexFormat, +use bevy::{ + render::{ + mesh::{MeshVertexAttribute, VertexAttributeValues}, + render_resource::VertexFormat, + }, + sprite::Anchor, }; use crate::prelude::*; -/// Struct containing hit data -/// The point in Global (not Local) space and the distance from the Camera +/// Hit data for 2d sprites #[derive(Debug)] -pub(crate) struct Hit { +pub(crate) struct Hit2d { + point: Vec2, +} + +/// Hit data for 3d objects in Global (not Local) space and the distance from the Camera +#[derive(Debug)] +pub(crate) struct Hit3d { distance: f32, point: Vec3, } @@ -51,7 +59,7 @@ impl Triangle { /// Heavily synthesized from these two resources: /// * Textbook: https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-rendering-a-triangle/ray-triangle-intersection-geometric-solution.html /// * Example: https://github.com/aevyrie/bevy_mod_raycast/blob/435d8ef100738797161ac3a9b910ea346a4ed6e6/src/raycast.rs#L43 -pub(crate) fn intersects(ray: &Ray, mesh: &Mesh, gt: &GlobalTransform) -> Option { +pub(crate) fn intersects3d(ray: &Ray, mesh: &Mesh, gt: &GlobalTransform) -> Option { let attr = MeshVertexAttribute::new("Vertex_Position", 0, VertexFormat::Float32x3); if let Some(verts) = mesh.attribute(attr) { if let Some(idxs) = mesh.indices() { @@ -96,7 +104,7 @@ pub(crate) fn intersects(ray: &Ray, mesh: &Mesh, gt: &GlobalTransform) -> Option && triangle.normal().dot(c1) > 0.0 && triangle.normal().dot(c2) > 0.0 }; - hit.then_some(Hit { + hit.then_some(Hit3d { distance: d, point: p, }) @@ -115,3 +123,24 @@ pub(crate) fn intersects(ray: &Ray, mesh: &Mesh, gt: &GlobalTransform) -> Option None } } + +pub(crate) fn intersects2d( + sprite_size: Vec2, + anchor: &Anchor, + transform: &GlobalTransform, + pos: Vec2, +) -> bool { + // Implementation credit goes to the sprite bevy_mod_picking backend + // TODO: Upstream changes + let p = transform.translation(); + + let size = { + let (transform_scale, _, _) = transform.to_scale_rotation_translation(); + sprite_size * transform_scale.truncate() + }; + + let center = p.truncate() - (anchor.as_vec() * size); + let rect = Rect::from_center_half_size(center, size / 2.0); + + rect.contains(pos) +} diff --git a/src/main.rs b/src/main.rs index b0d1139..984fe61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ mod debug; mod display2d; mod display3d; mod game; -mod hit3d; +mod hit; mod loading; mod menu; mod prelude; diff --git a/src/menu.rs b/src/menu.rs index 03ebfe7..87a60f3 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -66,7 +66,7 @@ fn init_menu_ui(mut commands: Commands) { .with_children(|parent| { parent .spawn(( - GameState::Display2d, + GameState::Display3d, ButtonBundle { style: Style { padding: UiRect::all(Val::Px(5.0)), @@ -79,7 +79,7 @@ fn init_menu_ui(mut commands: Commands) { )) .with_children(|parent| { parent.spawn(( - GameState::Display2d, + GameState::Display3d, TextBundle::from_section( "Start", TextStyle {