Select refactors for 2d, sync select/deselects

bevy0.12
Elijah C. Voigt 2 years ago
parent fdd3ccaa84
commit 729c74e932

BIN
assets/models/Martian Chess.glb (Stored with Git LFS)

Binary file not shown.

@ -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::<game::Selected>()),
place_piece
.run_if(in_state(GameState::Display2d))
.run_if(any_with_component::<game::Selected>()),
cancel_place
.run_if(in_state(GameState::Display2d))
.run_if(on_event::<MouseButtonInput>()),
select.run_if(in_state(GameState::Display2d)).run_if(
|buttons: Res<Input<MouseButton>>| -> bool {
buttons.just_pressed(MouseButton::Left)
},
),
snap_back_cancel
.run_if(in_state(GameState::Display2d))
.run_if(any_component_removed::<game::Selected>()),
@ -157,7 +149,13 @@ fn initialize_board(board: Option<Res<Board>>, 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<Res<Board>>, mut commands: Commands) {
index.clone(),
side,
SpriteSheetBundle { ..default() },
game::Selectable,
));
});
});
@ -249,74 +248,67 @@ fn set_transform(
});
}
fn active_tile(
mut events: EventReader<CursorMoved>,
sprite_q: Query<(
/// Select pieces and tiles in 2d
fn select(
candidates: Query<
(
Entity,
&TextureAtlasSprite,
&Handle<TextureAtlas>,
&GlobalTransform,
&BoardIndex,
)>,
camera_q: Query<(&Camera, &GlobalTransform), With<Display2d>>,
),
(
With<game::Selectable>,
Without<game::Selected>,
With<Display2d>,
),
>,
windows: Query<&Window, With<PrimaryWindow>>,
cameras: Query<(&Camera, &GlobalTransform), With<Display2d>>,
atlases: Res<Assets<TextureAtlas>>,
mut active: ResMut<ActiveTile>,
mut commands: Commands,
) {
events.iter().for_each(|CursorMoved { position, .. }| {
if let Some(position) = camera_q
// For each window (there is only one)
windows
.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 = {
.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("Get this rect texture"))
.expect("get this rect")
.map(|atlas| {
atlas.textures.get(*index).expect("Atlas Sprite Texture")
})
.expect("Atlas Sprite Rectangle")
.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)
hit::intersects2d(sprite_size, anchor, transform, pos)
.then_some((entity, transform))
},
);
if active.idx != idx.cloned() {
active.idx = idx.cloned();
}
}
});
}
fn select_piece(
mut events: EventReader<MouseButtonInput>,
pieces: Query<(Entity, &BoardIndex), (With<game::Piece>, With<Display2d>)>,
active: Res<ActiveTile>,
mut commands: Commands,
mut writer: EventWriter<game::GameEvent>,
) {
events
.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)
)
.max_by(|(_, a), (_, b)| {
a.translation().z.partial_cmp(&b.translation().z).unwrap()
})
.for_each(|entity| {
commands.entity(entity).insert(game::Selected);
writer.send(game::GameEvent::SelectPiece);
.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<MouseButtonInput>,
current: Query<
(Entity, &BoardIndex),
(With<game::Selected>, With<game::Piece>, With<Display2d>),
>,
pieces: Query<&BoardIndex, (Without<game::Selected>, With<game::Piece>, With<Display2d>)>,
active: Res<ActiveTile>,
mut board: ResMut<Board>,
mut commands: Commands,
mut game_events: EventWriter<game::GameEvent>,
mut move_events: EventWriter<game::Move>,
) {
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::<game::Selected>();
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<MouseButtonInput>,
current: Query<Entity, (With<game::Selected>, With<game::Piece>, With<Display2d>)>,
mut commands: Commands,
mut writer: EventWriter<game::GameEvent>,
) {
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::<game::Selected>();
writer.send(game::GameEvent::PlacePiece);
}
})
}
fn snap_back_cancel(
mut events: RemovedComponents<game::Selected>,
query: Query<&game::BoardIndex, (With<game::Piece>, With<Display2d>)>,
@ -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()
});
}
})

@ -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.
// 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::<Piece>),
set_board_model.run_if(any_component_added::<game::BoardComponent>),
set_tile_hitbox.run_if(any_component_added::<game::Tile>),
set_piece_position.run_if(any_component_changed::<BoardIndex>),
set_piece_texture.run_if(any_component_changed::<Side>),
select_3d
select
.run_if(in_state(GameState::Display3d))
.run_if(on_event::<MouseButtonInput>()),
pick_up.run_if(any_component_added::<game::Selected>),
@ -42,15 +51,18 @@ impl Plugin for Display3dPlugin {
Update,
(
move_camera
.run_if(resource_exists::<debug::DebugEnabled>())
.run_if(in_state(GameState::Display3d))
.run_if(on_event::<MouseMotion>()),
gizmo_system.run_if(in_state(GameState::Display3d)),
gizmo_system
.run_if(resource_exists::<debug::DebugEnabled>())
.run_if(in_state(GameState::Display3d)),
mouse_zoom
.run_if(resource_exists::<debug::DebugEnabled>())
.run_if(in_state(GameState::Display3d))
.run_if(on_event::<MouseWheel>()),
selected_gizmo,
)
.run_if(resource_exists::<debug::DebugEnabled>()),
selected_gizmo.run_if(resource_exists::<debug::DebugEnabled>()),
),
)
.add_systems(
OnEnter(GameState::Display3d),
@ -98,27 +110,6 @@ fn load_assets(
/// Initialize the 3d board
fn initialize(mut commands: Commands, board: Res<game::Board>, assets: Res<AssetsMap>) {
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<game::Board>, assets: Res<Asset
visibility: Visibility::Hidden,
..default()
},
game::Selectable,
));
});
@ -172,12 +164,55 @@ fn initialize(mut commands: Commands, board: Res<game::Board>, assets: Res<Asset
piece.clone(),
index.clone(),
SceneBundle { ..default() },
game::Selectable,
));
});
});
});
}
fn hydrate_camera(
events: Query<(&Name, Entity), Added<Camera>>,
assets: Res<AssetsMap>,
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<Image>>, assets: Res<AssetsMap>) {
let image = images.get_mut(&assets.skybox).unwrap();
info!("Loaded skybox image");
@ -290,9 +325,16 @@ fn mouse_zoom(
mut events: EventReader<MouseWheel>,
mut camera: Query<&mut Transform, (With<Display3d>, With<Camera>)>,
) {
events.iter().for_each(|MouseWheel { y, .. }| {
events.iter().for_each(|MouseWheel { unit, y, .. }| {
camera.iter_mut().for_each(|mut t| {
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<MouseButtonInput>,
query: Query<(Entity, &Handle<Mesh>, &GlobalTransform)>,
meshes: Res<Assets<Mesh>>,
cameras: Query<(&Camera, &GlobalTransform)>,
windows: Query<&Window, With<PrimaryWindow>>,
parents: Query<Entity, (With<game::Piece>, With<Display3d>)>,
selectable: Query<Entity, (With<game::Selectable>, With<Display3d>)>,
children: Query<&Children>,
mut commands: Commands,
selected: Query<Entity, (With<game::Selected>, With<game::Piece>, With<Display3d>)>,
selected: Query<
Entity,
(
With<game::Selected>,
With<game::Selectable>,
With<Display3d>,
),
>,
) {
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, &gt).and_then(|_hit| {
parents
hit::intersects3d(&ray, mesh, &gt).and_then(|_hit| {
selectable
.iter()
.find(|&parent| {
children
@ -472,7 +522,14 @@ fn select_3d(
}
fn selected_gizmo(
selected: Query<&Transform, (With<game::Selected>, With<game::Piece>, With<Display3d>)>,
selected: Query<
&Transform,
(
With<game::Selected>,
With<game::Selectable>,
With<Display3d>,
),
>,
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(
player
.start_with_transition(
animation.expect("Pickup Animation").clone(),
Duration::from_secs_f32(0.75),
);
player.play_with_transition(
)
.play_with_transition(
idle.expect("Idle animation").clone(),
Duration::from_secs_f32(1.5),
);
player.repeat();
)
.repeat();
}
})
});
@ -520,7 +578,7 @@ fn pick_up(
fn put_down(
mut events: RemovedComponents<game::Selected>,
mut query: Query<&game::Piece, (With<game::Piece>, With<Display3d>)>,
mut query: Query<&game::Piece, (With<game::Piece>, With<game::Selectable>, With<Display3d>)>,
assets_map: Res<AssetsMap>,
gltfs: Res<Assets<Gltf>>,
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(
player
.play_with_transition(
animation.expect("PutDown Animation").clone(),
Duration::from_secs_f32(0.75),
);
)
.stop_repeating();
}
})
}

@ -6,7 +6,6 @@ impl Plugin for GamePlugin {
fn build(&self, app: &mut App) {
app.add_event::<GameEvent>()
.add_event::<Move>()
.init_resource::<ActiveTile>()
.add_systems(Startup, setup_board)
.add_systems(
Update,
@ -14,14 +13,16 @@ impl Plugin for GamePlugin {
update_board::<display2d::Display2d>.run_if(on_event::<Move>()),
update_board::<display3d::Display3d>.run_if(on_event::<Move>()),
set_side.run_if(on_event::<Move>()), // TODO: correct run_if?
cancel_place.run_if(|buttons: Res<Input<MouseButton>>| -> bool {
buttons.just_pressed(MouseButton::Right)
}),
select_sync.run_if(any_component_added::<Selected>),
deselect_sync.run_if(any_component_removed::<Selected>()),
),
)
.add_systems(
PostUpdate,
(
debug_hovering.run_if(resource_exists::<debug::DebugEnabled>()),
debug_board.run_if(resource_exists::<debug::DebugEnabled>()),
),
(debug_board.run_if(resource_exists::<debug::DebugEnabled>()),),
);
}
}
@ -202,14 +203,14 @@ impl std::fmt::Display for Board {
}
}
#[derive(Debug, Default, Resource)]
pub(crate) struct ActiveTile {
pub idx: Option<BoardIndex>,
}
/// 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<ActiveTile>,
board: Res<Board>,
mut debug_info: ResMut<debug::DebugInfo>,
) {
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<Board>, mut debug_info: ResMut<debug::DebugInfo>) {
debug_info.set("board".into(), format!("\n{}", *board));
}
@ -312,3 +298,47 @@ pub(crate) fn set_side(mut events: Query<(&mut Side, &BoardIndex), Changed<Board
Err(e) => warn!("{:?}", e),
});
}
fn select_sync(
events: Query<Entity, Added<Selected>>,
pieces: Query<(Entity, &BoardIndex), (With<Selectable>, With<Piece>)>,
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<Selected>,
pieces: Query<(Entity, &BoardIndex), (With<Selectable>, With<Piece>)>,
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::<Selected>();
}
}
})
}
fn cancel_place(
current: Query<Entity, (With<game::Selected>, With<game::Piece>)>,
mut commands: Commands,
) {
current.iter().for_each(|entity| {
info!("De-selecting {:?}", entity);
commands.entity(entity).remove::<game::Selected>();
});
}

@ -1,14 +1,22 @@
use bevy::render::{
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<Hit> {
pub(crate) fn intersects3d(ray: &Ray, mesh: &Mesh, gt: &GlobalTransform) -> Option<Hit3d> {
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)
}

@ -8,7 +8,7 @@ mod debug;
mod display2d;
mod display3d;
mod game;
mod hit3d;
mod hit;
mod loading;
mod menu;
mod prelude;

@ -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 {

Loading…
Cancel
Save