pub struct World { /* private fields */ }Expand description
The ECS: entities, components, queries, resources, commands. Holds everything in the simulation: entities, their components, and global resources.
use ponko_ecs::*;
struct Pos(f32, f32);
struct Vel(f32, f32);
let mut world = World::new();
let crab = world.spawn((Pos(0.0, 0.0), Vel(1.0, 2.0)));
world.query::<(&mut Pos, &Vel)>().for_each(|(p, v)| {
p.0 += v.0;
p.1 += v.1;
});
assert_eq!(world.get::<Pos>(crab).unwrap().1, 2.0);
world.despawn(crab);
assert!(world.get::<Pos>(crab).is_none());Implementations§
Source§impl World
impl World
Sourcepub fn new() -> World
pub fn new() -> World
An empty world.
Examples found in repository?
16fn main() {
17 let mut world = World::new();
18 world.spawn_batch((0..1_000_000).map(|_| (Pos { x: 0.0, y: 0.0 }, Vel { x: 1.0, y: 2.0 })));
19 let mut samples = Vec::with_capacity(200);
20 for i in 0..210 {
21 let start = Instant::now();
22 world.query::<(&mut Pos, &Vel)>().for_each(|(p, v)| {
23 p.x += v.x;
24 p.y += v.y;
25 });
26 black_box(&world);
27 let elapsed = start.elapsed().as_secs_f64() * 1000.0;
28 if i >= 10 {
29 samples.push(elapsed);
30 }
31 }
32 samples.sort_by(f64::total_cmp);
33 let checksum: f64 = world
34 .query::<&Pos>()
35 .iter()
36 .map(|p| f64::from(p.x) + f64::from(p.y))
37 .sum();
38 assert_eq!(checksum, 630_000_000.0);
39 println!(
40 "Rust: 1000000 entities, median {:.6} ms/update, checksum {checksum:.0}",
41 (samples[99] + samples[100]) / 2.0
42 );
43}Sourcepub fn spawn<B>(&mut self, bundle: B) -> Entitywhere
B: Bundle,
pub fn spawn<B>(&mut self, bundle: B) -> Entitywhere
B: Bundle,
Creates an entity with the given components and returns its handle.
Components come as a tuple; note the trailing comma for one component:
world.spawn((Health(10),)).
Examples found in repository?
34fn spawn_crab(ctx: &mut Ctx, at: Vec2) {
35 let s = ctx.world.resource_mut::<Settings>();
36 let angle = s.random() * std::f32::consts::TAU;
37 let speed = 40.0 + s.random() * 120.0;
38 let tint = Color::rgb(0.5 + s.random() * 0.5, 0.5 + s.random() * 0.5, 0.5 + s.random() * 0.5);
39 let crab = s.crab;
40 ctx.world.spawn((
41 Transform2D::at(at),
42 Sprite::new(crab).with_color(tint),
43 Vel(Vec2::from_angle(angle) * speed),
44 Life(LIFETIME),
45 ));
46}More examples
47fn main() {
48 Game::new("Crabby Engine: hot patching", 1280, 720)
49 .sim_system(spin)
50 .sim_system(wobble)
51 .sim_system(report)
52 .run(|ctx| {
53 let crab = ctx.load_texture("crab.png");
54 for i in 0..12 {
55 let at = vec2(160.0 + (i % 6) as f32 * 190.0, 220.0 + (i / 6) as f32 * 280.0);
56 ctx.world
57 .spawn((Transform2D::at(at), Sprite::new(crab), Crab(i as f32 * 0.7)));
58 }
59 });
60}41fn spawn_car(ctx: &mut Ctx, at: Vec3) -> Entity {
42 let mut car = Vehicle3D::new();
43 for (x, z) in [(-0.8, -1.3), (0.8, -1.3), (-0.8, 1.3), (0.8, 1.3)] {
44 let mut wheel = Wheel3D::new(vec3(x, -0.3, z), 0.35);
45 wheel.sideways_friction.stiffness = 1.3;
46 car = car.with_wheel(wheel);
47 }
48 ctx.world.spawn((
49 Transform3D::at(at),
50 RigidBody3D::dynamic()
51 .mass(1200.0)
52 .center_of_mass(vec3(0.0, -0.35, 0.0)),
53 Collider3D::cuboid(vec3(1.8, 0.6, 4.2)),
54 Velocity3D::default(),
55 car,
56 ))
57}
58// ANCHOR_END: car
59
60// ANCHOR: drive
61/// The game's half of driving, before each physics step: rear-wheel
62/// drive, brakes on all four, steering that eases in and tightens less at
63/// speed.
64fn drive(ctx: &mut Ctx) {
65 let controls = *ctx.world.resource::<Controls>();
66 let dt = ctx.dt();
67 let car = ctx.world.resource::<Player>().car;
68 let forward = forward_speed(&ctx.world, car);
69 let wanted = controls.steer * 0.6 / (1.0 + forward.abs() * 0.08);
70 let player = ctx.world.resource_mut::<Player>();
71 player.steer += (wanted - player.steer).clamp(-2.0 * dt, 2.0 * dt);
72 let steer = player.steer;
73 let Some(vehicle) = ctx.world.get_mut::<Vehicle3D>(car) else {
74 return;
75 };
76 // Pressing against the way it rolls brakes; from (almost) still it drives.
77 let braking = controls.throttle * forward < 0.0 && forward.abs() > 0.5;
78 for (i, wheel) in vehicle.wheels_mut().iter_mut().enumerate() {
79 let front = i < 2;
80 wheel.steer_angle = if front { steer } else { 0.0 };
81 wheel.motor_torque = if front || braking {
82 0.0
83 } else {
84 controls.throttle * 900.0
85 };
86 wheel.brake_torque = if braking {
87 3000.0
88 } else if controls.handbrake && !front {
89 5000.0
90 } else {
91 0.0
92 };
93 }
94}
95// ANCHOR_END: drive
96
97fn speed_of(world: &World, car: Entity) -> f32 {
98 world.get::<Velocity3D>(car).map_or(0.0, |v| v.linear.length())
99}
100
101/// Speed along where the car faces (negative backing up).
102fn forward_speed(world: &World, car: Entity) -> f32 {
103 match (world.get::<Velocity3D>(car), world.get::<Transform3D>(car)) {
104 (Some(v), Some(t)) => v.linear.dot(t.forward()),
105 _ => 0.0,
106 }
107}
108
109/// Keys to controls (windowed), in the same tick as `drive`.
110fn keys(ctx: &mut Ctx) {
111 let axis = |plus: bool, minus: bool| plus as i32 as f32 - minus as i32 as f32;
112 let controls = Controls {
113 throttle: axis(ctx.key_down(Key::W), ctx.key_down(Key::S)),
114 steer: axis(ctx.key_down(Key::D), ctx.key_down(Key::A)),
115 handbrake: ctx.key_down(Key::Space),
116 };
117 *ctx.world.resource_mut::<Controls>() = controls;
118 if ctx.key_pressed(Key::R) {
119 let car = ctx.world.resource::<Player>().car;
120 if let Some(t) = ctx.world.get_mut::<Transform3D>(car) {
121 let p = t.position;
122 *t = Transform3D::at(vec3(p.x, height(p.x, p.z) + 1.5, p.z));
123 }
124 }
125}
126
127// ANCHOR: wheels
128/// Every frame: each wheel drawn where its car's last step put it (steer,
129/// spin and suspension travel), following the chassis's smoothed pose.
130fn place_wheels(ctx: &mut Ctx) {
131 let player = ctx.world.resource::<Player>();
132 let car = player.car;
133 let (Some(&vehicle), Some(&chassis)) = (ctx.world.get::<Vehicle3D>(car), ctx.world.get::<Transform3D>(car)) else {
134 return;
135 };
136 for i in 0..player.wheels.len() {
137 let e = ctx.world.resource::<Player>().wheels[i];
138 let pose = vehicle.wheels()[i].pose(&chassis);
139 if let Some(t) = ctx.world.get_mut::<Transform3D>(e) {
140 *t = Transform3D { scale: t.scale, ..pose };
141 }
142 }
143}
144// ANCHOR_END: wheels
145
146/// Every frame: the camera trails the car.
147fn chase(ctx: &mut Ctx) {
148 let car = ctx.world.resource::<Player>().car;
149 let Some(t) = ctx.world.get::<Transform3D>(car).copied() else {
150 return;
151 };
152 let back = -t.forward().with_y(0.0).normalize_or(Vec3::Z);
153 let eye = t.position + back * 9.0 + Vec3::Y * 3.5;
154 let k = (ctx.dt() * 4.0).min(1.0);
155 ctx.camera3d.position = ctx.camera3d.position.lerp(eye, k);
156 let look = Transform3D::at(ctx.camera3d.position).looking_at(t.position + Vec3::Y);
157 ctx.camera3d.rotation = look.rotation;
158}
159
160fn build_world(ctx: &mut Ctx, draw: bool) {
161 let corner = vec3(-SIZE / 2.0, 0.0, -SIZE / 2.0);
162 let step = SIZE / (N - 1) as f32;
163 let heights: Vec<f32> = (0..N * N)
164 .map(|i| height(corner.x + (i % N) as f32 * step, corner.z + (i / N) as f32 * step))
165 .collect();
166 let ground = ctx.physics3d.heightfield(&heights, SIZE);
167 let terrain = ctx
168 .world
169 .spawn((Transform3D::at(corner), RigidBody3D::fixed(), Collider3D::mesh(ground)));
170 let car = spawn_car(ctx, vec3(0.0, height(0.0, 0.0) + 1.5, 0.0));
171 ctx.world.insert_resource(Player {
172 car,
173 steer: 0.0,
174 wheels: Vec::new(),
175 });
176 ctx.world.insert_resource(Controls::default());
177 if !draw {
178 return;
179 }
180 let look = grass(ctx);
181 let tile = ctx.create_terrain_tile(&heights, SIZE, look);
182 ctx.world.insert(terrain, tile);
183 let paint = ctx.create_material(MaterialDesc::color(Color::hex(0xC8402E)));
184 let body = ctx.create_mesh(&MeshData::cube(1.0), paint);
185 if let Some(t) = ctx.world.get_mut::<Transform3D>(car) {
186 t.scale = vec3(1.8, 0.6, 4.2);
187 }
188 ctx.world.insert(car, Model::new(body));
189 let rubber = ctx.create_material(MaterialDesc::color(Color::hex(0x202020)));
190 let tyre = ctx.create_mesh(&MeshData::cube(1.0), rubber);
191 for _ in 0..4 {
192 let wheel = ctx.world.spawn((
193 Transform3D {
194 scale: vec3(0.3, 0.7, 0.7),
195 ..Transform3D::default()
196 },
197 Model::new(tyre),
198 ));
199 ctx.world.resource_mut::<Player>().wheels.push(wheel);
200 }
201}144fn play(net: Net) {
145 Game::new("Lockstep", 800, 600)
146 .lockstep(net)
147 .system(read_keys) // frame system: input out
148 .sim_system(move_crabs) // sim system: everyone's input in
149 .system(draw)
150 .run(|ctx| {
151 ctx.world.reflect::<Crab>(); // compared between peers
152 for player in 0..2 {
153 ctx.world.spawn((
154 Transform2D::at(vec2(250.0 + 300.0 * player as f32, 300.0)),
155 Crab {
156 player,
157 push: Vec2::ZERO,
158 },
159 ));
160 }
161 });
162 // ANCHOR_END: session
163}5fn setup(ctx: &mut Ctx) {
6 let samples: Vec<f32> = (0..44100)
7 .map(|i| (i as f32 * 220.0 * std::f32::consts::TAU / 44100.0).sin() * 0.1)
8 .collect();
9 let hum = ctx.create_sound(44100, &samples);
10 let settings = AudioSettings3D {
11 sound: SoundParams {
12 volume: 0.4,
13 looping: true,
14 ..Default::default()
15 },
16 min_distance: 2.0,
17 max_distance: 60.0,
18 rolloff: AudioRolloff::Linear,
19 ..Default::default()
20 };
21 ctx.world
22 .spawn((Transform3D::at(vec3(4.0, 0.0, 0.0)), AudioSource3D::new(hum, settings)));
23 ctx.play_sound_at(&hum, vec3(-4.0, 0.0, 0.0), AudioSettings3D::default());
24}83fn load_character(ctx: &mut Ctx) {
84 // ANCHOR: skin_load
85 let mesh = ctx.load_mesh("models/character.glb");
86 let skeleton = ctx.load_skeleton("models/character.glb");
87 let hand = skeleton.find("hand_r").expect("model needs hand_r");
88 let character = ctx.world.spawn((Transform3D::default(), Model::new(mesh), skeleton));
89 let skeleton = ctx.world.get_mut::<Skeleton>(character).unwrap();
90 skeleton.pose[hand].rotation = Quat::from_rotation_y(0.3);
91 let local_hand = skeleton.joint_matrix(hand);
92 let transform = ctx.world.get::<Transform3D>(character).unwrap();
93 let world_hand = (transform.matrix() * local_hand).transform_point3(Vec3::ZERO);
94 println!("Hand at {world_hand}");
95 // ANCHOR_END: skin_load
96}
97
98// ANCHOR: clip_code
99fn bend_clip(skeleton: &Skeleton) -> AnimationClip {
100 let rotation = AnimationTrack::new(
101 vec![0.0, 1.0, 2.0],
102 [0.0, 0.3, 0.0]
103 .map(|angle| Vec4::from_array(Quat::from_rotation_z(angle).to_array()))
104 .to_vec(),
105 Interpolation::Linear,
106 );
107 AnimationClip::new(
108 "bend",
109 skeleton,
110 vec![JointAnimation {
111 joint: 1,
112 name: "joint_1".into(),
113 translation: None,
114 rotation: Some(rotation),
115 scale: None,
116 }],
117 )
118}
119// ANCHOR_END: clip_code
120
121#[allow(dead_code)]
122fn play_clips(ctx: &mut Ctx) {
123 // ANCHOR: clip_play
124 let mut skeleton = ctx.load_skeleton("models/character.glb");
125 let walk = ctx.load_clip("models/character.glb", "walk");
126 let run = ctx.load_clip("models/character.glb", "run");
127 let time = ctx.time() as f32;
128 let loop_time = |clip: &AnimationClip| {
129 if clip.duration() > 0.0 {
130 time.rem_euclid(clip.duration())
131 } else {
132 0.0
133 }
134 };
135 skeleton.reset_pose();
136 walk.sample(loop_time(&walk), &mut skeleton);
137 run.sample_blended(loop_time(&run), 0.5, &mut skeleton);
138 // Keep clips and the skeleton as components/resources for subsequent frames.
139 // ANCHOR_END: clip_play
140}
141
142// ANCHOR: animator_graph
143fn bend_animator(skeleton: &Skeleton) -> Animator {
144 let idle = AnimationClip::new("idle", skeleton, vec![]);
145 let bend = bend_clip(skeleton);
146 let states = vec![
147 AnimationState::new(Motion::Clip(idle)),
148 AnimationState::new(Motion::Clip(bend)),
149 ];
150 let transition = |from, to, active| AnimationTransition {
151 from: Some(from),
152 to,
153 conditions: vec![AnimationCondition::Bool(0, active)],
154 exit_time: None,
155 duration: 0.12,
156 offset: 0.0,
157 };
158 let graph = AnimationGraph::new(
159 vec![AnimationParameter::Bool(false)],
160 states,
161 vec![transition(0, 1, true), transition(1, 0, false)],
162 0,
163 );
164 Animator::new(graph)
165}
166// ANCHOR_END: animator_graph
167
168#[allow(dead_code)]
169fn animate_now() {
170 // ANCHOR: animator_update
171 let mut skeleton = chain();
172 let mut animator = bend_animator(&skeleton);
173 animator.manual = true;
174 animator.set_bool(0, true);
175 animator.update(0.016, &mut skeleton);
176 // Or spawn (Animator, Skeleton) together for automatic updates.
177 animator.crossfade(0, 0.12, 0.0);
178 animator.update(0.016, &mut skeleton);
179 for name in animator.drain_events() {
180 println!("Animation event: {name}");
181 }
182 // ANCHOR_END: animator_update
183}
184
185#[allow(dead_code)]
186fn layers_and_root_motion(skeleton: &Skeleton, wave: AnimationClip) -> Animator {
187 // ANCHOR: animator_layers
188 let mut animator = bend_animator(skeleton);
189 let waving = AnimationGraph::new(vec![], vec![AnimationState::new(Motion::Clip(wave))], vec![], 0);
190 let mut layer = AnimationLayer::new(waving);
191 layer.mask = JointMask::descendants(skeleton, "joint_3"); // that joint and its children
192 layer.weight = 0.8;
193 let wave = animator.add_layer(layer);
194 animator.layer_mut(wave).weight = 0.5; // fade it any time; its graph has its own parameters
195 // The clip's own steps move the entity, instead of sliding in place.
196 animator.root_joint = 0;
197 animator.apply_root_motion = true;
198 // ANCHOR_END: animator_layers
199 animator
200}
201
202fn main() {
203 Game::new("Crabby: skinned meshes", 1000, 700).system(sway).run(|ctx| {
204 ctx.clear(Color::hex(0xABCBDD));
205 ctx.camera3d.position = vec3(6.0, 4.0, 9.0);
206 ctx.camera3d.look_at(vec3(0.0, 1.7, 0.0));
207 let green = ctx.create_material(MaterialDesc::color(Color::hex(0x398F79)));
208 let gold = ctx.create_material(MaterialDesc::color(Color::hex(0xF2BD50)));
209 let white = ctx.default_material();
210 let ground = ctx.create_mesh(&MeshData::plane(20.0), white);
211 ctx.world.spawn((Transform3D::default(), Model::new(ground)));
212 // ANCHOR: skin_create
213 let skeleton = chain();
214 let mesh = ctx.create_skinned_mesh(&tube(0.0, 3.5, 0.22), green, &skeleton);
215 // A separate skeleton with matching names demonstrates name binding.
216 let band = ctx.create_skinned_mesh(&tube(2.2, 2.5, 0.25), gold, &chain());
217 // ANCHOR_END: skin_create
218 let ball = ctx.create_mesh(&MeshData::sphere(0.18, 16), gold);
219 let bend = bend_clip(&skeleton);
220 for i in 0..3 {
221 let tentacle = ctx.world.spawn((
222 Transform3D::at(vec3((i as f32 - 1.0) * 2.5, 0.0, 0.0)),
223 Model::new(mesh),
224 skeleton.clone(),
225 Sway(i as f32 * 1.8),
226 Bend(bend.clone()),
227 ));
228 if i == 2 {
229 ctx.world.insert(tentacle, bend_animator(&skeleton));
230 }
231 if i == 0 {
232 // ANCHOR: skin_parts
233 ctx.world.spawn((Model::new(band), SkinOf(tentacle)));
234 let tip = skeleton.find("joint_7").unwrap();
235 ctx.world.spawn((
236 Transform3D::default(),
237 Model::new(ball),
238 AttachToJoint {
239 skeleton: tentacle,
240 joint: tip,
241 offset: Transform3D::at(vec3(0.0, 0.12, 0.0)),
242 },
243 ));
244 // ANCHOR_END: skin_parts
245 }
246 }
247 });
248}- examples/editor.rs
- examples/tiles.rs
- examples/gamepad.rs
- examples/particles.rs
- examples/navmesh3d.rs
- examples/battle.rs
- examples/ants.rs
- examples/physics.rs
- examples/ui_panel.rs
- examples/ui.rs
- examples/common/lights_scene.rs
- examples/physics3d.rs
- examples/ribbons.rs
- examples/3d.rs
- examples/compute.rs
- examples/streaming.rs
- examples/shaders.rs
- examples/terrain.rs
Sourcepub fn spawn_batch<B>(&mut self, bundles: impl IntoIterator<Item = B>)where
B: Bundle,
pub fn spawn_batch<B>(&mut self, bundles: impl IntoIterator<Item = B>)where
B: Bundle,
Spawns many entities of the same shape. Faster than calling spawn in a loop.
Examples found in repository?
44fn spawn(ctx: &mut Ctx, n: usize, at: Vec2) {
45 let b = ctx.world.resource_mut::<Bench>();
46 let crab = b.crab;
47 let mut batch = Vec::with_capacity(n);
48 for _ in 0..n {
49 let r = &mut b.rng;
50 let vel = vec2(r.next_f32() * 400.0 - 200.0, r.next_f32() * 400.0 - 300.0);
51 let color = Color::rgb(
52 0.6 + r.next_f32() * 0.4,
53 0.6 + r.next_f32() * 0.4,
54 0.6 + r.next_f32() * 0.4,
55 );
56 batch.push((
57 Transform2D {
58 scale: Vec2::splat(b.scale),
59 ..Transform2D::at(at)
60 },
61 Sprite::new(crab).with_color(color),
62 Vel(vel),
63 ));
64 }
65 ctx.world.spawn_batch(batch);
66}More examples
87fn spawn_boids(ctx: &mut Ctx, n: usize, bounds: Vec2) {
88 ctx.world.reflect::<Vel>();
89 let tex = ctx.load_texture("crab.png");
90 let mut s = 0x9E37_79B9_7F4A_7C15u64;
91 let mut rand = move || {
92 s ^= s << 13;
93 s ^= s >> 7;
94 s ^= s << 17;
95 (s >> 40) as f32 / (1u64 << 24) as f32
96 };
97 let mut batch = Vec::with_capacity(n);
98 for _ in 0..n {
99 let pos = vec2(rand(), rand()) * bounds;
100 let vel = Vec2::from_angle(rand() * std::f32::consts::TAU) * MIN_SPEED;
101 let tint = Color::rgb(0.5 + rand() * 0.5, 0.7 + rand() * 0.3, 0.9);
102 batch.push((
103 Transform2D::at(pos).scaled(Vec2::splat(0.5)),
104 Sprite::new(tex).with_color(tint),
105 Vel(vel),
106 ));
107 }
108 ctx.world.spawn_batch(batch);
109}88fn setup(ctx: &mut Ctx, side: u32) {
89 let tiles = tileset(ctx);
90 let mut map = TileMap::new(tiles, side, side);
91 for y in 0..side {
92 for x in 0..side {
93 map.set(x, y, Some(terrain(x as f32, y as f32)));
94 }
95 }
96 ctx.world.spawn((Transform2D::default(), map));
97
98 let blob = creature(ctx);
99 let mut rng = Rng::new(7);
100 let world = (side * TILE) as f32;
101 ctx.world.spawn_batch((0..20_000).map(|_| {
102 let at = vec2(rng.range(0.0, world), rng.range(0.0, world));
103 let mut walk = Animation::new(blob, 0..8, rng.range(6.0, 12.0));
104 walk.time = rng.f32(); // out of step with each other
105 (
106 Transform2D::at(at),
107 blob.sprite(0),
108 walk,
109 Wander(rng.unit_vec2() * 20.0),
110 )
111 }));
112 ctx.camera.zoom = 2.0;
113 ctx.camera.look_at(vec2(world, world) / 2.0);
114}16fn main() {
17 let mut world = World::new();
18 world.spawn_batch((0..1_000_000).map(|_| (Pos { x: 0.0, y: 0.0 }, Vel { x: 1.0, y: 2.0 })));
19 let mut samples = Vec::with_capacity(200);
20 for i in 0..210 {
21 let start = Instant::now();
22 world.query::<(&mut Pos, &Vel)>().for_each(|(p, v)| {
23 p.x += v.x;
24 p.y += v.y;
25 });
26 black_box(&world);
27 let elapsed = start.elapsed().as_secs_f64() * 1000.0;
28 if i >= 10 {
29 samples.push(elapsed);
30 }
31 }
32 samples.sort_by(f64::total_cmp);
33 let checksum: f64 = world
34 .query::<&Pos>()
35 .iter()
36 .map(|p| f64::from(p.x) + f64::from(p.y))
37 .sum();
38 assert_eq!(checksum, 630_000_000.0);
39 println!(
40 "Rust: 1000000 entities, median {:.6} ms/update, checksum {checksum:.0}",
41 (samples[99] + samples[100]) / 2.0
42 );
43}123fn setup(ctx: &mut Ctx, agents: usize, texture: Option<Texture>) {
124 let seed = 7;
125 let nav = make_map(seed);
126 let mut rng = Rng::new(seed ^ 1);
127 let goal = walkable_point(&nav, &mut rng);
128 let mut paths = Paths {
129 nav,
130 flow: FlowField::new(),
131 goal,
132 compute_ms: 0.0,
133 };
134 paths.set_goal(goal);
135 ctx.world.reflect::<Agent>();
136 let starts: Vec<Vec2> = (0..agents).map(|_| walkable_point(&paths.nav, &mut rng)).collect();
137 ctx.world.insert_resource(paths);
138 match texture {
139 Some(tex) => ctx.world.spawn_batch(starts.into_iter().map(|p| {
140 (
141 Transform2D::at(p),
142 Sprite::new(tex).with_color(Color::rgb(1.0, 0.85, 0.4)),
143 Agent { vel: Vec2::ZERO },
144 )
145 })),
146 None => ctx.world.spawn_batch(
147 starts
148 .into_iter()
149 .map(|p| (Transform2D::at(p), Agent { vel: Vec2::ZERO })),
150 ),
151 };
152}34fn main() {
35 let agents = env_number("AGENTS", 1_000_000.0) as usize;
36 let ticks = env_number("TICKS", 600.0) as u64;
37
38 let spawn_start = Instant::now();
39 let mut setup_done = Instant::now();
40 let world = Game::new("headless", 1280, 720)
41 .sim_system(wander)
42 .run_headless(ticks, |ctx| {
43 let tex = ctx.load_texture("crab.png");
44 ctx.world.spawn_batch((0..agents).map(|i| {
45 let pos = vec2((i % 4096) as f32, (i / 4096 % 4096) as f32);
46 (
47 Transform2D::at(pos),
48 Sprite::new(tex),
49 Agent {
50 heading: i as f32,
51 speed: 30.0,
52 },
53 )
54 }));
55 println!(
56 "spawned {agents} agents in {:.0} ms",
57 spawn_start.elapsed().as_secs_f64() * 1e3
58 );
59 setup_done = Instant::now();
60 });
61 let secs = setup_done.elapsed().as_secs_f64();
62 println!(
63 "{ticks} ticks in {:.2} s: {:.0} ticks/s, {:.2} ms/tick ({} entities, {:.0} bytes/entity allocated)",
64 secs,
65 ticks as f64 / secs,
66 secs * 1e3 / ticks as f64,
67 world.len(),
68 world.allocated_bytes() as f64 / world.len().max(1) as f64
69 );
70}Sourcepub fn spawn_at<B>(&mut self, e: Entity, bundle: B) -> boolwhere
B: Bundle,
pub fn spawn_at<B>(&mut self, e: Entity, bundle: B) -> boolwhere
B: Bundle,
Spawns an entity with exactly the handle e, for restoring saved
worlds so that components referring to entities stay valid. Returns
false (dropping bundle) if e is taken, or was despawned in this
world already (old handles stay dead).
use ponko_ecs::{Entity, World};
let mut world = World::new();
let saved = Entity::from_bits(7 | 3 << 32); // slot 7, generation 3
assert!(world.spawn_at(saved, ("hello",)));
assert_eq!(world.get::<&str>(saved), Some(&"hello"));
assert!(!world.spawn_at(saved, ("again",))); // taken
let fresh = world.spawn(("new",)); // fresh handles never collide
assert_ne!(fresh, saved);Sourcepub fn allocated_bytes(&self) -> usize
pub fn allocated_bytes(&self) -> usize
Memory held for entity data: component columns and handles of every archetype, including spare capacity. Doesn’t count resources or the world’s bookkeeping (per-slot generation, archetype index).
use ponko_ecs::World;
struct Pos(f32, f32);
let mut world = World::new();
world.spawn_batch((0..1000).map(|i| (Pos(i as f32, 0.0),)));
// 8 bytes of Pos + 8 bytes of entity handle each, at least.
assert!(world.allocated_bytes() >= 1000 * 16);Examples found in repository?
34fn main() {
35 let agents = env_number("AGENTS", 1_000_000.0) as usize;
36 let ticks = env_number("TICKS", 600.0) as u64;
37
38 let spawn_start = Instant::now();
39 let mut setup_done = Instant::now();
40 let world = Game::new("headless", 1280, 720)
41 .sim_system(wander)
42 .run_headless(ticks, |ctx| {
43 let tex = ctx.load_texture("crab.png");
44 ctx.world.spawn_batch((0..agents).map(|i| {
45 let pos = vec2((i % 4096) as f32, (i / 4096 % 4096) as f32);
46 (
47 Transform2D::at(pos),
48 Sprite::new(tex),
49 Agent {
50 heading: i as f32,
51 speed: 30.0,
52 },
53 )
54 }));
55 println!(
56 "spawned {agents} agents in {:.0} ms",
57 spawn_start.elapsed().as_secs_f64() * 1e3
58 );
59 setup_done = Instant::now();
60 });
61 let secs = setup_done.elapsed().as_secs_f64();
62 println!(
63 "{ticks} ticks in {:.2} s: {:.0} ticks/s, {:.2} ms/tick ({} entities, {:.0} bytes/entity allocated)",
64 secs,
65 ticks as f64 / secs,
66 secs * 1e3 / ticks as f64,
67 world.len(),
68 world.allocated_bytes() as f64 / world.len().max(1) as f64
69 );
70}Sourcepub fn clear(&mut self)
pub fn clear(&mut self)
Despawns every entity. Resources stay.
use ponko_ecs::World;
let mut world = World::new();
let e = world.spawn((1u8,));
world.clear();
assert!(world.is_empty() && !world.is_alive(e));Sourcepub fn reset(&mut self)
pub fn reset(&mut self)
Despawns every entity and forgets every entity id, as if the world
were new; resources and registered components stay. Afterwards, old
handles may match new entities, so use this only when replacing the
whole world, such as when loading a save (whose ids then come back
with spawn_at). For everything else use
clear.
use ponko_ecs::World;
let mut world = World::new();
let e = world.spawn((1u8,));
world.reset();
assert!(world.spawn_at(e, (2u8,))); // the id is free againSourcepub fn despawn(&mut self, e: Entity) -> bool
pub fn despawn(&mut self, e: Entity) -> bool
Removes an entity and drops its components. Returns false if it was
already gone.
Examples found in repository?
96fn drop_things(ctx: &mut Ctx) {
97 let at = ctx.mouse_world();
98 if ctx.mouse_pressed(MouseButton::Left) {
99 ctx.world.spawn((
100 Transform2D::at(at),
101 RigidBody::dynamic(),
102 Collider::rect(vec2(30.0, 30.0)).friction(0.8),
103 ));
104 }
105 if ctx.mouse_pressed(MouseButton::Right) {
106 ctx.world.spawn((
107 Transform2D::at(at),
108 RigidBody::dynamic(),
109 Collider::circle(12.0).restitution(0.6),
110 ));
111 }
112 if ctx.mouse_pressed(MouseButton::Middle) {
113 let rock: Vec<Vec2> = (0..5)
114 .map(|i| Vec2::from_angle(i as f32 * std::f32::consts::TAU / 5.0) * 18.0)
115 .collect();
116 ctx.world.spawn((
117 Transform2D::at(at),
118 RigidBody::dynamic(),
119 Collider::polygon(&rock).friction(0.7),
120 ));
121 }
122 if ctx.key_pressed(Key::R) {
123 let all: Vec<Entity> = ctx.world.query::<Entity>().with::<RigidBody>().iter().collect();
124 for e in all {
125 ctx.world.despawn(e);
126 }
127 build_level(ctx);
128 }
129 if ctx.key_pressed(Key::Escape) {
130 ctx.quit();
131 }
132}More examples
215fn forage(ctx: &mut Ctx) {
216 let turn = ctx.turns(2);
217 let mut food_grid = SpatialGrid::new(8.0);
218 food_grid.rebuild_from(
219 ctx.world
220 .query::<(Entity, &Transform2D, &Food)>()
221 .iter()
222 .map(|(e, t, _)| (e, t.position)),
223 );
224 let grid = &food_grid;
225 ctx.world
226 .query::<(Entity, &Transform2D, &mut Ant)>()
227 .par_for_each(|(e, t, ant)| {
228 ant.event = Event::None;
229 if ant.energy <= 0.0 {
230 ant.event = Event::Starved;
231 return;
232 }
233 if !turn.includes(e) {
234 return;
235 }
236 let at_home = t.position.distance(ant.home_at) < NEST_RADIUS;
237 if ant.carrying {
238 if at_home {
239 ant.carrying = false;
240 ant.since = 0.0;
241 ant.energy = LIFESPAN;
242 ant.heading += std::f32::consts::PI;
243 ant.event = Event::Delivered;
244 }
245 } else if at_home {
246 ant.since = 0.0;
247 } else if let Some(f) = grid.nearest(t.position, 4.0, |_| true) {
248 ant.carrying = true;
249 ant.since = 0.0;
250 ant.heading += std::f32::consts::PI;
251 ant.event = Event::PickedUp(f.entity);
252 }
253 });
254
255 let (mut picked, mut delivered) = (Vec::new(), Vec::new());
256 let cmds = &mut ctx.commands;
257 ctx.world
258 .query::<(Entity, &Ant)>()
259 .for_each(|(e, ant)| match ant.event {
260 Event::None => {}
261 Event::Starved => cmds.despawn(e),
262 Event::PickedUp(food) => picked.push(food),
263 Event::Delivered => delivered.push(ant.home),
264 });
265 for f in picked {
266 if let Some(food) = ctx.world.get_mut::<Food>(f) {
267 food.amount = food.amount.saturating_sub(1);
268 if food.amount == 0 {
269 ctx.world.despawn(f);
270 }
271 }
272 }
273 for nest in delivered {
274 if let Some(n) = ctx.world.get_mut::<Nest>(nest) {
275 n.stored += 1;
276 n.delivered += 1;
277 }
278 }
279}82fn main() {
83 let bench = std::env::var("BENCH").is_ok();
84 let shot = std::env::var("SHOT").ok();
85 let mut cells = CellStreamer::new(TILE, LOAD, KEEP);
86 let mut tiles: HashMap<(i32, i32), Tile> = HashMap::new();
87 let mut ground: Option<Material> = None;
88 let (mut yaw, mut speed, mut altitude) = (0.4f32, if bench { 600.0f32 } else { 120.0 }, 90.0f32);
89 // The streaming's cost on the game's thread, for BENCH.
90 let (mut uploaded, mut freed, mut upload_time, mut worst_upload) = (0usize, 0usize, 0.0f64, 0.0f64);
91 Game::new("Crabby Engine: streaming", 1280, 720)
92 .vsync(!bench)
93 .system(move |ctx: &mut Ctx| {
94 if ctx.key_pressed(Key::Escape) {
95 ctx.quit();
96 }
97 let dt = ctx.dt();
98 if ctx.key_down(Key::Left) {
99 yaw += dt * 0.8;
100 }
101 if ctx.key_down(Key::Right) {
102 yaw -= dt * 0.8;
103 }
104 if ctx.key_down(Key::W) {
105 speed = (speed + 80.0 * dt).min(600.0);
106 }
107 if ctx.key_down(Key::S) {
108 speed = (speed - 80.0 * dt).max(0.0);
109 }
110 if ctx.key_down(Key::E) {
111 altitude += 40.0 * dt;
112 }
113 if ctx.key_down(Key::Q) {
114 altitude = (altitude - 40.0 * dt).max(5.0);
115 }
116 // Fly on, keeping above the hills ahead.
117 let rotation = Quat::from_rotation_y(yaw);
118 let mut at = ctx.camera3d.position + rotation * Vec3::NEG_Z * speed * dt;
119 let ahead = at + rotation * Vec3::NEG_Z * 120.0;
120 let floor = height(at.x, at.z).max(height(ahead.x, ahead.z));
121 at.y += (floor + altitude - at.y) * (dt * 1.5).min(1.0);
122 ctx.camera3d.position = at;
123 ctx.camera3d.rotation = rotation * Quat::from_rotation_x(-0.22);
124
125 // Which tiles to start and drop.
126 cells.update(at, STARTS);
127 for &cell in cells.to_load() {
128 let corner = cells.corner(cell);
129 tiles.insert(cell, Tile::Building(Job::spawn(move || tile_data(corner))));
130 }
131 for cell in cells.to_drop() {
132 // A job still building is dropped with its result.
133 if let Some(Tile::Live(entity, tile)) = tiles.remove(cell) {
134 ctx.world.despawn(entity);
135 ctx.free_terrain_tile(tile);
136 freed += 1;
137 }
138 }
139 // Finished tiles go on the GPU, a few a frame.
140 let ground = *ground.get_or_insert_with(|| {
141 let shader = ctx.create_shader("ground", GROUND);
142 ctx.create_material(MaterialDesc {
143 shader: Some(shader),
144 ..MaterialDesc::color(Color::WHITE)
145 })
146 });
147 let mut uploads = 0;
148 for (&cell, tile) in tiles.iter_mut() {
149 if uploads == UPLOADS {
150 break;
151 }
152 if let Tile::Building(job) = tile
153 && let Some(data) = job.take()
154 {
155 let start = Instant::now();
156 let made = ctx.upload_terrain_tile(&data, ground);
157 let entity = ctx.world.spawn((Transform3D::at(cells.corner(cell)), made));
158 let took = start.elapsed().as_secs_f64() * 1000.0;
159 upload_time += took;
160 worst_upload = worst_upload.max(took);
161 uploaded += 1;
162 uploads += 1;
163 *tile = Tile::Live(entity, made);
164 }
165 }
166
167 if ctx.frame() == 400
168 && let Some(path) = &shot
169 {
170 ctx.save_screenshot(path);
171 }
172 if ctx.frame() == 402 && shot.is_some() {
173 ctx.quit();
174 }
175 if bench && ctx.frame() == 3000 {
176 let live = tiles.values().filter(|t| matches!(t, Tile::Live(..))).count();
177 println!(
178 "{uploaded} tiles uploaded ({:.2} ms each, worst {worst_upload:.2} ms), {freed} freed, {live} live; {:?}",
179 upload_time / uploaded.max(1) as f64,
180 ctx.render_stats()
181 );
182 ctx.quit();
183 }
184 })
185 .run(move |ctx| {
186 ctx.post.bloom = 0.08;
187 ctx.lighting.sun_direction = Sky::sun_direction_at(15.5);
188 ctx.lighting.shadow_distance = 500.0;
189 ctx.camera3d.far = LOAD + TILE;
190 // Fog thick enough that tiles appear behind it, not at an edge.
191 ctx.sky.fog_density = 0.0007;
192 ctx.sky.fog_height = 40.0;
193 ctx.sky.fog_falloff = 90.0;
194 ctx.camera3d.position = vec3(0.0, height(0.0, 0.0) + 60.0, 0.0);
195 });
196}Sourcepub fn len(&self) -> usize
pub fn len(&self) -> usize
Number of live entities.
Examples found in repository?
104fn main() {
105 Game::new("Crabby Engine: ECS", 1280, 720)
106 .sim_system(movement)
107 .sim_system(aging)
108 .system(controls)
109 .run(|ctx| {
110 let crab = ctx.load_texture("crab.png");
111 ctx.world.insert_resource(Settings {
112 rng: 0x9E37_79B9_7F4A_7C15,
113 crab,
114 });
115 let center = ctx.window_size() / 2.0;
116 for _ in 0..500 {
117 spawn_crab(ctx, center);
118 }
119
120 // Optional per-frame closure: show stats in the title bar once a second.
121 let mut next_report = 1.0;
122 move |ctx: &mut Ctx| {
123 if ctx.time() >= next_report {
124 next_report += 1.0;
125 let sim = if ctx.is_paused() {
126 "paused".to_string()
127 } else {
128 format!("{}x", ctx.sim_speed())
129 };
130 println!(
131 "crabs: {:>6} fps: {:>5.1} sim: {sim:>6} tick: {}",
132 ctx.world.len(),
133 ctx.fps(),
134 ctx.sim_tick()
135 );
136 }
137 }
138 });
139}More examples
34fn main() {
35 let agents = env_number("AGENTS", 1_000_000.0) as usize;
36 let ticks = env_number("TICKS", 600.0) as u64;
37
38 let spawn_start = Instant::now();
39 let mut setup_done = Instant::now();
40 let world = Game::new("headless", 1280, 720)
41 .sim_system(wander)
42 .run_headless(ticks, |ctx| {
43 let tex = ctx.load_texture("crab.png");
44 ctx.world.spawn_batch((0..agents).map(|i| {
45 let pos = vec2((i % 4096) as f32, (i / 4096 % 4096) as f32);
46 (
47 Transform2D::at(pos),
48 Sprite::new(tex),
49 Agent {
50 heading: i as f32,
51 speed: 30.0,
52 },
53 )
54 }));
55 println!(
56 "spawned {agents} agents in {:.0} ms",
57 spawn_start.elapsed().as_secs_f64() * 1e3
58 );
59 setup_done = Instant::now();
60 });
61 let secs = setup_done.elapsed().as_secs_f64();
62 println!(
63 "{ticks} ticks in {:.2} s: {:.0} ticks/s, {:.2} ms/tick ({} entities, {:.0} bytes/entity allocated)",
64 secs,
65 ticks as f64 / secs,
66 secs * 1e3 / ticks as f64,
67 world.len(),
68 world.allocated_bytes() as f64 / world.len().max(1) as f64
69 );
70}95fn report(ctx: &mut Ctx) {
96 if ctx.mouse_down(MouseButton::Left) {
97 let at = ctx.mouse_pos();
98 spawn(ctx, 1000, at);
99 }
100 // "update + draw calls" = every system except this one, as measured by the
101 // engine (smoothed averages; draw_sprites hasn't run yet this frame).
102 let systems_ms: f32 = ctx
103 .system_timings()
104 .iter()
105 .filter(|t| !t.name.ends_with("report"))
106 .map(|t| t.avg_ms)
107 .sum();
108 let (time, count, render) = (ctx.time(), ctx.world.len(), ctx.render_stats());
109 let b = ctx.world.resource_mut::<Bench>();
110 let now = Instant::now();
111 let frame_ms = now.duration_since(b.frame_start).as_secs_f32() * 1000.0;
112 b.frame_start = now;
113 b.worst_ms = b.worst_ms.max(frame_ms);
114 b.frames += 1;
115 if time >= b.report_at {
116 println!(
117 "crabs: {:>8} fps: {:>6.1} avg frame: {:>6.2} ms worst: {:>6.2} ms update+draw calls: {:>6.2} ms render cpu: {:>5.2} ms gpu: {}",
118 count,
119 b.frames,
120 1000.0 / b.frames as f64,
121 b.worst_ms,
122 systems_ms,
123 render.cpu_ms,
124 render.gpu_ms.map_or("n/a".into(), |g| format!("{g:.2} ms")),
125 );
126 b.report_at += 1.0;
127 b.frames = 0;
128 b.worst_ms = 0.0;
129 }
130 let quit_after = b.quit_after;
131 if ctx.key_pressed(Key::Escape) || (quit_after > 0.0 && time >= quit_after) {
132 ctx.quit();
133 }
134}111fn main() {
112 let n = env_number("BOIDS", 20_000.0) as usize;
113 let headless = env_number("HEADLESS", 0.0) != 0.0;
114
115 if headless {
116 // Keep density like the windowed version: scale the world with the count.
117 let side = (n as f32 / 20_000.0).sqrt() * 1280.0;
118 let bounds = Vec2::splat(side);
119 let ticks = env_number("TICKS", 300.0) as u64;
120 let mut start = Instant::now();
121 let world = Game::new("boids", 1, 1)
122 .sim_system(flocking(bounds))
123 .run_headless(ticks, |ctx| {
124 spawn_boids(ctx, n, bounds);
125 start = Instant::now();
126 });
127 let secs = start.elapsed().as_secs_f64();
128 println!(
129 "{} boids, {ticks} ticks: {:.2} ms/tick ({:.0} ticks/s)",
130 world.len(),
131 secs * 1e3 / ticks as f64,
132 ticks as f64 / secs
133 );
134 return;
135 }
136
137 let bounds = vec2(1280.0, 720.0);
138 Game::new("Crabby Engine: boids", 1280, 720)
139 .sim_system(flocking(bounds))
140 .system(controls)
141 .run(move |ctx| {
142 spawn_boids(ctx, n, bounds);
143 let mut next = 1.0;
144 move |ctx: &mut Ctx| {
145 if ctx.time() >= next {
146 next += 1.0;
147 let sim_ms: f32 = ctx.system_timings().iter().filter(|t| t.sim).map(|t| t.avg_ms).sum();
148 println!(
149 "boids: {} fps: {:.0} sim: {:.2} ms/frame tick: {}",
150 ctx.world.len(),
151 ctx.fps(),
152 sim_ms,
153 ctx.sim_tick()
154 );
155 }
156 }
157 });
158}Sourcepub fn get<T>(&self, e: Entity) -> Option<&T>where
T: Component,
pub fn get<T>(&self, e: Entity) -> Option<&T>where
T: Component,
Component T of entity e, or None if e is dead or doesn’t have one.
Examples found in repository?
97fn speed_of(world: &World, car: Entity) -> f32 {
98 world.get::<Velocity3D>(car).map_or(0.0, |v| v.linear.length())
99}
100
101/// Speed along where the car faces (negative backing up).
102fn forward_speed(world: &World, car: Entity) -> f32 {
103 match (world.get::<Velocity3D>(car), world.get::<Transform3D>(car)) {
104 (Some(v), Some(t)) => v.linear.dot(t.forward()),
105 _ => 0.0,
106 }
107}
108
109/// Keys to controls (windowed), in the same tick as `drive`.
110fn keys(ctx: &mut Ctx) {
111 let axis = |plus: bool, minus: bool| plus as i32 as f32 - minus as i32 as f32;
112 let controls = Controls {
113 throttle: axis(ctx.key_down(Key::W), ctx.key_down(Key::S)),
114 steer: axis(ctx.key_down(Key::D), ctx.key_down(Key::A)),
115 handbrake: ctx.key_down(Key::Space),
116 };
117 *ctx.world.resource_mut::<Controls>() = controls;
118 if ctx.key_pressed(Key::R) {
119 let car = ctx.world.resource::<Player>().car;
120 if let Some(t) = ctx.world.get_mut::<Transform3D>(car) {
121 let p = t.position;
122 *t = Transform3D::at(vec3(p.x, height(p.x, p.z) + 1.5, p.z));
123 }
124 }
125}
126
127// ANCHOR: wheels
128/// Every frame: each wheel drawn where its car's last step put it (steer,
129/// spin and suspension travel), following the chassis's smoothed pose.
130fn place_wheels(ctx: &mut Ctx) {
131 let player = ctx.world.resource::<Player>();
132 let car = player.car;
133 let (Some(&vehicle), Some(&chassis)) = (ctx.world.get::<Vehicle3D>(car), ctx.world.get::<Transform3D>(car)) else {
134 return;
135 };
136 for i in 0..player.wheels.len() {
137 let e = ctx.world.resource::<Player>().wheels[i];
138 let pose = vehicle.wheels()[i].pose(&chassis);
139 if let Some(t) = ctx.world.get_mut::<Transform3D>(e) {
140 *t = Transform3D { scale: t.scale, ..pose };
141 }
142 }
143}
144// ANCHOR_END: wheels
145
146/// Every frame: the camera trails the car.
147fn chase(ctx: &mut Ctx) {
148 let car = ctx.world.resource::<Player>().car;
149 let Some(t) = ctx.world.get::<Transform3D>(car).copied() else {
150 return;
151 };
152 let back = -t.forward().with_y(0.0).normalize_or(Vec3::Z);
153 let eye = t.position + back * 9.0 + Vec3::Y * 3.5;
154 let k = (ctx.dt() * 4.0).min(1.0);
155 ctx.camera3d.position = ctx.camera3d.position.lerp(eye, k);
156 let look = Transform3D::at(ctx.camera3d.position).looking_at(t.position + Vec3::Y);
157 ctx.camera3d.rotation = look.rotation;
158}
159
160fn build_world(ctx: &mut Ctx, draw: bool) {
161 let corner = vec3(-SIZE / 2.0, 0.0, -SIZE / 2.0);
162 let step = SIZE / (N - 1) as f32;
163 let heights: Vec<f32> = (0..N * N)
164 .map(|i| height(corner.x + (i % N) as f32 * step, corner.z + (i / N) as f32 * step))
165 .collect();
166 let ground = ctx.physics3d.heightfield(&heights, SIZE);
167 let terrain = ctx
168 .world
169 .spawn((Transform3D::at(corner), RigidBody3D::fixed(), Collider3D::mesh(ground)));
170 let car = spawn_car(ctx, vec3(0.0, height(0.0, 0.0) + 1.5, 0.0));
171 ctx.world.insert_resource(Player {
172 car,
173 steer: 0.0,
174 wheels: Vec::new(),
175 });
176 ctx.world.insert_resource(Controls::default());
177 if !draw {
178 return;
179 }
180 let look = grass(ctx);
181 let tile = ctx.create_terrain_tile(&heights, SIZE, look);
182 ctx.world.insert(terrain, tile);
183 let paint = ctx.create_material(MaterialDesc::color(Color::hex(0xC8402E)));
184 let body = ctx.create_mesh(&MeshData::cube(1.0), paint);
185 if let Some(t) = ctx.world.get_mut::<Transform3D>(car) {
186 t.scale = vec3(1.8, 0.6, 4.2);
187 }
188 ctx.world.insert(car, Model::new(body));
189 let rubber = ctx.create_material(MaterialDesc::color(Color::hex(0x202020)));
190 let tyre = ctx.create_mesh(&MeshData::cube(1.0), rubber);
191 for _ in 0..4 {
192 let wheel = ctx.world.spawn((
193 Transform3D {
194 scale: vec3(0.3, 0.7, 0.7),
195 ..Transform3D::default()
196 },
197 Model::new(tyre),
198 ));
199 ctx.world.resource_mut::<Player>().wheels.push(wheel);
200 }
201}
202
203/// A one-layer terrain material: grass everywhere.
204fn grass(ctx: &mut Ctx) -> Material {
205 let green: Vec<u8> = (0..16 * 16)
206 .flat_map(|i| {
207 let shade = 200 + (i * 37 % 56) as u8;
208 [
209 (70 * shade as u32 / 255) as u8,
210 (120 * shade as u32 / 255) as u8,
211 45,
212 255,
213 ]
214 })
215 .collect();
216 let layers = ctx.create_texture_array(16, 16, &[&green], TextureKind::Color);
217 let weights = ctx.create_texture_array(1, 1, &[&[255, 0, 0, 0]], TextureKind::Data);
218 let corner = vec2(-SIZE / 2.0, -SIZE / 2.0);
219 ctx.create_terrain_material(&TerrainMaterialDesc::new(layers, weights, corner, vec2(SIZE, SIZE)))
220}
221
222/// The script for `HEADLESS=1`: full throttle for 4 s, then steer right
223/// for 2 s, then brake to a stop and hold it with the handbrake.
224fn autopilot(ctx: &mut Ctx) {
225 let t = ctx.sim_tick() as f32 * ctx.dt();
226 let car = ctx.world.resource::<Player>().car;
227 let rolling = forward_speed(&ctx.world, car) > 0.3;
228 *ctx.world.resource_mut::<Controls>() = Controls {
229 throttle: if t < 6.0 {
230 1.0
231 } else if rolling {
232 -1.0
233 } else {
234 0.0
235 },
236 steer: if (4.0..6.0).contains(&t) { 1.0 } else { 0.0 },
237 handbrake: t >= 6.0 && !rolling,
238 };
239}
240
241/// What the car did, second by second.
242#[derive(Default)]
243struct Log(Vec<(f32, Vec3)>);
244
245fn record(ctx: &mut Ctx) {
246 if ctx.sim_tick().is_multiple_of(50) {
247 let car = ctx.world.resource::<Player>().car;
248 let speed = speed_of(&ctx.world, car);
249 let facing = ctx.world.get::<Transform3D>(car).map_or(Vec3::NEG_Z, |t| t.forward());
250 ctx.world.resource_mut::<Log>().0.push((speed, facing));
251 }
252}More examples
72fn blast(ctx: &mut Ctx) {
73 if !ctx.key_pressed(Key::Space) {
74 return;
75 }
76 let center = ctx.mouse_world();
77 let mut near = Vec::new();
78 ctx.physics.entities_in_circle(center, 200.0, &mut near);
79 for e in near {
80 let Some(t) = ctx.world.get::<Transform2D>(e) else {
81 continue;
82 };
83 let away = (t.position - center).normalize_or(Vec2::NEG_Y);
84 if let Some(mass) = ctx.physics.mass(e) {
85 ctx.physics.apply_impulse(e, away * mass * 600.0);
86 }
87 }
88}83fn load_character(ctx: &mut Ctx) {
84 // ANCHOR: skin_load
85 let mesh = ctx.load_mesh("models/character.glb");
86 let skeleton = ctx.load_skeleton("models/character.glb");
87 let hand = skeleton.find("hand_r").expect("model needs hand_r");
88 let character = ctx.world.spawn((Transform3D::default(), Model::new(mesh), skeleton));
89 let skeleton = ctx.world.get_mut::<Skeleton>(character).unwrap();
90 skeleton.pose[hand].rotation = Quat::from_rotation_y(0.3);
91 let local_hand = skeleton.joint_matrix(hand);
92 let transform = ctx.world.get::<Transform3D>(character).unwrap();
93 let world_hand = (transform.matrix() * local_hand).transform_point3(Vec3::ZERO);
94 println!("Hand at {world_hand}");
95 // ANCHOR_END: skin_load
96}153fn eyes(ctx: &mut Ctx) {
154 if ctx.mouse_pressed(MouseButton::Left) {
155 if ctx.cursor_locked() {
156 shove(ctx);
157 } else {
158 ctx.set_cursor_locked(true);
159 }
160 } else if ctx.key_pressed(Key::Escape) {
161 if ctx.cursor_locked() {
162 ctx.set_cursor_locked(false);
163 } else {
164 ctx.quit();
165 }
166 }
167 let turn = if ctx.cursor_locked() {
168 ctx.mouse_motion() * 0.003
169 } else {
170 Vec2::ZERO
171 };
172 let p = ctx.world.resource_mut::<Player>();
173 p.yaw -= turn.x;
174 p.pitch = (p.pitch - turn.y).clamp(-1.5, 1.5);
175 let (id, yaw, pitch) = (p.id, p.yaw, p.pitch);
176 if let Some(t) = ctx.world.get::<Transform3D>(id) {
177 ctx.camera3d.position = t.position + Vec3::Y * 1.65;
178 ctx.camera3d.rotation = Quat::from_rotation_y(yaw) * Quat::from_rotation_x(pitch);
179 }
180}54fn move_crabs(ctx: &mut Ctx) {
55 let pads: Vec<Pad> = (0..ctx.net.players())
56 .map(|p| ctx.net.input::<Pad>(p).unwrap_or_default())
57 .collect();
58 let dt = ctx.dt();
59 let crabs: Vec<(Entity, Vec2, u32)> = ctx
60 .world
61 .query::<(Entity, &Transform2D, &Crab)>()
62 .iter()
63 .map(|(e, t, c)| (e, t.position, c.player))
64 .collect();
65 for &(e, at, player) in &crabs {
66 let pad = &pads[player as usize];
67 if pad.shove {
68 for &(other, other_at, _) in &crabs {
69 if other != e && at.distance(other_at) < 120.0 {
70 ctx.world.get_mut::<Crab>(other).unwrap().push += (other_at - at).normalize_or_zero() * 900.0;
71 }
72 }
73 }
74 let crab = *ctx.world.get::<Crab>(e).unwrap();
75 let t = ctx.world.get_mut::<Transform2D>(e).unwrap();
76 t.position += (pad.dir.normalize_or_zero() * 250.0 + crab.push) * dt;
77 t.position = t.position.clamp(vec2(40.0, 40.0), vec2(760.0, 560.0));
78 ctx.world.get_mut::<Crab>(e).unwrap().push = crab.push * 0.9;
79 }
80}Sourcepub fn get_mut<T>(&mut self, e: Entity) -> Option<&mut T>where
T: Component,
pub fn get_mut<T>(&mut self, e: Entity) -> Option<&mut T>where
T: Component,
Mutable version of get.
Examples found in repository?
83fn load_character(ctx: &mut Ctx) {
84 // ANCHOR: skin_load
85 let mesh = ctx.load_mesh("models/character.glb");
86 let skeleton = ctx.load_skeleton("models/character.glb");
87 let hand = skeleton.find("hand_r").expect("model needs hand_r");
88 let character = ctx.world.spawn((Transform3D::default(), Model::new(mesh), skeleton));
89 let skeleton = ctx.world.get_mut::<Skeleton>(character).unwrap();
90 skeleton.pose[hand].rotation = Quat::from_rotation_y(0.3);
91 let local_hand = skeleton.joint_matrix(hand);
92 let transform = ctx.world.get::<Transform3D>(character).unwrap();
93 let world_hand = (transform.matrix() * local_hand).transform_point3(Vec3::ZERO);
94 println!("Hand at {world_hand}");
95 // ANCHOR_END: skin_load
96}More examples
113fn walk(ctx: &mut Ctx) {
114 let keys = [Key::W, Key::A, Key::S, Key::D, Key::LeftShift].map(|k| ctx.key_down(k));
115 let jump = ctx.key_down(Key::Space);
116 let player = ctx.world.resource::<Player>();
117 let (id, yaw) = (player.id, player.yaw);
118 let Some(c) = ctx.world.get_mut::<Character3D>(id) else {
119 return;
120 };
121 let forward = vec3(-yaw.sin(), 0.0, -yaw.cos());
122 let right = vec3(yaw.cos(), 0.0, -yaw.sin());
123 let axis = |plus: bool, minus: bool| plus as i32 as f32 - minus as i32 as f32;
124 let wish = (forward * axis(keys[0], keys[2]) + right * axis(keys[3], keys[1])).normalize_or_zero();
125 let speed = if keys[4] { 8.0 } else { 4.0 };
126 c.velocity.x = wish.x * speed;
127 c.velocity.z = wish.z * speed;
128 if c.grounded && jump {
129 c.velocity.y = 5.0;
130 }
131}54fn move_crabs(ctx: &mut Ctx) {
55 let pads: Vec<Pad> = (0..ctx.net.players())
56 .map(|p| ctx.net.input::<Pad>(p).unwrap_or_default())
57 .collect();
58 let dt = ctx.dt();
59 let crabs: Vec<(Entity, Vec2, u32)> = ctx
60 .world
61 .query::<(Entity, &Transform2D, &Crab)>()
62 .iter()
63 .map(|(e, t, c)| (e, t.position, c.player))
64 .collect();
65 for &(e, at, player) in &crabs {
66 let pad = &pads[player as usize];
67 if pad.shove {
68 for &(other, other_at, _) in &crabs {
69 if other != e && at.distance(other_at) < 120.0 {
70 ctx.world.get_mut::<Crab>(other).unwrap().push += (other_at - at).normalize_or_zero() * 900.0;
71 }
72 }
73 }
74 let crab = *ctx.world.get::<Crab>(e).unwrap();
75 let t = ctx.world.get_mut::<Transform2D>(e).unwrap();
76 t.position += (pad.dir.normalize_or_zero() * 250.0 + crab.push) * dt;
77 t.position = t.position.clamp(vec2(40.0, 40.0), vec2(760.0, 560.0));
78 ctx.world.get_mut::<Crab>(e).unwrap().push = crab.push * 0.9;
79 }
80}64fn drive(ctx: &mut Ctx) {
65 let controls = *ctx.world.resource::<Controls>();
66 let dt = ctx.dt();
67 let car = ctx.world.resource::<Player>().car;
68 let forward = forward_speed(&ctx.world, car);
69 let wanted = controls.steer * 0.6 / (1.0 + forward.abs() * 0.08);
70 let player = ctx.world.resource_mut::<Player>();
71 player.steer += (wanted - player.steer).clamp(-2.0 * dt, 2.0 * dt);
72 let steer = player.steer;
73 let Some(vehicle) = ctx.world.get_mut::<Vehicle3D>(car) else {
74 return;
75 };
76 // Pressing against the way it rolls brakes; from (almost) still it drives.
77 let braking = controls.throttle * forward < 0.0 && forward.abs() > 0.5;
78 for (i, wheel) in vehicle.wheels_mut().iter_mut().enumerate() {
79 let front = i < 2;
80 wheel.steer_angle = if front { steer } else { 0.0 };
81 wheel.motor_torque = if front || braking {
82 0.0
83 } else {
84 controls.throttle * 900.0
85 };
86 wheel.brake_torque = if braking {
87 3000.0
88 } else if controls.handbrake && !front {
89 5000.0
90 } else {
91 0.0
92 };
93 }
94}
95// ANCHOR_END: drive
96
97fn speed_of(world: &World, car: Entity) -> f32 {
98 world.get::<Velocity3D>(car).map_or(0.0, |v| v.linear.length())
99}
100
101/// Speed along where the car faces (negative backing up).
102fn forward_speed(world: &World, car: Entity) -> f32 {
103 match (world.get::<Velocity3D>(car), world.get::<Transform3D>(car)) {
104 (Some(v), Some(t)) => v.linear.dot(t.forward()),
105 _ => 0.0,
106 }
107}
108
109/// Keys to controls (windowed), in the same tick as `drive`.
110fn keys(ctx: &mut Ctx) {
111 let axis = |plus: bool, minus: bool| plus as i32 as f32 - minus as i32 as f32;
112 let controls = Controls {
113 throttle: axis(ctx.key_down(Key::W), ctx.key_down(Key::S)),
114 steer: axis(ctx.key_down(Key::D), ctx.key_down(Key::A)),
115 handbrake: ctx.key_down(Key::Space),
116 };
117 *ctx.world.resource_mut::<Controls>() = controls;
118 if ctx.key_pressed(Key::R) {
119 let car = ctx.world.resource::<Player>().car;
120 if let Some(t) = ctx.world.get_mut::<Transform3D>(car) {
121 let p = t.position;
122 *t = Transform3D::at(vec3(p.x, height(p.x, p.z) + 1.5, p.z));
123 }
124 }
125}
126
127// ANCHOR: wheels
128/// Every frame: each wheel drawn where its car's last step put it (steer,
129/// spin and suspension travel), following the chassis's smoothed pose.
130fn place_wheels(ctx: &mut Ctx) {
131 let player = ctx.world.resource::<Player>();
132 let car = player.car;
133 let (Some(&vehicle), Some(&chassis)) = (ctx.world.get::<Vehicle3D>(car), ctx.world.get::<Transform3D>(car)) else {
134 return;
135 };
136 for i in 0..player.wheels.len() {
137 let e = ctx.world.resource::<Player>().wheels[i];
138 let pose = vehicle.wheels()[i].pose(&chassis);
139 if let Some(t) = ctx.world.get_mut::<Transform3D>(e) {
140 *t = Transform3D { scale: t.scale, ..pose };
141 }
142 }
143}
144// ANCHOR_END: wheels
145
146/// Every frame: the camera trails the car.
147fn chase(ctx: &mut Ctx) {
148 let car = ctx.world.resource::<Player>().car;
149 let Some(t) = ctx.world.get::<Transform3D>(car).copied() else {
150 return;
151 };
152 let back = -t.forward().with_y(0.0).normalize_or(Vec3::Z);
153 let eye = t.position + back * 9.0 + Vec3::Y * 3.5;
154 let k = (ctx.dt() * 4.0).min(1.0);
155 ctx.camera3d.position = ctx.camera3d.position.lerp(eye, k);
156 let look = Transform3D::at(ctx.camera3d.position).looking_at(t.position + Vec3::Y);
157 ctx.camera3d.rotation = look.rotation;
158}
159
160fn build_world(ctx: &mut Ctx, draw: bool) {
161 let corner = vec3(-SIZE / 2.0, 0.0, -SIZE / 2.0);
162 let step = SIZE / (N - 1) as f32;
163 let heights: Vec<f32> = (0..N * N)
164 .map(|i| height(corner.x + (i % N) as f32 * step, corner.z + (i / N) as f32 * step))
165 .collect();
166 let ground = ctx.physics3d.heightfield(&heights, SIZE);
167 let terrain = ctx
168 .world
169 .spawn((Transform3D::at(corner), RigidBody3D::fixed(), Collider3D::mesh(ground)));
170 let car = spawn_car(ctx, vec3(0.0, height(0.0, 0.0) + 1.5, 0.0));
171 ctx.world.insert_resource(Player {
172 car,
173 steer: 0.0,
174 wheels: Vec::new(),
175 });
176 ctx.world.insert_resource(Controls::default());
177 if !draw {
178 return;
179 }
180 let look = grass(ctx);
181 let tile = ctx.create_terrain_tile(&heights, SIZE, look);
182 ctx.world.insert(terrain, tile);
183 let paint = ctx.create_material(MaterialDesc::color(Color::hex(0xC8402E)));
184 let body = ctx.create_mesh(&MeshData::cube(1.0), paint);
185 if let Some(t) = ctx.world.get_mut::<Transform3D>(car) {
186 t.scale = vec3(1.8, 0.6, 4.2);
187 }
188 ctx.world.insert(car, Model::new(body));
189 let rubber = ctx.create_material(MaterialDesc::color(Color::hex(0x202020)));
190 let tyre = ctx.create_mesh(&MeshData::cube(1.0), rubber);
191 for _ in 0..4 {
192 let wheel = ctx.world.spawn((
193 Transform3D {
194 scale: vec3(0.3, 0.7, 0.7),
195 ..Transform3D::default()
196 },
197 Model::new(tyre),
198 ));
199 ctx.world.resource_mut::<Player>().wheels.push(wheel);
200 }
201}314fn resolve() -> impl FnMut(&mut Ctx) {
315 let mut hits: Vec<(Team, Hit)> = Vec::new();
316 move |ctx: &mut Ctx| {
317 hits.clear();
318 ctx.world.query::<&Unit>().for_each(|u| {
319 if let Some(hit) = u.hit {
320 hits.push((u.team, hit));
321 }
322 });
323 let battle = ctx.world.resource::<Battle>();
324 let bases = battle.base_entities;
325 for &(team, hit) in &hits {
326 match hit {
327 Hit::Unit(target) => {
328 if let Some(u) = ctx.world.get_mut::<Unit>(target) {
329 u.hp -= DAMAGE;
330 }
331 }
332 Hit::Base => {
333 if let Some(base) = ctx.world.get_mut::<Base>(bases[team.enemy().index()]) {
334 base.hp = (base.hp - DAMAGE).max(0.0);
335 }
336 }
337 }
338 }
339 let mut alive = [0; 2];
340 let (cmds, events) = (&mut ctx.commands, &mut ctx.events);
341 ctx.world
342 .query::<(Entity, &Transform2D, &Unit)>()
343 .for_each(|(e, t, u)| {
344 if u.hp <= 0.0 {
345 cmds.despawn(e);
346 events.send(Fallen {
347 team: u.team,
348 at: t.position,
349 });
350 } else {
351 alive[u.team.index()] += 1;
352 }
353 });
354 let mut fallen = None;
355 ctx.world.query::<&Base>().for_each(|b| {
356 if b.hp <= 0.0 {
357 fallen = Some(b.team);
358 }
359 });
360 let score = ctx.world.resource_mut::<Score>();
361 score.alive = alive;
362 if let (Some(team), None) = (fallen, score.winner) {
363 score.winner = Some(team.enemy());
364 score.rounds_won[team.enemy().index()] += 1;
365 }
366 }
367}215fn forage(ctx: &mut Ctx) {
216 let turn = ctx.turns(2);
217 let mut food_grid = SpatialGrid::new(8.0);
218 food_grid.rebuild_from(
219 ctx.world
220 .query::<(Entity, &Transform2D, &Food)>()
221 .iter()
222 .map(|(e, t, _)| (e, t.position)),
223 );
224 let grid = &food_grid;
225 ctx.world
226 .query::<(Entity, &Transform2D, &mut Ant)>()
227 .par_for_each(|(e, t, ant)| {
228 ant.event = Event::None;
229 if ant.energy <= 0.0 {
230 ant.event = Event::Starved;
231 return;
232 }
233 if !turn.includes(e) {
234 return;
235 }
236 let at_home = t.position.distance(ant.home_at) < NEST_RADIUS;
237 if ant.carrying {
238 if at_home {
239 ant.carrying = false;
240 ant.since = 0.0;
241 ant.energy = LIFESPAN;
242 ant.heading += std::f32::consts::PI;
243 ant.event = Event::Delivered;
244 }
245 } else if at_home {
246 ant.since = 0.0;
247 } else if let Some(f) = grid.nearest(t.position, 4.0, |_| true) {
248 ant.carrying = true;
249 ant.since = 0.0;
250 ant.heading += std::f32::consts::PI;
251 ant.event = Event::PickedUp(f.entity);
252 }
253 });
254
255 let (mut picked, mut delivered) = (Vec::new(), Vec::new());
256 let cmds = &mut ctx.commands;
257 ctx.world
258 .query::<(Entity, &Ant)>()
259 .for_each(|(e, ant)| match ant.event {
260 Event::None => {}
261 Event::Starved => cmds.despawn(e),
262 Event::PickedUp(food) => picked.push(food),
263 Event::Delivered => delivered.push(ant.home),
264 });
265 for f in picked {
266 if let Some(food) = ctx.world.get_mut::<Food>(f) {
267 food.amount = food.amount.saturating_sub(1);
268 if food.amount == 0 {
269 ctx.world.despawn(f);
270 }
271 }
272 }
273 for nest in delivered {
274 if let Some(n) = ctx.world.get_mut::<Nest>(nest) {
275 n.stored += 1;
276 n.delivered += 1;
277 }
278 }
279}Sourcepub fn has<T>(&self, e: Entity) -> boolwhere
T: Component,
pub fn has<T>(&self, e: Entity) -> boolwhere
T: Component,
true if e is alive and has component T.
Sourcepub fn insert<T>(&mut self, e: Entity, value: T) -> boolwhere
T: Component,
pub fn insert<T>(&mut self, e: Entity, value: T) -> boolwhere
T: Component,
Adds component value to e, replacing any existing T.
Returns false (and drops value) if e is dead.
Examples found in repository?
160fn build_world(ctx: &mut Ctx, draw: bool) {
161 let corner = vec3(-SIZE / 2.0, 0.0, -SIZE / 2.0);
162 let step = SIZE / (N - 1) as f32;
163 let heights: Vec<f32> = (0..N * N)
164 .map(|i| height(corner.x + (i % N) as f32 * step, corner.z + (i / N) as f32 * step))
165 .collect();
166 let ground = ctx.physics3d.heightfield(&heights, SIZE);
167 let terrain = ctx
168 .world
169 .spawn((Transform3D::at(corner), RigidBody3D::fixed(), Collider3D::mesh(ground)));
170 let car = spawn_car(ctx, vec3(0.0, height(0.0, 0.0) + 1.5, 0.0));
171 ctx.world.insert_resource(Player {
172 car,
173 steer: 0.0,
174 wheels: Vec::new(),
175 });
176 ctx.world.insert_resource(Controls::default());
177 if !draw {
178 return;
179 }
180 let look = grass(ctx);
181 let tile = ctx.create_terrain_tile(&heights, SIZE, look);
182 ctx.world.insert(terrain, tile);
183 let paint = ctx.create_material(MaterialDesc::color(Color::hex(0xC8402E)));
184 let body = ctx.create_mesh(&MeshData::cube(1.0), paint);
185 if let Some(t) = ctx.world.get_mut::<Transform3D>(car) {
186 t.scale = vec3(1.8, 0.6, 4.2);
187 }
188 ctx.world.insert(car, Model::new(body));
189 let rubber = ctx.create_material(MaterialDesc::color(Color::hex(0x202020)));
190 let tyre = ctx.create_mesh(&MeshData::cube(1.0), rubber);
191 for _ in 0..4 {
192 let wheel = ctx.world.spawn((
193 Transform3D {
194 scale: vec3(0.3, 0.7, 0.7),
195 ..Transform3D::default()
196 },
197 Model::new(tyre),
198 ));
199 ctx.world.resource_mut::<Player>().wheels.push(wheel);
200 }
201}More examples
202fn main() {
203 Game::new("Crabby: skinned meshes", 1000, 700).system(sway).run(|ctx| {
204 ctx.clear(Color::hex(0xABCBDD));
205 ctx.camera3d.position = vec3(6.0, 4.0, 9.0);
206 ctx.camera3d.look_at(vec3(0.0, 1.7, 0.0));
207 let green = ctx.create_material(MaterialDesc::color(Color::hex(0x398F79)));
208 let gold = ctx.create_material(MaterialDesc::color(Color::hex(0xF2BD50)));
209 let white = ctx.default_material();
210 let ground = ctx.create_mesh(&MeshData::plane(20.0), white);
211 ctx.world.spawn((Transform3D::default(), Model::new(ground)));
212 // ANCHOR: skin_create
213 let skeleton = chain();
214 let mesh = ctx.create_skinned_mesh(&tube(0.0, 3.5, 0.22), green, &skeleton);
215 // A separate skeleton with matching names demonstrates name binding.
216 let band = ctx.create_skinned_mesh(&tube(2.2, 2.5, 0.25), gold, &chain());
217 // ANCHOR_END: skin_create
218 let ball = ctx.create_mesh(&MeshData::sphere(0.18, 16), gold);
219 let bend = bend_clip(&skeleton);
220 for i in 0..3 {
221 let tentacle = ctx.world.spawn((
222 Transform3D::at(vec3((i as f32 - 1.0) * 2.5, 0.0, 0.0)),
223 Model::new(mesh),
224 skeleton.clone(),
225 Sway(i as f32 * 1.8),
226 Bend(bend.clone()),
227 ));
228 if i == 2 {
229 ctx.world.insert(tentacle, bend_animator(&skeleton));
230 }
231 if i == 0 {
232 // ANCHOR: skin_parts
233 ctx.world.spawn((Model::new(band), SkinOf(tentacle)));
234 let tip = skeleton.find("joint_7").unwrap();
235 ctx.world.spawn((
236 Transform3D::default(),
237 Model::new(ball),
238 AttachToJoint {
239 skeleton: tentacle,
240 joint: tip,
241 offset: Transform3D::at(vec3(0.0, 0.12, 0.0)),
242 },
243 ));
244 // ANCHOR_END: skin_parts
245 }
246 }
247 });
248}Sourcepub fn remove<T>(&mut self, e: Entity) -> Option<T>where
T: Component,
pub fn remove<T>(&mut self, e: Entity) -> Option<T>where
T: Component,
Takes component T off e and returns it.
Sourcepub fn query<Q>(&mut self) -> Query<'_, Q>where
Q: QueryData,
pub fn query<Q>(&mut self) -> Query<'_, Q>where
Q: QueryData,
Loops over every entity that has the components in Q.
Q is &T, &mut T, Option<&T>, Option<&mut T>, Entity, or a
tuple of those. See Query for filters and iterators.
§Panics
If Q asks for the same component twice and at least once mutably.
Examples found in repository?
16fn spin(ctx: &mut Ctx) {
17 let dt = ctx.dt();
18 for t in ctx.world.query::<&mut Transform2D>().iter() {
19 t.rotation += -6.0 * dt;
20 }
21}
22// ANCHOR_END: spin
23
24/// Crabs breathe. Try a bigger amplitude, or a different shape.
25fn wobble(ctx: &mut Ctx) {
26 let time = ctx.sim_time() as f32;
27 for (t, crab) in ctx.world.query::<(&mut Transform2D, &Crab)>().iter() {
28 let s = 1.0 + 0.15 * (time * 3.0 + crab.0).sin();
29 t.scale = vec2(s, s);
30 }
31}
32
33/// Prints the first crab's rotation once a second, so you can see that
34/// state carries over a patch.
35fn report(ctx: &mut Ctx) {
36 let tick = ctx.sim_tick();
37 if tick.is_multiple_of(60)
38 && let Some(t) = ctx.world.query::<&Transform2D>().iter().next()
39 {
40 println!("tick {tick:>5} rotation {:>7.2}", t.rotation);
41 }
42}More examples
117fn wander(ctx: &mut Ctx) {
118 let dt = ctx.dt();
119 ctx.world
120 .query::<(&mut Transform2D, &Wander)>()
121 .par_for_each(|(t, w)| t.position += w.0 * dt);
122}
123
124// ANCHOR: paint
125fn paint(ctx: &mut Ctx) {
126 if !ctx.mouse_down(MouseButton::Left) || ctx.mouse_over_ui() {
127 return;
128 }
129 let at = ctx.mouse_world();
130 ctx.world.query::<(&Transform2D, &mut TileMap)>().for_each(|(t, map)| {
131 if let Some(tile) = map.tile_at(t.position, at) {
132 map.set(tile.x, tile.y, Some(STONE));
133 }
134 });
135}59fn ring_the_bell(ctx: &mut Ctx) {
60 let mut rang = false;
61 for c in ctx.events.read::<CollisionStarted>() {
62 rang |= c.sensor && [c.a, c.b].iter().any(|&e| ctx.world.has::<Bell>(e));
63 }
64 let dt = ctx.dt();
65 for bell in ctx.world.query::<&mut Bell>().iter() {
66 bell.rang = if rang { 1.0 } else { (bell.rang - dt * 2.0).max(0.0) };
67 }
68}
69// ANCHOR_END: collisions
70
71// ANCHOR: queries
72fn blast(ctx: &mut Ctx) {
73 if !ctx.key_pressed(Key::Space) {
74 return;
75 }
76 let center = ctx.mouse_world();
77 let mut near = Vec::new();
78 ctx.physics.entities_in_circle(center, 200.0, &mut near);
79 for e in near {
80 let Some(t) = ctx.world.get::<Transform2D>(e) else {
81 continue;
82 };
83 let away = (t.position - center).normalize_or(Vec2::NEG_Y);
84 if let Some(mass) = ctx.physics.mass(e) {
85 ctx.physics.apply_impulse(e, away * mass * 600.0);
86 }
87 }
88}
89
90/// What's under the mouse, straight down: a laser from the cursor.
91fn laser(ctx: &mut Ctx) -> Option<RayHit> {
92 ctx.physics.cast_ray(ctx.mouse_world(), vec2(0.0, 1.0), H)
93}
94// ANCHOR_END: queries
95
96fn drop_things(ctx: &mut Ctx) {
97 let at = ctx.mouse_world();
98 if ctx.mouse_pressed(MouseButton::Left) {
99 ctx.world.spawn((
100 Transform2D::at(at),
101 RigidBody::dynamic(),
102 Collider::rect(vec2(30.0, 30.0)).friction(0.8),
103 ));
104 }
105 if ctx.mouse_pressed(MouseButton::Right) {
106 ctx.world.spawn((
107 Transform2D::at(at),
108 RigidBody::dynamic(),
109 Collider::circle(12.0).restitution(0.6),
110 ));
111 }
112 if ctx.mouse_pressed(MouseButton::Middle) {
113 let rock: Vec<Vec2> = (0..5)
114 .map(|i| Vec2::from_angle(i as f32 * std::f32::consts::TAU / 5.0) * 18.0)
115 .collect();
116 ctx.world.spawn((
117 Transform2D::at(at),
118 RigidBody::dynamic(),
119 Collider::polygon(&rock).friction(0.7),
120 ));
121 }
122 if ctx.key_pressed(Key::R) {
123 let all: Vec<Entity> = ctx.world.query::<Entity>().with::<RigidBody>().iter().collect();
124 for e in all {
125 ctx.world.despawn(e);
126 }
127 build_level(ctx);
128 }
129 if ctx.key_pressed(Key::Escape) {
130 ctx.quit();
131 }
132}
133
134fn draw(ctx: &mut Ctx) {
135 ctx.clear(Color::hex(0x1d2330));
136 let shapes: Vec<(Transform2D, Collider, bool, f32)> = ctx
137 .world
138 .query::<(&Transform2D, &Collider, &RigidBody, Option<&Bell>)>()
139 .iter()
140 .map(|(t, c, b, bell)| (*t, *c, b.kind == BodyKind::Fixed, bell.map_or(0.0, |b| b.rang)))
141 .collect();
142 for (t, c, fixed, rang) in shapes {
143 let color = if c.sensor {
144 Color::rgba(1.0, 0.3 + 0.7 * (1.0 - rang), 0.3, 0.35)
145 } else if fixed {
146 Color::hex(0x4a5568)
147 } else {
148 Color::hex(0xf6ad55)
149 };
150 match c.shape {
151 Shape::Circle { radius } => {
152 ctx.draw_circle(t.position, radius, color);
153 // A spoke shows the spin.
154 let spoke = t.position + Vec2::from_angle(t.rotation) * radius;
155 ctx.draw_line(t.position, spoke, 2.0, Color::hex(0x1d2330));
156 }
157 Shape::Rect { size } => {
158 // Four corners turned with the body, drawn as edges.
159 let (sin, cos) = t.rotation.sin_cos();
160 let turn = |v: Vec2| t.position + vec2(v.x * cos - v.y * sin, v.x * sin + v.y * cos);
161 let h = size / 2.0;
162 let corners = [vec2(-h.x, -h.y), vec2(h.x, -h.y), vec2(h.x, h.y), vec2(-h.x, h.y)].map(turn);
163 if fixed || c.sensor {
164 ctx.draw_rect(Rect::new(t.position.x - h.x, t.position.y - h.y, size.x, size.y), color);
165 } else {
166 for i in 0..4 {
167 ctx.draw_line(corners[i], corners[(i + 1) % 4], 3.0, color);
168 }
169 }
170 }
171 Shape::Capsule { height, radius } => {
172 let up = Vec2::from_angle(t.rotation + std::f32::consts::FRAC_PI_2) * height / 2.0;
173 ctx.draw_line(t.position - up, t.position + up, radius * 2.0, color);
174 }
175 Shape::Polygon { .. } => {
176 let (sin, cos) = t.rotation.sin_cos();
177 let turn = |v: Vec2| t.position + vec2(v.x * cos - v.y * sin, v.x * sin + v.y * cos);
178 let corners: Vec<Vec2> = c.shape.points().iter().map(|&p| turn(p + c.offset)).collect();
179 for i in 0..corners.len() {
180 ctx.draw_line(corners[i], corners[(i + 1) % corners.len()], 3.0, color);
181 }
182 }
183 _ => {} // shapes added in later versions
184 }
185 }
186 if let Some(hit) = laser(ctx) {
187 ctx.draw_line(ctx.mouse_world(), hit.point, 1.0, Color::rgba(1.0, 0.2, 0.2, 0.8));
188 ctx.draw_circle(hit.point, 4.0, Color::rgb(1.0, 0.2, 0.2));
189 }
190 let bodies = ctx.physics.body_count();
191 ctx.draw_text(
192 &format!(
193 "{bodies} bodies | left click: crate right click: ball middle click: rock space: blast R: reset"
194 ),
195 vec2(30.0, 30.0),
196 18.0,
197 Color::WHITE,
198 );
199}17fn extent(world: &mut World) -> (Vec3, f32) {
18 let (mut min, mut max) = (Vec3::splat(f32::MAX), Vec3::splat(f32::MIN));
19 world.query::<(&Transform3D, &Model)>().for_each(|(t, _)| {
20 min = min.min(t.position);
21 max = max.max(t.position);
22 });
23 if min.x > max.x {
24 return (Vec3::ZERO, 10.0);
25 }
26 ((min + max) * 0.5, (max - min).max_element().max(10.0))
27}
28
29fn camera_from_env() -> Option<(Vec3, f32, f32)> {
30 let v: Vec<f32> = std::env::var("CAMERA")
31 .ok()?
32 .split(',')
33 .map(|n| n.trim().parse().ok())
34 .collect::<Option<_>>()?;
35 let [x, y, z, yaw, pitch] = v[..] else { return None };
36 Some((vec3(x, y, z), yaw.to_radians(), pitch.to_radians()))
37}
38
39fn main() {
40 let scene = std::env::var("SCENE").unwrap_or_else(|_| {
41 eprintln!("say which scene: SCENE=town/Town.json (a file under assets/)");
42 std::process::exit(2);
43 });
44 if std::env::var("HEADLESS").is_ok() {
45 Game::new("scene viewer", 1, 1).run_headless(0, |ctx| {
46 let start = Instant::now();
47 ctx.load_scene(&scene).unwrap_or_else(|e| panic!("{e}"));
48 let models = ctx.world.query::<&Model>().iter().count();
49 let (middle, size) = extent(&mut ctx.world);
50 println!(
51 "{scene}: {models} models in {:.2} s, around {middle} across {size:.0} m",
52 start.elapsed().as_secs_f32()
53 );
54 });
55 return;
56 }
57 let shot = std::env::var("SHOT").ok();
58 let (mut yaw, mut pitch) = (0.0f32, -0.3f32);
59 Game::new("Crabby Engine: scene viewer", 1280, 720)
60 .system(move |ctx: &mut Ctx| {
61 if ctx.key_pressed(Key::Escape) {
62 ctx.quit();
63 }
64 let dt = ctx.dt();
65 if ctx.frame() == 1 {
66 let (y, x, _) = ctx.camera3d.rotation.to_euler(glam::EulerRot::YXZ);
67 (yaw, pitch) = (y, x);
68 }
69 let turn = 1.2 * dt;
70 if ctx.key_down(Key::Left) {
71 yaw += turn;
72 }
73 if ctx.key_down(Key::Right) {
74 yaw -= turn;
75 }
76 if ctx.key_down(Key::Up) {
77 pitch = (pitch + turn).min(1.5);
78 }
79 if ctx.key_down(Key::Down) {
80 pitch = (pitch - turn).max(-1.5);
81 }
82 let rotation = Quat::from_rotation_y(yaw) * Quat::from_rotation_x(pitch);
83 let speed = if ctx.key_down(Key::LeftShift) { 120.0 } else { 20.0 } * dt;
84 let mut go = Vec3::ZERO;
85 for (key, way) in [
86 (Key::W, Vec3::NEG_Z),
87 (Key::S, Vec3::Z),
88 (Key::A, Vec3::NEG_X),
89 (Key::D, Vec3::X),
90 ] {
91 if ctx.key_down(key) {
92 go += rotation * way;
93 }
94 }
95 if ctx.key_down(Key::E) {
96 go += Vec3::Y;
97 }
98 if ctx.key_down(Key::Q) {
99 go -= Vec3::Y;
100 }
101 ctx.camera3d.position += go * speed;
102 ctx.camera3d.rotation = rotation;
103 if ctx.frame() == 300
104 && let Some(path) = &shot
105 {
106 ctx.save_screenshot(path);
107 }
108 if ctx.frame() == 302 && shot.is_some() {
109 ctx.quit();
110 }
111 })
112 .run(move |ctx| {
113 ctx.load_scene(&scene).unwrap_or_else(|e| panic!("{e}"));
114 ctx.lighting.sun_direction = Sky::sun_direction_at(15.0);
115 ctx.lighting.shadow_distance = 400.0;
116 ctx.camera3d.far = 3000.0;
117 ctx.post.bloom = 0.05;
118 let (middle, size) = extent(&mut ctx.world);
119 let (at, yaw, pitch) =
120 camera_from_env().unwrap_or((middle + vec3(0.0, size * 0.25, size * 0.4), 0.0, -0.5));
121 ctx.camera3d.position = at;
122 ctx.camera3d.rotation = Quat::from_rotation_y(yaw) * Quat::from_rotation_x(pitch);
123 });
124}Sourcepub fn sort_by<T>(&mut self, key: impl Fn(&T) -> u64 + Sync)where
T: Component,
pub fn sort_by<T>(&mut self, key: impl Fn(&T) -> u64 + Sync)where
T: Component,
Reorders entities in memory so that, within each archetype that has
component T, rows are sorted by key(&T) (ascending; ties keep no
particular order). Handles and component values don’t change, only
the order queries visit them in.
Use it to put entities that are processed together next to each other: sorting by a spatial cell key every few dozen ticks makes neighbor queries hit the CPU cache instead of main memory.
struct Pos(f32);
let mut world = World::new();
for x in [3.0, 1.0, 2.0] {
world.spawn((Pos(x),));
}
world.sort_by::<Pos>(|p| p.0 as u64);
let order: Vec<f32> = world.query::<&Pos>().iter().map(|p| p.0).collect();
assert_eq!(order, [1.0, 2.0, 3.0]);Examples found in repository?
More examples
27fn flocking(bounds: Vec2) -> impl FnMut(&mut Ctx) {
28 let mut grid = SpatialGrid::with_data(RADIUS);
29 move |ctx: &mut Ctx| {
30 let dt = ctx.dt();
31 // Keep boids that are close in space close in memory, so neighbor
32 // lookups hit the cache (1M boids: 62 -> 40 ms/tick).
33 if ctx.sim_tick().is_multiple_of(SPATIAL_SORT_EVERY) {
34 ctx.world.sort_by::<Transform2D>(|t| spatial_key(t.position, RADIUS));
35 }
36 grid.rebuild_with_data(
37 ctx.world
38 .query::<(Entity, &Transform2D, &Vel)>()
39 .iter()
40 .map(|(e, t, v)| (e, t.position, v.0)),
41 );
42 let grid = &grid;
43 ctx.world
44 .query::<(Entity, &mut Transform2D, &mut Vel)>()
45 .par_for_each(|(me, t, v)| {
46 let (mut away, mut heading, mut center, mut n) = (Vec2::ZERO, Vec2::ZERO, Vec2::ZERO, 0.0);
47 grid.for_each_in_radius(t.position, RADIUS, |o| {
48 if o.entity == me {
49 return;
50 }
51 let d = t.position - o.position;
52 away += d / d.length_squared().max(1.0);
53 heading += o.data;
54 center += o.position;
55 n += 1.0;
56 });
57 let mut steer = away * 900.0;
58 if n > 0.0 {
59 steer += (heading / n - v.0) * 1.5 + (center / n - t.position) * 1.2;
60 }
61 let speed = (v.0 + steer * dt).length().clamp(MIN_SPEED, MAX_SPEED);
62 v.0 = (v.0 + steer * dt).normalize_or(Vec2::X) * speed;
63 t.position = (t.position + v.0 * dt).rem_euclid(bounds);
64 t.rotation = v.0.to_angle();
65 });
66 }
67}Sourcepub fn insert_resource<R>(&mut self, value: R)
pub fn insert_resource<R>(&mut self, value: R)
Stores a global value (game settings, a random generator, the score…), replacing any existing value of the same type.
Examples found in repository?
105fn main() {
106 Game::new("Crabby Engine: camera, shapes, text, UI", 1280, 720)
107 .system(camera_controls)
108 .system(scene)
109 .system(hud)
110 .run(|ctx| {
111 let crab = ctx.load_texture("crab.png");
112 let plop = plop(ctx);
113 ctx.world.insert_resource(Scene {
114 crab,
115 plop,
116 follow: false,
117 dropped: 0,
118 });
119 });
120}More examples
136fn main() {
137 let start_count = env_number("CRABS", 10_000.0) as usize;
138 let quit_after = env_number("SECONDS", 0.0);
139
140 Game::new("Crabby Engine: bunnymark (ECS)", 1280, 720)
141 .vsync(false)
142 .system(bounce)
143 .system(report)
144 .run(move |ctx| {
145 let crab = ctx.load_texture("crab.png");
146 ctx.world.insert_resource(Bench {
147 crab,
148 rng: Rng(0x9E37_79B9_7F4A_7C15),
149 report_at: 1.0,
150 frames: 0,
151 worst_ms: 0.0,
152 frame_start: Instant::now(),
153 quit_after,
154 scale: env_number("SCALE", 1.0) as f32,
155 });
156 spawn(ctx, start_count, vec2(20.0, 20.0));
157 });
158}12fn main() {
13 // ANCHOR: query
14 let nav = navigation();
15 let start = nav.sample_position(vec3(-8.0, 1.0, 0.0), 2.0).unwrap();
16 let destination = vec3(8.0, 0.0, 0.0);
17 let corners = nav.find_path(start, destination).unwrap();
18 assert!(corners.len() >= 3);
19 // ANCHOR_END: query
20
21 // ANCHOR: agents
22 let mut world = Game::new("Navigation server", 1, 1)
23 .fixed_hz(60)
24 .sim_system(step_navmesh3d)
25 .run_headless(600, |ctx| {
26 ctx.world.insert_resource(nav);
27 let mut agent = NavAgent3D::default();
28 agent.destination = Some(destination);
29 ctx.world.spawn((Transform3D::at(start), agent));
30 // World-space AABB; move its center or despawn it to reopen polygons.
31 ctx.world
32 .spawn((NavObstacle3D::new(vec3(0.0, 1.0, 8.0), vec3(1.0, 1.0, 1.0)),));
33 });
34 let position = world.query::<&Transform3D>().iter().next().unwrap().position;
35 assert!(position.distance(destination) < 0.3);
36 // ANCHOR_END: agents
37}96fn steer(ctx: &mut Ctx) {
97 let dt = ctx.dt();
98 let random = ctx.random();
99 // Taken out for the query, which borrows the world, then put back.
100 let paths = ctx.world.remove_resource::<Paths>().expect("paths");
101 ctx.world
102 .query::<(Entity, &mut Transform2D, &mut Agent)>()
103 .par_for_each(|(e, t, a)| {
104 let mut rng = random.of(e);
105 let cell = paths.nav.cell_at(t.position);
106 let slow = cell.and_then(|c| paths.nav.cost(c.x, c.y)).unwrap_or(1).max(1) as f32;
107 let want = paths.flow.direction_at(t.position) * (SPEED / slow);
108 a.vel = a.vel.lerp(want, 0.15) + rng.unit_vec2() * 3.0;
109 // Slide along walls instead of walking into them.
110 let next = t.position + a.vel * dt;
111 if paths.nav.walkable(next) {
112 t.position = next;
113 } else if paths.nav.walkable(vec2(next.x, t.position.y)) {
114 t.position.x = next.x;
115 } else if paths.nav.walkable(vec2(t.position.x, next.y)) {
116 t.position.y = next.y;
117 }
118 });
119 ctx.world.insert_resource(paths);
120}
121// ANCHOR_END: steer
122
123fn setup(ctx: &mut Ctx, agents: usize, texture: Option<Texture>) {
124 let seed = 7;
125 let nav = make_map(seed);
126 let mut rng = Rng::new(seed ^ 1);
127 let goal = walkable_point(&nav, &mut rng);
128 let mut paths = Paths {
129 nav,
130 flow: FlowField::new(),
131 goal,
132 compute_ms: 0.0,
133 };
134 paths.set_goal(goal);
135 ctx.world.reflect::<Agent>();
136 let starts: Vec<Vec2> = (0..agents).map(|_| walkable_point(&paths.nav, &mut rng)).collect();
137 ctx.world.insert_resource(paths);
138 match texture {
139 Some(tex) => ctx.world.spawn_batch(starts.into_iter().map(|p| {
140 (
141 Transform2D::at(p),
142 Sprite::new(tex).with_color(Color::rgb(1.0, 0.85, 0.4)),
143 Agent { vel: Vec2::ZERO },
144 )
145 })),
146 None => ctx.world.spawn_batch(
147 starts
148 .into_iter()
149 .map(|p| (Transform2D::at(p), Agent { vel: Vec2::ZERO })),
150 ),
151 };
152}104fn main() {
105 Game::new("Crabby Engine: ECS", 1280, 720)
106 .sim_system(movement)
107 .sim_system(aging)
108 .system(controls)
109 .run(|ctx| {
110 let crab = ctx.load_texture("crab.png");
111 ctx.world.insert_resource(Settings {
112 rng: 0x9E37_79B9_7F4A_7C15,
113 crab,
114 });
115 let center = ctx.window_size() / 2.0;
116 for _ in 0..500 {
117 spawn_crab(ctx, center);
118 }
119
120 // Optional per-frame closure: show stats in the title bar once a second.
121 let mut next_report = 1.0;
122 move |ctx: &mut Ctx| {
123 if ctx.time() >= next_report {
124 next_report += 1.0;
125 let sim = if ctx.is_paused() {
126 "paused".to_string()
127 } else {
128 format!("{}x", ctx.sim_speed())
129 };
130 println!(
131 "crabs: {:>6} fps: {:>5.1} sim: {sim:>6} tick: {}",
132 ctx.world.len(),
133 ctx.fps(),
134 ctx.sim_tick()
135 );
136 }
137 }
138 });
139}160fn build_world(ctx: &mut Ctx, draw: bool) {
161 let corner = vec3(-SIZE / 2.0, 0.0, -SIZE / 2.0);
162 let step = SIZE / (N - 1) as f32;
163 let heights: Vec<f32> = (0..N * N)
164 .map(|i| height(corner.x + (i % N) as f32 * step, corner.z + (i / N) as f32 * step))
165 .collect();
166 let ground = ctx.physics3d.heightfield(&heights, SIZE);
167 let terrain = ctx
168 .world
169 .spawn((Transform3D::at(corner), RigidBody3D::fixed(), Collider3D::mesh(ground)));
170 let car = spawn_car(ctx, vec3(0.0, height(0.0, 0.0) + 1.5, 0.0));
171 ctx.world.insert_resource(Player {
172 car,
173 steer: 0.0,
174 wheels: Vec::new(),
175 });
176 ctx.world.insert_resource(Controls::default());
177 if !draw {
178 return;
179 }
180 let look = grass(ctx);
181 let tile = ctx.create_terrain_tile(&heights, SIZE, look);
182 ctx.world.insert(terrain, tile);
183 let paint = ctx.create_material(MaterialDesc::color(Color::hex(0xC8402E)));
184 let body = ctx.create_mesh(&MeshData::cube(1.0), paint);
185 if let Some(t) = ctx.world.get_mut::<Transform3D>(car) {
186 t.scale = vec3(1.8, 0.6, 4.2);
187 }
188 ctx.world.insert(car, Model::new(body));
189 let rubber = ctx.create_material(MaterialDesc::color(Color::hex(0x202020)));
190 let tyre = ctx.create_mesh(&MeshData::cube(1.0), rubber);
191 for _ in 0..4 {
192 let wheel = ctx.world.spawn((
193 Transform3D {
194 scale: vec3(0.3, 0.7, 0.7),
195 ..Transform3D::default()
196 },
197 Model::new(tyre),
198 ));
199 ctx.world.resource_mut::<Player>().wheels.push(wheel);
200 }
201}
202
203/// A one-layer terrain material: grass everywhere.
204fn grass(ctx: &mut Ctx) -> Material {
205 let green: Vec<u8> = (0..16 * 16)
206 .flat_map(|i| {
207 let shade = 200 + (i * 37 % 56) as u8;
208 [
209 (70 * shade as u32 / 255) as u8,
210 (120 * shade as u32 / 255) as u8,
211 45,
212 255,
213 ]
214 })
215 .collect();
216 let layers = ctx.create_texture_array(16, 16, &[&green], TextureKind::Color);
217 let weights = ctx.create_texture_array(1, 1, &[&[255, 0, 0, 0]], TextureKind::Data);
218 let corner = vec2(-SIZE / 2.0, -SIZE / 2.0);
219 ctx.create_terrain_material(&TerrainMaterialDesc::new(layers, weights, corner, vec2(SIZE, SIZE)))
220}
221
222/// The script for `HEADLESS=1`: full throttle for 4 s, then steer right
223/// for 2 s, then brake to a stop and hold it with the handbrake.
224fn autopilot(ctx: &mut Ctx) {
225 let t = ctx.sim_tick() as f32 * ctx.dt();
226 let car = ctx.world.resource::<Player>().car;
227 let rolling = forward_speed(&ctx.world, car) > 0.3;
228 *ctx.world.resource_mut::<Controls>() = Controls {
229 throttle: if t < 6.0 {
230 1.0
231 } else if rolling {
232 -1.0
233 } else {
234 0.0
235 },
236 steer: if (4.0..6.0).contains(&t) { 1.0 } else { 0.0 },
237 handbrake: t >= 6.0 && !rolling,
238 };
239}
240
241/// What the car did, second by second.
242#[derive(Default)]
243struct Log(Vec<(f32, Vec3)>);
244
245fn record(ctx: &mut Ctx) {
246 if ctx.sim_tick().is_multiple_of(50) {
247 let car = ctx.world.resource::<Player>().car;
248 let speed = speed_of(&ctx.world, car);
249 let facing = ctx.world.get::<Transform3D>(car).map_or(Vec3::NEG_Z, |t| t.forward());
250 ctx.world.resource_mut::<Log>().0.push((speed, facing));
251 }
252}
253
254/// Runs the script and checks what the car did.
255fn headless() {
256 let world = Game::new("vehicle", 1, 1)
257 .sim_system(autopilot)
258 .sim_system(drive)
259 .physics3d(vec3(0.0, -9.81, 0.0))
260 .sim_system(record)
261 .run_headless(500, |ctx| {
262 build_world(ctx, false);
263 ctx.world.insert_resource(Log::default());
264 });
265 let log = &world.resource::<Log>().0;
266 for (second, (speed, facing)) in log.iter().enumerate() {
267 println!("{:2} s: {speed:5.2} m/s, facing {facing:.2}", second + 1);
268 }
269 let (accelerated, _) = log[3];
270 let (_, turned) = log[5];
271 let (stopped, _) = log[log.len() - 1];
272 assert!(accelerated > 8.0, "accelerates: {accelerated} m/s after 4 s");
273 assert!(turned.x > 0.3, "steers right: facing {turned}");
274 assert!(stopped < 0.5, "stops: {stopped} m/s");
275 println!("accelerates, steers and stops");
276}Sourcepub fn resource<R>(&self) -> &R
pub fn resource<R>(&self) -> &R
Examples found in repository?
113fn walk(ctx: &mut Ctx) {
114 let keys = [Key::W, Key::A, Key::S, Key::D, Key::LeftShift].map(|k| ctx.key_down(k));
115 let jump = ctx.key_down(Key::Space);
116 let player = ctx.world.resource::<Player>();
117 let (id, yaw) = (player.id, player.yaw);
118 let Some(c) = ctx.world.get_mut::<Character3D>(id) else {
119 return;
120 };
121 let forward = vec3(-yaw.sin(), 0.0, -yaw.cos());
122 let right = vec3(yaw.cos(), 0.0, -yaw.sin());
123 let axis = |plus: bool, minus: bool| plus as i32 as f32 - minus as i32 as f32;
124 let wish = (forward * axis(keys[0], keys[2]) + right * axis(keys[3], keys[1])).normalize_or_zero();
125 let speed = if keys[4] { 8.0 } else { 4.0 };
126 c.velocity.x = wish.x * speed;
127 c.velocity.z = wish.z * speed;
128 if c.grounded && jump {
129 c.velocity.y = 5.0;
130 }
131}
132// ANCHOR_END: walk
133
134// ANCHOR: queries
135/// Left click shoves whatever is in front of the eye, up to 30 m away.
136fn shove(ctx: &mut Ctx) {
137 let me = ctx.world.resource::<Player>().id;
138 let (eye, forward) = (ctx.camera3d.position, ctx.camera3d.rotation * Vec3::NEG_Z);
139 let filter = QueryFilter3D::default().exclude(me);
140 if let Some(hit) = ctx.physics3d.cast_ray(eye, forward, 30.0, filter)
141 && let Some(mass) = ctx.physics3d.mass(hit.entity)
142 {
143 ctx.physics3d
144 .apply_impulse_at(hit.entity, forward * mass * 6.0, hit.point);
145 }
146}More examples
64fn drive(ctx: &mut Ctx) {
65 let controls = *ctx.world.resource::<Controls>();
66 let dt = ctx.dt();
67 let car = ctx.world.resource::<Player>().car;
68 let forward = forward_speed(&ctx.world, car);
69 let wanted = controls.steer * 0.6 / (1.0 + forward.abs() * 0.08);
70 let player = ctx.world.resource_mut::<Player>();
71 player.steer += (wanted - player.steer).clamp(-2.0 * dt, 2.0 * dt);
72 let steer = player.steer;
73 let Some(vehicle) = ctx.world.get_mut::<Vehicle3D>(car) else {
74 return;
75 };
76 // Pressing against the way it rolls brakes; from (almost) still it drives.
77 let braking = controls.throttle * forward < 0.0 && forward.abs() > 0.5;
78 for (i, wheel) in vehicle.wheels_mut().iter_mut().enumerate() {
79 let front = i < 2;
80 wheel.steer_angle = if front { steer } else { 0.0 };
81 wheel.motor_torque = if front || braking {
82 0.0
83 } else {
84 controls.throttle * 900.0
85 };
86 wheel.brake_torque = if braking {
87 3000.0
88 } else if controls.handbrake && !front {
89 5000.0
90 } else {
91 0.0
92 };
93 }
94}
95// ANCHOR_END: drive
96
97fn speed_of(world: &World, car: Entity) -> f32 {
98 world.get::<Velocity3D>(car).map_or(0.0, |v| v.linear.length())
99}
100
101/// Speed along where the car faces (negative backing up).
102fn forward_speed(world: &World, car: Entity) -> f32 {
103 match (world.get::<Velocity3D>(car), world.get::<Transform3D>(car)) {
104 (Some(v), Some(t)) => v.linear.dot(t.forward()),
105 _ => 0.0,
106 }
107}
108
109/// Keys to controls (windowed), in the same tick as `drive`.
110fn keys(ctx: &mut Ctx) {
111 let axis = |plus: bool, minus: bool| plus as i32 as f32 - minus as i32 as f32;
112 let controls = Controls {
113 throttle: axis(ctx.key_down(Key::W), ctx.key_down(Key::S)),
114 steer: axis(ctx.key_down(Key::D), ctx.key_down(Key::A)),
115 handbrake: ctx.key_down(Key::Space),
116 };
117 *ctx.world.resource_mut::<Controls>() = controls;
118 if ctx.key_pressed(Key::R) {
119 let car = ctx.world.resource::<Player>().car;
120 if let Some(t) = ctx.world.get_mut::<Transform3D>(car) {
121 let p = t.position;
122 *t = Transform3D::at(vec3(p.x, height(p.x, p.z) + 1.5, p.z));
123 }
124 }
125}
126
127// ANCHOR: wheels
128/// Every frame: each wheel drawn where its car's last step put it (steer,
129/// spin and suspension travel), following the chassis's smoothed pose.
130fn place_wheels(ctx: &mut Ctx) {
131 let player = ctx.world.resource::<Player>();
132 let car = player.car;
133 let (Some(&vehicle), Some(&chassis)) = (ctx.world.get::<Vehicle3D>(car), ctx.world.get::<Transform3D>(car)) else {
134 return;
135 };
136 for i in 0..player.wheels.len() {
137 let e = ctx.world.resource::<Player>().wheels[i];
138 let pose = vehicle.wheels()[i].pose(&chassis);
139 if let Some(t) = ctx.world.get_mut::<Transform3D>(e) {
140 *t = Transform3D { scale: t.scale, ..pose };
141 }
142 }
143}
144// ANCHOR_END: wheels
145
146/// Every frame: the camera trails the car.
147fn chase(ctx: &mut Ctx) {
148 let car = ctx.world.resource::<Player>().car;
149 let Some(t) = ctx.world.get::<Transform3D>(car).copied() else {
150 return;
151 };
152 let back = -t.forward().with_y(0.0).normalize_or(Vec3::Z);
153 let eye = t.position + back * 9.0 + Vec3::Y * 3.5;
154 let k = (ctx.dt() * 4.0).min(1.0);
155 ctx.camera3d.position = ctx.camera3d.position.lerp(eye, k);
156 let look = Transform3D::at(ctx.camera3d.position).looking_at(t.position + Vec3::Y);
157 ctx.camera3d.rotation = look.rotation;
158}
159
160fn build_world(ctx: &mut Ctx, draw: bool) {
161 let corner = vec3(-SIZE / 2.0, 0.0, -SIZE / 2.0);
162 let step = SIZE / (N - 1) as f32;
163 let heights: Vec<f32> = (0..N * N)
164 .map(|i| height(corner.x + (i % N) as f32 * step, corner.z + (i / N) as f32 * step))
165 .collect();
166 let ground = ctx.physics3d.heightfield(&heights, SIZE);
167 let terrain = ctx
168 .world
169 .spawn((Transform3D::at(corner), RigidBody3D::fixed(), Collider3D::mesh(ground)));
170 let car = spawn_car(ctx, vec3(0.0, height(0.0, 0.0) + 1.5, 0.0));
171 ctx.world.insert_resource(Player {
172 car,
173 steer: 0.0,
174 wheels: Vec::new(),
175 });
176 ctx.world.insert_resource(Controls::default());
177 if !draw {
178 return;
179 }
180 let look = grass(ctx);
181 let tile = ctx.create_terrain_tile(&heights, SIZE, look);
182 ctx.world.insert(terrain, tile);
183 let paint = ctx.create_material(MaterialDesc::color(Color::hex(0xC8402E)));
184 let body = ctx.create_mesh(&MeshData::cube(1.0), paint);
185 if let Some(t) = ctx.world.get_mut::<Transform3D>(car) {
186 t.scale = vec3(1.8, 0.6, 4.2);
187 }
188 ctx.world.insert(car, Model::new(body));
189 let rubber = ctx.create_material(MaterialDesc::color(Color::hex(0x202020)));
190 let tyre = ctx.create_mesh(&MeshData::cube(1.0), rubber);
191 for _ in 0..4 {
192 let wheel = ctx.world.spawn((
193 Transform3D {
194 scale: vec3(0.3, 0.7, 0.7),
195 ..Transform3D::default()
196 },
197 Model::new(tyre),
198 ));
199 ctx.world.resource_mut::<Player>().wheels.push(wheel);
200 }
201}
202
203/// A one-layer terrain material: grass everywhere.
204fn grass(ctx: &mut Ctx) -> Material {
205 let green: Vec<u8> = (0..16 * 16)
206 .flat_map(|i| {
207 let shade = 200 + (i * 37 % 56) as u8;
208 [
209 (70 * shade as u32 / 255) as u8,
210 (120 * shade as u32 / 255) as u8,
211 45,
212 255,
213 ]
214 })
215 .collect();
216 let layers = ctx.create_texture_array(16, 16, &[&green], TextureKind::Color);
217 let weights = ctx.create_texture_array(1, 1, &[&[255, 0, 0, 0]], TextureKind::Data);
218 let corner = vec2(-SIZE / 2.0, -SIZE / 2.0);
219 ctx.create_terrain_material(&TerrainMaterialDesc::new(layers, weights, corner, vec2(SIZE, SIZE)))
220}
221
222/// The script for `HEADLESS=1`: full throttle for 4 s, then steer right
223/// for 2 s, then brake to a stop and hold it with the handbrake.
224fn autopilot(ctx: &mut Ctx) {
225 let t = ctx.sim_tick() as f32 * ctx.dt();
226 let car = ctx.world.resource::<Player>().car;
227 let rolling = forward_speed(&ctx.world, car) > 0.3;
228 *ctx.world.resource_mut::<Controls>() = Controls {
229 throttle: if t < 6.0 {
230 1.0
231 } else if rolling {
232 -1.0
233 } else {
234 0.0
235 },
236 steer: if (4.0..6.0).contains(&t) { 1.0 } else { 0.0 },
237 handbrake: t >= 6.0 && !rolling,
238 };
239}
240
241/// What the car did, second by second.
242#[derive(Default)]
243struct Log(Vec<(f32, Vec3)>);
244
245fn record(ctx: &mut Ctx) {
246 if ctx.sim_tick().is_multiple_of(50) {
247 let car = ctx.world.resource::<Player>().car;
248 let speed = speed_of(&ctx.world, car);
249 let facing = ctx.world.get::<Transform3D>(car).map_or(Vec3::NEG_Z, |t| t.forward());
250 ctx.world.resource_mut::<Log>().0.push((speed, facing));
251 }
252}
253
254/// Runs the script and checks what the car did.
255fn headless() {
256 let world = Game::new("vehicle", 1, 1)
257 .sim_system(autopilot)
258 .sim_system(drive)
259 .physics3d(vec3(0.0, -9.81, 0.0))
260 .sim_system(record)
261 .run_headless(500, |ctx| {
262 build_world(ctx, false);
263 ctx.world.insert_resource(Log::default());
264 });
265 let log = &world.resource::<Log>().0;
266 for (second, (speed, facing)) in log.iter().enumerate() {
267 println!("{:2} s: {speed:5.2} m/s, facing {facing:.2}", second + 1);
268 }
269 let (accelerated, _) = log[3];
270 let (_, turned) = log[5];
271 let (stopped, _) = log[log.len() - 1];
272 assert!(accelerated > 8.0, "accelerates: {accelerated} m/s after 4 s");
273 assert!(turned.x > 0.3, "steers right: facing {turned}");
274 assert!(stopped < 0.5, "stops: {stopped} m/s");
275 println!("accelerates, steers and stops");
276}283fn grow(bounds: Vec2) -> impl FnMut(&mut Ctx) {
284 move |ctx: &mut Ctx| {
285 let mut rng = ctx.random().stream(0);
286 let sprite = ctx.world.resource::<AntLook>().0;
287 let mut counts = std::collections::HashMap::new();
288 ctx.world
289 .query::<&Ant>()
290 .for_each(|a| *counts.entry(a.home).or_insert(0u32) += 1);
291 let mut nests = Vec::new();
292 ctx.world
293 .query::<(Entity, &Transform2D, &mut Nest)>()
294 .for_each(|(e, t, n)| {
295 n.ants = counts.get(&e).copied().unwrap_or(0);
296 let born = n.stored.min(n.max_ants.saturating_sub(n.ants));
297 n.stored -= born;
298 nests.push((e, t.position, born));
299 });
300 for &(home, at, born) in &nests {
301 for _ in 0..born {
302 spawn_ant(&mut ctx.commands, sprite, home, at, rng.angle());
303 }
304 }
305 let piles = ctx.world.query::<&Food>().count();
306 let homes: Vec<Vec2> = nests.iter().map(|n| n.1).collect();
307 for _ in piles..food_piles(bounds) {
308 spawn_food(&mut ctx.commands, &mut rng, bounds, &homes);
309 }
310 }
311}
312
313// -------------------------------------------------------------------- setup
314
315fn food_piles(bounds: Vec2) -> usize {
316 ((bounds.x * bounds.y) / 60_000.0).ceil() as usize
317}
318
319fn spawn_ant(cmds: &mut Commands, sprite: Option<Texture>, home: Entity, at: Vec2, heading: f32) {
320 let ant = Ant {
321 home,
322 home_at: at,
323 heading,
324 carrying: false,
325 since: 0.0,
326 energy: LIFESPAN,
327 event: Event::None,
328 };
329 let t = Transform2D::at(at).rotated(heading);
330 match sprite {
331 Some(tex) => cmds.spawn((t, ant, Sprite::new(tex).with_color(Color::hex(0xD4553A)))),
332 None => cmds.spawn((t, ant)),
333 }
334}
335
336fn spawn_food(cmds: &mut Commands, rng: &mut Rng, bounds: Vec2, nests: &[Vec2]) {
337 // Not too close to any nest, so trails have to form.
338 let p = loop {
339 let p = vec2(rng.range(20.0, bounds.x - 20.0), rng.range(20.0, bounds.y - 20.0));
340 if nests.iter().all(|n| p.distance(*n) > 180.0) {
341 break p;
342 }
343 };
344 cmds.spawn((Transform2D::at(p), Food { amount: 400 }));
345}
346
347/// The simulation, shared by the windowed and headless runs.
348fn colonies(game: Game, bounds: Vec2) -> Game {
349 game.sim_system(steer(bounds))
350 .sim_system(lay_trails)
351 .sim_system(forage)
352 .sim_system_every(4, evaporate)
353 .sim_system_every(30, grow(bounds))
354}
355
356/// Nests on a grid, one per `NEST_AREA`; ants and food around them.
357fn setup(ctx: &mut Ctx, bounds: Vec2, ants: usize, sprite: Option<Texture>) {
358 ctx.world.reflect::<Ant>();
359 ctx.world.reflect::<Nest>();
360 ctx.world.reflect::<Food>();
361 ctx.world.insert_resource(Field::new(bounds));
362 ctx.world.insert_resource(AntLook(sprite));
363 let mut rng = ctx.random().stream(1);
364 let (cols, rows) = ((bounds.x / NEST_AREA.x) as usize, (bounds.y / NEST_AREA.y) as usize);
365 let (cols, rows) = (cols.max(1), rows.max(1));
366 let per_nest = (ants / (cols * rows)).max(1);
367 let mut homes = Vec::new();
368 for row in 0..rows {
369 for col in 0..cols {
370 let at = (vec2(col as f32, row as f32) + 0.5) * bounds / vec2(cols as f32, rows as f32);
371 let nest = Nest {
372 stored: 0,
373 delivered: 0,
374 max_ants: per_nest as u32 * 2,
375 ants: per_nest as u32,
376 };
377 let home = ctx.world.spawn((Transform2D::at(at), nest));
378 homes.push(at);
379 for _ in 0..per_nest {
380 spawn_ant(&mut ctx.commands, sprite, home, at, rng.angle());
381 }
382 }
383 }
384 for _ in 0..food_piles(bounds) {
385 spawn_food(&mut ctx.commands, &mut rng, bounds, &homes);
386 }
387}
388
389fn delivered(world: &mut World) -> u64 {
390 world.query::<&Nest>().iter().map(|n| n.delivered).sum()
391}
392
393// ------------------------------------------------------------------ drawing
394
395fn controls(ctx: &mut Ctx) {
396 if ctx.key_pressed(Key::Space) {
397 if ctx.is_paused() { ctx.resume() } else { ctx.pause() }
398 }
399 if ctx.key_pressed(Key::Period) {
400 ctx.step();
401 }
402 for (key, speed) in [(Key::Num1, 1.0), (Key::Num2, 4.0), (Key::Num3, 16.0), (Key::Num4, 64.0)] {
403 if ctx.key_pressed(key) {
404 ctx.set_sim_speed(speed);
405 }
406 }
407 if ctx.key_pressed(Key::Escape) {
408 ctx.quit();
409 }
410}
411
412/// Colony stats and sim controls, in a panel on top of the world.
413fn hud(ctx: &mut Ctx) {
414 let ants = ctx.world.query::<&Ant>().count();
415 let nests = ctx.world.query::<&Nest>().count();
416 let food = delivered(&mut ctx.world);
417 let panel = Rect::new(12.0, 12.0, 260.0, 156.0);
418 ctx.ui_panel(panel);
419 let mut at = panel.min + ctx.ui_style.padding;
420 let speed = if ctx.is_paused() {
421 "paused".to_string()
422 } else {
423 format!("{}x", ctx.sim_speed())
424 };
425 let stats = format!(
426 "{ants} ants in {nests} colonies\n{food} food delivered\nspeed {speed}, {:.0} FPS",
427 ctx.fps()
428 );
429 at.y += ctx.ui_label(at, &stats).y + 8.0;
430 let pause = if ctx.is_paused() { "Run" } else { "Pause" };
431 if ctx.ui_button(Rect::new(at.x, at.y, 72.0, 30.0), pause) {
432 if ctx.is_paused() { ctx.resume() } else { ctx.pause() }
433 }
434 for (i, speed) in [1.0f32, 4.0, 16.0].into_iter().enumerate() {
435 let r = Rect::new(at.x + 80.0 + i as f32 * 54.0, at.y, 48.0, 30.0);
436 if ctx.ui_button(r, &format!("{speed}x")) {
437 ctx.set_sim_speed(speed);
438 ctx.resume();
439 }
440 }
441}
442
443/// Draws the pheromone field (one texture, updated every frame), the food
444/// piles and the nests. Ants are sprites, drawn by the engine.
445fn draw_world(bounds: Vec2) -> impl FnMut(&mut Ctx) {
446 let mut pixels = Vec::new();
447 let mut dots: Vec<(Vec2, f32, Color)> = Vec::new();
448 let mut field_texture = None;
449 move |ctx: &mut Ctx| {
450 let (w, h) = {
451 let f = ctx.world.resource::<Field>();
452 (f.w, f.h)
453 };
454 let field_tex =
455 *field_texture.get_or_insert_with(|| ctx.create_texture(w as u32, h as u32, &vec![0; w * h * 4]));
456 let field = ctx.world.resource::<Field>();
457 pixels.clear();
458 pixels.extend(field.to_food.iter().zip(&field.to_home).flat_map(|(&food, &home)| {
459 let g = (food.sqrt() * 200.0) as u8;
460 let b = (home.sqrt() * 120.0) as u8;
461 [g / 3, g, b.max(g / 4), 255]
462 }));
463 ctx.update_texture(&field_tex, &pixels);
464 ctx.clear(Color::BLACK);
465 let scale = bounds / vec2(w as f32, h as f32);
466 ctx.draw_ex(
467 &field_tex,
468 bounds * 0.5,
469 DrawParams {
470 scale,
471 z: -10.0,
472 ..Default::default()
473 },
474 );
475
476 dots.clear();
477 ctx.world.query::<(&Transform2D, &Food)>().for_each(|(t, f)| {
478 dots.push((t.position, 2.0 + (f.amount as f32).sqrt() / 2.0, Color::hex(0x7BD389)));
479 });
480 ctx.world.query::<(&Transform2D, &Nest)>().for_each(|(t, _)| {
481 dots.push((t.position, NEST_RADIUS, Color::hex(0x8E5A3C)));
482 });
483 for &(p, radius, color) in &dots {
484 ctx.draw_circle(p, radius, color);
485 }
486 }
487}20fn scene(ctx: &mut Ctx) {
21 let t = ctx.time() as f32;
22
23 // The world: a grid of tiles, a few shapes, a crab in orbit.
24 for y in 0..20 {
25 for x in 0..20 {
26 let shade = if (x + y) % 2 == 0 { 0.16 } else { 0.19 };
27 let tile = Rect::new(x as f32 * 64.0, y as f32 * 64.0, 64.0, 64.0);
28 ctx.draw_rect(tile, Color::rgb(shade, shade + 0.03, shade));
29 }
30 }
31 let center = vec2(640.0, 640.0);
32 ctx.draw_circle(center, 180.0, Color::hex(0x3a86ff).with_alpha(0.15));
33 let orbit = center + Vec2::from_angle(t * 0.6) * 180.0;
34 ctx.draw_line(center, orbit, 3.0, Color::hex(0xffbe0b));
35 let crab = ctx.world.resource::<Scene>().crab;
36 ctx.draw_ex(
37 &crab,
38 orbit,
39 DrawParams {
40 rotation: t,
41 ..Default::default()
42 },
43 );
44
45 // A label that sticks to the crab: world position -> window pixels.
46 let above = ctx.camera.world_to_screen(orbit) + vec2(-28.0, -40.0);
47 ctx.draw_text("orbiter", above, 16.0, Color::hex(0xffbe0b));
48
49 if ctx.world.resource::<Scene>().follow {
50 ctx.camera.look_at(orbit);
51 }
52 if ctx.mouse_pressed(MouseButton::Left) && !ctx.mouse_over_ui() {
53 let at = ctx.mouse_world();
54 ctx.world.spawn((Transform2D::at(at), Sprite::new(crab)));
55 let plop = ctx.world.resource::<Scene>().plop;
56 let speed = 0.8 + ctx.random().stream(0).f32() * 0.4;
57 ctx.play_sound_ex(
58 &plop,
59 SoundParams {
60 speed,
61 ..Default::default()
62 },
63 );
64 ctx.world.resource_mut::<Scene>().dropped += 1;
65 }
66}
67// ANCHOR_END: scene
68
69// ANCHOR: hud
70fn hud(ctx: &mut Ctx) {
71 let panel = Rect::new(12.0, 12.0, 230.0, 172.0);
72 ctx.ui_panel(panel);
73 let mut at = panel.min + ctx.ui_style.padding;
74 let (zoom, dropped) = (ctx.camera.zoom, ctx.world.resource::<Scene>().dropped);
75 let lines = format!("zoom {zoom:.2}x\ncrabs dropped: {dropped}\n{:.0} FPS", ctx.fps());
76 at.y += ctx.ui_label(at, &lines).y + 8.0;
77
78 let follow = ctx.world.resource::<Scene>().follow;
79 let label = if follow { "Stop following" } else { "Follow orbiter" };
80 if ctx.ui_button(Rect::new(at.x, at.y, 210.0, 30.0), label) {
81 ctx.world.resource_mut::<Scene>().follow = !follow;
82 }
83 at.y += 38.0;
84 if ctx.ui_button(Rect::new(at.x, at.y, 210.0, 30.0), "Reset camera") {
85 ctx.camera.zoom = 1.0;
86 ctx.camera.position = Vec2::ZERO;
87 ctx.world.resource_mut::<Scene>().follow = false;
88 }
89}154fn draw_map(ctx: &mut Ctx) -> impl FnMut(&mut Ctx) + use<> {
155 let (w, h) = (MAP.x as usize, MAP.y as usize);
156 let tex = ctx.create_texture(MAP.x, MAP.y, &vec![0; w * h * 4]);
157 ctx.set_texture_filter(&tex, TextureFilter::Nearest);
158 let mut pixels = vec![0u8; w * h * 4];
159 let mut heat = false;
160 let mut drawn: Option<(Vec2, bool)> = None;
161 move |ctx: &mut Ctx| {
162 if ctx.key_pressed(Key::H) {
163 heat = !heat;
164 }
165 let paths = ctx.world.resource::<Paths>();
166 let goal = paths.goal;
167 if drawn != Some((goal, heat)) {
168 drawn = Some((goal, heat));
169 for y in 0..h {
170 for x in 0..w {
171 let center = paths.nav.center_of(uvec2(x as u32, y as u32));
172 let px = match paths.nav.cost(x as u32, y as u32).unwrap() {
173 NavGrid::WALL => [40, 44, 52, 255],
174 MUD => [70, 52, 30, 255],
175 _ => [24, 40, 30, 255],
176 };
177 let px = match paths.flow.distance_at(center) {
178 Some(d) if heat && px[0] != 40 => {
179 let t = (d / 1500.0).min(1.0);
180 [
181 (255.0 * (1.0 - t)) as u8 / 2 + px[0] / 2,
182 px[1],
183 (255.0 * t) as u8 / 2 + px[2] / 2,
184 255,
185 ]
186 }
187 _ => px,
188 };
189 pixels[(y * w + x) * 4..][..4].copy_from_slice(&px);
190 }
191 }
192 ctx.update_texture(&tex, &pixels);
193 }
194 ctx.clear(Color::BLACK);
195 let size = MAP.as_vec2() * CELL;
196 ctx.draw_ex(
197 &tex,
198 size / 2.0,
199 DrawParams {
200 scale: Vec2::splat(CELL),
201 z: -10.0,
202 ..Default::default()
203 },
204 );
205 ctx.draw_circle(goal, 8.0, Color::rgba(1.0, 0.3, 0.3, 0.8));
206 }
207}
208
209fn click_to_move_goal(ctx: &mut Ctx) {
210 if ctx.mouse_pressed(MouseButton::Left) && !ctx.mouse_over_ui() {
211 let at = ctx.mouse_world();
212 let paths = ctx.world.resource_mut::<Paths>();
213 if paths.nav.walkable(at) {
214 paths.set_goal(at);
215 }
216 }
217 if ctx.key_pressed(Key::Space) {
218 if ctx.is_paused() { ctx.resume() } else { ctx.pause() }
219 }
220 if ctx.key_pressed(Key::Escape) {
221 ctx.quit();
222 }
223}
224
225fn hud(ctx: &mut Ctx) {
226 let panel = Rect::new(12.0, 12.0, 260.0, 104.0);
227 ctx.ui_panel(panel);
228 let at = panel.min + ctx.ui_style.padding;
229 let agents = ctx.world.query::<&Agent>().count();
230 let ms = ctx.world.resource::<Paths>().compute_ms;
231 let text = format!(
232 "{agents} agents, {:.0} FPS\nfield: {}x{} cells, {ms:.2} ms\nclick: new goal H: distances",
233 ctx.fps(),
234 MAP.x,
235 MAP.y
236 );
237 ctx.ui_label(at, &text);
238}218fn fight() -> impl FnMut(&mut Ctx) {
219 // Per team: a fine grid for friends (only the closest few matter) and a
220 // coarse one for enemies (seen from afar). Marching armies far from the
221 // enemy then cost almost nothing to search, however packed they are.
222 let mut near = [
223 SpatialGrid::new(PERSONAL_SPACE * 2.0),
224 SpatialGrid::new(PERSONAL_SPACE * 2.0),
225 ];
226 let mut far = [SpatialGrid::new(SIGHT), SpatialGrid::new(SIGHT)];
227 let mut positions: Vec<(Entity, Vec2, Team)> = Vec::new();
228 move |ctx: &mut Ctx| {
229 let (dt, random, look) = (ctx.dt(), ctx.random(), ctx.turns(4));
230 let crowding = ctx.sim_tick().is_multiple_of(2);
231 ctx.world
232 .query::<(Entity, &Transform2D, &Unit)>()
233 .par_collect_into(&mut positions, |(e, t, u)| (e, t.position, u.team));
234 // The four grids are independent: build them at the same time.
235 let [near_red, near_blue] = &mut near;
236 let [far_red, far_blue] = &mut far;
237 let all = &positions;
238 let of = |team: Team| all.iter().filter(move |p| p.2 == team).map(|&(e, at, _)| (e, at));
239 rayon::scope(|s| {
240 if crowding {
241 s.spawn(|_| near_red.rebuild_from(of(Team::Red)));
242 s.spawn(|_| near_blue.rebuild_from(of(Team::Blue)));
243 }
244 s.spawn(|_| far_red.rebuild_from(of(Team::Red)));
245 s.spawn(|_| far_blue.rebuild_from(of(Team::Blue)));
246 });
247 let over = ctx.world.resource::<Score>().winner.is_some();
248 let battle = ctx.world.remove_resource::<Battle>().expect("battle");
249 let (near, far, b) = (&near, &far, &battle);
250 ctx.world
251 .query::<(Entity, &mut Transform2D, &mut Unit)>()
252 .par_for_each(|(e, t, unit)| {
253 let mut rng = random.of(e);
254 let me = t.position;
255 unit.hit = None;
256 unit.cooldown -= dt;
257 if crowding {
258 let mut push = Vec2::ZERO;
259 near[unit.team.index()].for_each_in_radius(me, PERSONAL_SPACE, |other| {
260 if other.entity != e {
261 let away = me - other.position;
262 push += away / (away.length_squared() + 0.01);
263 }
264 });
265 unit.push = push.clamp_length_max(1.0);
266 }
267 let mut nearest = None;
268 if unit.engaged || look.includes(e) {
269 let enemies = &far[unit.team.enemy().index()];
270 nearest = enemies
271 .nearest(me, REACH, |_| true)
272 .or_else(|| enemies.nearest(me, SIGHT, |_| true))
273 .map(|g| (g.position.distance_squared(me), g.entity, g.position));
274 unit.engaged = nearest.is_some();
275 }
276 let enemy_base = b.bases[unit.team.enemy().index()];
277 let mut want = match nearest {
278 Some((d2, target, _)) if d2 < REACH * REACH => {
279 if unit.cooldown <= 0.0 {
280 unit.cooldown = COOLDOWN * rng.range(0.8, 1.2);
281 unit.hit = Some(Hit::Unit(target));
282 }
283 Vec2::ZERO
284 }
285 Some((_, _, at)) => (at - me).normalize_or_zero() * SPEED,
286 None if me.distance(enemy_base) < BASE_RADIUS + REACH => {
287 if unit.cooldown <= 0.0 && !over {
288 unit.cooldown = COOLDOWN;
289 unit.hit = Some(Hit::Base);
290 }
291 Vec2::ZERO
292 }
293 None => b.fields[unit.team.index()].direction_at(me) * SPEED,
294 };
295 let slow = b.nav.cell_at(me).and_then(|c| b.nav.cost(c.x, c.y)).unwrap_or(1).max(1) as f32;
296 want = want / slow + unit.push * SPEED;
297 unit.vel = unit.vel.lerp(want, 0.2) + rng.unit_vec2() * 2.0;
298 // Slide along walls instead of walking into them.
299 let next = me + unit.vel * dt;
300 if b.nav.walkable(next) {
301 t.position = next;
302 } else if b.nav.walkable(vec2(next.x, me.y)) {
303 t.position.x = next.x;
304 } else if b.nav.walkable(vec2(me.x, next.y)) {
305 t.position.y = next.y;
306 }
307 });
308 ctx.world.insert_resource(battle);
309 }
310}
311
312/// Applies this tick's hits in order (so the result doesn't depend on
313/// thread timing), removes the dead, and checks for a fallen base.
314fn resolve() -> impl FnMut(&mut Ctx) {
315 let mut hits: Vec<(Team, Hit)> = Vec::new();
316 move |ctx: &mut Ctx| {
317 hits.clear();
318 ctx.world.query::<&Unit>().for_each(|u| {
319 if let Some(hit) = u.hit {
320 hits.push((u.team, hit));
321 }
322 });
323 let battle = ctx.world.resource::<Battle>();
324 let bases = battle.base_entities;
325 for &(team, hit) in &hits {
326 match hit {
327 Hit::Unit(target) => {
328 if let Some(u) = ctx.world.get_mut::<Unit>(target) {
329 u.hp -= DAMAGE;
330 }
331 }
332 Hit::Base => {
333 if let Some(base) = ctx.world.get_mut::<Base>(bases[team.enemy().index()]) {
334 base.hp = (base.hp - DAMAGE).max(0.0);
335 }
336 }
337 }
338 }
339 let mut alive = [0; 2];
340 let (cmds, events) = (&mut ctx.commands, &mut ctx.events);
341 ctx.world
342 .query::<(Entity, &Transform2D, &Unit)>()
343 .for_each(|(e, t, u)| {
344 if u.hp <= 0.0 {
345 cmds.despawn(e);
346 events.send(Fallen {
347 team: u.team,
348 at: t.position,
349 });
350 } else {
351 alive[u.team.index()] += 1;
352 }
353 });
354 let mut fallen = None;
355 ctx.world.query::<&Base>().for_each(|b| {
356 if b.hp <= 0.0 {
357 fallen = Some(b.team);
358 }
359 });
360 let score = ctx.world.resource_mut::<Score>();
361 score.alive = alive;
362 if let (Some(team), None) = (fallen, score.winner) {
363 score.winner = Some(team.enemy());
364 score.rounds_won[team.enemy().index()] += 1;
365 }
366 }
367}
368
369/// Bases send new units (a few per tick, up to the cap) while they stand.
370fn reinforce(ctx: &mut Ctx) {
371 let mut rng = ctx.random().stream(1);
372 let sprite = ctx.world.resource::<Look>().0;
373 let cap = ctx.world.resource::<Battle>().cap;
374 let (alive, winner) = {
375 let s = ctx.world.resource::<Score>();
376 (s.alive, s.winner)
377 };
378 if winner.is_some() {
379 return;
380 }
381 // A full army rebuilds in about 40 seconds.
382 let per_tick = cap as f32 / (40.0 * 60.0);
383 let mut spawns = Vec::new();
384 ctx.world.query::<(&Transform2D, &mut Base)>().for_each(|(t, base)| {
385 let room = cap.saturating_sub(alive[base.team.index()]);
386 base.budget = (base.budget + per_tick).min(room as f32);
387 let n = base.budget as usize;
388 base.budget -= n as f32;
389 spawns.push((base.team, t.position, n));
390 });
391 for (team, at, n) in spawns {
392 for _ in 0..n {
393 spawn_unit(
394 &mut ctx.commands,
395 sprite,
396 team,
397 at + rng.unit_vec2() * rng.range(0.0, BASE_RADIUS),
398 );
399 }
400 }
401}
402
403/// Blue's general: attack red's base, or defend when outnumbered 2 to 1.
404fn blue_ai(ctx: &mut Ctx) {
405 let alive = ctx.world.resource::<Score>().alive;
406 let b = ctx.world.resource_mut::<Battle>();
407 let goal = if alive[1] * 2 < alive[0] {
408 b.bases[1]
409 } else {
410 b.bases[0]
411 };
412 if b.rally[1] != goal {
413 b.set_rally(Team::Blue, goal);
414 }
415}
416
417// -------------------------------------------------------------------- setup
418
419fn spawn_unit(cmds: &mut Commands, sprite: Option<Texture>, team: Team, at: Vec2) {
420 let unit = Unit {
421 team,
422 hp: HP,
423 vel: Vec2::ZERO,
424 cooldown: 0.0,
425 engaged: false,
426 push: Vec2::ZERO,
427 hit: None,
428 };
429 match sprite {
430 Some(tex) => cmds.spawn((Transform2D::at(at), Sprite::new(tex).with_color(team.color()), unit)),
431 None => cmds.spawn((Transform2D::at(at), unit)),
432 }
433}
434
435fn setup(ctx: &mut Ctx, units: usize, seed: u64, sprite: Option<Texture>) {
436 let scale = (units as f32 / UNITS_PER_MAP).max(1.0).sqrt();
437 let size = (MAP.as_vec2() * scale).as_uvec2();
438 let (nav, bases) = make_map(size, seed);
439 ctx.world.reflect::<Unit>();
440 ctx.world.reflect::<Base>();
441 let max_hp = 400.0 * (units as f32 / 1000.0).sqrt();
442 let base_entities = [Team::Red, Team::Blue].map(|team| {
443 ctx.world.spawn((
444 Transform2D::at(bases[team.index()]),
445 Base {
446 team,
447 hp: max_hp,
448 max_hp,
449 budget: 0.0,
450 },
451 ))
452 });
453 let mut battle = Battle {
454 nav,
455 fields: [FlowField::new(), FlowField::new()],
456 rally: bases,
457 bases,
458 base_entities,
459 cap: units / 2,
460 field_ms: 0.0,
461 };
462 battle.set_rally(Team::Red, bases[1]);
463 battle.set_rally(Team::Blue, bases[0]);
464 ctx.world.insert_resource(battle);
465 ctx.world.reflect_resource::<Score>();
466 ctx.events.reflect::<Fallen>();
467 ctx.world.insert_resource(Score::default());
468 ctx.world.insert_resource(Look(sprite));
469 start_round(ctx);
470}
471
472/// Both armies at full strength around their bases, bases repaired.
473fn start_round(ctx: &mut Ctx) {
474 let mut rng = ctx.random().stream(ctx.sim_tick());
475 let cmds = &mut ctx.commands;
476 ctx.world.query::<(Entity, &Unit)>().for_each(|(e, _)| cmds.despawn(e));
477 ctx.world.query::<&mut Base>().for_each(|b| b.hp = b.max_hp);
478 let sprite = ctx.world.resource::<Look>().0;
479 ctx.world.resource_mut::<Score>().winner = None;
480 let battle = ctx.world.resource_mut::<Battle>();
481 let (cap, bases) = (battle.cap, battle.bases);
482 let nav = &battle.nav;
483 // Spread each army over the walkable ground around its base.
484 let mut spots = Vec::with_capacity(cap * 2);
485 for team in [Team::Red, Team::Blue] {
486 let spread = (cap as f32).sqrt() * 2.2;
487 while spots.len() < cap * (team.index() + 1) {
488 let p = bases[team.index()] + rng.unit_vec2() * rng.range(0.0, 1.0).sqrt() * spread;
489 if nav.walkable(p) {
490 spots.push((team, p));
491 }
492 }
493 }
494 for (team, at) in spots {
495 spawn_unit(&mut ctx.commands, sprite, team, at);
496 }
497}
498
499/// The systems both the window and headless runs use.
500fn battle_systems(game: Game) -> Game {
501 game.named_sim_system("fight", 1, fight())
502 .named_sim_system("resolve", 1, resolve())
503 .sim_system(reinforce)
504 .sim_system_every(30, blue_ai)
505 .sim_system_every(60, keep_neighbors_close)
506}
507
508/// Once a second, units are reordered in memory by where they stand, so
509/// neighbors on the map are neighbors in memory: grid rebuilds, flow field
510/// lookups and searches then mostly hit the cache.
511fn keep_neighbors_close(ctx: &mut Ctx) {
512 ctx.world.sort_by::<Transform2D>(|t| spatial_key(t.position, 64.0));
513}
514
515// ------------------------------------------------------------------ playing
516
517fn controls(ctx: &mut Ctx) {
518 let home = ctx.world.resource::<Battle>().bases;
519 let rally = if ctx.mouse_pressed(MouseButton::Left) && !ctx.mouse_over_ui() {
520 Some(ctx.mouse_world())
521 } else if ctx.key_pressed(Key::A) {
522 Some(home[1])
523 } else if ctx.key_pressed(Key::D) {
524 Some(home[0])
525 } else {
526 None
527 };
528 if let Some(at) = rally {
529 let b = ctx.world.resource_mut::<Battle>();
530 if b.nav.walkable(at) {
531 b.set_rally(Team::Red, at);
532 }
533 }
534 if ctx.key_pressed(Key::R) {
535 start_round(ctx);
536 }
537 if ctx.key_pressed(Key::Space) {
538 if ctx.is_paused() { ctx.resume() } else { ctx.pause() }
539 }
540 for (key, speed) in [(Key::Num1, 1.0), (Key::Num2, 2.0), (Key::Num3, 4.0)] {
541 if ctx.key_pressed(key) {
542 ctx.set_sim_speed(speed);
543 }
544 }
545 if ctx.key_pressed(Key::Escape) {
546 ctx.quit();
547 }
548}
549
550/// The map as one texture (a pixel per cell), the bases with their health,
551/// and red's rally point. Units are sprites, drawn by the engine.
552fn draw_map(ctx: &mut Ctx) -> impl FnMut(&mut Ctx) + use<> {
553 let size = ctx.world.resource::<Battle>().nav.size();
554 let mut pixels = vec![0u8; (size.x * size.y * 4) as usize];
555 {
556 let nav = &ctx.world.resource::<Battle>().nav;
557 for y in 0..size.y {
558 for x in 0..size.x {
559 // A little per-cell noise so the ground doesn't look flat.
560 let n = ((x.wrapping_mul(73_856_093) ^ y.wrapping_mul(19_349_663)) % 9) as u8;
561 let px = match nav.cost(x, y).unwrap() {
562 NavGrid::WALL => [52 + n, 56 + n, 66 + n, 255],
563 MUD => [74 + n, 56 + n, 34, 255],
564 _ => [30 + n, 46 + n, 32, 255],
565 };
566 pixels[((y * size.x + x) * 4) as usize..][..4].copy_from_slice(&px);
567 }
568 }
569 }
570 let tex = ctx.create_texture(size.x, size.y, &pixels);
571 ctx.set_texture_filter(&tex, TextureFilter::Nearest);
572 move |ctx: &mut Ctx| {
573 ctx.clear(Color::BLACK);
574 let world = size.as_vec2() * CELL;
575 ctx.draw_ex(
576 &tex,
577 world / 2.0,
578 DrawParams {
579 scale: Vec2::splat(CELL),
580 z: -10.0,
581 ..Default::default()
582 },
583 );
584 let rally = ctx.world.resource::<Battle>().rally[0];
585 let mut bases = [(Vec2::ZERO, 0.0, Team::Red); 2];
586 ctx.world.query::<(&Transform2D, &Base)>().for_each(|(t, b)| {
587 bases[b.team.index()] = (t.position, b.hp / b.max_hp, b.team);
588 });
589 for (at, health, team) in bases {
590 ctx.draw_circle(at, BASE_RADIUS, team.color().with_alpha(0.35));
591 ctx.draw_circle(at, BASE_RADIUS * 0.4, team.color());
592 let bar = Rect::new(at.x - BASE_RADIUS, at.y - BASE_RADIUS - 8.0, BASE_RADIUS * 2.0, 3.0);
593 ctx.draw_rect(bar, Color::rgba(0.0, 0.0, 0.0, 0.6));
594 ctx.draw_rect(
595 Rect::new(bar.min.x, bar.min.y, bar.size().x * health, 3.0),
596 team.color(),
597 );
598 }
599 ctx.draw_circle(rally, 5.0, Color::rgba(1.0, 0.9, 0.3, 0.9));
600 }
601}
602
603fn hud(ctx: &mut Ctx) {
604 let field_ms = ctx.world.resource::<Battle>().field_ms;
605 let s = ctx.world.resource::<Score>();
606 let (alive, won, winner) = (s.alive, s.rounds_won, s.winner);
607 let sim_ms: f32 = ctx.system_timings().iter().filter(|t| t.sim).map(|t| t.avg_ms).sum();
608 let speed = if ctx.is_paused() {
609 "paused".to_string()
610 } else {
611 format!("{}x", ctx.sim_speed())
612 };
613 let panel = Rect::new(12.0, 12.0, 300.0, 150.0);
614 ctx.ui_panel(panel);
615 let at = panel.min + ctx.ui_style.padding;
616 let text = format!(
617 "red {} vs blue {} (rounds {}-{})\nsim {sim_ms:.2} ms/tick, {:.0} FPS, {speed}\nfield {field_ms:.2} ms\nclick: rally A: attack D: defend\nR: restart 1-3: speed",
618 alive[0],
619 alive[1],
620 won[0],
621 won[1],
622 ctx.fps()
623 );
624 ctx.ui_label(at, &text);
625 if let Some(team) = winner {
626 let msg = if team == Team::Red {
627 "Red wins! R for another round"
628 } else {
629 "Blue wins. R to try again"
630 };
631 let center = ctx.window_size() / 2.0;
632 let size = ctx.measure_text(msg, 32.0);
633 ctx.draw_text(msg, center - size / 2.0, 32.0, team.color());
634 }
635}
636
637fn main() {
638 let units = env_number("UNITS", 20_000.0) as usize;
639 let seed = env_number("SEED", 1.0) as u64;
640 if env_number("HEADLESS", 0.0) != 0.0 {
641 let ticks = env_number("TICKS", 600.0) as u64;
642 let mut start = Instant::now();
643 let world = battle_systems(Game::new("battle", 1, 1).seed(seed))
644 .sim_system(|ctx: &mut Ctx| {
645 // Headless rounds restart on their own.
646 if ctx.world.resource::<Score>().winner.is_some() {
647 start_round(ctx);
648 }
649 if ctx.sim_tick().is_multiple_of(120) {
650 let b = ctx.world.resource::<Score>();
651 let timings: Vec<String> = ctx
652 .system_timings()
653 .iter()
654 .filter(|t| t.sim && t.avg_ms >= 0.01)
655 .map(|t| format!("{} {:.2} ms", t.name.rsplit("::").next().unwrap_or(t.name), t.avg_ms))
656 .collect();
657 println!(
658 "tick {}: red {} blue {}, rounds {}-{} [{}]",
659 ctx.sim_tick(),
660 b.alive[0],
661 b.alive[1],
662 b.rounds_won[0],
663 b.rounds_won[1],
664 timings.join(", ")
665 );
666 }
667 })
668 .run_headless(ticks, |ctx| {
669 setup(ctx, units, seed, None);
670 start = Instant::now();
671 });
672 let secs = start.elapsed().as_secs_f64();
673 let b = world.resource::<Battle>();
674 println!(
675 "{units} units, {}x{} cells, {ticks} ticks: {:.2} ms/tick ({:.0} ticks/s)",
676 b.nav.size().x,
677 b.nav.size().y,
678 secs * 1e3 / ticks as f64,
679 ticks as f64 / secs
680 );
681 return;
682 }
683
684 battle_systems(Game::new("Crabby Engine: battle", 1280, 720).seed(seed))
685 .system(controls)
686 .system(camera_controls)
687 .system(hud)
688 .run(move |ctx| {
689 let dot = ctx.create_texture(2, 2, &[255; 2 * 2 * 4]);
690 setup(ctx, units, seed, Some(dot));
691 let world = ctx.world.resource::<Battle>().nav.size().as_vec2() * CELL;
692 ctx.camera.zoom = (ctx.camera.viewport() / world).min_element();
693 ctx.camera.look_at(world / 2.0);
694 draw_map(ctx)
695 });
696}Sourcepub fn resource_mut<R>(&mut self) -> &mut R
pub fn resource_mut<R>(&mut self) -> &mut R
Mutable version of resource.
Examples found in repository?
More examples
209fn click_to_move_goal(ctx: &mut Ctx) {
210 if ctx.mouse_pressed(MouseButton::Left) && !ctx.mouse_over_ui() {
211 let at = ctx.mouse_world();
212 let paths = ctx.world.resource_mut::<Paths>();
213 if paths.nav.walkable(at) {
214 paths.set_goal(at);
215 }
216 }
217 if ctx.key_pressed(Key::Space) {
218 if ctx.is_paused() { ctx.resume() } else { ctx.pause() }
219 }
220 if ctx.key_pressed(Key::Escape) {
221 ctx.quit();
222 }
223}
224
225fn hud(ctx: &mut Ctx) {
226 let panel = Rect::new(12.0, 12.0, 260.0, 104.0);
227 ctx.ui_panel(panel);
228 let at = panel.min + ctx.ui_style.padding;
229 let agents = ctx.world.query::<&Agent>().count();
230 let ms = ctx.world.resource::<Paths>().compute_ms;
231 let text = format!(
232 "{agents} agents, {:.0} FPS\nfield: {}x{} cells, {ms:.2} ms\nclick: new goal H: distances",
233 ctx.fps(),
234 MAP.x,
235 MAP.y
236 );
237 ctx.ui_label(at, &text);
238}
239
240fn main() {
241 let agents = env_number("AGENTS", 20_000.0) as usize;
242 if env_number("HEADLESS", 0.0) != 0.0 {
243 let ticks = env_number("TICKS", 600.0) as u64;
244 let mut start = Instant::now();
245 let mut rng = Rng::new(99);
246 let mut field_ms = Vec::new();
247 Game::new("flow", 1, 1)
248 .sim_system(steer)
249 .sim_system(move |ctx: &mut Ctx| {
250 // A new goal every 2 seconds, like a player clicking around.
251 let tick = ctx.sim_tick();
252 if tick % 120 == 119 {
253 let paths = ctx.world.resource_mut::<Paths>();
254 let goal = walkable_point(&paths.nav, &mut rng);
255 paths.set_goal(goal);
256 field_ms.push(paths.compute_ms);
257 let avg = field_ms.iter().sum::<f32>() / field_ms.len() as f32;
258 println!(
259 "new goal at tick {tick}: field {:.2} ms (avg {avg:.2})",
260 paths.compute_ms
261 );
262 }
263 })
264 .run_headless(ticks, |ctx| {
265 setup(ctx, agents, None);
266 start = Instant::now();
267 });
268 let secs = start.elapsed().as_secs_f64();
269 println!(
270 "{agents} agents, {ticks} ticks: {:.2} ms/tick ({:.0} ticks/s)",
271 secs * 1e3 / ticks as f64,
272 ticks as f64 / secs
273 );
274 return;
275 }
276
277 Game::new("Crabby Engine: flow fields", 1280, 720)
278 .sim_system(steer)
279 .system(click_to_move_goal)
280 .system(camera_controls)
281 .system(hud)
282 .run(move |ctx| {
283 let dot = ctx.create_texture(2, 2, &[255; 2 * 2 * 4]);
284 setup(ctx, agents, Some(dot));
285 draw_map(ctx)
286 });
287}34fn spawn_crab(ctx: &mut Ctx, at: Vec2) {
35 let s = ctx.world.resource_mut::<Settings>();
36 let angle = s.random() * std::f32::consts::TAU;
37 let speed = 40.0 + s.random() * 120.0;
38 let tint = Color::rgb(0.5 + s.random() * 0.5, 0.5 + s.random() * 0.5, 0.5 + s.random() * 0.5);
39 let crab = s.crab;
40 ctx.world.spawn((
41 Transform2D::at(at),
42 Sprite::new(crab).with_color(tint),
43 Vel(Vec2::from_angle(angle) * speed),
44 Life(LIFETIME),
45 ));
46}44fn spawn(ctx: &mut Ctx, n: usize, at: Vec2) {
45 let b = ctx.world.resource_mut::<Bench>();
46 let crab = b.crab;
47 let mut batch = Vec::with_capacity(n);
48 for _ in 0..n {
49 let r = &mut b.rng;
50 let vel = vec2(r.next_f32() * 400.0 - 200.0, r.next_f32() * 400.0 - 300.0);
51 let color = Color::rgb(
52 0.6 + r.next_f32() * 0.4,
53 0.6 + r.next_f32() * 0.4,
54 0.6 + r.next_f32() * 0.4,
55 );
56 batch.push((
57 Transform2D {
58 scale: Vec2::splat(b.scale),
59 ..Transform2D::at(at)
60 },
61 Sprite::new(crab).with_color(color),
62 Vel(vel),
63 ));
64 }
65 ctx.world.spawn_batch(batch);
66}
67
68fn bounce(ctx: &mut Ctx) {
69 let dt = ctx.dt();
70 let bounds = ctx.window_size();
71 let gravity = 600.0;
72 ctx.world.query::<(&mut Transform2D, &mut Vel)>().for_each(|(t, v)| {
73 let (p, v) = (&mut t.position, &mut v.0);
74 v.y += gravity * dt;
75 *p += *v * dt;
76 if p.x < 0.0 {
77 p.x = 0.0;
78 v.x = v.x.abs();
79 }
80 if p.x > bounds.x {
81 p.x = bounds.x;
82 v.x = -v.x.abs();
83 }
84 if p.y > bounds.y {
85 p.y = bounds.y;
86 v.y = -v.y.abs() * 0.85;
87 }
88 if p.y < 0.0 {
89 p.y = 0.0;
90 v.y = 0.0;
91 }
92 });
93}
94
95fn report(ctx: &mut Ctx) {
96 if ctx.mouse_down(MouseButton::Left) {
97 let at = ctx.mouse_pos();
98 spawn(ctx, 1000, at);
99 }
100 // "update + draw calls" = every system except this one, as measured by the
101 // engine (smoothed averages; draw_sprites hasn't run yet this frame).
102 let systems_ms: f32 = ctx
103 .system_timings()
104 .iter()
105 .filter(|t| !t.name.ends_with("report"))
106 .map(|t| t.avg_ms)
107 .sum();
108 let (time, count, render) = (ctx.time(), ctx.world.len(), ctx.render_stats());
109 let b = ctx.world.resource_mut::<Bench>();
110 let now = Instant::now();
111 let frame_ms = now.duration_since(b.frame_start).as_secs_f32() * 1000.0;
112 b.frame_start = now;
113 b.worst_ms = b.worst_ms.max(frame_ms);
114 b.frames += 1;
115 if time >= b.report_at {
116 println!(
117 "crabs: {:>8} fps: {:>6.1} avg frame: {:>6.2} ms worst: {:>6.2} ms update+draw calls: {:>6.2} ms render cpu: {:>5.2} ms gpu: {}",
118 count,
119 b.frames,
120 1000.0 / b.frames as f64,
121 b.worst_ms,
122 systems_ms,
123 render.cpu_ms,
124 render.gpu_ms.map_or("n/a".into(), |g| format!("{g:.2} ms")),
125 );
126 b.report_at += 1.0;
127 b.frames = 0;
128 b.worst_ms = 0.0;
129 }
130 let quit_after = b.quit_after;
131 if ctx.key_pressed(Key::Escape) || (quit_after > 0.0 && time >= quit_after) {
132 ctx.quit();
133 }
134}31fn orbit(ctx: &mut Ctx) {
32 if ctx.key_pressed(Key::Space) {
33 if ctx.is_paused() { ctx.resume() } else { ctx.pause() }
34 }
35 if ctx.key_pressed(Key::Escape) {
36 ctx.quit();
37 }
38 let drag = if ctx.mouse_down(MouseButton::Left) {
39 ctx.mouse_delta()
40 } else {
41 Vec2::ZERO
42 };
43 let wheel = ctx.mouse_wheel().y;
44 let o = ctx.world.resource_mut::<Orbit>();
45 o.yaw -= drag.x * 0.005;
46 o.pitch = (o.pitch + drag.y * 0.005).clamp(0.05, 1.5);
47 o.distance = (o.distance * 0.9f32.powf(wheel)).clamp(3.0, 400.0);
48 let (yaw, pitch, distance) = (o.yaw, o.pitch, o.distance);
49 let offset = vec3(yaw.sin() * pitch.cos(), pitch.sin(), yaw.cos() * pitch.cos()) * distance;
50 ctx.camera3d.position = offset;
51 ctx.camera3d.look_at(Vec3::ZERO);
52}
53
54/// Frame times summed after the warmup.
55struct Bench {
56 seconds: f64,
57 start: Option<std::time::Instant>,
58 last: Option<std::time::Instant>,
59 frames: u64,
60 frame_ms: f64,
61 render_cpu_ms: f64,
62 /// Frame time minus waiting for the display: the work.
63 busy_ms: f64,
64 /// GPU time of the frame's passes.
65 gpu_ms: f64,
66}
67
68fn bench(ctx: &mut Ctx) {
69 let now = std::time::Instant::now();
70 let cpu = ctx.render_stats().cpu_ms as f64;
71 let gpu = ctx.render_stats().gpu_ms.unwrap_or(0.0) as f64;
72 let wait = ctx.display_wait_ms() as f64;
73 let b = ctx.world.resource_mut::<Bench>();
74 let start = *b.start.get_or_insert(now);
75 let elapsed = now.duration_since(start).as_secs_f64();
76 let last = b.last.replace(now);
77 let measuring = last.is_some() && elapsed > 3.0;
78 let first = measuring && b.frames == 0;
79 if let (true, Some(last)) = (measuring, last) {
80 b.frames += 1;
81 b.frame_ms += now.duration_since(last).as_secs_f64() * 1000.0;
82 b.render_cpu_ms += cpu;
83 b.gpu_ms += gpu;
84 b.busy_ms += now.duration_since(last).as_secs_f64() * 1000.0 - wait;
85 }
86 let done = elapsed > 3.0 + b.seconds;
87 let (frames, frame_ms, render_cpu_ms, busy_ms, gpu_ms) =
88 (b.frames, b.frame_ms, b.render_cpu_ms, b.busy_ms, b.gpu_ms);
89 if first && let Ok(path) = std::env::var("BENCH_SHOT") {
90 ctx.save_screenshot(path);
91 }
92 if done {
93 let n = frames as f64;
94 println!(
95 "{{\"engine\":\"crabby\",\"frames\":{frames},\"fps\":{:.3},\"frame_ms\":{:.3},\"render_cpu_ms\":{:.3},\"busy_ms\":{:.3},\"gpu_ms\":{:.3}}}",
96 n / (frame_ms / 1000.0),
97 frame_ms / n,
98 render_cpu_ms / n,
99 busy_ms / n,
100 gpu_ms / n
101 );
102 ctx.quit();
103 }
104}153fn eyes(ctx: &mut Ctx) {
154 if ctx.mouse_pressed(MouseButton::Left) {
155 if ctx.cursor_locked() {
156 shove(ctx);
157 } else {
158 ctx.set_cursor_locked(true);
159 }
160 } else if ctx.key_pressed(Key::Escape) {
161 if ctx.cursor_locked() {
162 ctx.set_cursor_locked(false);
163 } else {
164 ctx.quit();
165 }
166 }
167 let turn = if ctx.cursor_locked() {
168 ctx.mouse_motion() * 0.003
169 } else {
170 Vec2::ZERO
171 };
172 let p = ctx.world.resource_mut::<Player>();
173 p.yaw -= turn.x;
174 p.pitch = (p.pitch - turn.y).clamp(-1.5, 1.5);
175 let (id, yaw, pitch) = (p.id, p.yaw, p.pitch);
176 if let Some(t) = ctx.world.get::<Transform3D>(id) {
177 ctx.camera3d.position = t.position + Vec3::Y * 1.65;
178 ctx.camera3d.rotation = Quat::from_rotation_y(yaw) * Quat::from_rotation_x(pitch);
179 }
180}Sourcepub fn get_resource<R>(&self) -> Option<&R>
pub fn get_resource<R>(&self) -> Option<&R>
The resource of type R, if there is one.
Sourcepub fn get_resource_mut<R>(&mut self) -> Option<&mut R>
pub fn get_resource_mut<R>(&mut self) -> Option<&mut R>
Mutable version of get_resource.
Sourcepub fn remove_resource<R>(&mut self) -> Option<R>
pub fn remove_resource<R>(&mut self) -> Option<R>
Removes and returns the resource of type R.
Examples found in repository?
96fn steer(ctx: &mut Ctx) {
97 let dt = ctx.dt();
98 let random = ctx.random();
99 // Taken out for the query, which borrows the world, then put back.
100 let paths = ctx.world.remove_resource::<Paths>().expect("paths");
101 ctx.world
102 .query::<(Entity, &mut Transform2D, &mut Agent)>()
103 .par_for_each(|(e, t, a)| {
104 let mut rng = random.of(e);
105 let cell = paths.nav.cell_at(t.position);
106 let slow = cell.and_then(|c| paths.nav.cost(c.x, c.y)).unwrap_or(1).max(1) as f32;
107 let want = paths.flow.direction_at(t.position) * (SPEED / slow);
108 a.vel = a.vel.lerp(want, 0.15) + rng.unit_vec2() * 3.0;
109 // Slide along walls instead of walking into them.
110 let next = t.position + a.vel * dt;
111 if paths.nav.walkable(next) {
112 t.position = next;
113 } else if paths.nav.walkable(vec2(next.x, t.position.y)) {
114 t.position.x = next.x;
115 } else if paths.nav.walkable(vec2(t.position.x, next.y)) {
116 t.position.y = next.y;
117 }
118 });
119 ctx.world.insert_resource(paths);
120}More examples
122fn steer(bounds: Vec2) -> impl FnMut(&mut Ctx) {
123 let mut food_grid = SpatialGrid::new(SIGHT);
124 move |ctx: &mut Ctx| {
125 let (dt, random) = (ctx.dt(), ctx.random());
126 food_grid.rebuild_from(
127 ctx.world
128 .query::<(Entity, &Transform2D, &Food)>()
129 .iter()
130 .map(|(e, t, _)| (e, t.position)),
131 );
132 let field = ctx.world.remove_resource::<Field>().expect("field");
133 let (grid, f) = (&food_grid, &field);
134 ctx.world
135 .query::<(Entity, &mut Transform2D, &mut Ant)>()
136 .par_for_each(|(e, t, ant)| {
137 let mut rng = random.of(e);
138 let p = t.position;
139 let target = if ant.carrying {
140 (p.distance(ant.home_at) < SIGHT * 3.0).then_some(ant.home_at)
141 } else {
142 grid.nearest(p, SIGHT, |_| true).map(|food| food.position)
143 };
144 if let Some(target) = target {
145 ant.heading = (target - p).to_angle();
146 } else {
147 let layer = if ant.carrying { &f.to_home } else { &f.to_food };
148 let smell = |angle: f32| {
149 let at = p + Vec2::from_angle(ant.heading + angle) * SENSE_DIST;
150 f.index(at).map_or(-1.0, |i| layer[i])
151 };
152 let (left, ahead, right) = (smell(-SENSE_ANGLE), smell(0.0), smell(SENSE_ANGLE));
153 if ant.carrying && left.max(ahead).max(right) < FAINT {
154 // No trail to follow: real ants keep track of where
155 // home is (path integration) and head that way.
156 let off = (ant.home_at - p).to_angle() - ant.heading;
157 let off = (off + std::f32::consts::PI).rem_euclid(std::f32::consts::TAU) - std::f32::consts::PI;
158 ant.heading += off.clamp(-2.0 * dt, 2.0 * dt);
159 } else if left > ahead && left > right {
160 ant.heading -= 4.0 * dt;
161 } else if right > ahead && right > left {
162 ant.heading += 4.0 * dt;
163 }
164 }
165 ant.heading += rng.range(-2.5, 2.5) * dt;
166 let mut next = p + Vec2::from_angle(ant.heading) * SPEED * dt;
167 // Bounce off the edges.
168 if next.x < 0.0 || next.x >= bounds.x {
169 ant.heading = std::f32::consts::PI - ant.heading;
170 next.x = next.x.clamp(0.0, bounds.x - 0.01);
171 }
172 if next.y < 0.0 || next.y >= bounds.y {
173 ant.heading = -ant.heading;
174 next.y = next.y.clamp(0.0, bounds.y - 0.01);
175 }
176 t.position = next;
177 t.rotation = ant.heading;
178 ant.since += dt;
179 ant.energy -= dt;
180 });
181 ctx.world.insert_resource(field);
182 }
183}
184
185/// Every tick: ants mark where they walk. Searching ants lay "home is this
186/// way", carrying ants lay "food is this way"; fresher trails are stronger.
187fn lay_trails(ctx: &mut Ctx) {
188 let mut field = ctx.world.remove_resource::<Field>().expect("field");
189 ctx.world.query::<(&Transform2D, &Ant)>().for_each(|(t, ant)| {
190 if let Some(i) = field.index(t.position) {
191 let strength = (1.0 - ant.since / 30.0).max(0.0);
192 let layer = if ant.carrying {
193 &mut field.to_food
194 } else {
195 &mut field.to_home
196 };
197 layer[i] = layer[i].max(strength);
198 }
199 });
200 ctx.world.insert_resource(field);
201}218fn fight() -> impl FnMut(&mut Ctx) {
219 // Per team: a fine grid for friends (only the closest few matter) and a
220 // coarse one for enemies (seen from afar). Marching armies far from the
221 // enemy then cost almost nothing to search, however packed they are.
222 let mut near = [
223 SpatialGrid::new(PERSONAL_SPACE * 2.0),
224 SpatialGrid::new(PERSONAL_SPACE * 2.0),
225 ];
226 let mut far = [SpatialGrid::new(SIGHT), SpatialGrid::new(SIGHT)];
227 let mut positions: Vec<(Entity, Vec2, Team)> = Vec::new();
228 move |ctx: &mut Ctx| {
229 let (dt, random, look) = (ctx.dt(), ctx.random(), ctx.turns(4));
230 let crowding = ctx.sim_tick().is_multiple_of(2);
231 ctx.world
232 .query::<(Entity, &Transform2D, &Unit)>()
233 .par_collect_into(&mut positions, |(e, t, u)| (e, t.position, u.team));
234 // The four grids are independent: build them at the same time.
235 let [near_red, near_blue] = &mut near;
236 let [far_red, far_blue] = &mut far;
237 let all = &positions;
238 let of = |team: Team| all.iter().filter(move |p| p.2 == team).map(|&(e, at, _)| (e, at));
239 rayon::scope(|s| {
240 if crowding {
241 s.spawn(|_| near_red.rebuild_from(of(Team::Red)));
242 s.spawn(|_| near_blue.rebuild_from(of(Team::Blue)));
243 }
244 s.spawn(|_| far_red.rebuild_from(of(Team::Red)));
245 s.spawn(|_| far_blue.rebuild_from(of(Team::Blue)));
246 });
247 let over = ctx.world.resource::<Score>().winner.is_some();
248 let battle = ctx.world.remove_resource::<Battle>().expect("battle");
249 let (near, far, b) = (&near, &far, &battle);
250 ctx.world
251 .query::<(Entity, &mut Transform2D, &mut Unit)>()
252 .par_for_each(|(e, t, unit)| {
253 let mut rng = random.of(e);
254 let me = t.position;
255 unit.hit = None;
256 unit.cooldown -= dt;
257 if crowding {
258 let mut push = Vec2::ZERO;
259 near[unit.team.index()].for_each_in_radius(me, PERSONAL_SPACE, |other| {
260 if other.entity != e {
261 let away = me - other.position;
262 push += away / (away.length_squared() + 0.01);
263 }
264 });
265 unit.push = push.clamp_length_max(1.0);
266 }
267 let mut nearest = None;
268 if unit.engaged || look.includes(e) {
269 let enemies = &far[unit.team.enemy().index()];
270 nearest = enemies
271 .nearest(me, REACH, |_| true)
272 .or_else(|| enemies.nearest(me, SIGHT, |_| true))
273 .map(|g| (g.position.distance_squared(me), g.entity, g.position));
274 unit.engaged = nearest.is_some();
275 }
276 let enemy_base = b.bases[unit.team.enemy().index()];
277 let mut want = match nearest {
278 Some((d2, target, _)) if d2 < REACH * REACH => {
279 if unit.cooldown <= 0.0 {
280 unit.cooldown = COOLDOWN * rng.range(0.8, 1.2);
281 unit.hit = Some(Hit::Unit(target));
282 }
283 Vec2::ZERO
284 }
285 Some((_, _, at)) => (at - me).normalize_or_zero() * SPEED,
286 None if me.distance(enemy_base) < BASE_RADIUS + REACH => {
287 if unit.cooldown <= 0.0 && !over {
288 unit.cooldown = COOLDOWN;
289 unit.hit = Some(Hit::Base);
290 }
291 Vec2::ZERO
292 }
293 None => b.fields[unit.team.index()].direction_at(me) * SPEED,
294 };
295 let slow = b.nav.cell_at(me).and_then(|c| b.nav.cost(c.x, c.y)).unwrap_or(1).max(1) as f32;
296 want = want / slow + unit.push * SPEED;
297 unit.vel = unit.vel.lerp(want, 0.2) + rng.unit_vec2() * 2.0;
298 // Slide along walls instead of walking into them.
299 let next = me + unit.vel * dt;
300 if b.nav.walkable(next) {
301 t.position = next;
302 } else if b.nav.walkable(vec2(next.x, me.y)) {
303 t.position.x = next.x;
304 } else if b.nav.walkable(vec2(me.x, next.y)) {
305 t.position.y = next.y;
306 }
307 });
308 ctx.world.insert_resource(battle);
309 }
310}Sourcepub unsafe fn insert_resource_dynamic(
&mut self,
name: &str,
layout: Layout,
value: *const u8,
)
pub unsafe fn insert_resource_dynamic( &mut self, name: &str, layout: Layout, value: *const u8, )
Stores a resource that has no Rust type, by name: a copy of
layout.size() bytes from value, replacing any resource of that
name. This is how C and C++ games keep global values.
use ponko_ecs::*;
use std::alloc::Layout;
let mut world = World::new();
let score: [u32; 2] = [3, 1];
// SAFETY: `score` is 8 readable bytes.
unsafe { world.insert_resource_dynamic("Score", Layout::new::<[u32; 2]>(), score.as_ptr().cast()) };
let p = world.get_resource_dynamic("Score").unwrap() as *const [u32; 2];
// SAFETY: the resource holds the bytes of a [u32; 2].
assert_eq!(unsafe { *p }, [3, 1]);§Safety
value points to layout.size() readable bytes (any pointer for size 0).
§Panics
If a resource of that name exists with a different layout.
Sourcepub fn get_resource_dynamic(&self, name: &str) -> Option<*mut u8>
pub fn get_resource_dynamic(&self, name: &str) -> Option<*mut u8>
Pointer to resource name (one stored with
insert_resource_dynamic), or None.
It stays valid until the resource is replaced or removed.
Sourcepub fn resource_layout_dynamic(&self, name: &str) -> Option<Layout>
pub fn resource_layout_dynamic(&self, name: &str) -> Option<Layout>
The size and alignment of resource name, if it exists.
Sourcepub fn remove_resource_dynamic(&mut self, name: &str) -> bool
pub fn remove_resource_dynamic(&mut self, name: &str) -> bool
Removes resource name; false if there was none.
Sourcepub fn apply(&mut self, commands: &mut Commands)
pub fn apply(&mut self, commands: &mut Commands)
Applies commands recorded while the world was busy (e.g. during a query).
Leaves commands empty and ready to reuse.
Sourcepub fn register<T>(&mut self) -> ComponentIdwhere
T: Component,
pub fn register<T>(&mut self) -> ComponentIdwhere
T: Component,
The id of Rust component T, registering it on first use.
Sourcepub unsafe fn register_component(
&mut self,
name: &str,
size: usize,
align: usize,
drop: Option<unsafe extern "C-unwind" fn(*mut u8)>,
) -> ComponentId
pub unsafe fn register_component( &mut self, name: &str, size: usize, align: usize, drop: Option<unsafe extern "C-unwind" fn(*mut u8)>, ) -> ComponentId
Registers a component known only by name, size and alignment: the path for components defined in C/C++ or loaded at runtime.
Registering the same name again with the same layout returns the same id.
§Safety
Values of this component must be safe to move with memcpy, and to send
and share between threads. drop, if given, must accept any value
written as this component.
§Panics
If the name is taken with a different layout, or the layout is invalid.
Sourcepub fn component_id<T>(&self) -> Option<ComponentId>where
T: Component,
pub fn component_id<T>(&self) -> Option<ComponentId>where
T: Component,
The id of Rust component T, if it has been used in this world.
Sourcepub fn component_by_name(&self, name: &str) -> Option<ComponentId>
pub fn component_by_name(&self, name: &str) -> Option<ComponentId>
Looks a component up by name (registered name, or Rust type name).
Sourcepub fn component_at(&self, index: usize) -> Option<ComponentId>
pub fn component_at(&self, index: usize) -> Option<ComponentId>
The component with number index (ComponentId::index), if registered.
let mut world = ponko_ecs::World::new();
world.spawn((1.5f32,));
let id = world.component_id::<f32>().unwrap();
assert_eq!(world.component_at(id.index()), Some(id));
assert_eq!(world.component_at(99), None);Sourcepub fn component_info(&self, id: ComponentId) -> &ComponentInfo
pub fn component_info(&self, id: ComponentId) -> &ComponentInfo
Sourcepub fn components(&self) -> &[ComponentInfo]
pub fn components(&self) -> &[ComponentInfo]
Every component type registered so far, indexed by ComponentId::index.
Sourcepub fn archetypes(&self) -> &[Archetype]
pub fn archetypes(&self) -> &[Archetype]
The archetype tables, for tools such as the inspector.
Sourcepub fn components_of(&self, e: Entity) -> Option<&[ComponentId]>
pub fn components_of(&self, e: Entity) -> Option<&[ComponentId]>
The components entity e has (sorted by id), or None if it is dead.
struct Pos(f32);
struct Vel(f32);
let mut world = World::new();
let e = world.spawn((Pos(0.0), Vel(1.0)));
let names: Vec<&str> = world.components_of(e).unwrap()
.iter()
.map(|&id| world.component_info(id).name())
.collect();
assert_eq!(names.len(), 2);
world.despawn(e);
assert!(world.components_of(e).is_none());Sourcepub fn get_dynamic(&self, e: Entity, id: ComponentId) -> Option<*mut u8>
pub fn get_dynamic(&self, e: Entity, id: ComponentId) -> Option<*mut u8>
Pointer to component id of entity e, or None.
Sourcepub unsafe fn spawn_dynamic(
&mut self,
components: &[(ComponentId, *const u8)],
) -> Entity
pub unsafe fn spawn_dynamic( &mut self, components: &[(ComponentId, *const u8)], ) -> Entity
Sourcepub unsafe fn spawn_at_dynamic(
&mut self,
e: Entity,
components: &[(ComponentId, *const u8)],
) -> bool
pub unsafe fn spawn_at_dynamic( &mut self, e: Entity, components: &[(ComponentId, *const u8)], ) -> bool
spawn_dynamic with a given id, like
spawn_at: returns false (and takes nothing) if
that slot is taken.
use ponko_ecs::{Entity, World};
let mut world = World::new();
// SAFETY: f32 is plain bytes.
let id = unsafe { world.register_component("Speed", 4, 4, None) };
let saved = Entity::from_bits(5 | 2 << 32);
let speed = 1.5f32;
// SAFETY: points to a valid f32, which needs no drop.
assert!(unsafe { world.spawn_at_dynamic(saved, &[(id, (&speed as *const f32).cast())]) });
assert!(!unsafe { world.spawn_at_dynamic(saved, &[(id, (&speed as *const f32).cast())]) });
assert_eq!(world.len(), 1);§Safety
As for spawn_dynamic. When this returns
false, the values stay the caller’s.
§Panics
If a component id appears twice.
Sourcepub unsafe fn insert_dynamic(
&mut self,
e: Entity,
id: ComponentId,
value: *const u8,
) -> bool
pub unsafe fn insert_dynamic( &mut self, e: Entity, id: ComponentId, value: *const u8, ) -> bool
Adds (or replaces) component id on e from a raw value.
Returns false if e is dead; the value is then still the caller’s.
§Safety
value must point to a valid value of component id; on success the
world takes ownership of it.
Sourcepub fn remove_dynamic(&mut self, e: Entity, id: ComponentId) -> bool
pub fn remove_dynamic(&mut self, e: Entity, id: ComponentId) -> bool
Removes component id from e, dropping the value. Returns false if
e is dead or didn’t have it.
Sourcepub fn for_each_chunk(&mut self, ids: &[ComponentId], f: impl FnMut(Chunk<'_>))
pub fn for_each_chunk(&mut self, ids: &[ComponentId], f: impl FnMut(Chunk<'_>))
Calls f once per archetype that has all of ids, with raw column
pointers in ids order. This is how C/C++ systems see the world: whole
columns, never one call per entity.
§Panics
With more than 32 component ids.
Trait Implementations§
Source§impl WorldReflect for World
impl WorldReflect for World
Source§fn reflect<T: Component + Serialize + DeserializeOwned>(&mut self)
fn reflect<T: Component + Serialize + DeserializeOwned>(&mut self)
T visible to reflection under its type name without
the module path (my_game::Health becomes "Health").
Registering the same type again does nothing. Read moreSource§unsafe fn reflect_fields(&mut self, id: ComponentId, fields: &[Field])
unsafe fn reflect_fields(&mut self, id: ComponentId, fields: &[Field])
World::register_component, such as a C or C++ component) visible
to reflection, by describing its fields. Its JSON is an object with
one entry per field; bytes no field covers are zero in new values.
Describing the same component again replaces its fields. Read moreSource§fn reflected_components(&self) -> Vec<&str>
fn reflected_components(&self) -> Vec<&str>
Source§fn set_json(
&mut self,
e: Entity,
name: &str,
value: Value,
) -> Result<(), ReflectError>
fn set_json( &mut self, e: Entity, name: &str, value: Value, ) -> Result<(), ReflectError>
name of entity e from JSON, adding the component
if the entity doesn’t have it. Read moreSource§fn remove_json(&mut self, e: Entity, name: &str) -> Result<bool, ReflectError>
fn remove_json(&mut self, e: Entity, name: &str) -> Result<bool, ReflectError>
Source§fn entity_json(&self, e: Entity) -> Result<Value, ReflectError>
fn entity_json(&self, e: Entity) -> Result<Value, ReflectError>
e, as one JSON object keyed by
component name. Components that aren’t reflected are left out. Read moreSource§fn spawn_json(&mut self, components: Value) -> Result<Entity, ReflectError>
fn spawn_json(&mut self, components: Value) -> Result<Entity, ReflectError>
entity_json returns. On error nothing
is left spawned. Read moreSource§fn reflect_resource<R: Send + Sync + 'static + Serialize + DeserializeOwned>(
&mut self,
)
fn reflect_resource<R: Send + Sync + 'static + Serialize + DeserializeOwned>( &mut self, )
Source§fn reflected_resources(&self) -> Vec<&str>
fn reflected_resources(&self) -> Vec<&str>
Source§fn resource_json(&self, name: &str) -> Result<Value, ReflectError>
fn resource_json(&self, name: &str) -> Result<Value, ReflectError>
Source§fn set_resource_json(
&mut self,
name: &str,
value: Value,
) -> Result<(), ReflectError>
fn set_resource_json( &mut self, name: &str, value: Value, ) -> Result<(), ReflectError>
Source§unsafe fn reflect_resource_fields(
&mut self,
name: &str,
layout: Layout,
fields: &[Field],
)
unsafe fn reflect_resource_fields( &mut self, name: &str, layout: Layout, fields: &[Field], )
name, one with no Rust type (stored with
World::insert_resource_dynamic, the C and C++ path), visible to
reflection by listing its fields, like
reflect_fields does for components.
Describing it again replaces the fields. Read moreSource§fn save(&self) -> Value
fn save(&self) -> Value
"not_saved" counts them by name, so nothing goes missing
silently. Reflected resources (reflect_resource)
are saved under "resources"; others are not. Read moreAuto Trait Implementations§
impl !RefUnwindSafe for World
impl !UnwindSafe for World
impl Forget for World
impl Freeze for World
impl Send for World
impl Sync for World
impl Unpin for World
impl UnsafeUnpin for World
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
impl<T> Component for T
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be
downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further
downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSend for T
impl<T> DowncastSend for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<S, T> Duplex<S> for Twhere
T: FromSample<S> + ToSample<S>,
Source§impl<S> FromSample<S> for S
impl<S> FromSample<S> for S
fn from_sample_(s: S) -> S
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<F, T> IntoSample<T> for Fwhere
T: FromSample<F>,
impl<F, T> IntoSample<T> for Fwhere
T: FromSample<F>,
fn into_sample(self) -> T
impl<T> MaybeSync for T
impl<T> MaybeSync for T
Source§impl<T> Pointable for T
impl<T> Pointable for T
impl<T> Read<Exclusive, BecauseExclusive> for Twhere
T: ?Sized,
Source§impl<R, P> ReadPrimitive<R> for P
impl<R, P> ReadPrimitive<R> for P
Source§fn read_from_little_endian(read: &mut R) -> Result<Self, Error>
fn read_from_little_endian(read: &mut R) -> Result<Self, Error>
ReadEndian::read_from_little_endian().Source§impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
Source§fn to_subset(&self) -> Option<SS>
fn to_subset(&self) -> Option<SS>
self from the equivalent element of its
superset. Read moreSource§fn is_in_subset(&self) -> bool
fn is_in_subset(&self) -> bool
self is actually part of its subset T (and can be converted to it).Source§fn to_subset_unchecked(&self) -> SS
fn to_subset_unchecked(&self) -> SS
self.to_subset but without any property checks. Always succeeds.Source§fn from_subset(element: &SS) -> SP
fn from_subset(element: &SS) -> SP
self to the equivalent element of its superset.