initial commit
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
result
|
51
.vscode/launch.json
vendored
Normal 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
@ -0,0 +1,6 @@
|
||||
{
|
||||
"nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix",
|
||||
"rust-analyzer.linkedProjects": [
|
||||
"./Cargo.toml"
|
||||
]
|
||||
}
|
6179
Cargo.lock
generated
Normal file
37
Cargo.toml
Normal 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 = "*"
|
BIN
assets/textures/Letters/letterA.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/textures/Letters/letterB.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/textures/Letters/letterC.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/textures/Letters/letterD.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/textures/Letters/letterE.png
Normal file
After Width: | Height: | Size: 634 B |
BIN
assets/textures/Letters/letterF.png
Normal file
After Width: | Height: | Size: 639 B |
BIN
assets/textures/Letters/letterG.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/textures/Letters/letterH.png
Normal file
After Width: | Height: | Size: 637 B |
BIN
assets/textures/Letters/letterI.png
Normal file
After Width: | Height: | Size: 386 B |
BIN
assets/textures/Letters/letterJ.png
Normal file
After Width: | Height: | Size: 920 B |
BIN
assets/textures/Letters/letterK.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/textures/Letters/letterL.png
Normal file
After Width: | Height: | Size: 482 B |
BIN
assets/textures/Letters/letterM.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/textures/Letters/letterN.png
Normal file
After Width: | Height: | Size: 1000 B |
BIN
assets/textures/Letters/letterO.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/textures/Letters/letterP.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/textures/Letters/letterQ.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/textures/Letters/letterR.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/textures/Letters/letterS.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/textures/Letters/letterT.png
Normal file
After Width: | Height: | Size: 604 B |
BIN
assets/textures/Letters/letterU.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/textures/Letters/letterV.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/textures/Letters/letterW.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/textures/Letters/letterX.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/textures/Letters/letterY.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/textures/Letters/letterZ.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/textures/Numbers/number0.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/textures/Numbers/number1.png
Normal file
After Width: | Height: | Size: 819 B |
BIN
assets/textures/Numbers/number2.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/textures/Numbers/number3.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/textures/Numbers/number4.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/textures/Numbers/number5.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/textures/Numbers/number6.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/textures/Numbers/number7.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/textures/Numbers/number8.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/textures/Numbers/number9.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/textures/Planes/planeBlue1.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/Planes/planeBlue2.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/Planes/planeBlue3.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/Planes/planeGreen1.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/Planes/planeGreen2.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/Planes/planeGreen3.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/Planes/planeRed1.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/Planes/planeRed2.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/Planes/planeRed3.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/Planes/planeYellow1.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/Planes/planeYellow2.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/Planes/planeYellow3.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
assets/textures/UI/UIbg.png
Normal file
After Width: | Height: | Size: 639 B |
BIN
assets/textures/UI/buttonLarge.png
Normal file
After Width: | Height: | Size: 477 B |
BIN
assets/textures/UI/buttonSmall.png
Normal file
After Width: | Height: | Size: 468 B |
BIN
assets/textures/UI/medalBronze.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
assets/textures/UI/medalGold.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
assets/textures/UI/medalSilver.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/textures/UI/tap.png
Normal file
After Width: | Height: | Size: 849 B |
BIN
assets/textures/UI/tapLeft.png
Normal file
After Width: | Height: | Size: 671 B |
BIN
assets/textures/UI/tapRight.png
Normal file
After Width: | Height: | Size: 646 B |
BIN
assets/textures/UI/tapTick.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/textures/UI/textGameOver.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
assets/textures/UI/textGetReady.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
assets/textures/background.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
assets/textures/groundDirt.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
assets/textures/groundGrass.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
assets/textures/groundIce.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
assets/textures/groundRock.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
assets/textures/groundSnow.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
assets/textures/puffLarge.png
Normal file
After Width: | Height: | Size: 357 B |
BIN
assets/textures/puffSmall.png
Normal file
After Width: | Height: | Size: 285 B |
BIN
assets/textures/rock.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
assets/textures/rockDown.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
assets/textures/rockGrass.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
assets/textures/rockGrassDown.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
assets/textures/rockIce.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
assets/textures/rockIceDown.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
assets/textures/rockSnow.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
assets/textures/rockSnowDown.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
assets/textures/starBronze.png
Normal file
After Width: | Height: | Size: 734 B |
BIN
assets/textures/starGold.png
Normal file
After Width: | Height: | Size: 734 B |
BIN
assets/textures/starSilver.png
Normal file
After Width: | Height: | Size: 725 B |
96
flake.lock
generated
Normal 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
@ -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
@ -0,0 +1 @@
|
||||
(builtins.getFlake (toString ./.)).devShells.${builtins.currentSystem}.default
|
44
src/asset_loader.rs
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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,
|
||||
}
|