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

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])],
));
}