Commit: bc6129e
Parent: 0a1aec6

Breeding bunnies

Mårten Åsberg committed on 2026-04-16 at 13:44
Might breed too fast, especially after a while
assets/models/3D Leap Land/Notes.txt +38 -0
diff --git a/assets/models/3D Leap Land/Notes.txt b/assets/models/3D Leap Land/Notes.txt
new file mode 100644
index 0000000..aff8127
@@ -0,0 +1,38 @@
Thank you for checking out 3D Leap Land.
These assets are free for personal and commercial use, no attribution required (Creative Commons Zero, CC0).
Colors:
-------
Easily change the colors for all assets by modifying "palette.png" located in the "tex" folder.
Materials:
----------
For the sake of simplicity, all assets were developed using only 2 materials, metallic and non-metallic. Some models might contain both metallic and non-metallic elements. In such cases, the metallic material will always be the first slot and non-metallic second. Multiple materials only supported in the FBX versions.
Animations:
-----------
Animations are done through blend-shapes, only supported in the FBX versions.
These are the models with blend-shapes:
- Water2 & Water4
- Crumbling Rock Platform
- Spring
- Flag
- Treasure Chest
- Rope
- Slime
- Bee
- Wooden Spikes
- Cannon
- Detonater
Follow on Twitter for updates:
http://twitter.com/AEssssam
\ No newline at end of file
assets/models/3D Leap Land/glb/heart.glb +0 -0
diff --git a/assets/models/3D Leap Land/glb/heart.glb b/assets/models/3D Leap Land/glb/heart.glb
new file mode 100644
index 0000000..4c2ee55
Binary files /dev/null and b/assets/models/3D Leap Land/glb/heart.glb differ
docs/screenshot.png +0 -0
diff --git a/docs/screenshot.png b/docs/screenshot.png
index f2de93c..d2bd5e9 100644
Binary files a/docs/screenshot.png and b/docs/screenshot.png differ
src/bunnies/bunny.rs +108 -5
diff --git a/src/bunnies/bunny.rs b/src/bunnies/bunny.rs
index 7dd8748..a3e3a61 100644
@@ -7,7 +7,7 @@ use bevy::{
component::Component,
entity::Entity,
query::{With, Without},
system::{Commands, Query, Res, ResMut, Single},
system::{Commands, In, Query, Res, ResMut, Single},
},
gltf::GltfAssetLabel,
math::{Dir3, Vec3, primitives::Cuboid},
@@ -19,11 +19,20 @@ use bevy::{
use rand::RngExt;
use crate::{Rng, bunnies::locator::Locator, dog::Dog, obstacles::Obstacles};
use crate::{
Rng,
bunnies::{
heart::{add_heart_system, spawn_heart},
locator::Locator,
},
dog::Dog,
obstacles::Obstacles,
};
pub(super) fn add_bunny_systems(app: &mut App) -> &mut App {
app.add_systems(Startup, setup)
.add_systems(Update, (calculate, jump))
.add_systems(Update, (calculate, jump, look_for_partner, breed));
add_heart_system(app)
}
#[derive(Component)]
@@ -35,10 +44,19 @@ enum JumpState {
Cooldown { ready: Duration },
}
#[derive(Component)]
enum Breeding {
Initiator { done: Duration, mid_point: Vec3 },
Partner { done: Duration },
}
const BUNNY_COUNT: usize = 5;
const DETECTION_DISTANCE: f32 = 5.0;
const SHORT_JUMP_DISTANCE: f32 = 1.0;
const LONG_JUMP_DISTANCE: f32 = 3.0;
const BREED_DISTANCE: f32 = 3.0;
const BREED_PROBABILITY: f64 = 0.1;
const BREEDING_DURATION: f64 = 1.0;
const LONE_JUMPS_PER_SECOND: f64 = 1.0;
const COLLECTIVE_JUMPS_PER_SECOND: f64 = 1.0;
const JUMP_HEIGHT: f32 = 1.0;
@@ -70,8 +88,12 @@ fn setup(
}
}
type IdleBunniesQuery<'w, 's, 't> =
Query<'w, 's, (Entity, &'t mut Transform), (With<Bunny>, Without<JumpState>)>;
type IdleBunniesQuery<'w, 's, 't> = Query<
'w,
's,
(Entity, &'t mut Transform),
(With<Bunny>, Without<JumpState>, Without<Breeding>),
>;
fn calculate(
mut commands: Commands,
@@ -289,3 +311,84 @@ fn cooldown(commands: &mut Commands, time: &Time, bunny: Entity, ready: Duration
commands.entity(bunny).remove::<JumpState>();
}
}
fn look_for_partner(
mut commands: Commands,
bunnies: IdleBunniesQuery,
time: Res<Time>,
locator: Res<Locator>,
mut rng: ResMut<Rng>,
) {
bunnies.iter().for_each(|(bunny, bunny_transform)| {
if !rng.random_bool(time.delta_secs_f64() * BREED_PROBABILITY) {
return;
}
let Some((partner, _)) = locator
.get_nearby(bunny, bunny_transform.translation, BREED_DISTANCE)
.drain(..)
.find(|(_, (p, _))| bunny_transform.translation.x < p.x)
else {
return;
};
commands.run_system_cached_with(start_breeding, (bunny, partner));
});
}
fn start_breeding(
In((a, b)): In<(Entity, Entity)>,
mut commands: Commands,
time: Res<Time>,
mut transforms: Query<&mut Transform>,
) {
let Ok([mut a_transform, mut b_transform]) = transforms.get_many_mut([a, b]) else {
return;
};
let mid_point = (a_transform.translation + b_transform.translation) / 2.0;
let breeding_duration = Duration::from_secs_f64(BREEDING_DURATION);
let done = time.elapsed() + breeding_duration;
let dir = a_transform.translation - b_transform.translation;
a_transform.look_to(dir, Vec3::Y);
let dir = b_transform.translation - a_transform.translation;
b_transform.look_to(dir, Vec3::Y);
commands
.entity(a)
.insert(Breeding::Initiator { done, mid_point });
commands.entity(b).insert(Breeding::Partner { done });
commands.run_system_cached_with(spawn_heart, (mid_point, breeding_duration));
}
fn breed(
mut commands: Commands,
time: Res<Time>,
bunnies: Query<(Entity, &Breeding, &SceneRoot, &Mesh3d), With<Bunny>>,
) {
bunnies
.iter()
.for_each(|(bunny, breeding, model, mesh)| match breeding {
Breeding::Initiator { done, mid_point } => {
if time.elapsed() < *done {
return;
}
commands.entity(bunny).remove::<Breeding>();
commands.spawn((
model.clone(),
Transform::from_translation(*mid_point),
mesh.clone(),
Bunny,
));
}
Breeding::Partner { done } => {
if time.elapsed() < *done {
return;
}
commands.entity(bunny).remove::<Breeding>();
}
});
}
src/bunnies/heart.rs +140 -0
diff --git a/src/bunnies/heart.rs b/src/bunnies/heart.rs
new file mode 100644
index 0000000..d2752aa
@@ -0,0 +1,140 @@
use std::time::Duration;
use bevy::{
animation::{
AnimatedBy, AnimationClip, AnimationPlayer, AnimationTargetId, animated_field,
animation_curves::{AnimatableCurve, AnimatedField},
graph::{AnimationGraph, AnimationGraphHandle},
},
app::{App, Update},
asset::{AssetServer, Assets},
color::Color,
ecs::{
component::Component,
entity::Entity,
hierarchy::Children,
name::Name,
observer::On,
query::With,
system::{Commands, In, Query, Res, ResMut},
},
gltf::GltfAssetLabel,
math::{Quat, Vec3, curve::UnevenSampleAutoCurve},
pbr::{MeshMaterial3d, StandardMaterial},
scene::{SceneInstanceReady, SceneRoot},
time::Time,
transform::components::Transform,
};
pub(super) fn add_heart_system(app: &mut App) -> &mut App {
app.add_observer(heart_ready);
app.add_systems(Update, destroy_heart)
}
pub(super) fn spawn_heart(
In((mid_point, time_to_live)): In<(Vec3, Duration)>,
mut commands: Commands,
time: Res<Time>,
asset_server: Res<AssetServer>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut animations: ResMut<Assets<AnimationClip>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
) {
let model =
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/3D Leap Land/glb/heart.glb"));
let name = Name::new("heart");
let mut animation = AnimationClip::default();
let animation_target_id = AnimationTargetId::from_name(&name);
animation.add_curve_to_target(
animation_target_id,
AnimatableCurve::new(
animated_field!(Transform::translation),
UnevenSampleAutoCurve::new([
(0.0, mid_point.with_y(2.0)),
(0.5, mid_point.with_y(3.0)),
(1.0, mid_point.with_y(2.0)),
])
.unwrap(),
),
);
animation.add_curve_to_target(
animation_target_id,
AnimatableCurve::new(
animated_field!(Transform::rotation),
UnevenSampleAutoCurve::new([
(0.0, Quat::IDENTITY),
(
0.25,
Quat::from_axis_angle(Vec3::Y, std::f32::consts::PI * 0.5),
),
(
0.50,
Quat::from_axis_angle(Vec3::Y, std::f32::consts::PI * 1.0),
),
(
0.75,
Quat::from_axis_angle(Vec3::Y, std::f32::consts::PI * 1.5),
),
(1.0, Quat::IDENTITY),
])
.unwrap(),
),
);
let (graph, animation_index) = AnimationGraph::from_clip(animations.add(animation));
let mut player = AnimationPlayer::default();
player.play(animation_index).repeat().set_speed(0.5);
let mut entity = commands.spawn_empty();
entity.insert((
SceneRoot(model),
MeshMaterial3d(materials.add(Color::linear_rgb(1.0, 0.0, 0.0))),
Transform::from_scale(Vec3::splat(0.01)),
name,
AnimationGraphHandle(graphs.add(graph)),
player,
animation_target_id,
AnimatedBy(entity.id()),
Heart {
destroy_time: time.elapsed() + time_to_live,
},
));
}
#[derive(Component)]
struct Heart {
destroy_time: Duration,
}
fn heart_ready(
scene_ready: On<SceneInstanceReady>,
children: Query<&Children>,
hearts: Query<(), With<Heart>>,
mesh_materials: Query<&MeshMaterial3d<StandardMaterial>>,
mut asset_materials: ResMut<Assets<StandardMaterial>>,
) {
let Ok(()) = hearts.get(scene_ready.entity) else {
return;
};
for descendant in children.iter_descendants(scene_ready.entity) {
let Ok(id) = mesh_materials.get(descendant) else {
continue;
};
let Some(material) = asset_materials.get_mut(id.id()) else {
continue;
};
material.base_color = Color::linear_rgb(1.0, 0.0, 0.0);
}
}
fn destroy_heart(mut commands: Commands, time: Res<Time>, hearts: Query<(Entity, &Heart)>) {
hearts.iter().for_each(|(heart, Heart { destroy_time })| {
if time.elapsed() < *destroy_time {
return;
}
commands.entity(heart).despawn();
});
}
src/bunnies/mod.rs +1 -0
diff --git a/src/bunnies/mod.rs b/src/bunnies/mod.rs
index 836cb62..fcf2192 100644
@@ -1,4 +1,5 @@
mod bunny;
mod heart;
mod locator;
use bevy::app::App;