initial commit

This commit is contained in:
Zuckerberg 2024-07-18 22:47:28 -06:00
commit df82ddba6e
94 changed files with 7224 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
result

51
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,51 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'bevy_test'",
"cargo": {
"args": [
"build",
"--bin=bevy_test",
"--package=bevy_test"
],
"filter": {
"name": "bevy_test",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}",
"env": {
"CARGO_MANIFEST_DIR": "${workspaceFolder}"
}
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'bevy_test'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=bevy_test",
"--package=bevy_test"
],
"filter": {
"name": "bevy_test",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}",
"env": {
"CARGO_MANIFEST_DIR": "${workspaceFolder}"
}
}
]
}

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix",
"rust-analyzer.linkedProjects": [
"./Cargo.toml"
]
}

6179
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

37
Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
authors = ["Zuckerberg <zuckerberg@neet.dev>"]
edition = "2021"
name = "bevy_test"
version = "0.1.0"
# Enable max optimizations for dependencies, but not for our code:
[profile.dev.package."*"]
opt-level = 3
# Enable only a small amount of optimization in debug mode
#[profile.dev]
#opt-level = 1
# Allow debugging in release mode
#[profile.release]
#debug = 1
[dependencies]
rand = "*"
serialport = "*"
crc = "*"
[dependencies.bevy]
version = "0.14.0"
default-features = true
features = ["wayland"]
[dependencies.bevy_rapier3d]
version = "0.26.0"
features = ["enhanced-determinism", "serde-serialize"]
[dependencies.rapier3d]
version = "0.21.0"
[dependencies.noise]
version = "*"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
assets/textures/UI/UIbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
assets/textures/UI/tap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

BIN
assets/textures/rock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
assets/textures/rockIce.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

96
flake.lock generated Normal file
View File

@ -0,0 +1,96 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1721138476,
"narHash": "sha256-+W5eZOhhemLQxelojLxETfbFbc19NWawsXBlapYpqIA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ad0b5eed1b6031efaed382844806550c3dcb4206",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1721355572,
"narHash": "sha256-I4TQ2guV9jTmZsXeWt5HMojcaqNZHII4zu0xIKZEovM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d5bc7b1b21cf937fb8ff108ae006f6776bdb163d",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

105
flake.nix Normal file
View File

@ -0,0 +1,105 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rust-overlay.url = "github:oxalica/rust-overlay";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
flake-utils.lib.eachDefaultSystem (
system:
let
overlays = [
(import rust-overlay)
];
pkgs = import nixpkgs {
inherit system overlays;
};
app = "bevy_test";
rust = pkgs.rust-bin.stable.latest.default.override { extensions = [ "rust-src" ]; };
rustPlatform = pkgs.makeRustPlatform {
cargo = rust;
rustc = rust;
};
shellInputs = [
rust
pkgs.clang
];
appNativeBuildInputs = with pkgs; [
pkg-config
];
appBuildInputs = appRuntimeInputs ++ (with pkgs; [
udev
alsaLib
wayland
vulkan-tools
vulkan-headers
vulkan-validation-layers
]);
appRuntimeInputs = with pkgs; [
vulkan-loader
libxkbcommon
];
in
{
devShells =
let
game = pkgs.mkShell {
nativeBuildInputs = appNativeBuildInputs;
buildInputs = shellInputs ++ appBuildInputs;
shellHook = ''
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.lib.makeLibraryPath appRuntimeInputs}"
'';
};
in
rec {
${app} = game;
default = game;
};
packages =
let
game = rustPlatform.buildRustPackage {
pname = app;
version = "0.1.0";
src = ./.;
cargoLock = {
lockFile = ./Cargo.lock;
};
nativeBuildInputs = appNativeBuildInputs;
buildInputs = appBuildInputs;
postInstall = ''
cp -r assets $out/bin/
'';
};
in
rec {
${app} = game;
default = game;
};
apps =
let
game = {
type = "app";
program = "${self.packages.${app}}/bin/${app}";
};
in
rec {
${app} = game;
default = game;
};
checks.build = self.packages.${system}.${app};
}
);
}

1
shell.nix Normal file
View File

@ -0,0 +1 @@
(builtins.getFlake (toString ./.)).devShells.${builtins.currentSystem}.default

44
src/asset_loader.rs Normal file
View File

@ -0,0 +1,44 @@
use bevy::{asset::LoadedFolder, prelude::*};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, States)]
pub enum AssetLoadState {
#[default]
AssetsLoading,
AssetsLoaded,
}
pub struct AssetLoaderPlugin;
impl Plugin for AssetLoaderPlugin {
fn build(&self, app: &mut App) {
app.init_state::<AssetLoadState>()
.add_systems(OnEnter(AssetLoadState::AssetsLoading), load_assets)
.add_systems(
Update,
check_assets.run_if(in_state(AssetLoadState::AssetsLoading)),
);
}
}
#[derive(Resource, Default)]
struct AssetFolder(Handle<LoadedFolder>);
fn load_assets(mut commands: Commands, asset_server: Res<AssetServer>) {
println!("Loading assets.");
// load multiple, assets from a folder
commands.insert_resource(AssetFolder(asset_server.load_folder(".")));
}
fn check_assets(
mut next_state: ResMut<NextState<AssetLoadState>>,
rpg_sprite_folder: Res<AssetFolder>,
mut events: EventReader<AssetEvent<LoadedFolder>>,
) {
// Advance the `AppState` once all sprite handles have been loaded by the `AssetServer`
for event in events.read() {
if event.is_loaded_with_dependencies(&rpg_sprite_folder.0) {
next_state.set(AssetLoadState::AssetsLoaded);
println!("Assets loaded.");
}
}
}

113
src/background.rs Normal file
View File

@ -0,0 +1,113 @@
use crate::{asset_loader::AssetLoadState::AssetsLoaded, system_set::MovementSet};
use bevy::{prelude::*, sprite::Anchor, window::PrimaryWindow};
pub struct BackgroundPlugin;
impl Plugin for BackgroundPlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(AssetsLoaded), setup).add_systems(
Update,
update_parallax.in_set(MovementSet::PostCameraMovement),
);
}
}
#[derive(Component)]
struct Parallax {
speed: f32,
tile_width: u32,
start_offset: f32,
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
assets: Res<Assets<Image>>,
window: Query<&Window, With<PrimaryWindow>>,
) {
let window = window.single();
let ground = asset_server.load("textures/groundSnow.png");
let ground_size = assets.get(&ground).unwrap().size();
let ground_tiles_needed = (window.width() * 4.0) as u32 / ground_size.x;
let ground_tile_start_offset = -1.5 * window.width();
commands.spawn((
SpriteBundle {
texture: ground,
sprite: Sprite {
custom_size: Some(Vec2::new(
(ground_size.x * ground_tiles_needed) as f32,
ground_size.y as f32,
)),
anchor: Anchor::BottomLeft,
..default()
},
transform: Transform::from_xyz(ground_tile_start_offset, -0.5 * window.height(), 10.0),
..default()
},
ImageScaleMode::Tiled {
tile_x: true,
tile_y: true,
stretch_value: 1.0,
},
Parallax {
speed: 0.0, // same speed as the camera
tile_width: ground_size.x,
start_offset: ground_tile_start_offset,
},
));
let background = asset_server.load("textures/background.png");
let background_size = assets.get(&background).unwrap().size();
let background_tiles_needed = (window.width() * 4.0) as u32 / background_size.x;
let background_tile_start_offset = -1.5 * window.width();
commands.spawn((
SpriteBundle {
texture: background,
sprite: Sprite {
custom_size: Some(Vec2::new(
(background_size.x * background_tiles_needed) as f32,
background_size.y as f32,
)),
anchor: Anchor::BottomLeft,
..default()
},
transform: Transform::from_xyz(
background_tile_start_offset,
-0.5 * window.height(),
-1.0,
),
..default()
},
ImageScaleMode::Tiled {
tile_x: true,
tile_y: true,
stretch_value: 1.0,
},
Parallax {
speed: 0.5, // same speed as the camera
tile_width: background_size.x,
start_offset: background_tile_start_offset,
},
));
}
fn update_parallax(
mut parallax: Query<(&Parallax, &mut Transform), Without<Camera2d>>,
camera: Query<&Transform, With<Camera2d>>,
) {
let Ok(camera) = camera.get_single() else {
return;
};
for (parallax, mut transform) in &mut parallax {
// + parallax.start_offset
let mut new_offset = camera.translation.x * parallax.speed;
let tiling_undershoot =
((camera.translation.x - new_offset) / parallax.tile_width as f32) as i32;
if tiling_undershoot != 0 {
new_offset += parallax.tile_width as f32 * tiling_undershoot as f32;
}
transform.translation.x = new_offset + parallax.start_offset;
}
}

62
src/camera.rs Normal file
View File

@ -0,0 +1,62 @@
use bevy::{prelude::*, window::PrimaryWindow};
use crate::{player::Player, system_set::MovementSet};
const CAM_LERP_FACTOR: f32 = 1000.;
pub struct CameraPlugin;
impl Plugin for CameraPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, setup).add_systems(
Update,
(move_camera, follow_player).in_set(MovementSet::PostPlayerMovement),
);
}
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
}
fn move_camera(
keys: Res<ButtonInput<KeyCode>>,
mut camera: Query<&mut Transform, With<Camera2d>>,
time: Res<Time>,
) {
let Ok(mut camera) = camera.get_single_mut() else {
return;
};
if keys.pressed(KeyCode::KeyW) {
camera.translation.y += time.delta_seconds() * CAM_LERP_FACTOR
}
if keys.pressed(KeyCode::KeyS) {
camera.translation.y -= time.delta_seconds() * CAM_LERP_FACTOR
}
// if keys.pressed(KeyCode::KeyA) {
// camera.translation.x -= time.delta_seconds() * CAM_LERP_FACTOR
// }
// if keys.pressed(KeyCode::KeyD) {
// camera.translation.x += time.delta_seconds() * CAM_LERP_FACTOR
// }
}
fn follow_player(
mut camera: Query<&mut Transform, With<Camera2d>>,
player: Query<&mut Transform, (With<Player>, Without<Camera2d>)>,
window: Query<&Window, With<PrimaryWindow>>,
) {
let window = window.single();
let Ok(mut camera) = camera.get_single_mut() else {
return;
};
let Ok(player) = player.get_single() else {
return;
};
// Always position the camera so the player is in the first 1/4th of the screen
camera.translation.x = player.translation.x + window.width() / 4.0;
}

41
src/main.rs Normal file
View File

@ -0,0 +1,41 @@
mod asset_loader;
mod background;
mod camera;
mod obstacle;
mod player;
mod system_set;
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Tappy Plane".into(),
name: Some("TappyPlane.app".into()),
resolution: (800., 480.).into(),
..default()
}),
..default()
}))
// .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
.add_plugins((
asset_loader::AssetLoaderPlugin,
camera::CameraPlugin,
background::BackgroundPlugin,
player::PlayerPlugin,
obstacle::ObstaclePlugin,
system_set::SystemSetPlugin,
))
.add_systems(Update, quit_on_escape_system)
.run();
}
fn quit_on_escape_system(
keys: Res<ButtonInput<KeyCode>>,
mut app_exit_events: ResMut<Events<AppExit>>,
) {
if keys.just_pressed(KeyCode::Escape) {
app_exit_events.send(AppExit::Success);
}
}

202
src/obstacle.rs Normal file
View File

@ -0,0 +1,202 @@
use crate::{
asset_loader::AssetLoadState::AssetsLoaded,
player::Player,
system_set::{DespawnSet, MovementSet},
};
use std::time::Duration;
use bevy::{prelude::*, sprite::Anchor, window::PrimaryWindow};
use rand::RngCore;
const OBSTACLE_SPAWN_DELAY: f32 = 2.0;
pub struct ObstaclePlugin;
impl Plugin for ObstaclePlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(AssetsLoaded), setup)
.add_systems(
Update,
add_obstacles.in_set(MovementSet::PostPlayerMovement),
)
.add_systems(
Update,
despawn_offscreen.in_set(DespawnSet::DespawnOffscreen),
);
}
}
#[derive(Component)]
struct ObstacleTimer {
timer: Timer,
obstacles_placed: u32,
}
#[derive(Component)]
pub struct Obstacle {
pub p1: Vec2,
pub p2: Vec2,
pub p3: Vec2,
}
#[derive(Component)]
pub struct Star {
pub radius: f32,
pub points: u32,
}
fn setup(mut commands: Commands) {
commands.spawn(ObstacleTimer {
timer: Timer::new(
Duration::from_secs_f32(OBSTACLE_SPAWN_DELAY),
TimerMode::Repeating,
),
obstacles_placed: 0,
});
}
fn add_obstacles(
mut commands: Commands,
mut timer: Query<&mut ObstacleTimer>,
window: Query<&Window, With<PrimaryWindow>>,
player: Query<&Transform, With<Player>>,
asset_server: Res<AssetServer>,
assets: Res<Assets<Image>>,
time: Res<Time>,
) {
let window = window.single();
let Ok(mut timer) = timer.get_single_mut() else {
return;
};
let Ok(player) = player.get_single() else {
return;
};
timer.timer.tick(time.delta());
if timer.timer.just_finished() {
let mut rng = rand::thread_rng();
let is_roof_obstacle = rng.next_u32() % 2 == 0;
let obstacle_scale = if rng.next_u32() % 2 == 0 { 1.0 } else { 0.75 };
let texture: Handle<Image> = match rng.next_u32() % 4 {
0 => asset_server.load("textures/rock.png"),
1 => asset_server.load("textures/rockGrass.png"),
2 => asset_server.load("textures/rockIce.png"),
3 => asset_server.load("textures/rockSnow.png"),
_ => unreachable!(),
};
let transform = Transform::from_xyz(
window.width() + player.translation.x,
(if is_roof_obstacle { 1.0 } else { -1.0 }) * window.height() / 2.0,
5.0,
) * Transform::from_scale(Vec3::splat(obstacle_scale));
let obstacle_texture = assets.get(&texture).unwrap();
let obstacle_width = (obstacle_texture.width() as f32 * obstacle_scale) as f32;
let obstacle_height = (obstacle_texture.height() as f32 * obstacle_scale) as f32;
let obstacle_tip = Vec2::new(
transform.translation.x,
transform.translation.y
+ (if is_roof_obstacle {
-1.0 * obstacle_height
} else {
obstacle_height
}),
);
let obstacle_left = Vec2::new(
transform.translation.x - obstacle_width / 2.0,
transform.translation.y,
);
let obstacle_right = Vec2::new(
transform.translation.x + obstacle_width / 2.0,
transform.translation.y,
);
commands.spawn((
SpriteBundle {
sprite: Sprite {
anchor: if is_roof_obstacle {
Anchor::TopCenter
} else {
Anchor::BottomCenter
},
flip_y: is_roof_obstacle,
..default()
},
texture: texture,
transform: transform,
..default()
},
Obstacle {
p1: obstacle_tip,
p2: obstacle_left,
p3: obstacle_right,
},
));
timer.obstacles_placed += 1;
// Place star at tip of obstacle
let star_value = rng.next_u32() % 4;
if star_value != 0 {
let texture: Handle<Image> = match star_value {
1 => asset_server.load("textures/starBronze.png"),
2 => asset_server.load("textures/starSilver.png"),
3 => asset_server.load("textures/starGold.png"),
_ => unreachable!(),
};
let star_texture = assets.get(&texture).unwrap();
let star_width = star_texture.width() as f32;
let transform = Transform::from_xyz(
transform.translation.x + star_width / 4.0,
transform.translation.y
+ (obstacle_height + star_width * 2.0)
* (if is_roof_obstacle { -1.0 } else { 1.0 }),
6.0,
);
commands.spawn((
SpriteBundle {
sprite: Sprite {
anchor: Anchor::Center,
..default()
},
texture: texture,
transform: transform,
..default()
},
Star {
radius: star_width,
points: star_value,
},
));
}
}
}
fn despawn_offscreen(
mut commands: Commands,
window: Query<&Window, With<PrimaryWindow>>,
player: Query<&Transform, With<Player>>,
obstacles: Query<(Entity, &Transform), Or<(With<Obstacle>, With<Star>)>>,
) {
let window = window.single();
let Ok(player) = player.get_single() else {
return;
};
for (entity, transform) in &obstacles {
if transform.translation.x < player.translation.x - window.width() {
commands.entity(entity).despawn();
}
}
}

235
src/player.rs Normal file
View File

@ -0,0 +1,235 @@
use std::time::Duration;
use bevy::prelude::*;
use crate::{
asset_loader::AssetLoadState::AssetsLoaded,
obstacle::{Obstacle, Star},
system_set::{DespawnSet, MovementSet},
};
const PLANE_SPEED: f32 = 200.0;
const TAP_ACCELERATION: f32 = 50000.0;
const GRAVITY_ACCERATION_RATE: f32 = -1000.0;
// const GRAVITY_ACCERATION_RATE: f32 = 0.0;
const MAX_VELOCITY: f32 = 1000.0;
const MAX_TILT_DEGREES: f32 = 45.0;
pub struct PlayerPlugin;
impl Plugin for PlayerPlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(AssetsLoaded), setup).add_systems(
Update,
(
move_plane.in_set(MovementSet::PlayerMovement),
animate_plane,
obstacle_collider.before(DespawnSet::DespawnCollectedStars),
collect_stars.in_set(DespawnSet::DespawnCollectedStars),
),
);
}
}
#[derive(Component)]
pub struct Player {
acceleration: f32,
velocity: f32,
animation_index: usize,
animation_timer: Timer,
textures: Vec<Handle<Image>>,
}
#[derive(Component)]
struct PlayerCollider {
radius: u32,
}
#[derive(Component)]
pub struct Score {
points: u32,
}
impl Player {
fn new(fps: u8, textures: Vec<Handle<Image>>) -> Self {
Self {
acceleration: 0.0,
velocity: 0.0,
animation_index: 0,
animation_timer: Timer::new(
Duration::from_secs_f32(1.0 / (fps as f32)),
TimerMode::Repeating,
),
textures: textures,
}
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, assets: Res<Assets<Image>>) {
let texture1: Handle<Image> = asset_server.load("textures/Planes/planeGreen1.png");
let texture2: Handle<Image> = asset_server.load("textures/Planes/planeGreen2.png");
let texture3: Handle<Image> = asset_server.load("textures/Planes/planeGreen3.png");
let player_radius = assets.get(&texture1).unwrap().width() / 2;
commands.spawn((
SpriteBundle {
texture: texture1.clone(),
..default()
},
Player::new(20, vec![texture1, texture2.clone(), texture3, texture2]),
PlayerCollider {
// Add a little wiggle room for collision detection
radius: (player_radius as f32 * 0.8) as u32,
},
Score { points: 0 },
));
}
fn animate_plane(mut player: Query<(&mut Player, &mut Handle<Image>)>, time: Res<Time>) {
let Ok((mut player, mut handle)) = player.get_single_mut() else {
return;
};
player.animation_timer.tick(time.delta());
if player.animation_timer.just_finished() {
player.animation_index += 1;
if player.animation_index == player.textures.len() {
player.animation_index = 0;
}
*handle = player.textures[player.animation_index].clone();
}
}
fn move_plane(
mut player: Query<(&mut Player, &mut Transform)>,
keys: Res<ButtonInput<KeyCode>>,
time: Res<Time>,
) {
let Ok((mut player, mut transform)) = player.get_single_mut() else {
return;
};
transform.translation.x += PLANE_SPEED * time.delta_seconds();
if keys.just_pressed(KeyCode::Space) {
player.acceleration = TAP_ACCELERATION;
} else {
player.acceleration = GRAVITY_ACCERATION_RATE;
}
player.velocity += player.acceleration * time.delta_seconds();
if player.velocity > MAX_VELOCITY {
player.velocity = MAX_VELOCITY;
}
if player.velocity < -MAX_VELOCITY {
player.velocity = -MAX_VELOCITY;
}
// println!("a {}, v {}", player.acceleration, player.velocity);
transform.translation.y += player.velocity * time.delta_seconds();
let degrees: f32 = player.velocity / MAX_VELOCITY * MAX_TILT_DEGREES;
transform.rotation = Quat::from_rotation_z(degrees.to_radians());
}
fn obstacle_collider(player: Query<(&Transform, &PlayerCollider)>, obstacles: Query<&Obstacle>) {
let Ok((player_transform, player_collider)) = player.get_single() else {
return;
};
let player_position = Vec2::new(
player_transform.translation.x,
player_transform.translation.y,
);
let player_radius = player_collider.radius as f32;
let mut collision_detected = false;
for obstacle in &obstacles {
if circle_collides_with_line_segment(
obstacle.p1,
obstacle.p2,
player_position,
player_radius,
) || circle_collides_with_line_segment(
obstacle.p1,
obstacle.p3,
player_position,
player_radius,
) || circle_collides_with_line_segment(
obstacle.p2,
obstacle.p3,
player_position,
player_radius,
) {
collision_detected = true;
break;
}
}
if collision_detected {
println!("Obstacle Collision detected");
} else {
// println!("No Collision detected");
}
}
fn circle_collides_with_line_segment(a: Vec2, b: Vec2, c: Vec2, r: f32) -> bool {
// Based on https://stackoverflow.com/a/1079478
let ac = c - a;
let ab = b - a;
let d = ac.project_onto(ab) + a;
let ad = d - a;
let k = if ab.x.abs() > ab.y.abs() {
ad.x / ab.x
} else {
ad.y / ab.y
};
let len_squared = if k <= 0.0 {
(c - a).length_squared()
} else if k >= 1.0 {
(c - b).length_squared()
} else {
(c - d).length_squared()
};
return r * r > len_squared;
}
fn collect_stars(
mut commands: Commands,
mut player: Query<(&Transform, &PlayerCollider, &mut Score)>,
stars: Query<(Entity, &Transform, &Star)>,
) {
let Ok((player_transform, player_collider, mut score)) = player.get_single_mut() else {
return;
};
let player_position = Vec2::new(
player_transform.translation.x,
player_transform.translation.y,
);
let player_radius = player_collider.radius as f32;
for (entity, star_transform, star) in &stars {
let star_position = Vec2::new(star_transform.translation.x, star_transform.translation.y);
let star_radius = star.radius as f32;
if circle_collides_with_circle(player_position, player_radius, star_position, star_radius) {
score.points += star.points;
println!("Player points: {}", score.points);
commands.entity(entity).despawn();
}
}
}
fn circle_collides_with_circle(c1: Vec2, r1: f32, c2: Vec2, r2: f32) -> bool {
return r1 * r1 + r2 * r2 > (c1 - c2).length_squared();
}

50
src/system_set.rs Normal file
View File

@ -0,0 +1,50 @@
use bevy::prelude::*;
pub struct SystemSetPlugin;
impl Plugin for SystemSetPlugin {
fn build(&self, app: &mut App) {
app.configure_sets(
Update,
(
(
MovementSet::PlayerMovement,
MovementSet::PostPlayerMovement,
MovementSet::PostCameraMovement,
)
.chain(),
(
DespawnSet::DespawnCollectedStars,
// TODO flush cmd buffer here
DespawnSet::DespawnOffscreen,
)
.chain(),
)
.chain(),
);
app.add_systems(
Update,
apply_deferred
.after(DespawnSet::DespawnCollectedStars)
.before(DespawnSet::DespawnOffscreen),
);
}
}
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum MovementSet {
// Player position is updated here
PlayerMovement,
// Systems that check the player position are updated here (such as the camera)
PostPlayerMovement,
// Systems that check the camera position are updated here
PostCameraMovement,
}
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum DespawnSet {
DespawnCollectedStars,
DespawnOffscreen,
}