Skip to main content

World

Struct World 

Source
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

Source

pub fn new() -> World

An empty world.

Examples found in repository?
examples/ffi_bench.rs (line 17)
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}
Source

pub fn spawn<B>(&mut self, bundle: B) -> Entity
where 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?
examples/ecs.rs (lines 40-45)
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
Hide additional examples
examples/hotpatch.rs (line 57)
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}
examples/vehicle.rs (lines 48-56)
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}
examples/lockstep.rs (lines 153-159)
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}
examples/audio3d.rs (line 22)
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}
examples/skinning.rs (line 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}
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}
Source

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?
examples/bunnymark_ecs.rs (line 65)
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
Hide additional examples
examples/boids.rs (line 108)
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}
examples/tiles.rs (lines 101-111)
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}
examples/ffi_bench.rs (line 18)
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}
examples/flow.rs (lines 139-145)
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}
examples/headless.rs (lines 44-54)
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}
Source

pub fn spawn_at<B>(&mut self, e: Entity, bundle: B) -> bool
where 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);
Source

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?
examples/headless.rs (line 68)
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}
Source

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));
Source

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 again
Source

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?
examples/physics.rs (line 125)
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
Hide additional examples
examples/ants.rs (line 269)
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}
examples/streaming.rs (line 134)
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}
Source

pub fn is_alive(&self, e: Entity) -> bool

true if e hasn’t been despawned.

Source

pub fn len(&self) -> usize

Number of live entities.

Examples found in repository?
examples/ecs.rs (line 132)
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
Hide additional examples
examples/headless.rs (line 67)
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}
examples/bunnymark_ecs.rs (line 108)
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}
examples/boids.rs (line 130)
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}
Source

pub fn is_empty(&self) -> bool

true if there are no live entities.

Source

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?
examples/vehicle.rs (line 98)
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
Hide additional examples
examples/physics.rs (line 80)
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}
examples/skinning.rs (line 92)
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}
examples/physics3d.rs (line 176)
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}
examples/lockstep.rs (line 74)
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}
Source

pub fn get_mut<T>(&mut self, e: Entity) -> Option<&mut T>
where T: Component,

Mutable version of get.

Examples found in repository?
examples/skinning.rs (line 89)
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
Hide additional examples
examples/physics3d.rs (line 118)
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}
examples/lockstep.rs (line 70)
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}
examples/vehicle.rs (line 73)
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}
examples/battle.rs (line 328)
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}
examples/ants.rs (line 266)
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}
Source

pub fn has<T>(&self, e: Entity) -> bool
where T: Component,

true if e is alive and has component T.

Examples found in repository?
examples/physics.rs (line 62)
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}
Source

pub fn insert<T>(&mut self, e: Entity, value: T) -> bool
where T: Component,

Adds component value to e, replacing any existing T. Returns false (and drops value) if e is dead.

Examples found in repository?
examples/vehicle.rs (line 182)
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
Hide additional examples
examples/skinning.rs (line 229)
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}
Source

pub fn remove<T>(&mut self, e: Entity) -> Option<T>
where T: Component,

Takes component T off e and returns it.

Source

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?
examples/hotpatch.rs (line 18)
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
Hide additional examples
examples/tiles.rs (line 120)
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}
examples/3d.rs (line 26)
24fn spin(ctx: &mut Ctx) {
25    let dt = ctx.dt();
26    ctx.world.query::<(&mut Transform3D, &Spin)>().for_each(|(t, s)| {
27        t.rotation *= Quat::from_rotation_y(s.0 * dt);
28    });
29}
examples/ribbons.rs (line 9)
7fn fly(ctx: &mut Ctx) {
8    let t = ctx.time() as f32;
9    ctx.world.query::<(&mut Transform3D, &Orbiting)>().for_each(|(at, _)| {
10        at.position = vec3(t.cos() * 3.0, 1.0 + (t * 2.0).sin() * 0.5, t.sin() * 3.0);
11    });
12}
examples/physics.rs (line 65)
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}
examples/scene_viewer.rs (line 19)
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}
Source

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?
examples/battle.rs (line 512)
511fn keep_neighbors_close(ctx: &mut Ctx) {
512    ctx.world.sort_by::<Transform2D>(|t| spatial_key(t.position, 64.0));
513}
More examples
Hide additional examples
examples/boids.rs (line 34)
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}
Source

pub fn insert_resource<R>(&mut self, value: R)
where R: Send + Sync + 'static,

Stores a global value (game settings, a random generator, the score…), replacing any existing value of the same type.

Examples found in repository?
examples/ui.rs (lines 113-118)
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
Hide additional examples
examples/bunnymark_ecs.rs (lines 146-155)
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}
examples/navmesh3d.rs (line 26)
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}
examples/flow.rs (line 119)
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}
examples/ecs.rs (lines 111-114)
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}
examples/vehicle.rs (lines 171-175)
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}
Source

pub fn resource<R>(&self) -> &R
where R: Send + Sync + 'static,

The resource of type R.

§Panics

If there is none; use get_resource to check.

Examples found in repository?
examples/physics3d.rs (line 116)
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
Hide additional examples
examples/vehicle.rs (line 65)
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}
examples/ants.rs (line 286)
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}
examples/ui.rs (line 35)
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}
examples/flow.rs (line 165)
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}
examples/battle.rs (line 247)
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}
Source

pub fn resource_mut<R>(&mut self) -> &mut R
where R: Send + Sync + 'static,

Mutable version of resource.

Examples found in repository?
examples/ants.rs (line 206)
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}
More examples
Hide additional examples
examples/flow.rs (line 212)
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}
examples/ecs.rs (line 35)
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}
examples/bunnymark_ecs.rs (line 45)
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}
examples/3d.rs (line 44)
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}
examples/physics3d.rs (line 172)
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}
Source

pub fn get_resource<R>(&self) -> Option<&R>
where R: Send + Sync + 'static,

The resource of type R, if there is one.

Source

pub fn get_resource_mut<R>(&mut self) -> Option<&mut R>
where R: Send + Sync + 'static,

Mutable version of get_resource.

Source

pub fn remove_resource<R>(&mut self) -> Option<R>
where R: Send + Sync + 'static,

Removes and returns the resource of type R.

Examples found in repository?
examples/flow.rs (line 100)
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
Hide additional examples
examples/ants.rs (line 132)
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}
examples/battle.rs (line 248)
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}
Source

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.

Source

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.

Source

pub fn resource_layout_dynamic(&self, name: &str) -> Option<Layout>

The size and alignment of resource name, if it exists.

Source

pub fn remove_resource_dynamic(&mut self, name: &str) -> bool

Removes resource name; false if there was none.

Source

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.

Source

pub fn register<T>(&mut self) -> ComponentId
where T: Component,

The id of Rust component T, registering it on first use.

Source

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.

Source

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.

Source

pub fn component_by_name(&self, name: &str) -> Option<ComponentId>

Looks a component up by name (registered name, or Rust type name).

Source

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);
Source

pub fn component_info(&self, id: ComponentId) -> &ComponentInfo

What the world knows about component id.

§Panics

If id came from a different world.

Source

pub fn components(&self) -> &[ComponentInfo]

Every component type registered so far, indexed by ComponentId::index.

Source

pub fn archetypes(&self) -> &[Archetype]

The archetype tables, for tools such as the inspector.

Source

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());
Source

pub fn get_dynamic(&self, e: Entity, id: ComponentId) -> Option<*mut u8>

Pointer to component id of entity e, or None.

Source

pub unsafe fn spawn_dynamic( &mut self, components: &[(ComponentId, *const u8)], ) -> Entity

Spawns an entity from raw component values.

§Safety

Each pointer must point to a valid value of its component; the world takes ownership (the caller must not drop it). Ids must be from this world.

§Panics

If a component id appears twice.

Source

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.

Source

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.

Source

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.

Source

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 Default for World

Source§

fn default() -> World

Returns the “default value” for a type. Read more
Source§

impl WorldReflect for World

Source§

fn reflect<T: Component + Serialize + DeserializeOwned>(&mut self)

Makes component 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 more
Source§

unsafe fn reflect_fields(&mut self, id: ComponentId, fields: &[Field])

Makes a component that has no Rust type (one registered with 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 more
Source§

fn reflected_components(&self) -> Vec<&str>

Names of all reflected components, in registration order. Read more
Source§

fn get_json(&self, e: Entity, name: &str) -> Result<Value, ReflectError>

Component name of entity e, as JSON. Read more
Source§

fn set_json( &mut self, e: Entity, name: &str, value: Value, ) -> Result<(), ReflectError>

Writes component name of entity e from JSON, adding the component if the entity doesn’t have it. Read more
Source§

fn remove_json(&mut self, e: Entity, name: &str) -> Result<bool, ReflectError>

Removes component name from entity e. Returns whether it had it. Read more
Source§

fn entity_json(&self, e: Entity) -> Result<Value, ReflectError>

Every reflected component of entity e, as one JSON object keyed by component name. Components that aren’t reflected are left out. Read more
Source§

fn spawn_json(&mut self, components: Value) -> Result<Entity, ReflectError>

Spawns an entity from a JSON object keyed by component name, the format entity_json returns. On error nothing is left spawned. Read more
Source§

fn reflect_resource<R: Send + Sync + 'static + Serialize + DeserializeOwned>( &mut self, )

Makes resource R visible to reflection under its type name without the module path, like reflect does for components. Tools (agents through ponko-mcp, the control endpoint) can then read and change it, and world saves include it. Registering the same type again does nothing. Read more
Source§

fn reflected_resources(&self) -> Vec<&str>

Names of the reflected resources, in registration order (whether or not they’re in the world right now). Read more
Source§

fn resource_json(&self, name: &str) -> Result<Value, ReflectError>

A reflected resource as JSON. Read more
Source§

fn set_resource_json( &mut self, name: &str, value: Value, ) -> Result<(), ReflectError>

Writes a reflected resource from JSON, inserting it if it’s missing. For a resource that exists, an object changes only the fields it has, like set_json. Read more
Source§

unsafe fn reflect_resource_fields( &mut self, name: &str, layout: Layout, fields: &[Field], )

Makes resource 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 more
Source§

fn save(&self) -> Value

The whole world as JSON: every entity that has reflected components, with its id, sorted by id. Components that aren’t reflected are not saved; "not_saved" counts them by name, so nothing goes missing silently. Reflected resources (reflect_resource) are saved under "resources"; others are not. Read more
Source§

fn load(&mut self, save: &Value) -> Result<(), ReflectError>

Replaces every entity with those in save (from save), with the same ids, so components that refer to entities stay valid. Reflected resources in the save are written; all other resources stay. Everything is checked before anything changes: on error the world is untouched. Read more

Auto 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> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> Component for T
where T: Send + Sync + 'static,

Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

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

Convert 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)

Convert &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)

Convert &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 T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

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

Converts 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)

Converts &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)

Converts &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<T> for T

Source§

fn downcast(&self) -> &T

Source§

impl<T> DowncastSend for T
where T: Any + Send,

Source§

fn into_any_send(self: Box<T>) -> Box<dyn Any + Send>

Converts Box<Trait> (where Trait: DowncastSend) to Box<dyn Any + Send>, which can then be downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_sync(self: Box<T>) -> Box<dyn Any + Send + Sync>

Converts Box<Trait> (where Trait: DowncastSync) to Box<dyn Any + Send + Sync>, which can then be downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Converts Arc<Trait> (where Trait: DowncastSync) to Arc<Any>, which can then be downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<S, T> Duplex<S> for T
where T: FromSample<S> + ToSample<S>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<S> FromSample<S> for S

Source§

fn from_sample_(s: S) -> S

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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 more
Source§

impl<F, T> IntoSample<T> for F
where T: FromSample<F>,

Source§

fn into_sample(self) -> T

Source§

impl<T> MaybeSync for T
where T: Sync + ?Sized,

Source§

impl<T> MaybeSync for T
where T: Sync + ?Sized,

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<R, P> ReadPrimitive<R> for P
where R: Read + ReadEndian<P>, P: Default,

Source§

fn read_from_little_endian(read: &mut R) -> Result<Self, Error>

Read this value from the supplied reader. Same as ReadEndian::read_from_little_endian().
Source§

fn read_from_big_endian(read: &mut R) -> Result<Self, Error>

Read this value from the supplied reader. Same as ReadEndian::read_from_big_endian().
Source§

fn read_from_native_endian(read: &mut R) -> Result<Self, Error>

Read this value from the supplied reader. Same as ReadEndian::read_from_native_endian().
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, S> SimdFrom<T, S> for T
where S: Simd,

Source§

fn simd_from(_simd: S, value: T) -> T

Source§

impl<F, T, S> SimdInto<T, S> for F
where T: SimdFrom<F, S>, S: Simd,

Source§

fn simd_into(self, simd: S) -> T

Source§

impl<SS, SP> SupersetOf<SS> for SP
where SS: SubsetOf<SP>,

Source§

fn to_subset(&self) -> Option<SS>

The inverse inclusion map: attempts to construct self from the equivalent element of its superset. Read more
Source§

fn is_in_subset(&self) -> bool

Checks if self is actually part of its subset T (and can be converted to it).
Source§

fn to_subset_unchecked(&self) -> SS

Use with care! Same as self.to_subset but without any property checks. Always succeeds.
Source§

fn from_subset(element: &SS) -> SP

The inclusion map: converts self to the equivalent element of its superset.
Source§

impl<T, U> ToSample<U> for T
where U: FromSample<T>,

Source§

fn to_sample_(self) -> U

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

fn upcast(&self) -> Option<&T>

Source§

impl<T> WasmNotSend for T
where T: Send,

Source§

impl<T> WasmNotSendSync for T

Source§

impl<T> WasmNotSync for T
where T: Sync,

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more