You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

251 lines
7.4 KiB
Rust

use bevy::{input::common_conditions::input_just_released, render::view::VisibleEntities};
use games::*;
fn main() {
App::new()
.add_plugins(BaseGamePlugin {
name: "flappy bird (with rewind)".into(),
})
.init_state::<PlayerState>()
.add_systems(Startup, (init_bird, init_ground, init_ui, tweak_camera.after(setup_camera)))
.add_systems(OnEnter(PlayerState::Alive), alive_bird)
.add_systems(OnExit(PlayerState::Alive), kill_bird)
.add_systems(
Update,
(
// Systems to run when player state changes
(
// Print out when this state changes for debugging purposes
debug_state_changes::<PlayerState>,
// Toggle (UI) elements when the player dies/alives
toggle_state_visibility::<PlayerState>,
).run_if(state_changed::<PlayerState>),
// Detect if the bird is "dead" by checking if it is visible
// from the point of view of the camera
detect_dead.run_if(in_state(PlayerState::Alive)),
// Toggle rewinding state when "R" is pressed/released
toggle_rewind.run_if(
input_just_pressed(KeyCode::KeyR).or(input_just_released(KeyCode::KeyR)),
),
// Systems to run in the "play" state
(
// Only flap when we press the space key
flap.run_if(input_just_pressed(KeyCode::Space)),
// Rewinding systems
record.run_if(any_component_changed::<Transform>),
)
.run_if(in_state(PlayerState::Alive)),
// Rewinding systems
rewind.run_if(in_state(PlayerState::Rewind)),
),
)
.run();
}
fn tweak_camera(
mut camera: Query<(&mut Camera, &mut AmbientLight), With<Camera>>,
) {
camera.iter_mut().for_each(|(mut c, mut al)| {
c.clear_color = ClearColorConfig::Custom(WHITE.into());
al.brightness = 100.0;
});
}
#[derive(Component)]
struct Bird;
#[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)]
enum PlayerState {
#[default]
Alive,
Rewind,
Dead,
}
// A tape tracking the bird's state every frame
#[derive(Component, Default)]
struct Tape {
translations: Vec<Vec3>,
rotations: Vec<Quat>,
}
fn init_bird(
mut commands: Commands,
server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let material = MeshMaterial3d(materials.add(StandardMaterial {
base_color_texture: Some(server.load("flappy/bevy.png")),
base_color: WHITE.into(),
alpha_mode: AlphaMode::Blend,
..default()
}));
let mesh = Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0))));
let name = Name::new("bird");
let t = Transform::from_xyz(0.0, 0.0, -10.0).with_rotation(Quat::from_rotation_x(PI / 2.0));
let physics = (
RigidBody::Dynamic,
Collider::capsule(1.0, 1.0), Mass(1.0),
ExternalForce::new(Vec3::X * 1.0).with_persistence(true),
ExternalImpulse::default().with_persistence(false),
LockedAxes::ROTATION_LOCKED.lock_translation_z(),
);
let tape = Tape::default();
commands.spawn((name, mesh, material, physics, t, Bird, tape));
}
#[derive(Component)]
struct Ground;
fn init_ground(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let material = MeshMaterial3d(materials.add(StandardMaterial {
base_color: GREEN.into(),
..default()
}));
let mesh = Mesh3d(meshes.add(Cuboid::new(10.0, 1.0, 1.0)));
let name = Name::new("ground");
let t = Transform::from_xyz(0.0, -4.0, -10.0);
let physics = (RigidBody::Static, Collider::cuboid(1.0, 1.0, 1.0));
commands.spawn((name, mesh, material, physics, t, Ground));
}
fn init_ui(
mut commands: Commands,
) {
commands.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
flex_direction: FlexDirection::Column,
..default()
},
PlayerState::Dead,
children![
Text::new("You Died"),
Text::new("Press R to Rewind"),
],
));
}
// TODO: Create floor (and ceiling?)
// Q: Move bird + camera or move world around bird & camera?
// TODO: Obstacles
fn flap(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
#[cfg(debug_assertions)] keycode: Res<ButtonInput<KeyCode>>,
mut bird: Query<(&Transform, &mut ExternalImpulse), With<Bird>>,
) {
debug_assert!(
matches!(state.get(), PlayerState::Alive),
"Only flap when playing"
);
debug_assert!(
keycode.just_pressed(KeyCode::Space),
"Only flap when space is just pressed"
);
bird.iter_mut().for_each(|(t, mut f)| {
f.apply_impulse(t.rotation * Vec3::NEG_Z * 5.0);
});
}
fn toggle_rewind(keycode: Res<ButtonInput<KeyCode>>, mut next: ResMut<NextState<PlayerState>>) {
debug_assert!(
keycode.just_pressed(KeyCode::KeyR) || keycode.just_released(KeyCode::KeyR),
"Only toggle rewind when R is pressed"
);
if keycode.just_pressed(KeyCode::KeyR) {
debug!("Toggling rewind ON");
next.set(PlayerState::Rewind)
} else {
debug!("Toggling rewind OFF");
next.set(PlayerState::Alive)
}
}
fn record(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
mut birds: Query<(&Transform, &mut Tape), With<Bird>>,
) {
debug_assert!(
matches!(state.get(), PlayerState::Alive),
"Only record in the alive state"
);
birds.iter_mut().for_each(|(transform, mut tape)| {
tape.translations.push(transform.translation);
tape.rotations.push(transform.rotation);
});
}
fn rewind(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
mut birds: Query<(&mut Transform, &mut Tape), With<Bird>>,
) {
debug_assert!(
matches!(state.get(), PlayerState::Rewind),
"Only rewind in the rewinding state"
);
birds.iter_mut().for_each(|(mut transform, mut tape)| {
if let Some(t) = tape.translations.pop() {
transform.translation = t;
}
if let Some(r) = tape.rotations.pop() {
transform.rotation = r;
}
});
}
// PERF: Runs more than it needs, should only execute when bird enters/exit frame
fn detect_dead(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
bird: Single<&ColliderAabb, With<Bird>>,
ground: Single<&ColliderAabb, With<Ground>>,
mut next: ResMut<NextState<PlayerState>>,
) {
debug_assert!(
matches!(state.get(), PlayerState::Alive),
"Only check if dead while alive"
);
if bird.intersects(*ground) {
next.set(PlayerState::Dead);
}
}
fn alive_bird(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
mut bird: Single<&mut RigidBody, With<Bird>>,
) {
debug_assert!(!matches!(state.get(), PlayerState::Dead));
debug!("Aliving bird");
**bird = RigidBody::Dynamic;
}
fn kill_bird(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
mut bird: Single<&mut RigidBody, With<Bird>>,
) {
debug_assert!(!matches!(state.get(), PlayerState::Alive));
debug!("Killing bird");
**bird = RigidBody::Static;
}