commit ff699d4e2134aefa644e6cb1e741792e945e43c0
Author: DerGrumpf
Date: Tue Oct 14 14:27:40 2025 +0200
Init
diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..d93a7f1
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,22 @@
+[target.x86_64-unknown-linux-gnu]
+linker = "clang"
+rustflags = [
+ "-C", "link-arg=-fuse-ld=lld",
+ "-C", "link-arg=-lxkbcommon",
+]
+
+[target.x86_64-pc-windows-msvc]
+linker = "rust-lld.exe"
+
+[target.x86_64-apple-darwin]
+rustflags = ["-C", "link-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld"]
+
+# [unstable]
+# codegen-backend = true
+# [profile.dev]
+# codegen-backend = "cranelift"
+# [profile.dev.package."*"]
+# codegen-backend = "llvm"
+
+[build]
+rustflags = ["-Z", "share-generics=y"]
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..3550a30
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..295f066
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,45 @@
+# Rust
+/target
+app-bevy/target
+lib-utils/target
+**/*.rs.bk
+Cargo.lock
+
+# Nix
+result
+.direnv
+
+# Editor-Specific
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Bevy
+/assets
+app-bevy/assets
+lib-utils/assets
+
+# Misc
+.idea
+tmp/
+
+# Logs
+*.log
+
+# Build artifacts
+*.o
+*.so
+*.dylib
+*.dll
+*.wasm
+*.pyc
+node_modules/
+
+# Backup Files
+*~
+
diff --git a/.projectile b/.projectile
new file mode 100644
index 0000000..e69de29
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..9395ca2
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,33 @@
+[workspace]
+members = [
+ "app-bevy",
+ "lib-utils",
+]
+resolver = "2" # wgpu/bevy need this
+
+[workspace.package]
+version = "0.14.0"
+edition = "2021"
+
+[workspace.dependencies]
+bevy = { version = "0.15.0", features = ["dynamic_linking", "wayland"] }
+log = { version = "*", features = ["max_level_debug", "release_max_level_warn"] }
+
+[profile.dev]
+opt-level = 1
+
+[profile.dev.package."*"]
+opt-level = 3
+
+[profile.release]
+codegen-units = 1
+lto = "thin"
+
+[profile.wasm-release]
+inherits = "release"
+opt-level = "s"
+lto = "thin"
+strip = "debuginfo"
+
+[workspace.metadata.rust-analyzer]
+rustc_private = true
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..86c97ba
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# 🎮 Bevy Autonomous Agent Demo
diff --git a/app-bevy/.gitignore b/app-bevy/.gitignore
new file mode 100644
index 0000000..9e90064
--- /dev/null
+++ b/app-bevy/.gitignore
@@ -0,0 +1,16 @@
+# Rust
+/target
+**/*.rs.bk
+Cargo.lock
+
+# Nix
+result
+.direnv
+
+# Editor-specific for people using these
+.vscode
+.idea
+
+# OS-specific for people using these
+.DS_Store
+Thumbs.db
diff --git a/app-bevy/Cargo.toml b/app-bevy/Cargo.toml
new file mode 100644
index 0000000..ae8a650
--- /dev/null
+++ b/app-bevy/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "autonomous-agents"
+version = { workspace = true }
+edition = { workspace = true }
+
+[dependencies]
+bevy = { workspace = true }
+log = { workspace = true }
+lib-utils = { path = "../lib-utils" }
+
+[features]
+default = ["bevy/dynamic_linking", "bevy/wayland"]
+nightly = ["bevy/dynamic_linking", "bevy/wayland"] # Remove dynamic_linking when shipping!
+
+[package.metadata.rust-analyzer]
+rustc_private = true
+
+[unstable]
+codegen-backend = true
diff --git a/app-bevy/src/main.rs b/app-bevy/src/main.rs
new file mode 100644
index 0000000..4361d16
--- /dev/null
+++ b/app-bevy/src/main.rs
@@ -0,0 +1,419 @@
+#![allow(dead_code)]
+use bevy::math::bounding::{Aabb2d, BoundingVolume, IntersectsVolume};
+use bevy::prelude::*;
+
+#[derive(Component, Default)]
+#[require(Transform)]
+struct Position(Vec2);
+
+#[derive(Component, Default)]
+struct Velocity(Vec2);
+
+const BALL_SPEED: f32 = 2.;
+
+#[derive(Component)]
+#[require(Position, Velocity, Collider)]
+struct Ball;
+
+#[derive(Component)]
+#[require(Position, Collider)]
+struct Paddle;
+
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+enum Collision {
+ Left,
+ Right,
+ Top,
+ Bottom,
+}
+
+#[derive(Component, Default)]
+struct Collider(Rectangle);
+
+#[derive(Component)]
+struct Player;
+
+#[derive(Component)]
+struct Ai;
+
+#[derive(Resource)]
+struct Score {
+ player: u32,
+ ai: u32,
+}
+
+#[derive(Event)]
+struct Scored {
+ scorer: Entity,
+}
+
+#[derive(Component)]
+struct PlayerScore;
+
+#[derive(Component)]
+struct AiScore;
+
+fn main() {
+ App::new()
+ .add_plugins(DefaultPlugins)
+ .insert_resource(Score { player: 0, ai: 0 })
+ .add_event::()
+ .add_systems(
+ Startup,
+ (
+ spawn_ball,
+ spawn_camera,
+ spawn_paddles,
+ spawn_gutters,
+ spawn_scoreboard,
+ ),
+ )
+ .add_systems(
+ FixedUpdate,
+ (
+ move_ball,
+ handle_collisions,
+ project_positions,
+ handle_player_input,
+ move_paddles,
+ constrain_paddle_position,
+ reset_ball,
+ update_score,
+ )
+ .chain(),
+ )
+ .run();
+}
+
+const BALL_SIZE: f32 = 25.;
+const BALL_SHAPE: Circle = Circle::new(BALL_SIZE);
+const BALL_COLOR: Color = Color::srgb(1., 0., 0.);
+
+fn spawn_ball(
+ mut commands: Commands,
+ mut meshes: ResMut>,
+ mut materials: ResMut>,
+) {
+ let mesh = meshes.add(BALL_SHAPE);
+ let material = materials.add(BALL_COLOR);
+ commands.spawn((
+ Ball,
+ Mesh2d(mesh),
+ MeshMaterial2d(material),
+ Position(Vec2::ZERO),
+ Velocity(Vec2::new(-BALL_SPEED, BALL_SPEED)),
+ Collider(Rectangle::new(BALL_SIZE, BALL_SIZE)),
+ ));
+}
+
+fn move_ball(ball: Single<(&mut Position, &Velocity), With>) {
+ let (mut position, velocity) = ball.into_inner();
+ position.0 += velocity.0 * BALL_SPEED;
+}
+
+fn spawn_camera(mut commands: Commands) {
+ commands.spawn((Camera2d, Transform::from_xyz(0., 0., 0.)));
+}
+
+const PADDLE_SHAPE: Rectangle = Rectangle {
+ half_size: Vec2::new(10., 25.),
+};
+const PADDLE_COLOR: Color = Color::srgb(0., 1., 0.);
+
+fn spawn_paddles(
+ mut commands: Commands,
+ mut meshes: ResMut>,
+ mut materials: ResMut>,
+ window: Single<&Window>,
+) {
+ let mesh = meshes.add(PADDLE_SHAPE);
+ let material = materials.add(PADDLE_COLOR);
+ let half_window_size = window.resolution.size() / 2.;
+ let padding = 20.;
+
+ let player_position = Vec2::new(-half_window_size.x + padding, 0.);
+
+ commands.spawn((
+ Player,
+ Paddle,
+ Mesh2d(mesh.clone()),
+ MeshMaterial2d(material.clone()),
+ Position(player_position),
+ Velocity(Vec2::ZERO),
+ Collider(PADDLE_SHAPE),
+ ));
+
+ let ai_position = Vec2::new(half_window_size.x - padding, 0.);
+
+ commands.spawn((
+ Ai,
+ Paddle,
+ Mesh2d(mesh.clone()),
+ MeshMaterial2d(material.clone()),
+ Position(ai_position),
+ Velocity(Vec2::ZERO),
+ Collider(PADDLE_SHAPE),
+ ));
+}
+
+fn project_positions(mut positionables: Query<(&mut Transform, &Position)>) {
+ for (mut transform, position) in &mut positionables {
+ transform.translation = position.0.extend(0.);
+ }
+}
+
+fn collide_with_side(ball: Aabb2d, wall: Aabb2d) -> Option {
+ if !ball.intersects(&wall) {
+ return None;
+ }
+
+ let closest_point = wall.closest_point(ball.center());
+ let offset = ball.center() - closest_point;
+
+ let side = if offset.x.abs() > offset.y.abs() {
+ if offset.x < 0. {
+ Collision::Left
+ } else {
+ Collision::Right
+ }
+ } else if offset.y > 0. {
+ Collision::Top
+ } else {
+ Collision::Bottom
+ };
+
+ Some(side)
+}
+
+impl Collider {
+ fn half_size(&self) -> Vec2 {
+ self.0.half_size
+ }
+}
+
+fn handle_collisions(
+ ball: Single<(&mut Velocity, &Position, &Collider), With>,
+ other_things: Query<(&Position, &Collider), Without>,
+) {
+ let (mut ball_velocity, ball_position, ball_collider) = ball.into_inner();
+
+ for (other_position, other_collider) in &other_things {
+ if let Some(collision) = collide_with_side(
+ Aabb2d::new(ball_position.0, ball_collider.half_size()),
+ Aabb2d::new(other_position.0, other_collider.half_size()),
+ ) {
+ match collision {
+ Collision::Left => {
+ ball_velocity.0.x *= -1.;
+ }
+ Collision::Right => {
+ ball_velocity.0.x *= -1.;
+ }
+ Collision::Top => {
+ ball_velocity.0.y *= -1.;
+ }
+ Collision::Bottom => {
+ ball_velocity.0.y *= -1.;
+ }
+ }
+ }
+ }
+}
+
+#[derive(Component)]
+#[require(Position, Collider)]
+struct Gutter;
+
+const GUTTER_COLOR: Color = Color::srgb(0., 0., 1.);
+const GUTTER_HEIGHT: f32 = 20.;
+
+fn spawn_gutters(
+ mut commands: Commands,
+ mut meshes: ResMut>,
+ mut materials: ResMut>,
+ window: Single<&Window>,
+) {
+ let material = materials.add(GUTTER_COLOR);
+ let padding = 20.;
+
+ let gutter_shape = Rectangle::new(window.resolution.width(), GUTTER_HEIGHT);
+ let mesh = meshes.add(gutter_shape);
+
+ let top_gutter_position = Vec2::new(0., window.resolution.height() / 2. - padding);
+
+ commands.spawn((
+ Gutter,
+ Mesh2d(mesh.clone()),
+ MeshMaterial2d(material.clone()),
+ Position(top_gutter_position),
+ Collider(gutter_shape),
+ ));
+
+ let bottom_gutter_position = Vec2::new(0., -window.resolution.height() / 2. + padding);
+
+ commands.spawn((
+ Gutter,
+ Mesh2d(mesh.clone()),
+ MeshMaterial2d(material.clone()),
+ Position(bottom_gutter_position),
+ Collider(gutter_shape),
+ ));
+}
+
+const PADDLE_SPEED: f32 = 5.;
+
+fn handle_player_input(
+ keyboard_input: Res>,
+ mut paddle_velocity: Single<&mut Velocity, With>,
+) {
+ if keyboard_input.pressed(KeyCode::ArrowUp) {
+ paddle_velocity.0.y = PADDLE_SPEED;
+ } else if keyboard_input.pressed(KeyCode::ArrowDown) {
+ paddle_velocity.0.y = -PADDLE_SPEED;
+ } else {
+ paddle_velocity.0.y = 0.;
+ }
+}
+
+fn move_paddles(mut paddles: Query<(&mut Position, &Velocity), With>) {
+ for (mut position, velocity) in &mut paddles {
+ position.0 += velocity.0;
+ }
+}
+
+fn constrain_paddle_position(
+ mut paddles: Query<(&mut Position, &Collider), (With, Without)>,
+ gutters: Query<(&Position, &Collider), (With, Without)>,
+) {
+ for (mut paddle_position, paddle_collider) in &mut paddles {
+ for (gutter_position, gutter_collider) in &gutters {
+ let paddle_aabb = Aabb2d::new(paddle_position.0, paddle_collider.half_size());
+ let gutter_aabb = Aabb2d::new(gutter_position.0, gutter_collider.half_size());
+
+ if let Some(collision) = collide_with_side(paddle_aabb, gutter_aabb) {
+ match collision {
+ Collision::Top => {
+ paddle_position.0.y = gutter_position.0.y
+ + gutter_collider.half_size().y
+ + paddle_collider.half_size().y;
+ }
+ Collision::Bottom => {
+ paddle_position.0.y = gutter_position.0.y
+ - gutter_collider.half_size().y
+ - paddle_collider.half_size().y;
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+}
+
+fn detect_goal(
+ ball: Single<(&Position, &Collider), With>,
+ player: Query, Without)>,
+ ai: Query, Without)>,
+ window: Single<&Window>,
+ mut scored_events: EventWriter,
+) {
+ let (ball_position, ball_collider) = ball.into_inner();
+ let half_window_size = window.resolution.size() / 2.;
+
+ if ball_position.0.x - ball_collider.half_size().x > half_window_size.x {
+ if let Ok(player_entity) = player.get_single() {
+ scored_events.send(Scored {
+ scorer: player_entity,
+ });
+ }
+ }
+
+ if ball_position.0.x + ball_collider.half_size().x < -half_window_size.x {
+ if let Ok(ai_entity) = ai.get_single() {
+ scored_events.send(Scored { scorer: ai_entity });
+ }
+ }
+}
+
+fn reset_ball(
+ mut scored_events: ResMut>,
+ ball: Single<(&mut Position, &mut Velocity), With>,
+) {
+ let mut reader = scored_events.get_reader();
+ for _event in reader.read(&scored_events) {
+ let (mut ball_position, mut ball_velocity) = ball.into_inner();
+ ball_position.0 = Vec2::ZERO;
+ ball_velocity.0 = Vec2::new(-BALL_SPEED, BALL_SPEED);
+ // Clear the event after handling to prevent repeated processing
+ scored_events.clear();
+ break;
+ }
+}
+
+fn update_score(
+ mut events: EventReader,
+ mut score: ResMut,
+ is_ai: Query<&Ai>,
+ is_player: Query<&Player>,
+) {
+ for event in events.read() {
+ if is_ai.get(event.scorer).is_ok() {
+ score.ai += 1;
+ info!("AI scored! {} - {}", score.player, score.ai);
+ } else if is_player.get(event.scorer).is_ok() {
+ score.player += 1;
+ info!("Player scored! {} - {}", score.player, score.ai);
+ }
+ }
+}
+
+fn spawn_scoreboard(mut commands: Commands) {
+ // Create a container that will center everything
+ let container = Node {
+ width: percent(100.0),
+ height: percent(100.0),
+ justify_content: JustifyContent::Center,
+ ..default()
+ };
+
+ // Then add a container for the text
+ let header = Node {
+ width: px(200.),
+ height: px(100.),
+ ..default()
+ };
+
+ // The players score on the left hand side
+ let player_score = (
+ PlayerScore,
+ Text::new("0"),
+ TextFont::from_font_size(72.0),
+ TextColor(Color::WHITE),
+ TextLayout::new_with_justify(Justify::Center),
+ Node {
+ position_type: PositionType::Absolute,
+ top: px(5.0),
+ left: px(25.0),
+ ..default()
+ },
+ );
+
+ // The AI score on the right hand side
+ let ai_score = (
+ AiScore,
+ Text::new("0"),
+ TextFont::from_font_size(72.0),
+ TextColor(Color::WHITE),
+ TextLayout::new_with_justify(Justify::Center),
+ Node {
+ position_type: PositionType::Absolute,
+ top: px(5.0),
+ right: px(25.0),
+ ..default()
+ },
+ );
+
+ commands.spawn((
+ container,
+ children![(header, children![player_score, ai_score])],
+ ));
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..aaee152
--- /dev/null
+++ b/flake.lock
@@ -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": 1725103162,
+ "narHash": "sha256-Ym04C5+qovuQDYL/rKWSR+WESseQBbNAe5DsXNx5trY=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "12228ff1752d7b7624a54e9c1af4b222b3c1073b",
+ "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": 1725243956,
+ "narHash": "sha256-0A5ZP8uDCyBdYUzayZfy6JFdTefP79oZVAjyqA/yuSI=",
+ "owner": "oxalica",
+ "repo": "rust-overlay",
+ "rev": "a10c8092d5f82622be79ed4dd12289f72011f850",
+ "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
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..95f4004
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,70 @@
+{
+ description = "Minimal Rust development environment for Bevy project";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ rust-overlay.url = "github:oxalica/rust-overlay";
+ flake-utils.url = "github:numtide/flake-utils";
+ };
+
+ outputs =
+ {
+ nixpkgs,
+ rust-overlay,
+ flake-utils,
+ ...
+ }:
+ flake-utils.lib.eachDefaultSystem (
+ system:
+ let
+ overlays = [ (import rust-overlay) ];
+ pkgs = import nixpkgs { inherit system overlays; };
+ rustToolchain = pkgs.rust-bin.stable.latest.default.override {
+ extensions = [
+ "rust-src"
+ "rust-analyzer"
+ "clippy"
+ ];
+ };
+ in
+ {
+ devShells.default = pkgs.mkShell {
+ nativeBuildInputs = with pkgs; [ pkg-config ];
+ buildInputs = with pkgs; [
+ rustup
+ rustToolchain
+ clang
+ llvmPackages_latest.bintools
+ udev
+ alsa-lib
+ vulkan-loader
+ xorg.libX11
+ xorg.libXcursor
+ xorg.libXi
+ xorg.libXrandr
+ libxkbcommon
+ wayland
+ glibc.dev
+ glib.dev
+ ];
+
+ shellHook = ''
+ export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin
+ export LD_LIBRARY_PATH=${
+ pkgs.lib.makeLibraryPath [
+ pkgs.vulkan-loader
+ pkgs.libxkbcommon
+ pkgs.wayland
+ pkgs.alsa-lib
+ pkgs.udev
+ ]
+ }:$LD_LIBRARY_PATH
+ export LIBCLANG_PATH="${pkgs.llvmPackages_latest.libclang.lib}/lib"
+ export BINDGEN_EXTRA_CLANG_ARGS="-I${pkgs.glibc.dev}/include -I${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include -I${pkgs.glib.dev}/include/glib-2.0 -I${pkgs.glib.out}/lib/glib-2.0/include/"
+ export RUSTFLAGS="-C link-arg=-fuse-ld=lld"
+ echo "Bevy development environment loaded!"
+ '';
+ };
+ }
+ );
+}
diff --git a/lib-utils/Cargo.toml b/lib-utils/Cargo.toml
new file mode 100644
index 0000000..8aac711
--- /dev/null
+++ b/lib-utils/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "lib-utils"
+version = { workspace = true }
+edition = { workspace = true }
+
+[dependencies]
+bevy = { workspace = true }
+log = { workspace = true }
diff --git a/lib-utils/src/lib.rs b/lib-utils/src/lib.rs
new file mode 100644
index 0000000..f628b1f
--- /dev/null
+++ b/lib-utils/src/lib.rs
@@ -0,0 +1,13 @@
+use bevy::prelude::*;
+
+pub fn print_hello_world() {
+ println!("Hello from utils crate!");
+}
+
+pub struct UtilsPlugin;
+
+impl Plugin for UtilsPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(Startup, print_hello_world);
+ }
+}
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 0000000..8e275b7
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,3 @@
+[toolchain]
+channel = "nightly"
+components = ["rustfmt", "clippy"]