I think the jitter-selected issue is fixed

bevy0.12
Elijah Voigt 2 years ago
parent 51888d5370
commit 8cee0b6fb5

@ -0,0 +1,609 @@
// TODO: Camera Animations
// Intro animation
// Turn changing animations
// TODO: Pickup/put-down sound in 3d
// TODO: Background during menu
use crate::{
game::{Board, BoardIndex, Piece, Side},
prelude::*,
};
use bevy::{
core_pipeline::{tonemapping::DebandDither, Skybox},
input::mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
render::{
render_resource::{TextureViewDescriptor, TextureViewDimension},
view::ColorGrading,
},
window::PrimaryWindow,
};
pub(crate) struct Display3dPlugin;
impl Plugin for Display3dPlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(GameState::Loading), load_assets)
.add_systems(
OnExit(GameState::Loading),
(initialize, fix_skybox.before(initialize)),
)
.add_systems(
Update,
(
hydrate_camera.run_if(any_component_added::<Camera3d>),
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
.run_if(in_state(GameState::Play))
.run_if(in_state(DisplayState::Display3d))
.run_if(on_event::<MouseButtonInput>()),
pick_up.run_if(any_component_added::<game::Selected>),
put_down.run_if(any_component_removed::<game::Selected>()),
),
)
.add_systems(
Update,
(
move_camera.run_if(on_event::<MouseMotion>()),
mouse_zoom.run_if(on_event::<MouseWheel>()),
gizmo_system,
selected_gizmo,
moves_gizmo,
)
.run_if(resource_exists::<debug::DebugEnabled>())
.run_if(in_state(GameState::Play))
.run_if(in_state(DisplayState::Display3d)),
)
.add_systems(
OnEnter(DisplayState::Display3d),
(activate::<Display3d>, set_piece_texture),
)
.add_systems(OnExit(DisplayState::Display3d), deactivate::<Display3d>);
}
}
#[derive(Debug, Component)]
pub(crate) struct Display3d;
#[derive(Debug, Resource)]
struct AssetsMap {
models: Handle<Gltf>,
skybox: Handle<Image>,
hitbox_shape: Handle<Mesh>,
hitbox_material: Handle<StandardMaterial>,
}
/// Load 3d models
/// This is kind of pulling double duty.
/// Both loads the GLTF file _and_ populates the ModelMap once that is loaded.
fn load_assets(
server: Res<AssetServer>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let hitbox_shape = meshes.add(shape::Box::new(1.0, 0.1, 1.0).into());
let hitbox_material = materials.add(StandardMaterial {
base_color: Color::NONE,
perceptual_roughness: 0.0,
reflectance: 0.0,
alpha_mode: AlphaMode::Blend,
..default()
});
commands.insert_resource(AssetsMap {
models: server.load("models/Martian Chess.glb"),
skybox: server.load("images/skybox.png"),
hitbox_shape,
hitbox_material,
});
}
/// Initialize the 3d board
fn initialize(mut commands: Commands, board: Res<game::Board>, assets: Res<AssetsMap>) {
info!("Initializing root");
commands
.spawn((
SpatialBundle {
visibility: Visibility::Hidden,
..default()
},
Display3d,
))
.with_children(|parent| {
info!("Initializing 3D lights!");
parent.spawn((
Display3d,
PointLightBundle {
point_light: PointLight {
intensity: 3000.0,
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(0.0, 10.0, 5.0),
..default()
},
));
info!("Intializing 3D Board!");
parent
.spawn((Display3d, game::BoardComponent, SceneBundle { ..default() }))
.with_children(|parent| {
// Hitboxes
game::tiles().for_each(|(index, tile)| {
parent.spawn((
Display3d,
index,
tile,
PbrBundle {
mesh: assets.hitbox_shape.clone(),
material: assets.hitbox_material.clone(),
visibility: Visibility::Hidden,
..default()
},
game::Selectable,
));
});
// Pieces
board.pieces().iter().for_each(|(index, piece)| {
let side = Board::side(*index).expect("Spawn valid side");
parent.spawn((
side,
Display3d,
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, 12.5, 6.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::NONE, 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");
// NOTE: PNGs do not have any metadata that could indicate they contain a cubemap texture,
// so they appear as one texture. The following code reconfigures the texture as necessary.
if image.texture_descriptor.array_layer_count() == 1 {
image.reinterpret_stacked_2d_as_array(
image.texture_descriptor.size.height / image.texture_descriptor.size.width,
);
image.texture_view_descriptor = Some(TextureViewDescriptor {
dimension: Some(TextureViewDimension::Cube),
..default()
});
}
}
/// Set the model for each piece based on the game::Piece::* marker
fn set_piece_model(
mut events: Query<(&mut Handle<Scene>, &Piece), (Added<game::Piece>, With<Display3d>)>,
assets_map: Res<AssetsMap>,
gltfs: Res<Assets<Gltf>>,
) {
events.iter_mut().for_each(|(mut handle, piece)| {
let gltf = gltfs.get(&assets_map.models).expect("Load GLTF content");
*handle = match piece {
game::Piece::Pawn => gltf.named_scenes.get("Pawn"),
game::Piece::Drone => gltf.named_scenes.get("Drone"),
game::Piece::Queen => gltf.named_scenes.get("Queen"),
}
.expect("Game board model")
.clone();
});
}
fn set_board_model(
mut events: Query<&mut Handle<Scene>, (With<game::BoardComponent>, With<Display3d>)>,
assets_map: Res<AssetsMap>,
gltfs: Res<Assets<Gltf>>,
) {
events.iter_mut().for_each(|mut handle| {
let gltf = gltfs.get(&assets_map.models).expect("Load GLTF content");
*handle = gltf
.named_scenes
.get("Gameboard")
.expect("Game board model")
.clone();
})
}
/// Sets a piece location given it's board index
fn set_piece_position(
mut events: Query<
(&mut Transform, &BoardIndex),
(With<game::Piece>, With<Display3d>, Changed<BoardIndex>),
>,
) {
events.iter_mut().for_each(|(mut t, i)| {
t.translation = board_translation(i);
});
}
/// Given a board index returns the Vec3 location in space
fn board_translation(&BoardIndex { x, y }: &BoardIndex) -> Vec3 {
// Scale x down by 4 to account for -4..4 scaling
let x = x as i8 - 4;
// Mirror y axis because our board index is inverted...
let y = -(y as i8) + 1;
let x = if x < 0 {
x as f32 * 1.3 + 0.325
} else {
x as f32 * 1.3 + 1.0
};
let y = y as f32 * 1.3 + 0.65;
Vec3::new(x, 0.0, y)
}
fn gizmo_system(mut gizmos: Gizmos) {
for y in 0..4 {
for x in 0..8 {
gizmos.cuboid(
Transform::from_translation(board_translation(&BoardIndex { x, y }))
.with_scale(Vec3::new(1.0, 0.1, 1.0)),
Color::PURPLE,
)
}
}
}
/// TODO: This has bad feel, needs to be tuned
fn move_camera(
buttons: Res<Input<MouseButton>>,
mut events: EventReader<MouseMotion>,
mut camera: Query<&mut Transform, (With<Display3d>, With<Camera>)>,
) {
events.iter().for_each(|MouseMotion { delta }| {
if buttons.pressed(MouseButton::Left) {
camera.iter_mut().for_each(|mut t| {
t.rotate_around(Vec3::ZERO, Quat::from_rotation_y(delta.x / 256.0));
t.rotate_around(Vec3::ZERO, Quat::from_rotation_x(delta.y / 256.0));
t.look_at(Vec3::ZERO, Vec3::Y);
});
}
});
}
fn mouse_zoom(
mut events: EventReader<MouseWheel>,
mut camera: Query<&mut Transform, (With<Display3d>, With<Camera>)>,
) {
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);
});
});
}
/// Set the Texture for a piece given it's position (left or right) on the bord.
/// Executed when Side is changed or upon entry to Display3d state
/// Getting this to run _after_ GLTF is loaded is a pain.
/// PERF: We are saving what to work on in a Vector which is bad.
/// CAVEAT: We are only exeucting this when a piece changes or state is changed.
fn set_piece_texture(
events: Query<(Entity, &Piece, &Side), (With<game::Piece>, With<Display3d>, Changed<Side>)>,
all: Query<(Entity, &Piece, &Side), (With<game::Piece>, With<Display3d>)>,
gltfs: Res<Assets<Gltf>>,
assets_map: Res<AssetsMap>,
children: Query<&Children>,
mut models: Query<(&Name, &mut Handle<StandardMaterial>)>,
) {
let pieces = if events.is_empty() {
all.iter().collect::<Vec<(Entity, &Piece, &Side)>>()
} else {
events.iter().collect::<Vec<(Entity, &Piece, &Side)>>()
};
pieces.iter().for_each(|(entity, piece, side)| {
if let Some(gltf) = gltfs.get(&assets_map.models) {
children.iter_descendants(*entity).for_each(|child| {
if let Ok((n, mut m)) = models.get_mut(child) {
match (*piece, *side, n.as_str()) {
(Piece::Queen, Side::A, "Queen.0") => {
*m = gltf
.named_materials
.get("Queen")
.expect("Load Red Queen texture")
.clone()
}
(Piece::Queen, Side::A, "Queen.1") => {
*m = gltf
.named_materials
.get("Dots")
.expect("Load Red Dots texture")
.clone()
}
(Piece::Queen, Side::B, "Queen.0") => {
*m = gltf
.named_materials
.get("QueenBlue")
.expect("Load Blue Queen texture")
.clone()
}
(Piece::Queen, Side::B, "Queen.1") => {
*m = gltf
.named_materials
.get("DotsBlue")
.expect("Load Red Dots texture")
.clone()
}
(Piece::Drone, Side::A, "Drone.0") => {
*m = gltf
.named_materials
.get("Drone")
.expect("Load Red Drone texture")
.clone()
}
(Piece::Drone, Side::A, "Drone.1") => {
*m = gltf
.named_materials
.get("Dots")
.expect("Load Red Dots texture")
.clone()
}
(Piece::Drone, Side::B, "Drone.0") => {
*m = gltf
.named_materials
.get("DroneBlue")
.expect("Load Blue Drone texture")
.clone()
}
(Piece::Drone, Side::B, "Drone.1") => {
*m = gltf
.named_materials
.get("DotsBlue")
.expect("Load Blue Dots texture")
.clone()
}
(Piece::Pawn, Side::A, "Pawn.0") => {
*m = gltf
.named_materials
.get("Pawn")
.expect("Load Red Pawn texture")
.clone()
}
(Piece::Pawn, Side::A, "Pawn.1") => {
*m = gltf
.named_materials
.get("Dots")
.expect("Load Red Dots texture")
.clone()
}
(Piece::Pawn, Side::B, "Pawn.0") => {
*m = gltf
.named_materials
.get("DroneBlue") // TODO: FIX
.expect("Load Blue Pawn texture")
.clone()
}
(Piece::Pawn, Side::B, "Pawn.1") => {
*m = gltf
.named_materials
.get("DotsBlue")
.expect("Load Blue Dots texture")
.clone()
}
_ => warn!("???"),
}
}
});
}
})
}
/// Select tiles and pieces in 3d
/// There is a bug where we are selecting multiple entities...
/// 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>>,
selectable: Query<Entity, (With<game::Selectable>, With<Display3d>)>,
children: Query<&Children>,
mut commands: Commands,
selected: Query<
Entity,
(
With<game::Selected>,
With<game::Selectable>,
With<Display3d>,
),
>,
) {
events
.iter()
.filter(|ev| ev.state == ButtonState::Pressed)
.for_each(|_| {
windows.iter().for_each(|window| {
window.cursor_position().and_then(|pos| {
cameras.iter().for_each(|(camera, gt)| {
camera.viewport_to_world(gt, pos).and_then(|ray| {
query
.iter()
.filter_map(|(entity, handle, gt)| {
meshes.get(handle).map(|mesh| (entity, mesh, gt))
})
.for_each(|(entity, mesh, gt)| {
hit::intersects3d(&ray, mesh, &gt).and_then(|_hit| {
selectable
.iter()
.find(|&e| {
// A child was hit (pieces)
let primary = entity == e;
// This entity was hit (tile hitboxes)
let secondary = children
.iter_descendants(e)
.any(|child| child == entity);
primary || secondary
})
.iter()
.for_each(|&e| {
commands.entity(e).insert(game::Selected);
});
Some(())
});
});
Some(())
});
});
Some(())
});
});
});
}
fn selected_gizmo(
selected: Query<&Transform, (With<game::Selected>, With<Display3d>)>,
mut gizmos: Gizmos,
) {
selected.iter().for_each(|transform| {
gizmos.cuboid(transform.clone(), Color::GREEN);
})
}
fn moves_gizmo(
selected: Query<&BoardIndex, (With<game::Piece>, With<game::Selected>, With<Display3d>)>,
board: Res<Board>,
mut gizmos: Gizmos,
) {
selected.iter().for_each(|idx| {
board
.possible_moves(*idx)
.iter()
.map(|i| Transform::from_translation(board_translation(i)))
.for_each(|t| gizmos.cuboid(t, Color::WHITE))
});
}
fn pick_up(
mut events: Query<
(Entity, &game::Piece),
(With<game::Piece>, With<Display3d>, Added<game::Selected>),
>,
assets_map: Res<AssetsMap>,
gltfs: Res<Assets<Gltf>>,
children: Query<&Children>,
mut players: Query<&mut AnimationPlayer>,
) {
events.iter_mut().for_each(|(entity, piece)| {
let gltf = gltfs.get(&assets_map.models).expect("Load GLTF content");
children.iter_descendants(entity).for_each(|child| {
if let Ok(mut player) = players.get_mut(child) {
let animation = match piece {
game::Piece::Queen => gltf.named_animations.get("QueenPickup"),
game::Piece::Drone => gltf.named_animations.get("DronePickup"),
game::Piece::Pawn => gltf.named_animations.get("PawnPickup"),
};
let idle = match piece {
game::Piece::Queen => gltf.named_animations.get("QueenIdle"),
game::Piece::Drone => gltf.named_animations.get("DroneIdle"),
game::Piece::Pawn => gltf.named_animations.get("PawnIdle"),
};
player
.start_with_transition(
animation.expect("Pickup Animation").clone(),
Duration::from_secs_f32(0.75),
)
.start_with_transition(
idle.expect("Idle animation").clone(),
Duration::from_secs_f32(1.5),
)
.repeat();
}
})
});
}
fn put_down(
mut events: RemovedComponents<game::Selected>,
mut query: Query<&game::Piece, (With<game::Piece>, With<game::Selectable>, With<Display3d>)>,
assets_map: Res<AssetsMap>,
gltfs: Res<Assets<Gltf>>,
children: Query<&Children>,
mut players: Query<&mut AnimationPlayer>,
) {
events.iter().for_each(|entity| {
if let Ok(piece) = query.get_mut(entity) {
let gltf = gltfs.get(&assets_map.models).expect("Load GLTF content");
children.iter_descendants(entity).for_each(|child| {
if let Ok(mut player) = players.get_mut(child) {
let animation = match piece {
game::Piece::Queen => gltf.named_animations.get("QueenPutDown"),
game::Piece::Drone => gltf.named_animations.get("DronePutDown"),
game::Piece::Pawn => gltf.named_animations.get("PawnPutDown"),
};
player
.start_with_transition(
animation.expect("PutDown Animation").clone(),
Duration::from_secs_f32(0.75),
)
.stop_repeating();
}
})
}
})
}
fn set_tile_hitbox(
mut events: Query<(&mut Transform, &BoardIndex), (With<Display3d>, Added<game::Tile>)>,
) {
events.iter_mut().for_each(|(mut transform, index)| {
*transform = Transform::from_translation(board_translation(index));
});
}

@ -456,7 +456,7 @@ fn select(
selectable: Query<Entity, (With<game::Selectable>, With<Display3d>)>,
children: Query<&Children>,
mut commands: Commands,
selected: Query<
_selected: Query<
Entity,
(
With<game::Selected>,
@ -545,6 +545,7 @@ fn pick_up(
let gltf = gltfs.get(&assets_map.models).expect("Load GLTF content");
children.iter_descendants(entity).for_each(|child| {
if let Ok(mut player) = players.get_mut(child) {
info!("Picking up {:?} {:?}", entity, piece);
let animation = match piece {
game::Piece::Queen => gltf.named_animations.get("QueenPickup"),
game::Piece::Drone => gltf.named_animations.get("DronePickup"),
@ -583,6 +584,7 @@ fn put_down(
let gltf = gltfs.get(&assets_map.models).expect("Load GLTF content");
children.iter_descendants(entity).for_each(|child| {
if let Ok(mut player) = players.get_mut(child) {
info!("Putting down {:?}", entity);
let animation = match piece {
game::Piece::Queen => gltf.named_animations.get("QueenPutDown"),
game::Piece::Drone => gltf.named_animations.get("DronePutDown"),

@ -12,12 +12,12 @@ impl Plugin for GamePlugin {
Update,
(
menu::exit_to_menu.run_if(in_state(GameState::Play)),
update_board.run_if(on_event::<Move>()),
update_board.run_if(on_event::<Move>()).after(select_sync),
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>),
select_sync.run_if(any_component_added::<Selected>).after(deselect_sync),
deselect_sync.run_if(any_component_removed::<Selected>()),
move_piece.run_if(any_component_added::<Selected>),
capture_piece.run_if(any_component_added::<Captured>),
@ -30,13 +30,13 @@ impl Plugin for GamePlugin {
.run_if(any_component_added::<Selected>),
),
)
.add_systems(
PostUpdate,
(
asserts::<display2d::Display2d>,
asserts::<display3d::Display3d>,
),
)
// .add_systems(
// PreUpdate,
// (
// asserts::<display2d::Display2d>,
// asserts::<display3d::Display3d>,
// ),
// )
.add_systems(
PostUpdate,
(debug_board.run_if(resource_exists::<debug::DebugEnabled>()),),
@ -162,7 +162,7 @@ impl Board {
match self.at(from.clone()) {
Some(from_val) => {
// The current epoch is the last epoch + 1
let epoch = self.moves.last().unwrap_or(&Move { ..default() }).epoch + 1;
let epoch = self.current_epoch();
// Local moves vec we can return
let mut moves = vec![];
@ -258,6 +258,10 @@ impl Board {
None => std::iter::empty().collect(),
}
}
pub(crate) fn current_epoch(&self) -> usize {
self.moves.last().unwrap_or(&Move { ..default() }).epoch + 1
}
}
impl std::fmt::Display for Board {
@ -340,7 +344,6 @@ fn debug_board(board: Res<Board>, mut debug_info: ResMut<debug::DebugInfo>) {
}
/// Update this method to use a diff between the board and the state of the 2d/3d worlds
/// Only update the tiles without a corresponding board piece
pub(crate) fn update_board(
mut events: EventReader<Move>,
mut pieces: Query<(Entity, &mut BoardIndex), With<Piece>>,
@ -352,9 +355,11 @@ pub(crate) fn update_board(
if *index == *from {
match to {
Some(to_idx) => {
info!("Moving piece {:?} to {:?}", entity, to_idx);
*index = to_idx.clone();
}
None => {
info!("Capturing piece {:?}", entity);
commands
.entity(entity)
.remove::<BoardIndex>()
@ -364,6 +369,7 @@ pub(crate) fn update_board(
}
});
selected.iter().for_each(|entity| {
info!("De-selecting selected piece {:?}", entity);
commands.entity(entity).remove::<Selected>();
});
})
@ -411,6 +417,7 @@ fn deselect_sync(
.iter()
.find(|(e, idx)| *idx == this_idx && *e != this_e)
{
info!("De-selecting entangled piece {:?}", entity);
commands.entity(entangled).remove::<Selected>();
}
}
@ -472,6 +479,7 @@ fn null_selections(
) {
events.iter().for_each(|entity| {
if selected_pieces.is_empty() {
info!("De-selecting piece that should not be selected {:?}", entity);
commands.entity(entity).remove::<Selected>();
writer.send(audio::AudioEvent::PutDown);
}
@ -492,11 +500,11 @@ fn asserts<T: Component>(
selected_tiles: Query<Entity, (With<Tile>, With<T>, With<Selected>)>,
) {
if selected_pieces.iter().len() > 2 {
panic!("More than two piece is selected");
panic!("More than two pieces selected");
}
if selected_tiles.iter().len() > 2 {
panic!(
"More than two tile is selected {:?}",
"More than two tiles selected {:?}",
selected_tiles.iter().len()
);
}

@ -11,14 +11,14 @@ use crate::prelude::*;
/// Hit data for 2d sprites
#[derive(Debug)]
pub(crate) struct Hit2d {
point: Vec2,
_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,
_point: Vec3,
}
/// A 3D Triangle used for ray-intersection tests
@ -106,7 +106,7 @@ pub(crate) fn intersects3d(ray: &Ray, mesh: &Mesh, gt: &GlobalTransform) -> Opti
};
hit.then_some(Hit3d {
distance: d,
point: p,
_point: p,
})
} else {
None

@ -170,7 +170,7 @@ pub(crate) fn any_component_added<C: Component>(q: Query<Entity, Added<C>>) -> b
!q.is_empty()
}
pub(crate) fn any_component_added_or_changed<C: Component>(
pub(crate) fn _any_component_added_or_changed<C: Component>(
q: Query<Entity, Or<(Added<C>, Changed<C>)>>,
) -> bool {
!q.is_empty()

Loading…
Cancel
Save