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.

605 lines
18 KiB
Rust

// Bevy basically forces "complex types" with Querys
#![allow(clippy::type_complexity)]
use bevy::platform::hash::RandomState;
use std::hash::BuildHasher;
use games::physics2d::*;
use games::*;
fn main() {
App::new()
.add_plugins((
BaseGamePlugin {
name: "flappy bird (with rewind)".into(),
game_type: GameType::Two,
},
Physics2dPlugin,
))
.insert_resource(Gravity(Vec2::NEG_Y * 9.8 * 100.0))
.init_resource::<Score>()
.init_resource::<RewindFrames>()
.init_resource::<Flaps>()
.init_resource::<Deaths>()
.init_state::<PlayerState>()
.add_systems(
Startup,
(
init_bird,
init_obstacles,
init_ui,
tweak_camera.after(create_camera_2d),
),
)
.add_systems(OnEnter(PlayerState::Alive), alive_bird)
.add_systems(OnEnter(PlayerState::Pause), pause_bird)
.add_systems(OnEnter(PlayerState::Stasis), pause_bird)
.add_systems(OnEnter(PlayerState::Rewind), pause_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_kb.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)),
// Camera follows when bird moves regardless of player state
camera_follow_bird.run_if(any_component_changed::<Transform>),
// Pause when the player presses Escape
pause_game.run_if(input_just_pressed(KeyCode::Escape)),
// Transition out of the pause screen if the player presses space
un_pause_game
.run_if(input_just_pressed(KeyCode::Space))
.run_if(in_state(PlayerState::Pause)),
// Stats that get synced to the UI
(
sync_resource_to_ui::<Score>.run_if(resource_changed::<Score>),
sync_resource_to_ui::<Flaps>.run_if(resource_changed::<Flaps>),
sync_resource_to_ui::<Deaths>.run_if(resource_changed::<Deaths>),
sync_resource_to_ui::<RewindFrames>.run_if(resource_changed::<RewindFrames>),
),
scoring.run_if(on_event::<CollisionEnded>),
),
)
.add_observer(flap)
.run();
}
fn tweak_camera(camera: Single<&mut Camera>) {
debug!("Tweaking camera");
let mut c = camera.into_inner();
c.clear_color = ClearColorConfig::Custom(WHITE.into());
}
#[derive(Component)]
struct Bird;
#[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)]
enum PlayerState {
Alive,
Rewind,
Stasis,
#[default]
Pause,
}
// A tape tracking the bird's state every frame
#[derive(Component, Default)]
struct Tape {
translations: Vec<Vec3>,
rotations: Vec<Quat>,
linear_velocities: Vec<LinearVelocity>,
angular_velocities: Vec<AngularVelocity>,
}
fn init_bird(
mut commands: Commands,
server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
let material = MeshMaterial2d(materials.add(ColorMaterial {
texture: Some(server.load("kenny.nl/1-bit-platformer-pack/tile_0380.png")),
color: ORANGE.into(),
alpha_mode: AlphaMode2d::Blend,
..default()
}));
let mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 1.0)));
let name = Name::new("bird");
let t = Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::splat(100.0));
let physics = (
RigidBody::Static,
Collider::circle(0.5),
Mass(10.0),
ExternalImpulse::default().with_persistence(false),
);
let tape = Tape::default();
commands.spawn((
name,
mesh,
material,
physics,
t,
Bird,
tape,
CollisionEventsEnabled,
));
}
#[derive(Component, Clone)]
struct Ground;
#[derive(Component, Clone)]
struct Pipe;
#[derive(Component, Clone)]
struct Hitbox;
fn init_obstacles(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
server: Res<AssetServer>,
) {
let ground = {
let material = MeshMaterial2d(materials.add(ColorMaterial {
texture: Some(server.load("kenny.nl/1-bit-platformer-pack/tile_0088.png")),
color: BLACK.into(),
..default()
}));
let mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 1.0)));
let name = Name::new("ground");
let physics = (RigidBody::Static, Collider::rectangle(1.0, 1.0));
(name, mesh, material, physics, Ground)
};
let pipe = {
let material = MeshMaterial2d(materials.add(ColorMaterial {
texture: Some(server.load("kenny.nl/1-bit-platformer-pack/tile_0247.png")),
color: GREEN.into(),
..default()
}));
let mesh = Mesh2d(meshes.add(Rectangle::new(1.0, 1.0)));
let physics = (RigidBody::Static, Collider::rectangle(1.0, 1.0));
let name = Name::new("pipe");
(
name.clone(),
mesh.clone(),
material.clone(),
physics.clone(),
Pipe,
)
};
let hitbox = {
let physics = (
RigidBody::Static,
Collider::rectangle(1.0, 10.0),
Sensor,
CollisionEventsEnabled,
);
let name = Name::new("hitbox");
(name.clone(), physics.clone(), Hitbox)
};
// TODO: Instead of spawning infinite floor/pipes, we should instead spawn enough for 1-3 a few
// screens and then "move" them around.
// This is considerably more complexity so can be implemented later, but would keep memory
// overhead fairly static.
(1..10).for_each(|i| {
// TODO: Jitter close/far of pipes for challenge
let jitter: f32 = {
let h = RandomState::default();
let x: u64 = h.hash_one(i);
((x % 10) * 20) as f32 - 100.0
};
info!("Jitter for {i} is {jitter}");
let above = {
let x = 300.0 * i as f32;
let y = 300.0 + jitter;
Transform::from_xyz(x, y, -1.0).with_scale(Vec3::splat(100.0))
};
commands.spawn((pipe.clone(), above));
let below = {
let x = 300.0 * i as f32;
let y = -200.0 + jitter;
Transform::from_xyz(x, y, -1.0).with_scale(Vec3::splat(100.0))
};
commands.spawn((pipe.clone(), below));
let hitbox_pos =
Transform::from_xyz(300.0 * i as f32, 0.0, 10.0).with_scale(Vec3::splat(100.0));
commands.spawn((hitbox_pos, hitbox.clone()));
let floor1 =
Transform::from_xyz(300.0 * i as f32 - 100.0, -300.0, 1.0).with_scale(Vec3::splat(100.0));
commands.spawn((ground.clone(), floor1));
let floor2 =
Transform::from_xyz(300.0 * i as f32, -300.0, 1.0).with_scale(Vec3::splat(100.0));
commands.spawn((ground.clone(), floor2));
let floor3 =
Transform::from_xyz(300.0 * i as f32 + 100.0, -300.0, 1.0).with_scale(Vec3::splat(100.0));
commands.spawn((ground.clone(), floor3));
});
}
fn init_ui(mut commands: Commands) {
commands.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
flex_direction: FlexDirection::Column,
..default()
},
PlayerState::Stasis,
children![
(
Text::new("You Died"),
TextLayout::new_with_justify(JustifyText::Center)
),
(
SyncResource::<Score>::default(),
Text::default(),
TextLayout::new_with_justify(JustifyText::Center)
),
(
SyncResource::<Flaps>::default(),
Text::default(),
TextLayout::new_with_justify(JustifyText::Center)
),
(
SyncResource::<RewindFrames>::default(),
Text::default(),
TextLayout::new_with_justify(JustifyText::Center)
),
(
SyncResource::<Deaths>::default(),
Text::default(),
TextLayout::new_with_justify(JustifyText::Center)
),
(
Text::new("Press R to Rewind"),
TextLayout::new_with_justify(JustifyText::Center)
),
],
));
fn start_game(_trigger: Trigger<Pointer<Click>>, mut next: ResMut<NextState<PlayerState>>) {
next.set(PlayerState::Alive);
}
commands
.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
flex_direction: FlexDirection::Column,
..default()
},
Button,
// TODO: Add Pause (basically Stasis) state
PlayerState::Pause,
children![Text::new("Go!"),],
))
.observe(start_game);
commands
.spawn(Node {
align_self: AlignSelf::End,
justify_self: JustifySelf::Center,
flex_direction: FlexDirection::Row,
..default()
})
.with_children(|parent| {
parent
.spawn((
Node { ..default() },
Button,
children![Text::new("Rewind!"),],
))
.observe(start_rewind)
.observe(end_rewind);
parent
.spawn((Node { ..default() }, Button, children![Text::new("Flap!"),]))
.observe(flap_button);
});
commands.spawn((
Node {
align_self: AlignSelf::Start,
justify_self: JustifySelf::Center,
..default()
},
BackgroundColor(WHITE.into()),
SyncResource::<Score>::default(),
Text::default(),
TextLayout::new_with_justify(JustifyText::Center),
));
}
fn start_rewind(_trigger: Trigger<Pointer<Pressed>>, mut next: ResMut<NextState<PlayerState>>) {
next.set(PlayerState::Rewind);
}
fn end_rewind(_trigger: Trigger<Pointer<Released>>, mut next: ResMut<NextState<PlayerState>>) {
next.set(PlayerState::Alive);
}
fn flap_button(
_trigger: Trigger<Pointer<Pressed>>,
mut commands: Commands,
bird: Single<Entity, With<Bird>>,
) {
let e = *bird;
debug!("Flapping {:?}", e);
commands.trigger_targets(Flap, e);
}
/// Pause the game when the player presses "Escape"
fn pause_game(mut next: ResMut<NextState<PlayerState>>) {
next.set(PlayerState::Pause);
}
fn un_pause_game(mut next: ResMut<NextState<PlayerState>>) {
next.set(PlayerState::Alive);
}
// TODO: Create floor (and ceiling?)
// Q: Move bird + camera or move world around bird & camera?
// TODO: Obstacles
#[derive(Component, Clone, Event)]
struct Flap;
// Observer for flapping
fn flap(
trigger: Trigger<Flap>,
mut bird: Query<&mut ExternalImpulse, With<Bird>>,
mut flaps: ResMut<Flaps>,
) {
info!("real flap for {:?}", trigger.target());
// Increment flap stat
flaps.0 += 1;
// Flap birds wings
if let Ok(mut f) = bird.get_mut(trigger.target()) {
f.apply_impulse(Vec2::Y * 5000.0 + Vec2::X * 1000.0);
}
}
fn flap_kb(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
#[cfg(debug_assertions)] keycode: Res<ButtonInput<KeyCode>>,
birds: Query<Entity, With<Bird>>,
mut commands: Commands,
) {
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"
);
birds.iter().for_each(|e| {
debug!("Flapping {:?}", e);
commands.trigger_targets(Flap, e);
});
}
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, &LinearVelocity, &AngularVelocity, &mut Tape), With<Bird>>,
) {
debug_assert!(
matches!(state.get(), PlayerState::Alive),
"Only record in the alive state"
);
birds
.iter_mut()
.for_each(|(transform, linear_velocity, angular_velocity, mut tape)| {
tape.translations.push(transform.translation);
tape.rotations.push(transform.rotation);
tape.linear_velocities.push(*linear_velocity);
tape.angular_velocities.push(*angular_velocity);
});
}
fn rewind(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
mut birds: Query<
(
&mut Transform,
&mut AngularVelocity,
&mut LinearVelocity,
&mut Tape,
),
With<Bird>,
>,
mut frames: ResMut<RewindFrames>,
) {
debug_assert!(
matches!(state.get(), PlayerState::Rewind),
"Only rewind in the rewinding state"
);
birds.iter_mut().for_each(
|(mut transform, mut angular_velocity, mut linear_velocity, mut tape)| {
match (tape.translations.pop(), tape.rotations.pop()) {
(Some(t), Some(r)) => {
transform.translation = t;
transform.rotation = r;
frames.0 += 1;
}
(None, None) => (),
_ => panic!("Translations and rotations are out of sync!"),
}
// TODO: Only need to set {angular|linear}_velocity at end of Rewind
if let Some(av) = tape.angular_velocities.pop() {
*angular_velocity = av;
}
if let Some(lv) = tape.linear_velocities.pop() {
*linear_velocity = lv;
}
},
);
}
// PERF: May run more than necessary, should be event-driven on aabb intersection
fn detect_dead(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
bird: Single<&ColliderAabb, With<Bird>>,
obstacles: Query<&ColliderAabb, Or<(With<Ground>, With<Pipe>)>>,
mut next: ResMut<NextState<PlayerState>>,
) {
debug_assert!(
matches!(state.get(), PlayerState::Alive),
"Only check if dead while alive"
);
if obstacles.iter().any(|obstacle| bird.intersects(obstacle)) {
next.set(PlayerState::Stasis);
}
}
fn alive_bird(
#[cfg(debug_assertions)] state: Res<State<PlayerState>>,
mut bird: Single<&mut RigidBody, With<Bird>>,
) {
debug_assert!(matches!(state.get(), PlayerState::Alive));
debug!("Setting bird to Dynamic");
**bird = RigidBody::Dynamic;
}
fn pause_bird(
state: Res<State<PlayerState>>,
mut bird: Single<&mut RigidBody, With<Bird>>,
mut deaths: ResMut<Deaths>,
) {
debug_assert!(!matches!(state.get(), PlayerState::Alive));
// Increment death count
if state.get() == &PlayerState::Stasis { deaths.0 += 1 }
debug!("Setting bird to Static");
**bird = RigidBody::Static;
}
fn camera_follow_bird(
bird: Single<&Transform, (With<Bird>, Changed<Transform>)>,
mut camera: Single<&mut Transform, (With<Camera>, Without<Bird>)>,
) {
camera.translation.x = bird.translation.x;
}
/// How many pipes the player has passed
#[derive(Resource, Default)]
struct Score(usize);
impl Display for Score {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "Score: {}", self.0)
}
}
/// Number of frames that were rewound
#[derive(Resource, Default)]
struct RewindFrames(usize);
impl Display for RewindFrames {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "Frames Rewound: {}", self.0)
}
}
// Track number of times player flapped their wings
#[derive(Resource, Default)]
struct Flaps(usize);
impl Display for Flaps {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "Flaps: {}", self.0)
}
}
// Track number of times player died
#[derive(Resource, Default)]
struct Deaths(usize);
impl Display for Deaths {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "Deaths: {}", self.0)
}
}
// TODO: Scoring is bugged, need to make it correct.
fn scoring(
mut events: EventReader<CollisionEnded>,
state: Res<State<PlayerState>>,
bird: Query<Entity, With<Bird>>,
hitboxes: Query<Entity, With<Hitbox>>,
mut score: ResMut<Score>,
) {
events
.read()
.filter(|CollisionEnded(a, b)| bird.contains(*a) && hitboxes.contains(*b))
.for_each(|_| {
debug!("Hit event while {:?}", state.get());
match state.get() {
PlayerState::Alive => score.0 = score.0.saturating_add(1),
PlayerState::Rewind => score.0 = score.0.saturating_sub(1),
PlayerState::Pause | PlayerState::Stasis => (),
}
})
}