This commit is contained in:
2025-10-14 14:27:40 +02:00
commit ff699d4e21
14 changed files with 746 additions and 0 deletions

22
.cargo/config.toml Normal file
View File

@@ -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"]

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

45
.gitignore vendored Normal file
View File

@@ -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
*~

0
.projectile Normal file
View File

33
Cargo.toml Normal file
View File

@@ -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

1
README.md Normal file
View File

@@ -0,0 +1 @@
# 🎮 Bevy Autonomous Agent Demo

16
app-bevy/.gitignore vendored Normal file
View File

@@ -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

19
app-bevy/Cargo.toml Normal file
View File

@@ -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

419
app-bevy/src/main.rs Normal file
View File

@@ -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::<Scored>()
.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<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
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<Ball>>) {
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<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
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<Collision> {
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<Ball>>,
other_things: Query<(&Position, &Collider), Without<Ball>>,
) {
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<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
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<ButtonInput<KeyCode>>,
mut paddle_velocity: Single<&mut Velocity, With<Player>>,
) {
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<Paddle>>) {
for (mut position, velocity) in &mut paddles {
position.0 += velocity.0;
}
}
fn constrain_paddle_position(
mut paddles: Query<(&mut Position, &Collider), (With<Paddle>, Without<Gutter>)>,
gutters: Query<(&Position, &Collider), (With<Gutter>, Without<Paddle>)>,
) {
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<Ball>>,
player: Query<Entity, (With<Player>, Without<Ai>)>,
ai: Query<Entity, (With<Ai>, Without<Player>)>,
window: Single<&Window>,
mut scored_events: EventWriter<Scored>,
) {
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<Events<Scored>>,
ball: Single<(&mut Position, &mut Velocity), With<Ball>>,
) {
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<Scored>,
mut score: ResMut<Score>,
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])],
));
}

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": 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
}

70
flake.nix Normal file
View File

@@ -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!"
'';
};
}
);
}

8
lib-utils/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "lib-utils"
version = { workspace = true }
edition = { workspace = true }
[dependencies]
bevy = { workspace = true }
log = { workspace = true }

13
lib-utils/src/lib.rs Normal file
View File

@@ -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);
}
}

3
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"
components = ["rustfmt", "clippy"]