pub struct Query<'w, Q>where
Q: QueryData,{ /* private fields */ }Expand description
The ECS: entities, components, queries, resources, commands.
A query over a World, created by World::query.
struct Pos(f32);
struct Vel(f32);
struct Frozen;
let mut world = World::new();
world.spawn((Pos(0.0), Vel(1.0)));
world.spawn((Pos(0.0), Vel(1.0), Frozen));
world.query::<(&mut Pos, &Vel)>().without::<Frozen>().for_each(|(p, v)| p.0 += v.0);
let moved = world.query::<&Pos>().iter().filter(|p| p.0 > 0.0).count();
assert_eq!(moved, 1);Implementations§
Source§impl<'w, Q> Query<'w, Q>where
Q: QueryData,
impl<'w, Q> Query<'w, Q>where
Q: QueryData,
Sourcepub fn with<T>(self) -> Query<'w, Q>where
T: Component,
pub fn with<T>(self) -> Query<'w, Q>where
T: Component,
Only match entities that also have component T (without fetching it).
§Panics
With more than 4 with filters on one query.
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}Sourcepub fn for_each(&mut self, f: impl FnMut(<Q as QueryData>::Item<'_>))
pub fn for_each(&mut self, f: impl FnMut(<Q as QueryData>::Item<'_>))
Runs f for every matching entity. The fastest way to loop.
Examples found in repository?
More examples
49fn movement(ctx: &mut Ctx) {
50 let dt = ctx.dt();
51 let bounds = ctx.window_size();
52 ctx.world.query::<(&mut Transform2D, &mut Vel)>().for_each(|(t, v)| {
53 t.position += v.0 * dt;
54 t.rotation = v.0.to_angle();
55 if t.position.x < 0.0 || t.position.x > bounds.x {
56 v.0.x = -v.0.x;
57 }
58 if t.position.y < 0.0 || t.position.y > bounds.y {
59 v.0.y = -v.0.y;
60 }
61 t.position = t.position.clamp(Vec2::ZERO, bounds);
62 });
63}
64
65/// Simulation: age, fade out, and despawn through commands.
66fn aging(ctx: &mut Ctx) {
67 let dt = ctx.dt();
68 let commands = &mut ctx.commands;
69 ctx.world
70 .query::<(Entity, &mut Life, &mut Sprite)>()
71 .for_each(|(e, life, sprite)| {
72 life.0 -= dt;
73 sprite.color = sprite.color.with_alpha((life.0 / LIFETIME).clamp(0.0, 1.0));
74 if life.0 <= 0.0 {
75 commands.despawn(e);
76 }
77 });
78}40fn walk(ctx: &mut Ctx) {
41 let dt = ctx.dt();
42 let bounds = ctx.window_size();
43 ctx.world.query::<(&mut Transform2D, &mut Walker)>().for_each(|(t, w)| {
44 let mut v = Vec2::from_angle(w.heading) * w.speed;
45 t.position += v * dt;
46 if !(0.0..=bounds.x).contains(&t.position.x) {
47 v.x = -v.x;
48 }
49 if !(0.0..=bounds.y).contains(&t.position.y) {
50 v.y = -v.y;
51 }
52 t.position = t.position.clamp(Vec2::ZERO, bounds);
53 w.heading = v.to_angle();
54 });
55}Sourcepub fn par_for_each(
&mut self,
f: impl Fn(<Q as QueryData>::Item<'_>) + Send + Sync,
)
pub fn par_for_each( &mut self, f: impl Fn(<Q as QueryData>::Item<'_>) + Send + Sync, )
Like for_each, but spread over all CPU cores.
Each archetype is cut into chunks of rows, and the chunks run in
parallel. Use it for heavy per-entity work over many entities; for a
few thousand cheap updates, for_each is just as fast.
f can’t capture &mut state (it runs on many threads at once). To
spawn or despawn, collect what you need and use commands afterwards.
struct Pos(f32);
struct Vel(f32);
let mut world = World::new();
world.spawn_batch((0..100_000).map(|i| (Pos(i as f32), Vel(1.0))));
world.query::<(&mut Pos, &Vel)>().par_for_each(|(p, v)| p.0 += v.0);Examples found in repository?
More examples
17fn wander(ctx: &mut Ctx) {
18 let dt = ctx.dt();
19 ctx.world
20 .query::<(&mut Transform2D, &mut Agent)>()
21 .par_for_each(|(t, a)| {
22 let p = &mut t.position;
23 a.heading += (p.x * 0.013 + p.y * 0.007 + a.heading).sin() * 0.8 * dt;
24 p.x = (p.x + a.heading.cos() * a.speed * dt).rem_euclid(WORLD);
25 p.y = (p.y + a.heading.sin() * a.speed * dt).rem_euclid(WORLD);
26 t.rotation = a.heading;
27 });
28}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}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}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}
202
203/// Every 4th tick: trails fade.
204fn evaporate(ctx: &mut Ctx) {
205 let keep = 0.985f32.powf(ctx.dt() * 60.0);
206 let field = ctx.world.resource_mut::<Field>();
207 for v in field.to_food.iter_mut().chain(field.to_home.iter_mut()) {
208 *v *= keep;
209 }
210}
211
212/// Ants take turns (each every 2nd tick) picking up food and dropping it at
213/// their nest; starving ants die. Each ant decides on all cores and notes
214/// what happened in its `event`; one quick pass then applies the events.
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}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 fn par_for_each_indexed(
&mut self,
f: impl Fn(usize, <Q as QueryData>::Item<'_>) + Send + Sync,
)
pub fn par_for_each_indexed( &mut self, f: impl Fn(usize, <Q as QueryData>::Item<'_>) + Send + Sync, )
Like par_for_each, but also passes each
entity’s position in the query, 0..count(), in the same order as
for_each and iter.
Use it to fill a buffer from all cores: every entity writes its own slot, so the result comes out in a stable order.
use std::sync::atomic::{AtomicU32, Ordering};
struct Pos(f32);
let mut world = World::new();
world.spawn_batch((0..10_000).map(|i| (Pos(i as f32),)));
let mut query = world.query::<&Pos>();
let out: Vec<AtomicU32> = (0..query.count()).map(|_| AtomicU32::new(0)).collect();
query.par_for_each_indexed(|i, p| out[i].store(p.0 as u32 * 2, Ordering::Relaxed));
assert_eq!(out[1234].load(Ordering::Relaxed), 2468);Sourcepub fn par_collect_into<R>(
&mut self,
out: &mut Vec<R>,
f: impl Fn(<Q as QueryData>::Item<'_>) -> R + Send + Sync,
)where
R: Send,
pub fn par_collect_into<R>(
&mut self,
out: &mut Vec<R>,
f: impl Fn(<Q as QueryData>::Item<'_>) -> R + Send + Sync,
)where
R: Send,
Replaces the contents of out with f of every matching entity,
computed on all cores, in the same order as for_each.
out keeps its memory between calls, so collecting every tick
allocates nothing once it’s big enough.
struct Pos(f32);
let mut world = World::new();
world.spawn_batch((0..10_000).map(|i| (Pos(i as f32),)));
let mut doubled = Vec::new();
world.query::<&Pos>().par_collect_into(&mut doubled, |p| p.0 * 2.0);
assert_eq!(doubled[1234], 2468.0);Examples found in repository?
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 fn iter(&mut self) -> QueryIter<'_, Q> ⓘ
pub fn iter(&mut self) -> QueryIter<'_, Q> ⓘ
An iterator over matching entities, for map/filter/sum and friends.
Convenient, but several times slower than for_each
on tight math loops (the compiler can’t vectorize across archetypes).
Use for_each for per-tick simulation work.
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
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}31fn move_players(ctx: &mut Ctx) {
32 let dt = ctx.dt();
33 // Read the pads first: the query below borrows the world.
34 let mut sticks = [Vec2::ZERO; MAX_GAMEPADS];
35 let mut spins = [false; MAX_GAMEPADS];
36 let mut grow = [0.0; MAX_GAMEPADS];
37 for &pad in ctx.gamepads() {
38 sticks[pad.slot()] = ctx.gamepad_stick(pad, GamepadStick::Left);
39 spins[pad.slot()] = ctx.gamepad_pressed(pad, GamepadButton::South);
40 grow[pad.slot()] = ctx.gamepad_axis(pad, GamepadAxis::RightTrigger);
41 }
42 for (t, player) in ctx.world.query::<(&mut Transform2D, &mut Player)>().iter() {
43 let Some(slot) = player.slot else { continue };
44 t.position += sticks[slot] * SPEED * dt;
45 if spins[slot] {
46 player.spin = std::f32::consts::TAU;
47 }
48 t.scale = Vec2::splat(1.0 + 2.0 * grow[slot]);
49 }
50}
51// ANCHOR_END: gamepad
52
53/// Arrow keys move the keyboard's crab.
54fn move_keyboard_player(ctx: &mut Ctx) {
55 let dt = ctx.dt();
56 let mut dir = Vec2::ZERO;
57 for (key, d) in [
58 (Key::Left, -Vec2::X),
59 (Key::Right, Vec2::X),
60 (Key::Up, -Vec2::Y),
61 (Key::Down, Vec2::Y),
62 ] {
63 if ctx.key_down(key) {
64 dir += d;
65 }
66 }
67 let spin = ctx.key_pressed(Key::Space);
68 for (t, player) in ctx.world.query::<(&mut Transform2D, &mut Player)>().iter() {
69 if player.slot.is_none() {
70 t.position += dir.normalize_or_zero() * SPEED * dt;
71 if spin {
72 player.spin = std::f32::consts::TAU;
73 }
74 }
75 }
76}
77
78/// One crab per connected pad: spawn on plug-in, despawn on unplug.
79fn join_and_leave(ctx: &mut Ctx) {
80 let mut connected = [false; MAX_GAMEPADS];
81 for pad in ctx.gamepads() {
82 connected[pad.slot()] = true;
83 }
84 let mut has_crab = [false; MAX_GAMEPADS];
85 for (e, player) in ctx.world.query::<(Entity, &Player)>().iter() {
86 if let Some(slot) = player.slot {
87 if connected[slot] {
88 has_crab[slot] = true;
89 } else {
90 ctx.commands.despawn(e);
91 }
92 }
93 }
94 for slot in (0..MAX_GAMEPADS).filter(|&s| connected[s] && !has_crab[s]) {
95 let mut sprite = Sprite::new(ctx.load_texture("crab.png"));
96 sprite.color = PLAYER_COLORS[slot];
97 let at = vec2(200.0 + slot as f32 * 120.0, 360.0);
98 ctx.world.spawn((
99 Transform2D::at(at),
100 sprite,
101 Player {
102 slot: Some(slot),
103 spin: 0.0,
104 },
105 ));
106 }
107}
108
109/// Spins play out over half a second.
110fn spin(ctx: &mut Ctx) {
111 let dt = ctx.dt();
112 for (t, player) in ctx.world.query::<(&mut Transform2D, &mut Player)>().iter() {
113 let step = player.spin.min(4.0 * std::f32::consts::TAU * dt);
114 t.rotation += step;
115 player.spin -= step;
116 }
117}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}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}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}
81// ANCHOR_END: simulate
82
83fn draw(ctx: &mut Ctx) {
84 ctx.clear(Color::hex(0x20304a));
85 let crabs: Vec<(Vec2, u32)> = ctx
86 .world
87 .query::<(&Transform2D, &Crab)>()
88 .iter()
89 .map(|(t, c)| (t.position, c.player))
90 .collect();
91 for (at, player) in crabs {
92 let color = [Color::hex(0xf56565), Color::hex(0x63b3ed)][player as usize % 2];
93 ctx.draw_circle(at, 30.0, color);
94 }
95 let net = &ctx.net;
96 let status = match net.state() {
97 NetState::Connecting => "waiting for the other player...".to_string(),
98 NetState::Running => format!(
99 "you are player {} ({}), tick {}{}",
100 net.player() + 1,
101 ["red", "blue"][net.player() % 2],
102 ctx.sim_tick(),
103 net.desync().map_or(String::new(), |d| format!(" DESYNC: {d}"))
104 ),
105 NetState::Lost => format!("connection lost: {}", net.error().unwrap_or("?")),
106 NetState::Offline => "offline".into(),
107 _ => String::new(),
108 };
109 ctx.draw_text(&status, vec2(20.0, 20.0), 20.0, Color::WHITE);
110 #[cfg(feature = "steam")]
111 if ctx.net.state() == NetState::Connecting
112 && let Some(lobby) = ctx.net.steam_lobby()
113 {
114 let text = format!("Steam lobby {}: press I to invite a friend", lobby.raw());
115 ctx.draw_text(&text, vec2(20.0, 50.0), 20.0, Color::WHITE);
116 if ctx.key_pressed(Key::I) {
117 ctx.net.steam_invite();
118 }
119 }
120 if ctx.key_pressed(Key::Escape) {
121 ctx.quit();
122 }
123}Sourcepub fn count(&mut self) -> usize
pub fn count(&mut self) -> usize
Number of matching entities (doesn’t visit them one by one).
Examples found in repository?
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}More examples
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}
488
489fn main() {
490 let ants = env_number("ANTS", ANTS_PER_NEST as f64) as usize;
491 let seed = env_number("SEED", DEFAULT_SEED as f64) as u64;
492 // One colony's worth of space per ANTS_PER_NEST ants.
493 let nests = (ants as f32 / ANTS_PER_NEST as f32).max(1.0);
494 let side = nests.sqrt().ceil();
495 let bounds = NEST_AREA * vec2(side, (nests / side).ceil());
496
497 if env_number("HEADLESS", 0.0) != 0.0 {
498 let ticks = env_number("TICKS", 600.0) as u64;
499 let mut start = Instant::now();
500 let mut world = colonies(Game::new("ants", 1, 1).seed(seed), bounds).run_headless(ticks, |ctx| {
501 setup(ctx, bounds, ants, None);
502 start = Instant::now();
503 });
504 let secs = start.elapsed().as_secs_f64();
505 println!(
506 "{ticks} ticks: {:.2} ms/tick ({:.0} ticks/s). {} ants, {} food delivered",
507 secs * 1e3 / ticks as f64,
508 ticks as f64 / secs,
509 world.query::<&Ant>().count(),
510 delivered(&mut world)
511 );
512 return;
513 }
514
515 colonies(Game::new("Crabby Engine: ants", 1280, 720).seed(seed), bounds)
516 .system(controls)
517 .system(camera_controls)
518 .system(draw_world(bounds))
519 .system(hud)
520 .run(move |ctx| {
521 let sprite = ctx.create_texture(3, 2, &[255; 3 * 2 * 4]);
522 setup(ctx, bounds, ants, Some(sprite));
523 // Start with the whole world in view.
524 let fit = ctx.window_size() / bounds;
525 ctx.camera.zoom = fit.x.min(fit.y);
526 ctx.camera.look_at(bounds / 2.0);
527 let mut next = 1.0;
528 move |ctx: &mut Ctx| {
529 if ctx.time() >= next {
530 next += 1.0;
531 let sim_ms: f32 = ctx.system_timings().iter().filter(|t| t.sim).map(|t| t.avg_ms).sum();
532 println!(
533 "ants: {} delivered: {} fps: {:.0} sim: {sim_ms:.2} ms/frame tick: {}",
534 ctx.world.query::<&Ant>().count(),
535 delivered(&mut ctx.world),
536 ctx.fps(),
537 ctx.sim_tick()
538 );
539 }
540 }
541 });
542}Auto Trait Implementations§
impl<'w, Q> !RefUnwindSafe for Query<'w, Q>
impl<'w, Q> !UnwindSafe for Query<'w, Q>
impl<'w, Q> Forget for Query<'w, Q>
impl<'w, Q> Freeze for Query<'w, Q>
impl<'w, Q> Send for Query<'w, Q>
impl<'w, Q> Sync for Query<'w, Q>
impl<'w, Q> Unpin for Query<'w, Q>
impl<'w, Q> UnsafeUnpin for Query<'w, Q>where
<Q as QueryData>::Ids: UnsafeUnpin,
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<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.