pub struct TickContext<'a, G: Game> { /* private fields */ }Expand description
The work Game::tick may do: simulate, never draw.
Implementations§
Source§impl<'a, G: Game> TickContext<'a, G>
impl<'a, G: Game> TickContext<'a, G>
Sourcepub fn down<A: InputButtonAction>(&self, action: A) -> bool
pub fn down<A: InputButtonAction>(&self, action: A) -> bool
Whether action is held right now.
Every tick of one drawn frame reads the same controls.
Examples found in repository?
692 fn advance(&mut self, ctx: &mut TickContext<'_, Scene>) -> f32 {
693 let control = ctx.axis2(Move::Walk).clamp_length_max(1.0);
694 let turn = Quat::from_rotation_y(self.camera_yaw);
695 let heading = turn * Vec3::X * control.x + turn * Vec3::NEG_Z * control.y;
696 let dt = ctx.dt().as_secs_f32();
697 if let Some(direction) = heading.try_normalize() {
698 let wanted = direction.x.atan2(direction.z);
699 let turn = (wanted - self.elf_yaw + core::f32::consts::PI).rem_euclid(TAU)
700 - core::f32::consts::PI;
701 self.elf_yaw += turn.clamp(-TURN_RATE * dt, TURN_RATE * dt);
702 }
703 let cap = if ctx.down(Button::Run) { 1.0 } else { WALK_CAP };
704 self.elf_pos += heading * cap * ELF_SPEED * dt;
705 heading.length() * cap
706 }Sourcepub fn pressed<A: InputButtonAction>(&self, action: A) -> bool
pub fn pressed<A: InputButtonAction>(&self, action: A) -> bool
Whether action went down since the last frame whose ticks ran.
A frame that runs no ticks holds its edges for the ticks that follow, so every press is read by the ticks of exactly one frame.
Examples found in repository?
More examples
797 fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
798 self.elapsed += ctx.dt();
799 self.orbit.yaw += AUTO_TURN_RATE * ctx.dt().as_secs_f32();
800
801 if let Some(dialogue) = &mut self.dialogue {
802 dialogue.tick();
803 }
804 if ctx.pressed(Trigger::Close) {
805 self.dialogue = None;
806 self.hailed = None;
807 }
808 if ctx.pressed(Trigger::Sheet) {
809 self.sheet_open = !self.sheet_open;
810 }
811 if ctx.pressed(Trigger::Hail) && !ctx.ui_wants_pointer() {
812 self.handle_hail(ctx);
813 }
814 }474 fn handle_drag(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
475 // Read before the check below for the UI's own claim on the
476 // pointer, so a release over it still frees a source a drag moved
477 // there.
478 if ctx.released(Button::Select) {
479 self.dragging = None;
480 }
481 if ctx.ui_wants_pointer() {
482 return;
483 }
484 let ray = ctx
485 .last_camera()
486 .ray_through(ctx.pointer(), ctx.window_size());
487
488 if ctx.pressed(Button::Select) {
489 self.dragging = self.sources.iter().position(|source| {
490 ray.hit_sphere(source.position, SOURCE_PICK_RADIUS)
491 .is_some()
492 });
493 }
494
495 let Some(index) = self.dragging else {
496 return;
497 };
498 let Some(distance) = ray.hit_plane(ray::Plane {
499 point: Vec3::ZERO,
500 normal: Vec3::Y,
501 }) else {
502 return;
503 };
504 let hit = ray.at(distance);
505 let dropped =
506 Vec2::new(hit.x, hit.z).clamp(Vec2::splat(-PLAY_BOUND), Vec2::splat(PLAY_BOUND));
507 self.sources[index].position = Vec3::new(dropped.x, SOURCE_HEIGHT, dropped.y);
508 }730 fn tick_elf(&mut self, ctx: &mut TickContext<'_, Scene>) {
731 let grounded = matches!(
732 self.elf_animator.state(),
733 ElfState::Idle | ElfState::Locomotion
734 );
735
736 self.elf_input.speed = self.advance(ctx);
737 self.elf_input.attack = ctx.pressed(Button::Attack);
738 self.elf_input.jump = ctx.pressed(Button::Jump);
739 self.elf_input.dance = ctx.pressed(Button::Dance);
740
741 self.elf_input.near_seat = self.elf_pos.distance(SEAT_POSITION) < SEAT_INTERACT_RADIUS;
742 self.elf_input.interact = ctx.pressed(Button::Interact);
743 if self.elf_input.interact && grounded && self.elf_input.near_seat {
744 self.elf_pos = SEAT_SPOT;
745 self.elf_yaw = SEAT_FACING;
746 }
747
748 let underfoot = self.patch_underfoot();
749 let entered_patch = underfoot.is_some() && !self.in_patch;
750 self.in_patch = underfoot.is_some();
751 self.hits += u32::from(entered_patch);
752 self.elf_input.hit = entered_patch && self.hits < FATAL_HITS;
753 self.elf_input.dying = entered_patch && self.hits >= FATAL_HITS;
754 if entered_patch {
755 self.last_event = match self.elf_input.dying {
756 true => "elf died",
757 false => "elf hit",
758 };
759 }
760 }
761
762 /// Starts a new [`Animator`] over the elf's own state, its position and
763 /// hit count reset with it.
764 fn restart_elf(&mut self) {
765 self.elf_animator = Animator::new();
766 self.elf_pos = ELF_START;
767 self.elf_prev = ELF_START;
768 self.elf_yaw = 0.0;
769 self.elf_height = 0.0;
770 self.elf_height_prev = 0.0;
771 self.jump_speed = 0.0;
772 self.elf_input = ElfInput::default();
773 self.hits = 0;
774 self.in_patch = false;
775 self.last_event = "new elf started";
776 }
777
778 fn panel(&self, ctx: &mut FrameContext<'_, Scene>) {
779 let state = match self.elf_animator.state() {
780 ElfState::Idle => "idle",
781 ElfState::Locomotion if self.elf_input.speed > WALK_CAP => "running",
782 ElfState::Locomotion => "walking",
783 ElfState::Attack => "attacking",
784 ElfState::Hit => "hit",
785 ElfState::Death => "dead",
786 ElfState::SitDown => "sitting down",
787 ElfState::Sit => "sitting",
788 ElfState::StandUp => "standing up",
789 ElfState::Jump => "jumping",
790 ElfState::Dance => "dancing",
791 };
792 ctx.ui(|ui| {
793 egui::Frame::new()
794 .fill(egui::Color32::from_black_alpha(PANEL_BACKDROP))
795 .inner_margin(PANEL_PADDING)
796 .corner_radius(f32::from(PANEL_PADDING))
797 .show(ui, |ui| {
798 ui.heading(panel_text(format!("elf is {state}")));
799 ui.label(panel_text(format!(
800 "hits taken {} of the {} red patches hurt for, {}",
801 self.hits, FATAL_HITS, self.last_event
802 )));
803 ui.label(panel_text(match self.elf_animator.transitioning() {
804 true => "fading between clips",
805 false => "one clip playing",
806 }));
807 ui.add_space(f32::from(PANEL_PADDING));
808 egui::Grid::new("controls").show(ui, |ui| {
809 for (key, does) in CONTROLS {
810 ui.label(panel_text(key));
811 ui.label(panel_text(does));
812 ui.end_row();
813 }
814 });
815 });
816 });
817 }
818
819 /// A prompt over the seat, each hurt patch, and the scrubbed elf,
820 /// naming what a player finds there; the seat's own prompt names the
821 /// live binding of `Button::Interact` by its own name, not one fixed
822 /// in the code, and is absent while the elf sits on it.
823 fn draw_prompts(&self, ctx: &mut FrameContext<'_, Scene>, camera: Camera) {
824 let sit_key = ctx
825 .bindings(Button::Interact)
826 .into_iter()
827 .next()
828 .map_or_else(|| "interact".to_owned(), |binding| binding.to_string());
829 let sit = ctx.text_layout(
830 &format!("{sit_key} sits"),
831 egui::FontId::proportional(PROMPT_SIZE),
832 );
833 let hurts = ctx.text_layout("hurts", egui::FontId::proportional(PROMPT_SIZE));
834 let walk_closer = ctx.text_layout("walk closer", egui::FontId::proportional(PROMPT_SIZE));
835
836 let mut prompts = vec![(
837 SCRUBBED_ELF_POSITION + Vec3::Y * (ELF_HEIGHT + PROMPT_LIFT),
838 walk_closer,
839 )];
840 if !self.elf_animator.state().seated() {
841 prompts.push((
842 SEAT_POSITION + Vec3::Y * (SEAT_HEAD_HEIGHT + PROMPT_LIFT),
843 sit,
844 ));
845 }
846 prompts.extend(HURT_PATCHES.map(|patch| (patch + Vec3::Y * PROMPT_LIFT, hurts.clone())));
847
848 let window_size = ctx.window_size();
849 let pixels_per_point = ctx.pixels_per_point();
850 ctx.ui(|ui| {
851 let painter = ui.painter();
852 for (point, galley) in prompts {
853 let Some(pixel) = camera.pixel_of(point, window_size) else {
854 continue;
855 };
856 let at = logical(pixel, pixels_per_point);
857 let ink = galley.mesh_bounds;
858 let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
859 let backdrop = egui::Rect::from_center_size(
860 at,
861 ink.size() + egui::Vec2::splat(PROMPT_PADDING * 2.0),
862 );
863 painter.rect_filled(
864 backdrop,
865 PROMPT_PADDING,
866 egui::Color32::from_black_alpha(PANEL_BACKDROP),
867 );
868 painter.galley(pos, galley, PANEL_TEXT_COLOR);
869 }
870 });
871 }
872}
873
874impl Game for Scene {
875 type Meshes = Shape;
876 type Sounds = NoSounds;
877 type InputActions = Controls;
878 type Skyboxes = Sky;
879 type SurfaceStyles = NoSurfaceStyles;
880 type PostEffects = NoPostEffects;
881
882 fn tick(&mut self, ctx: &mut TickContext<'_, Scene>) {
883 self.elf_prev = self.elf_pos;
884 self.elf_height_prev = self.elf_height;
885
886 if ctx.pressed(Button::Restart) {
887 self.restart_elf();
888 }
889 if ctx.pressed(Button::Hold) {
890 self.holding = true;
891 }
892 if ctx.pressed(Button::Release) {
893 self.holding = false;
894 }
895
896 self.elf_input.landed = self.fall(ctx.dt().as_secs_f32());
897 self.tick_elf(ctx);
898 ctx.animate(Elf, &mut self.elf_animator, &self.elf_input);
899
900 if self.elf_animator.entered(ElfState::Jump) {
901 self.jump_speed = JUMP_LAUNCH_SPEED;
902 }
903 if self.elf_animator.left(ElfState::StandUp) {
904 self.last_event = "elf stood up";
905 }
906 if self.elf_animator.entered(ElfState::Sit) {
907 self.last_event = "elf sat down";
908 }
909 if self.elf_animator.entered(ElfState::Death) {
910 self.last_event = "elf died";
911 }
912
913 let scrubbed_input = ScrubbedInput {
914 settled: settled_at(self.elf_pos.distance(SCRUBBED_ELF_POSITION)),
915 };
916 ctx.animate(Elf, &mut self.scrubbed_animator, &scrubbed_input);
917 ctx.animate(Butterfly, &mut self.butterfly_animator, &());
918 }414 fn handle_click(&mut self, ctx: &mut TickContext<'_, Board>) {
415 if ctx.ui_wants_pointer() || !ctx.pressed(Button::Select) {
416 return;
417 }
418 let ray = ctx
419 .last_camera()
420 .ray_through(ctx.pointer(), ctx.window_size());
421 let (lift, half) = unit_geometry(self.turn);
422
423 if self.current().target.is_none() {
424 let center = self.current().position;
425 if ray.hit_aabb(center - half, center + half).is_some() {
426 self.selected = !self.selected;
427 return;
428 }
429 }
430 if !self.selected {
431 return;
432 }
433
434 let Some(distance) = ray.hit_plane(ray::Plane {
435 point: Vec3::ZERO,
436 normal: Vec3::Y,
437 }) else {
438 return;
439 };
440 let Some(tile) = tile_at(ray.at(distance)) else {
441 return;
442 };
443 if tile == self.current().tile || tile == self.other().tile {
444 return;
445 }
446
447 let destination = tile_center(tile) + Vec3::Y * lift;
448 let heading = destination.x - self.current().position.x;
449 let current = self.current_mut();
450 if heading.abs() > f32::EPSILON {
451 current.facing_right = heading > 0.0;
452 }
453 current.target = Some(destination);
454 self.selected = false;
455 ctx.play(Sound::Click);
456 }1155 fn tick_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1156 self.push_out_of(Self::cave_obstacles(self.door_obstacle()));
1157 self.position.x = self.position.x.clamp(-CAVE_HALF_WIDTH, CAVE_HALF_WIDTH);
1158 self.position.z = self.position.z.clamp(CAVE_WALK_FAR_Z, CAVE_WALK_NEAR_Z);
1159
1160 let target = if self.position.z < DOOR_WALL_NEAR_Z {
1161 1.0
1162 } else {
1163 0.0
1164 };
1165 let step = 1.0 / GHOST_RAMP_TICKS as f32;
1166 self.ghost += (target - self.ghost).clamp(-step, step);
1167
1168 if !self.door_opening
1169 && ctx.pressed(Button::Interact)
1170 && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS
1171 {
1172 self.door_opening = true;
1173 self.swing_ticks = 0;
1174 ctx.play(Sound::Interact);
1175 }
1176 if self.door_opening && self.swing_ticks < DOOR_SWING_TICKS {
1177 self.swing_ticks += 1;
1178 }
1179
1180 if !self.gem_taken && self.position.distance(GEM_POSITION) < PICKUP_RADIUS {
1181 self.gem_taken = true;
1182 ctx.play(Sound::Gem);
1183 ctx.save(Flag::GemTaken, true);
1184 ctx.save(Position::X, self.position.x as f64);
1185 ctx.save(Position::Z, self.position.z as f64);
1186 }
1187
1188 if EXIT.holds(self.position) {
1189 if EXIT.holds(self.previous) {
1190 self.position.z = self.previous.z;
1191 } else {
1192 self.exit_cave(ctx);
1193 }
1194 }
1195 }
1196
1197 /// Puts the player back at [`PLAYER_SPAWN`] with the cave and the gem
1198 /// returned to their saved fallbacks, all in this tick: a reset saves
1199 /// every key's own fallback, since there is nothing to clear it to.
1200 fn reset(&mut self, ctx: &mut TickContext<'_, Keep>) {
1201 ctx.save(Position::X, Position::X.fallback());
1202 ctx.save(Position::Z, Position::Z.fallback());
1203 ctx.save(Flag::InCave, Flag::InCave.fallback());
1204 ctx.save(Flag::GemTaken, Flag::GemTaken.fallback());
1205
1206 self.area = Area::Overworld;
1207 self.position = PLAYER_SPAWN;
1208 self.previous = PLAYER_SPAWN;
1209 self.gem_taken = false;
1210 self.door_opening = false;
1211 self.swing_ticks = 0;
1212 self.ghost = 0.0;
1213 }
1214
1215 /// Steps into the cave at [`CAVE_SPAWN`], saving the transition.
1216 fn enter_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1217 self.area = Area::Cave;
1218 self.position = CAVE_SPAWN;
1219 self.previous = CAVE_SPAWN;
1220 ctx.save(Flag::InCave, true);
1221 ctx.save(Position::X, CAVE_SPAWN.x as f64);
1222 ctx.save(Position::Z, CAVE_SPAWN.z as f64);
1223 }
1224
1225 /// Steps back out to the mouth at [`RETURN_SPAWN`], saving the
1226 /// transition.
1227 fn exit_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1228 self.area = Area::Overworld;
1229 self.position = RETURN_SPAWN;
1230 self.previous = RETURN_SPAWN;
1231 ctx.save(Flag::InCave, false);
1232 ctx.save(Position::X, RETURN_SPAWN.x as f64);
1233 ctx.save(Position::Z, RETURN_SPAWN.z as f64);
1234 }
1235
1236 fn draw_ground(&self, ctx: &mut FrameContext<'_, Keep>) {
1237 for col in -GROUND_DRAW_HALF..=GROUND_DRAW_HALF {
1238 for row in -GROUND_DRAW_HALF..=GROUND_DRAW_HALF {
1239 ctx.draw(
1240 Ground
1241 .at(Vec3::new(
1242 col as f32 * TILE_SIZE,
1243 0.0,
1244 row as f32 * TILE_SIZE,
1245 ))
1246 .frame(ground_cell(col, row)),
1247 );
1248 }
1249 }
1250 }
1251
1252 /// Two staggered rows of bushes around the clearing, open where the path
1253 /// leaves it, drawn between the camera and the ground's edge. The rows
1254 /// running along `Z` skip their two ends, which the rows running along
1255 /// `X` already cover.
1256 fn draw_hedgerow(&self, ctx: &mut FrameContext<'_, Keep>) {
1257 for (row, half) in [HEDGE_INNER_HALF, HEDGE_OUTER_HALF].into_iter().enumerate() {
1258 let row = row as i32;
1259 // The inner row covers both corners; the outer one is half a
1260 // span in from each, backing the gaps the inner row leaves.
1261 let spans = ((2.0 * half / HEDGE_STEP).round() as i32).max(1);
1262 let span = 2.0 * half / spans as f32;
1263 let steps = spans - row;
1264 for step in 0..=steps {
1265 let along = -half + (step as f32 + 0.5 * row as f32) * span;
1266 let scale = if (step + row) % 2 == 0 { 1.0 } else { 0.8 };
1267 let (width, height) = (BUSH_WIDTH * scale, BUSH_HEIGHT * scale);
1268 // The path leaves through the rows running along `X`, so only
1269 // those two open around it.
1270 let gated = along.abs() < HEDGE_GATE_HALF;
1271 let corner = step == 0 || step == steps;
1272 let places = [
1273 (along, -half, gated),
1274 (along, half, gated),
1275 (-half, along, corner),
1276 (half, along, corner),
1277 ];
1278 for (x, z, skip) in places {
1279 if skip {
1280 continue;
1281 }
1282 ctx.draw(
1283 Bush.at(Transform::from_scale_rotation_translation(
1284 Vec3::new(width, height, width),
1285 Quat::IDENTITY,
1286 Vec3::new(x, height * 0.5, z),
1287 ))
1288 .upright(),
1289 );
1290 }
1291 }
1292 }
1293 }
1294
1295 /// The pond: a square of styled water, and the shoreline sprite laid over
1296 /// it, which rings the open middle and hides the water's own edges.
1297 fn draw_pond(&self, ctx: &mut FrameContext<'_, Keep>) {
1298 ctx.draw(
1299 Plane
1300 .at(Transform::from_scale_rotation_translation(
1301 Vec3::splat(POND_WATER_HALF * 2.0),
1302 Quat::IDENTITY,
1303 POND_CENTER,
1304 ))
1305 .material(Material::shaded(WATER_COLOR, WATER_LITNESS))
1306 .surface_style::<Water>(),
1307 );
1308 ctx.draw(
1309 Shore
1310 .at(Transform::from_scale_rotation_translation(
1311 Vec3::splat(POND_HALF * 2.0),
1312 Quat::IDENTITY,
1313 Vec3::new(POND_CENTER.x, 0.0, POND_CENTER.z),
1314 ))
1315 .frame(Sheet::new(UVec2::new(POND_CELLS, 1)).cell(POND_SHORE_CELL)),
1316 );
1317 }
1318
1319 fn draw_crates(&self, ctx: &mut FrameContext<'_, Keep>) {
1320 for &(x, z, turn) in &CRATE_POSITIONS {
1321 ctx.draw(Crate.at(Transform::from_scale_rotation_translation(
1322 Vec3::splat(CRATE_SIZE),
1323 Quat::from_rotation_y(turn),
1324 Vec3::new(x, CRATE_SIZE * 0.5, z),
1325 )));
1326 }
1327 }
1328
1329 /// The well: its rim in grey masonry, and the mouth cell laid over the
1330 /// rim's top face.
1331 fn draw_well(&self, ctx: &mut FrameContext<'_, Keep>) {
1332 let cells = Sheet::new(UVec2::new(WELL_CELLS, 1));
1333 ctx.draw(
1334 Well.at(Transform::from_scale_rotation_translation(
1335 WELL_SIZE,
1336 Quat::IDENTITY,
1337 WELL_POSITION + Vec3::Y * (WELL_SIZE.y * 0.5),
1338 ))
1339 .frame(cells.cell(WELL_RIM_CELL)),
1340 );
1341 ctx.draw(
1342 WellMouth
1343 .at(Transform::from_scale_rotation_translation(
1344 Vec3::new(WELL_SIZE.x, 1.0, WELL_SIZE.z),
1345 Quat::IDENTITY,
1346 WELL_POSITION + Vec3::Y * (WELL_SIZE.y + WELL_MOUTH_LIFT),
1347 ))
1348 .frame(cells.cell(WELL_MOUTH_CELL)),
1349 );
1350 }
1351
1352 fn draw_flora(&self, ctx: &mut FrameContext<'_, Keep>) {
1353 for &(x, z, rock) in &FLORA {
1354 let (width, height) = if rock {
1355 (ROCK_WIDTH, ROCK_HEIGHT)
1356 } else {
1357 (BUSH_WIDTH, BUSH_HEIGHT)
1358 };
1359 let standing = Transform::from_scale_rotation_translation(
1360 Vec3::new(width, height, width),
1361 Quat::IDENTITY,
1362 Vec3::new(x, height * 0.5, z),
1363 );
1364 let flora: Instance<Shape, _> = if rock {
1365 Rock.at(standing).into_set()
1366 } else {
1367 Bush.at(standing).into_set()
1368 };
1369 ctx.draw(flora.upright());
1370 }
1371 }
1372
1373 /// One stone box drawn on the ground at `at`, `size` across, sampling
1374 /// the part of the sheet `frame` covers.
1375 fn draw_stone(ctx: &mut FrameContext<'_, Keep>, at: Vec3, size: Vec3, frame: Frame) {
1376 ctx.draw(
1377 Stone
1378 .at(Transform::from_scale_rotation_translation(
1379 size,
1380 Quat::IDENTITY,
1381 at + Vec3::Y * (size.y * 0.5),
1382 ))
1383 .frame(frame),
1384 );
1385 }
1386
1387 /// Two stone pillars drawn where `mouth` blocks the player, each a
1388 /// capital over its own course of masonry, and, on the one the camera
1389 /// looks into, the lintel across their tops and the dark filling
1390 /// the opening under it.
1391 fn draw_mouth(ctx: &mut FrameContext<'_, Keep>, mouth: Mouth) {
1392 for at in mouth.pillars() {
1393 Self::draw_stone(ctx, at, MOUTH_PILLAR_SIZE, Frame::default());
1394 }
1395 if !mouth.looked_into() {
1396 return;
1397 }
1398
1399 Self::draw_stone(
1400 ctx,
1401 mouth.at + Vec3::Y * MOUTH_PILLAR_SIZE.y,
1402 MOUTH_LINTEL_SIZE,
1403 masonry(MOUTH_LINTEL_TILES),
1404 );
1405 ctx.draw(
1406 Quad.at(Transform::from_scale_rotation_translation(
1407 Vec3::new(MOUTH_PILLAR_OFFSET * 2.0, MOUTH_DARK_HEIGHT, 1.0),
1408 Quat::IDENTITY,
1409 mouth.at + Vec3::Y * (MOUTH_DARK_HEIGHT * 0.5),
1410 ))
1411 .material(Material::color(Color::BLACK)),
1412 );
1413 }
1414
1415 fn draw_cave_floor(&self, ctx: &mut FrameContext<'_, Keep>) {
1416 let half = CAVE_HALF_WIDTH as i32;
1417 let near = CAVE_NEAR_Z as i32;
1418 let far = CAVE_FAR_Z as i32;
1419 for col in -half..=half {
1420 for row in far..=near {
1421 let variant = (col * 13 + row * 7).rem_euclid(CAVE_COLUMNS as i32) as u32;
1422 ctx.draw(
1423 CaveFloor
1424 .at(Vec3::new(
1425 col as f32 * TILE_SIZE,
1426 0.0,
1427 row as f32 * TILE_SIZE,
1428 ))
1429 .frame(
1430 Sheet::new(UVec2::new(CAVE_COLUMNS, CAVE_ROWS))
1431 .cell_at(UVec2::new(variant, CAVE_FLOOR_ROW)),
1432 ),
1433 );
1434 }
1435 }
1436 }
1437
1438 /// The wall drawn at `at` over the meters `standing`, in courses
1439 /// [`WALL_HEIGHT`] tall from the floor up, each cut to the part of it the
1440 /// span leaves; its faces are picked by `seed` and its stone faded to
1441 /// `fade`, which is `1.0` wherever it is solid.
1442 fn draw_wall(
1443 ctx: &mut FrameContext<'_, Keep>,
1444 at: Vec2,
1445 standing: Range<f32>,
1446 seed: i32,
1447 fade: f32,
1448 ) {
1449 for course in 0..WALL_COURSES {
1450 let base = course as f32 * WALL_HEIGHT;
1451 let low = (standing.start - base).max(0.0);
1452 let high = (standing.end - base).min(WALL_HEIGHT);
1453 if high <= low {
1454 continue;
1455 }
1456
1457 let variant = (seed + course).rem_euclid(CAVE_COLUMNS as i32) as u32;
1458 ctx.draw(
1459 CaveWall
1460 .at(Transform::from_scale_rotation_translation(
1461 Vec3::new(TILE_SIZE, high - low, TILE_SIZE),
1462 Quat::IDENTITY,
1463 Vec3::new(at.x, base + (low + high) * 0.5, at.y),
1464 ))
1465 .frame(cave_wall_face(variant, low..high))
1466 .faded(fade),
1467 );
1468 }
1469 }
1470
1471 /// The room's two side walls and its back wall, full height, and the low
1472 /// wall closing its near end between the side walls and the mouth. The
1473 /// back wall stops short of the corners the side walls already fill, and
1474 /// the near one leaves the mouth's own tile open.
1475 fn draw_cave_walls(&self, ctx: &mut FrameContext<'_, Keep>) {
1476 let half = CAVE_HALF_WIDTH as i32 + 1;
1477 let near = CAVE_NEAR_Z as i32;
1478 let far = CAVE_FAR_Z as i32;
1479
1480 for row in far..=near {
1481 let z = row as f32 * TILE_SIZE;
1482 let west = Vec2::new(-half as f32 * TILE_SIZE, z);
1483 let east = Vec2::new(half as f32 * TILE_SIZE, z);
1484 Self::draw_wall(ctx, west, 0.0..WALL_TOP, row * 5, SOLID);
1485 Self::draw_wall(ctx, east, 0.0..WALL_TOP, row * 5 + 1, SOLID);
1486 }
1487 for col in (-half + 1)..half {
1488 let x = col as f32 * TILE_SIZE;
1489 let back = Vec2::new(x, far as f32 * TILE_SIZE);
1490 Self::draw_wall(ctx, back, 0.0..WALL_TOP, col * 5 + 2, SOLID);
1491 if col != 0 {
1492 let lip = Vec2::new(x, CAVE_LIP_Z);
1493 Self::draw_wall(ctx, lip, 0.0..CAVE_LIP_HEIGHT, col * 5 + 4, SOLID);
1494 }
1495 }
1496 }
1497
1498 /// The wall the door hangs in, run across the room between the side walls
1499 /// with one tile left open on the room's axis for the doorway and stone
1500 /// filling the column over the door. A player behind the wall is drawn
1501 /// through the stacks between them and the camera, at `seen_through`,
1502 /// faded by `ghost`; the rest of it stays solid, and keeps casting.
1503 fn draw_door_wall(ctx: &mut FrameContext<'_, Keep>, seen_through: Option<f32>, ghost: f32) {
1504 let stone = |x: f32| match seen_through {
1505 Some(at) if (x - at).abs() < GHOST_CORRIDOR_HALF => ghost_alpha(ghost),
1506 _ => SOLID,
1507 };
1508 let half = CAVE_HALF_WIDTH as i32;
1509
1510 for col in (-half..=half).filter(|&col| col != 0) {
1511 let x = col as f32 * TILE_SIZE;
1512 Self::draw_wall(
1513 ctx,
1514 Vec2::new(x, DOOR_Z),
1515 0.0..WALL_TOP,
1516 col * 5 + 3,
1517 stone(x),
1518 );
1519 }
1520 Self::draw_wall(
1521 ctx,
1522 Vec2::new(0.0, DOOR_Z),
1523 DOOR_HEIGHT..WALL_TOP,
1524 3,
1525 stone(0.0),
1526 );
1527 }
1528
1529 /// The two torches: an upright cutout post apiece, the flame's loop
1530 /// burning over its binding, and the light that flame casts.
1531 fn draw_torches(&self, ctx: &mut FrameContext<'_, Keep>) {
1532 let elapsed = self.simulated.as_secs_f32();
1533 let loop_cells = Sheet::new(UVec2::new(FLAME_CELLS, 1));
1534
1535 for (index, &(x, z)) in TORCH_POSITIONS.iter().enumerate() {
1536 let base = Vec3::new(x, 0.0, z);
1537 ctx.draw(
1538 Torch
1539 .at(Transform::from_scale_rotation_translation(
1540 Vec3::new(TORCH_SPRITE_WIDTH, TORCH_STAND_HEIGHT, 1.0),
1541 Quat::IDENTITY,
1542 base + Vec3::Y * (TORCH_STAND_HEIGHT * 0.5),
1543 ))
1544 .upright(),
1545 );
1546
1547 let phase = index as f32 * 2.1;
1548 let flicker = (elapsed * FLAME_FLICKER_SPEED + phase).sin();
1549 let flame_pos =
1550 base + Vec3::Y * (TORCH_STAND_HEIGHT + FLAME_LIFT + flicker * FLAME_BOB);
1551
1552 let light_pos = flame_pos + Vec3::new(0.0, TORCH_LIGHT_LIFT, TORCH_LIGHT_STANDOFF);
1553 ctx.light(Light::point(light_pos, TORCH_LIGHT_COLOR, TORCH_LIGHT_RANGE).shadow());
1554 // The pair burn an even share of the loop apart.
1555 let offset = index as u32 * FLAME_CELLS / TORCH_POSITIONS.len() as u32;
1556 ctx.draw(
1557 Flame
1558 .at(Transform::from_scale_rotation_translation(
1559 Vec3::splat(FLAME_SIZE),
1560 Quat::IDENTITY,
1561 flame_pos,
1562 ))
1563 .billboard()
1564 .roll(flicker * FLAME_ROLL)
1565 .frame(loop_cells.cell((elapsed * FLAME_RATE) as u32 + offset)),
1566 );
1567 }
1568 }
1569
1570 /// The door at its hinge — swung back against the wall once opened —
1571 /// drawn through alongside its wall, faded by `ghost`.
1572 fn draw_door(&self, ctx: &mut FrameContext<'_, Keep>, ghost: f32) {
1573 let fade = ghost_alpha(ghost);
1574 let swung = if self.door_opening {
1575 Quat::from_rotation_y(core::f32::consts::FRAC_PI_2)
1576 } else {
1577 Quat::IDENTITY
1578 };
1579
1580 ctx.draw(
1581 Door.at(Transform::from_rotation_translation(swung, DOOR_HINGE))
1582 .material(Material::shaded(DOOR_COLOR, DOOR_LITNESS))
1583 .faded(fade),
1584 );
1585 }
1586
1587 /// The posts and lintel framing the doorway, in a color the stone never
1588 /// is, standing clear of the wall so the opening reads as a door from
1589 /// across the chamber. Glowing of their own while the door is closed and
1590 /// within [`INTERACT_RADIUS`], the cue that it opens.
1591 fn draw_door_frame(&self, ctx: &mut FrameContext<'_, Keep>, ghost: f32) {
1592 let reachable =
1593 !self.door_opening && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS;
1594 let material =
1595 Material::shaded(DOOR_FRAME_COLOR, DOOR_FRAME_LITNESS).emissive(if reachable {
1596 DOOR_FRAME_GLOW
1597 } else {
1598 Color::BLACK
1599 });
1600 let fade = ghost_alpha(ghost);
1601 let z = DOOR_WALL_NEAR_Z + DOOR_FRAME_STANDOFF;
1602 let jamb_height = DOOR_HEIGHT + DOOR_FRAME_THICKNESS;
1603
1604 for side in SIDES {
1605 ctx.draw(
1606 Cube.at(Transform::from_scale_rotation_translation(
1607 Vec3::new(DOOR_FRAME_THICKNESS, jamb_height, DOOR_FRAME_THICKNESS),
1608 Quat::IDENTITY,
1609 Vec3::new(
1610 side * (DOORWAY_HALF + DOOR_FRAME_THICKNESS * 0.5),
1611 jamb_height * 0.5,
1612 z,
1613 ),
1614 ))
1615 .material(material)
1616 .faded(fade),
1617 );
1618 }
1619 ctx.draw(
1620 Cube.at(Transform::from_scale_rotation_translation(
1621 Vec3::new(
1622 DOOR_WIDTH + DOOR_FRAME_THICKNESS * 2.0,
1623 DOOR_FRAME_THICKNESS,
1624 DOOR_FRAME_THICKNESS,
1625 ),
1626 Quat::IDENTITY,
1627 Vec3::new(0.0, DOOR_HEIGHT + DOOR_FRAME_THICKNESS * 0.5, z),
1628 ))
1629 .material(material)
1630 .faded(fade),
1631 );
1632 }
1633
1634 /// A world prompt over the door: what opens it while the player is
1635 /// within [`INTERACT_RADIUS`] and it is closed, and that it swings while
1636 /// it does; gone once it has swung [`DOOR_SWING_TICKS`]. Laid out and
1637 /// placed like `examples/animation.rs`'s own prompt.
1638 fn draw_door_prompt(&self, ctx: &mut FrameContext<'_, Keep>, camera: Camera) {
1639 let near = self.position.distance(INTERACT_POINT) < INTERACT_RADIUS;
1640 let swinging = self.door_opening && self.swing_ticks < DOOR_SWING_TICKS;
1641 let text = if swinging {
1642 "opening"
1643 } else if near && !self.door_opening {
1644 "e opens the door"
1645 } else {
1646 return;
1647 };
1648
1649 let galley = ctx.text_layout(text, egui::FontId::proportional(DOOR_PROMPT_SIZE));
1650 let point = INTERACT_POINT + Vec3::Y * (DOOR_HEIGHT + DOOR_PROMPT_LIFT);
1651 let window_size = ctx.window_size();
1652 let pixels_per_point = ctx.pixels_per_point();
1653 let Some(pixel) = camera.pixel_of(point, window_size) else {
1654 return;
1655 };
1656
1657 ctx.ui(|ui| {
1658 let painter = ui.painter();
1659 let at = logical(pixel, pixels_per_point);
1660 let ink = galley.mesh_bounds;
1661 let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
1662 let backdrop = egui::Rect::from_center_size(
1663 at,
1664 ink.size() + egui::Vec2::splat(DOOR_PROMPT_PADDING * 2.0),
1665 );
1666 painter.rect_filled(
1667 backdrop,
1668 DOOR_PROMPT_PADDING,
1669 egui::Color32::from_black_alpha(DOOR_PROMPT_BACKDROP),
1670 );
1671 painter.galley(pos, galley, DOOR_PROMPT_COLOR);
1672 });
1673 }
1674
1675 /// The gem, spinning and bobbing over the chamber's floor, and the light
1676 /// it casts over it.
1677 fn draw_gem(&self, ctx: &mut FrameContext<'_, Keep>) {
1678 let t = self.simulated.as_secs_f32();
1679 let bob = (t * 2.0).sin() * GEM_BOB_HEIGHT;
1680 ctx.light(
1681 Light::point(
1682 GEM_POSITION + Vec3::Y * (bob + GEM_LIGHT_LIFT),
1683 GEM_LIGHT_COLOR,
1684 GEM_LIGHT_RANGE,
1685 )
1686 .shadow(),
1687 );
1688 ctx.draw(
1689 Gem.at(Transform::from_scale_rotation_translation(
1690 Vec3::ONE,
1691 Quat::from_rotation_y(t * GEM_SPIN_SPEED),
1692 GEM_POSITION + Vec3::Y * bob,
1693 ))
1694 .material(Material::shaded(GEM_COLOR, 0.7).emissive(GEM_COLOR.dimmed(1.6))),
1695 );
1696 }
1697
1698 /// The player: upright so it always faces the camera about `+Y`,
1699 /// windowed to its facing's row and the walk cycle's current frame.
1700 fn draw_walker(&self, ctx: &mut FrameContext<'_, Keep>, ground: Vec3) {
1701 let step = if self.walk_ticks > 0 {
1702 (self.walk_ticks / TICKS_PER_WALK_FRAME) % WALKER_COLUMNS
1703 } else {
1704 0
1705 };
1706 let cell = Sheet::new(UVec2::new(WALKER_COLUMNS, WALKER_ROWS))
1707 .cell_at(UVec2::new(step, self.facing as u32));
1708 let size = Vec2::new(WALKER_WIDTH, WALKER_HEIGHT);
1709
1710 ctx.draw(
1711 Walker
1712 .at(Transform::from_scale_rotation_translation(
1713 size.extend(1.0),
1714 Quat::IDENTITY,
1715 ground + Vec3::Y * (WALKER_HEIGHT * 0.5),
1716 ))
1717 .upright()
1718 .frame(cell),
1719 );
1720 }
1721
1722 fn frame_overworld(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1723 let drawn_at = self.previous.lerp(self.position, ctx.alpha());
1724 ctx.set_camera(Self::camera(drawn_at, OVERWORLD_CAMERA_OFFSET));
1725 ctx.light(Light::directional(SUN_DIRECTION, SUN_COLOR).shadow());
1726
1727 self.draw_ground(ctx);
1728 self.draw_hedgerow(ctx);
1729 self.draw_pond(ctx);
1730 self.draw_crates(ctx);
1731 self.draw_well(ctx);
1732 self.draw_flora(ctx);
1733 Self::draw_mouth(ctx, ENTRANCE);
1734 self.draw_walker(ctx, drawn_at);
1735 }
1736
1737 fn frame_cave(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1738 let drawn_at = self.previous.lerp(self.position, ctx.alpha());
1739 let camera = Self::camera(drawn_at, CAVE_CAMERA_OFFSET);
1740 ctx.set_camera(camera);
1741
1742 self.draw_cave_floor(ctx);
1743 self.draw_cave_walls(ctx);
1744 Self::draw_door_wall(ctx, (self.ghost > 0.0).then_some(drawn_at.x), self.ghost);
1745 Self::draw_mouth(ctx, EXIT);
1746 self.draw_torches(ctx);
1747 self.draw_door(ctx, self.ghost);
1748 self.draw_door_frame(ctx, self.ghost);
1749 if !self.gem_taken {
1750 self.draw_gem(ctx);
1751 }
1752 self.draw_walker(ctx, drawn_at);
1753 self.draw_door_prompt(ctx, camera);
1754 }
1755
1756 /// Instructions and the door's interact hint — gathered before `ctx.ui`,
1757 /// which cannot read `ctx`.
1758 fn overlay(&mut self, ctx: &mut FrameContext<'_, Keep>) {
1759 let near_door = self.area == Area::Cave
1760 && !self.door_opening
1761 && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS;
1762 let gem_taken = self.area == Area::Cave && self.gem_taken;
1763 let mut reset_clicked = false;
1764
1765 ctx.ui(|ui| {
1766 egui::Frame::new()
1767 .fill(egui::Color32::from_black_alpha(HUD_BACKDROP))
1768 .inner_margin(HUD_PADDING)
1769 .corner_radius(f32::from(HUD_PADDING))
1770 .show(ui, |ui| {
1771 ui.visuals_mut().override_text_color = Some(egui::Color32::WHITE);
1772 ui.label("wasd / arrows / stick to walk");
1773 if near_door {
1774 ui.label("e / west button to open the door");
1775 }
1776 if gem_taken {
1777 ui.label("gem recovered");
1778 }
1779 ui.label("kept between runs: position, gem, cave");
1780 ui.label("r to reset world");
1781 if ui.button("reset world").clicked() {
1782 reset_clicked = true;
1783 }
1784 });
1785 });
1786
1787 if reset_clicked {
1788 self.reset_requested = true;
1789 }
1790 }
1791}
1792
1793impl Game for Keep {
1794 type Meshes = Shape;
1795 type Sounds = Sound;
1796 type InputActions = Controls;
1797 type Skyboxes = NoSkyboxes;
1798 type SurfaceStyles = Looks;
1799 type PostEffects = NoPostEffects;
1800
1801 fn tick(&mut self, ctx: &mut TickContext<'_, Keep>) {
1802 self.previous = self.position;
1803
1804 if self.reset_requested || ctx.pressed(Button::Reset) {
1805 self.reset_requested = false;
1806 self.reset(ctx);
1807 return;
1808 }
1809
1810 let heading = if ctx.ui_wants_keyboard() {
1811 Vec2::ZERO
1812 } else {
1813 ctx.axis2(Move::Walk)
1814 };
1815 match Facing::from_heading(heading) {
1816 Some(facing) => {
1817 self.facing = facing;
1818 self.walk_ticks += 1;
1819 }
1820 None => self.walk_ticks = 0,
1821 }
1822 let stride = Vec3::new(heading.x, 0.0, -heading.y) * WALK_SPEED * ctx.dt().as_secs_f32();
1823 self.position += stride;
1824 self.simulated += ctx.dt();
1825
1826 match self.area {
1827 Area::Overworld => self.tick_overworld(ctx),
1828 Area::Cave => self.tick_cave(ctx),
1829 }
1830 }Sourcepub fn released<A: InputButtonAction>(&self, action: A) -> bool
pub fn released<A: InputButtonAction>(&self, action: A) -> bool
Whether action came up since the last frame whose ticks ran.
A release is held for the ticks that follow, the same as a press.
Examples found in repository?
474 fn handle_drag(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
475 // Read before the check below for the UI's own claim on the
476 // pointer, so a release over it still frees a source a drag moved
477 // there.
478 if ctx.released(Button::Select) {
479 self.dragging = None;
480 }
481 if ctx.ui_wants_pointer() {
482 return;
483 }
484 let ray = ctx
485 .last_camera()
486 .ray_through(ctx.pointer(), ctx.window_size());
487
488 if ctx.pressed(Button::Select) {
489 self.dragging = self.sources.iter().position(|source| {
490 ray.hit_sphere(source.position, SOURCE_PICK_RADIUS)
491 .is_some()
492 });
493 }
494
495 let Some(index) = self.dragging else {
496 return;
497 };
498 let Some(distance) = ray.hit_plane(ray::Plane {
499 point: Vec3::ZERO,
500 normal: Vec3::Y,
501 }) else {
502 return;
503 };
504 let hit = ray.at(distance);
505 let dropped =
506 Vec2::new(hit.x, hit.z).clamp(Vec2::splat(-PLAY_BOUND), Vec2::splat(PLAY_BOUND));
507 self.sources[index].position = Vec3::new(dropped.x, SOURCE_HEIGHT, dropped.y);
508 }Sourcepub fn clicks<A: InputButtonAction>(&self, action: A) -> u32
pub fn clicks<A: InputButtonAction>(&self, action: A) -> u32
Presses of action in a row, counting the one these ticks read:
1 for a single click, 2 for a double, 0 where they read no
press; see FrameContext::clicks.
The ticks of one frame read one press however many landed in them, so a control pressed twice inside one frame reads one press of two clicks.
Sourcepub fn axis<A: InputAxisAction>(&self, action: A) -> f32
pub fn axis<A: InputAxisAction>(&self, action: A) -> f32
Analog reading of action: a fraction in -1..=1 from a pad axis,
a joystick axis or a button composite, of which a trigger reads
0..=1, and the scaled distance, which nothing clamps, from a
PointerDelta or
WheelDelta lane.
Read those two in Game::frame: the ticks of
one frame each read the whole distance that frame moved.
Examples found in repository?
965 fn tick(&mut self, ctx: &mut TickContext<'_, Breakout>) {
966 if self.paused {
967 return;
968 }
969
970 let dt = ctx.dt().as_secs_f32();
971 self.paddle_flash = (self.paddle_flash - dt).max(0.0);
972 self.brick_flash = (self.brick_flash - dt).max(0.0);
973 self.life_lost_flash = (self.life_lost_flash - dt).max(0.0);
974 self.step_sparks(dt);
975
976 // Decay runs before the end-screen return below, so the last pulse and
977 // burst do not stay on screen.
978 if matches!(self.phase, Phase::Won | Phase::Lost) {
979 return;
980 }
981
982 let axis = if ctx.ui_wants_keyboard() {
983 0.0
984 } else {
985 ctx.axis(Move::Paddle)
986 };
987 self.step_paddle(axis, dt);
988
989 match self.phase {
990 Phase::Serving => self.hold_ball(ctx),
991 _ => self.step_ball(ctx, dt),
992 }
993 }Sourcepub fn axis2<A: InputAxis2Action>(&self, action: A) -> Vec2
pub fn axis2<A: InputAxis2Action>(&self, action: A) -> Vec2
action’s reading: a vector no longer than 1 from a stick or a
button composite, and the scaled distance, which nothing clamps,
from Axis2Binding::pointer.
Read the pointer in Game::frame: the ticks
of one frame each read the whole distance that frame moved.
Examples found in repository?
More examples
692 fn advance(&mut self, ctx: &mut TickContext<'_, Scene>) -> f32 {
693 let control = ctx.axis2(Move::Walk).clamp_length_max(1.0);
694 let turn = Quat::from_rotation_y(self.camera_yaw);
695 let heading = turn * Vec3::X * control.x + turn * Vec3::NEG_Z * control.y;
696 let dt = ctx.dt().as_secs_f32();
697 if let Some(direction) = heading.try_normalize() {
698 let wanted = direction.x.atan2(direction.z);
699 let turn = (wanted - self.elf_yaw + core::f32::consts::PI).rem_euclid(TAU)
700 - core::f32::consts::PI;
701 self.elf_yaw += turn.clamp(-TURN_RATE * dt, TURN_RATE * dt);
702 }
703 let cap = if ctx.down(Button::Run) { 1.0 } else { WALK_CAP };
704 self.elf_pos += heading * cap * ELF_SPEED * dt;
705 heading.length() * cap
706 }1801 fn tick(&mut self, ctx: &mut TickContext<'_, Keep>) {
1802 self.previous = self.position;
1803
1804 if self.reset_requested || ctx.pressed(Button::Reset) {
1805 self.reset_requested = false;
1806 self.reset(ctx);
1807 return;
1808 }
1809
1810 let heading = if ctx.ui_wants_keyboard() {
1811 Vec2::ZERO
1812 } else {
1813 ctx.axis2(Move::Walk)
1814 };
1815 match Facing::from_heading(heading) {
1816 Some(facing) => {
1817 self.facing = facing;
1818 self.walk_ticks += 1;
1819 }
1820 None => self.walk_ticks = 0,
1821 }
1822 let stride = Vec3::new(heading.x, 0.0, -heading.y) * WALK_SPEED * ctx.dt().as_secs_f32();
1823 self.position += stride;
1824 self.simulated += ctx.dt();
1825
1826 match self.area {
1827 Area::Overworld => self.tick_overworld(ctx),
1828 Area::Cave => self.tick_cave(ctx),
1829 }
1830 }Sourcepub fn pointer(&self) -> Vec2
pub fn pointer(&self) -> Vec2
Pointer position, in physical pixels from the drawing area’s top left; the origin until it is first seen.
The mouse and the first touch share it, and
window_size is in the same pixels, so
Camera::ray_through takes it as it is.
Examples found in repository?
634 fn handle_hail(&mut self, ctx: &mut TickContext<'_, Self>) {
635 let ray = ctx
636 .last_camera()
637 .ray_through(ctx.pointer(), ctx.window_size());
638 let Some(station) = hit_station(ray) else {
639 return;
640 };
641
642 if self.hailed != Some(station) {
643 self.hailed = Some(station);
644 let look = station.look();
645 self.dialogue = Some(Dialogue::start(look.name, look.lines.map(str::to_owned)));
646 return;
647 }
648 let Some(dialogue) = &mut self.dialogue else {
649 return;
650 };
651 if !dialogue.advance() {
652 self.dialogue = None;
653 self.hailed = None;
654 }
655 }More examples
474 fn handle_drag(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
475 // Read before the check below for the UI's own claim on the
476 // pointer, so a release over it still frees a source a drag moved
477 // there.
478 if ctx.released(Button::Select) {
479 self.dragging = None;
480 }
481 if ctx.ui_wants_pointer() {
482 return;
483 }
484 let ray = ctx
485 .last_camera()
486 .ray_through(ctx.pointer(), ctx.window_size());
487
488 if ctx.pressed(Button::Select) {
489 self.dragging = self.sources.iter().position(|source| {
490 ray.hit_sphere(source.position, SOURCE_PICK_RADIUS)
491 .is_some()
492 });
493 }
494
495 let Some(index) = self.dragging else {
496 return;
497 };
498 let Some(distance) = ray.hit_plane(ray::Plane {
499 point: Vec3::ZERO,
500 normal: Vec3::Y,
501 }) else {
502 return;
503 };
504 let hit = ray.at(distance);
505 let dropped =
506 Vec2::new(hit.x, hit.z).clamp(Vec2::splat(-PLAY_BOUND), Vec2::splat(PLAY_BOUND));
507 self.sources[index].position = Vec3::new(dropped.x, SOURCE_HEIGHT, dropped.y);
508 }414 fn handle_click(&mut self, ctx: &mut TickContext<'_, Board>) {
415 if ctx.ui_wants_pointer() || !ctx.pressed(Button::Select) {
416 return;
417 }
418 let ray = ctx
419 .last_camera()
420 .ray_through(ctx.pointer(), ctx.window_size());
421 let (lift, half) = unit_geometry(self.turn);
422
423 if self.current().target.is_none() {
424 let center = self.current().position;
425 if ray.hit_aabb(center - half, center + half).is_some() {
426 self.selected = !self.selected;
427 return;
428 }
429 }
430 if !self.selected {
431 return;
432 }
433
434 let Some(distance) = ray.hit_plane(ray::Plane {
435 point: Vec3::ZERO,
436 normal: Vec3::Y,
437 }) else {
438 return;
439 };
440 let Some(tile) = tile_at(ray.at(distance)) else {
441 return;
442 };
443 if tile == self.current().tile || tile == self.other().tile {
444 return;
445 }
446
447 let destination = tile_center(tile) + Vec3::Y * lift;
448 let heading = destination.x - self.current().position.x;
449 let current = self.current_mut();
450 if heading.abs() > f32::EPSILON {
451 current.facing_right = heading > 0.0;
452 }
453 current.target = Some(destination);
454 self.selected = false;
455 ctx.play(Sound::Click);
456 }Sourcepub fn play(&mut self, sound: impl Into<SoundCue<G::Sounds>>)
pub fn play(&mut self, sound: impl Into<SoundCue<G::Sounds>>)
Plays sound once, keeping wherever it is placed as of this tick.
Every call is a voice of its own, so the same sound twice over is heard twice; the ticks of one frame are played together at the end of it.
Examples found in repository?
399 fn hold_ball(&mut self, ctx: &mut TickContext<'_, Breakout>) {
400 self.ball_prev = self.ball_pos;
401 self.ball_pos.x = self.paddle_x;
402 self.ball_trail = [self.ball_pos; TRAIL_LEN + 1];
403
404 if !ctx.ui_wants_keyboard() && ctx.pressed(Button::Serve) {
405 self.launch();
406 ctx.play(Sound::Serve);
407 }
408 }
409
410 fn step_paddle(&mut self, axis: f32, dt: f32) {
411 self.paddle_prev_x = self.paddle_x;
412 self.paddle_x =
413 (self.paddle_x + axis * PADDLE_SPEED * dt).clamp(-PADDLE_LIMIT, PADDLE_LIMIT);
414 }
415
416 fn step_ball(&mut self, ctx: &mut TickContext<'_, Breakout>, dt: f32) {
417 self.ball_prev = self.ball_pos;
418 self.ball_pos += self.ball_vel * dt;
419
420 self.bounce_walls(ctx);
421 self.bounce_paddle(ctx);
422 self.bounce_bricks(ctx);
423 self.push_trail();
424
425 if self.ball_pos.z - BALL_RADIUS > COURT_HALF_DEPTH {
426 self.lose_life(ctx);
427 }
428 }
429
430 /// Shifts the ghost trail back one slot and records the ball's newly
431 /// resolved position at the front.
432 fn push_trail(&mut self) {
433 self.ball_trail.rotate_right(1);
434 self.ball_trail[0] = self.ball_pos;
435 }
436
437 fn bounce_walls(&mut self, ctx: &mut TickContext<'_, Breakout>) {
438 let left = -COURT_HALF_WIDTH + WALL_THICKNESS;
439 let right = COURT_HALF_WIDTH - WALL_THICKNESS;
440 let top = -COURT_HALF_DEPTH + WALL_THICKNESS;
441
442 let mut hit = false;
443 if self.ball_pos.x - BALL_RADIUS < left {
444 self.ball_pos.x = left + BALL_RADIUS;
445 self.ball_vel.x = self.ball_vel.x.abs();
446 hit = true;
447 } else if self.ball_pos.x + BALL_RADIUS > right {
448 self.ball_pos.x = right - BALL_RADIUS;
449 self.ball_vel.x = -self.ball_vel.x.abs();
450 hit = true;
451 }
452
453 if self.ball_pos.z - BALL_RADIUS < top {
454 self.ball_pos.z = top + BALL_RADIUS;
455 self.ball_vel.z = self.ball_vel.z.abs();
456 hit = true;
457 }
458
459 if hit {
460 ctx.play(Sound::Bounce.pitch(WALL_BOUNCE_PITCH));
461 }
462 }
463
464 /// Bounces the ball off the paddle, steering it by where it landed.
465 fn bounce_paddle(&mut self, ctx: &mut TickContext<'_, Breakout>) {
466 if self.ball_vel.z <= 0.0 {
467 return;
468 }
469 let reach_x = PADDLE_HALF_WIDTH + BALL_RADIUS;
470 let reach_z = PADDLE_HALF_DEPTH + BALL_RADIUS;
471 let dx = self.ball_pos.x - self.paddle_x;
472 let dz = self.ball_pos.z - PADDLE_Z;
473 if dx.abs() > reach_x || dz.abs() > reach_z {
474 return;
475 }
476
477 let offset = (dx / PADDLE_HALF_WIDTH).clamp(-1.0, 1.0);
478 self.ball_vel = Vec3::new(offset, 0.0, -1.0).normalize() * BALL_SPEED;
479 self.ball_pos.z = PADDLE_Z - reach_z;
480 self.paddle_flash = PADDLE_FLASH;
481 ctx.play(Sound::Bounce.pitch(PADDLE_BOUNCE_PITCH));
482 }
483
484 /// Bounces the ball off the nearest overlapping brick, damaging it.
485 fn bounce_bricks(&mut self, ctx: &mut TickContext<'_, Breakout>) {
486 let reach_x = BRICK_HALF_WIDTH + BALL_RADIUS;
487 let reach_z = BRICK_HALF_DEPTH + BALL_RADIUS;
488 let mut broken = None;
489
490 for brick in self
491 .bricks
492 .iter_mut()
493 .filter(|brick| brick.hits_remaining > 0)
494 {
495 let dx = self.ball_pos.x - brick.position.x;
496 let dz = self.ball_pos.z - brick.position.z;
497 if dx.abs() > reach_x || dz.abs() > reach_z {
498 continue;
499 }
500
501 if reach_x - dx.abs() < reach_z - dz.abs() {
502 self.ball_vel.x = if dx < 0.0 {
503 -self.ball_vel.x.abs()
504 } else {
505 self.ball_vel.x.abs()
506 };
507 } else {
508 self.ball_vel.z = if dz < 0.0 {
509 -self.ball_vel.z.abs()
510 } else {
511 self.ball_vel.z.abs()
512 };
513 }
514
515 brick.hits_remaining -= 1;
516 self.score += 10 * (BRICK_ROWS - brick.row) as u32;
517 ctx.play(Sound::Bounce.pitch(BRICK_BOUNCE_PITCH));
518 if brick.hits_remaining == 0 {
519 ctx.play(
520 Sound::BrickBreak
521 .at(brick.position)
522 .reference(BRICK_BREAK_REFERENCE),
523 );
524 self.brick_flash = BRICK_FLASH;
525 broken = Some((brick.position, BRICK_ROW_COLORS[brick.row]));
526 }
527 break;
528 }
529
530 if let Some((position, color)) = broken {
531 self.spawn_sparks(position, color);
532 }
533
534 if self.bricks.iter().all(|brick| brick.hits_remaining == 0) {
535 self.phase = Phase::Won;
536 ctx.play(Sound::LevelClear);
537 }
538 }
539
540 /// Sends [`SPARK_BURST_COUNT`] sparks outward and upward from a broken
541 /// brick's position, spread by index so no randomness is needed.
542 fn spawn_sparks(&mut self, position: Vec3, color: Color) {
543 for i in 0..SPARK_BURST_COUNT {
544 let t = i as f32 / SPARK_BURST_COUNT as f32;
545 let azimuth = t * TAU;
546 let rise = 0.6 + 0.4 * (t * 3.0).fract();
547 let speed = SPARK_SPEED_MIN.lerp(SPARK_SPEED_MAX, (t * 5.0).fract());
548 let direction = Vec3::new(azimuth.cos(), rise, azimuth.sin()).normalize();
549 self.sparks.push(Spark {
550 position,
551 velocity: direction * speed,
552 roll: azimuth,
553 age: 0.0,
554 color,
555 });
556 }
557 }
558
559 /// Integrates and ages the live sparks, dropping any past their
560 /// lifetime.
561 fn step_sparks(&mut self, dt: f32) {
562 for spark in &mut self.sparks {
563 spark.velocity.y -= SPARK_GRAVITY * dt;
564 spark.position += spark.velocity * dt;
565 spark.age += dt;
566 }
567 self.sparks.retain(|spark| spark.age < SPARK_LIFETIME);
568 }
569
570 fn lose_life(&mut self, ctx: &mut TickContext<'_, Breakout>) {
571 self.life_lost_flash = LIFE_LOST_FLASH;
572 self.lives = self.lives.saturating_sub(1);
573 if self.lives == 0 {
574 self.phase = Phase::Lost;
575 ctx.play(Sound::GameOver);
576 } else {
577 ctx.play(Sound::BallLost);
578 self.ready_serve();
579 }
580 }More examples
414 fn handle_click(&mut self, ctx: &mut TickContext<'_, Board>) {
415 if ctx.ui_wants_pointer() || !ctx.pressed(Button::Select) {
416 return;
417 }
418 let ray = ctx
419 .last_camera()
420 .ray_through(ctx.pointer(), ctx.window_size());
421 let (lift, half) = unit_geometry(self.turn);
422
423 if self.current().target.is_none() {
424 let center = self.current().position;
425 if ray.hit_aabb(center - half, center + half).is_some() {
426 self.selected = !self.selected;
427 return;
428 }
429 }
430 if !self.selected {
431 return;
432 }
433
434 let Some(distance) = ray.hit_plane(ray::Plane {
435 point: Vec3::ZERO,
436 normal: Vec3::Y,
437 }) else {
438 return;
439 };
440 let Some(tile) = tile_at(ray.at(distance)) else {
441 return;
442 };
443 if tile == self.current().tile || tile == self.other().tile {
444 return;
445 }
446
447 let destination = tile_center(tile) + Vec3::Y * lift;
448 let heading = destination.x - self.current().position.x;
449 let current = self.current_mut();
450 if heading.abs() > f32::EPSILON {
451 current.facing_right = heading > 0.0;
452 }
453 current.target = Some(destination);
454 self.selected = false;
455 ctx.play(Sound::Click);
456 }1155 fn tick_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1156 self.push_out_of(Self::cave_obstacles(self.door_obstacle()));
1157 self.position.x = self.position.x.clamp(-CAVE_HALF_WIDTH, CAVE_HALF_WIDTH);
1158 self.position.z = self.position.z.clamp(CAVE_WALK_FAR_Z, CAVE_WALK_NEAR_Z);
1159
1160 let target = if self.position.z < DOOR_WALL_NEAR_Z {
1161 1.0
1162 } else {
1163 0.0
1164 };
1165 let step = 1.0 / GHOST_RAMP_TICKS as f32;
1166 self.ghost += (target - self.ghost).clamp(-step, step);
1167
1168 if !self.door_opening
1169 && ctx.pressed(Button::Interact)
1170 && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS
1171 {
1172 self.door_opening = true;
1173 self.swing_ticks = 0;
1174 ctx.play(Sound::Interact);
1175 }
1176 if self.door_opening && self.swing_ticks < DOOR_SWING_TICKS {
1177 self.swing_ticks += 1;
1178 }
1179
1180 if !self.gem_taken && self.position.distance(GEM_POSITION) < PICKUP_RADIUS {
1181 self.gem_taken = true;
1182 ctx.play(Sound::Gem);
1183 ctx.save(Flag::GemTaken, true);
1184 ctx.save(Position::X, self.position.x as f64);
1185 ctx.save(Position::Z, self.position.z as f64);
1186 }
1187
1188 if EXIT.holds(self.position) {
1189 if EXIT.holds(self.previous) {
1190 self.position.z = self.previous.z;
1191 } else {
1192 self.exit_cave(ctx);
1193 }
1194 }
1195 }Sourcepub fn saved<K: SaveKey>(&self, key: K) -> K::Value
pub fn saved<K: SaveKey>(&self, key: K) -> K::Value
The value the last run to save key kept, or its fallback where
none did, or where what was kept no longer reads as the key’s own
value, with a debug log.
Sourcepub fn save<K: SaveKey>(&mut self, key: K, value: K::Value)
pub fn save<K: SaveKey>(&mut self, key: K, value: K::Value)
Keeps value under key, for the rest of this run and the runs
after it.
The store is written once the frame these ticks belong to is drawn, and only where a value changed, so saving every tick costs nothing.
Examples found in repository?
1155 fn tick_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1156 self.push_out_of(Self::cave_obstacles(self.door_obstacle()));
1157 self.position.x = self.position.x.clamp(-CAVE_HALF_WIDTH, CAVE_HALF_WIDTH);
1158 self.position.z = self.position.z.clamp(CAVE_WALK_FAR_Z, CAVE_WALK_NEAR_Z);
1159
1160 let target = if self.position.z < DOOR_WALL_NEAR_Z {
1161 1.0
1162 } else {
1163 0.0
1164 };
1165 let step = 1.0 / GHOST_RAMP_TICKS as f32;
1166 self.ghost += (target - self.ghost).clamp(-step, step);
1167
1168 if !self.door_opening
1169 && ctx.pressed(Button::Interact)
1170 && self.position.distance(INTERACT_POINT) < INTERACT_RADIUS
1171 {
1172 self.door_opening = true;
1173 self.swing_ticks = 0;
1174 ctx.play(Sound::Interact);
1175 }
1176 if self.door_opening && self.swing_ticks < DOOR_SWING_TICKS {
1177 self.swing_ticks += 1;
1178 }
1179
1180 if !self.gem_taken && self.position.distance(GEM_POSITION) < PICKUP_RADIUS {
1181 self.gem_taken = true;
1182 ctx.play(Sound::Gem);
1183 ctx.save(Flag::GemTaken, true);
1184 ctx.save(Position::X, self.position.x as f64);
1185 ctx.save(Position::Z, self.position.z as f64);
1186 }
1187
1188 if EXIT.holds(self.position) {
1189 if EXIT.holds(self.previous) {
1190 self.position.z = self.previous.z;
1191 } else {
1192 self.exit_cave(ctx);
1193 }
1194 }
1195 }
1196
1197 /// Puts the player back at [`PLAYER_SPAWN`] with the cave and the gem
1198 /// returned to their saved fallbacks, all in this tick: a reset saves
1199 /// every key's own fallback, since there is nothing to clear it to.
1200 fn reset(&mut self, ctx: &mut TickContext<'_, Keep>) {
1201 ctx.save(Position::X, Position::X.fallback());
1202 ctx.save(Position::Z, Position::Z.fallback());
1203 ctx.save(Flag::InCave, Flag::InCave.fallback());
1204 ctx.save(Flag::GemTaken, Flag::GemTaken.fallback());
1205
1206 self.area = Area::Overworld;
1207 self.position = PLAYER_SPAWN;
1208 self.previous = PLAYER_SPAWN;
1209 self.gem_taken = false;
1210 self.door_opening = false;
1211 self.swing_ticks = 0;
1212 self.ghost = 0.0;
1213 }
1214
1215 /// Steps into the cave at [`CAVE_SPAWN`], saving the transition.
1216 fn enter_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1217 self.area = Area::Cave;
1218 self.position = CAVE_SPAWN;
1219 self.previous = CAVE_SPAWN;
1220 ctx.save(Flag::InCave, true);
1221 ctx.save(Position::X, CAVE_SPAWN.x as f64);
1222 ctx.save(Position::Z, CAVE_SPAWN.z as f64);
1223 }
1224
1225 /// Steps back out to the mouth at [`RETURN_SPAWN`], saving the
1226 /// transition.
1227 fn exit_cave(&mut self, ctx: &mut TickContext<'_, Keep>) {
1228 self.area = Area::Overworld;
1229 self.position = RETURN_SPAWN;
1230 self.previous = RETURN_SPAWN;
1231 ctx.save(Flag::InCave, false);
1232 ctx.save(Position::X, RETURN_SPAWN.x as f64);
1233 ctx.save(Position::Z, RETURN_SPAWN.z as f64);
1234 }Sourcepub fn sound_unlocked(&self) -> bool
pub fn sound_unlocked(&self) -> bool
Whether the platform allows sound to start right now; see
FrameContext::sound_unlocked.
Sourcepub fn animate<M, P, S>(
&mut self,
mesh: M,
animator: &mut Animator<M, S>,
input: &S::Input,
)
pub fn animate<M, P, S>( &mut self, mesh: M, animator: &mut Animator<M, S>, input: &S::Input, )
Runs animator up to this tick against input, over the clips
mesh holds: where its state goes from here, and what the state it
lands in plays.
Takes a mesh of Game::Meshes and a machine
typed by that mesh, so a machine runs on the clips of the value the
game draws and no other. Every tick of one drawn
frame runs at one instant, as every one of them reads one snapshot
of the controls, and the frame draws at that same instant.
Examples found in repository?
728 fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
729 let dt = ctx.dt().as_secs_f32();
730 let start = Instant::now();
731 self.butterflies
732 .step(dt, self.world, self.settings.sequential);
733 self.last_tick_ms = start.elapsed().as_secs_f32() * 1000.0;
734
735 self.flown += dt;
736 for (group, flap) in self.flaps.iter_mut().enumerate() {
737 ctx.animate(Butterfly, flap, &flap_phase(group, self.flown));
738 }
739 }More examples
882 fn tick(&mut self, ctx: &mut TickContext<'_, Scene>) {
883 self.elf_prev = self.elf_pos;
884 self.elf_height_prev = self.elf_height;
885
886 if ctx.pressed(Button::Restart) {
887 self.restart_elf();
888 }
889 if ctx.pressed(Button::Hold) {
890 self.holding = true;
891 }
892 if ctx.pressed(Button::Release) {
893 self.holding = false;
894 }
895
896 self.elf_input.landed = self.fall(ctx.dt().as_secs_f32());
897 self.tick_elf(ctx);
898 ctx.animate(Elf, &mut self.elf_animator, &self.elf_input);
899
900 if self.elf_animator.entered(ElfState::Jump) {
901 self.jump_speed = JUMP_LAUNCH_SPEED;
902 }
903 if self.elf_animator.left(ElfState::StandUp) {
904 self.last_event = "elf stood up";
905 }
906 if self.elf_animator.entered(ElfState::Sit) {
907 self.last_event = "elf sat down";
908 }
909 if self.elf_animator.entered(ElfState::Death) {
910 self.last_event = "elf died";
911 }
912
913 let scrubbed_input = ScrubbedInput {
914 settled: settled_at(self.elf_pos.distance(SCRUBBED_ELF_POSITION)),
915 };
916 ctx.animate(Elf, &mut self.scrubbed_animator, &scrubbed_input);
917 ctx.animate(Butterfly, &mut self.butterfly_animator, &());
918 }Sourcepub fn dt(&self) -> Duration
pub fn dt(&self) -> Duration
This tick’s fixed time step: Config::tick_interval until
set_tick_interval changes it.
Examples found in repository?
More examples
804 fn tick(&mut self, ctx: &mut TickContext<'_, Board>) {
805 self.sprite.previous = self.sprite.position;
806 self.block.previous = self.block.position;
807
808 self.handle_click(ctx);
809
810 if self.current_mut().advance(ctx.dt()) {
811 if let Some(tile) = tile_at(self.current().position) {
812 self.current_mut().tile = tile;
813 }
814 self.turn = self.turn.other();
815 self.selected = false;
816 }
817 }728 fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
729 let dt = ctx.dt().as_secs_f32();
730 let start = Instant::now();
731 self.butterflies
732 .step(dt, self.world, self.settings.sequential);
733 self.last_tick_ms = start.elapsed().as_secs_f32() * 1000.0;
734
735 self.flown += dt;
736 for (group, flap) in self.flaps.iter_mut().enumerate() {
737 ctx.animate(Butterfly, flap, &flap_phase(group, self.flown));
738 }
739 }797 fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
798 self.elapsed += ctx.dt();
799 self.orbit.yaw += AUTO_TURN_RATE * ctx.dt().as_secs_f32();
800
801 if let Some(dialogue) = &mut self.dialogue {
802 dialogue.tick();
803 }
804 if ctx.pressed(Trigger::Close) {
805 self.dialogue = None;
806 self.hailed = None;
807 }
808 if ctx.pressed(Trigger::Sheet) {
809 self.sheet_open = !self.sheet_open;
810 }
811 if ctx.pressed(Trigger::Hail) && !ctx.ui_wants_pointer() {
812 self.handle_hail(ctx);
813 }
814 }692 fn advance(&mut self, ctx: &mut TickContext<'_, Scene>) -> f32 {
693 let control = ctx.axis2(Move::Walk).clamp_length_max(1.0);
694 let turn = Quat::from_rotation_y(self.camera_yaw);
695 let heading = turn * Vec3::X * control.x + turn * Vec3::NEG_Z * control.y;
696 let dt = ctx.dt().as_secs_f32();
697 if let Some(direction) = heading.try_normalize() {
698 let wanted = direction.x.atan2(direction.z);
699 let turn = (wanted - self.elf_yaw + core::f32::consts::PI).rem_euclid(TAU)
700 - core::f32::consts::PI;
701 self.elf_yaw += turn.clamp(-TURN_RATE * dt, TURN_RATE * dt);
702 }
703 let cap = if ctx.down(Button::Run) { 1.0 } else { WALK_CAP };
704 self.elf_pos += heading * cap * ELF_SPEED * dt;
705 heading.length() * cap
706 }
707
708 /// Integrates [`Self::elf_height`] under [`GRAVITY`] from
709 /// [`Self::jump_speed`], held at the ground; `true` the tick it
710 /// returns there from above it.
711 fn fall(&mut self, dt: f32) -> bool {
712 let off_ground = self.elf_height > 0.0;
713 self.jump_speed -= GRAVITY * dt;
714 self.elf_height = (self.elf_height + self.jump_speed * dt).max(0.0);
715 if self.elf_height == 0.0 {
716 self.jump_speed = 0.0;
717 }
718 off_ground && self.elf_height == 0.0
719 }
720
721 /// The hurt patch `elf_pos` stands inside, if any.
722 fn patch_underfoot(&self) -> Option<Vec3> {
723 HURT_PATCHES
724 .into_iter()
725 .find(|&patch| self.elf_pos.distance(patch) < HURT_RADIUS)
726 }
727
728 /// Reads the controls and moves the elf, filling [`Self::elf_input`]
729 /// for [`ElfState`] to read.
730 fn tick_elf(&mut self, ctx: &mut TickContext<'_, Scene>) {
731 let grounded = matches!(
732 self.elf_animator.state(),
733 ElfState::Idle | ElfState::Locomotion
734 );
735
736 self.elf_input.speed = self.advance(ctx);
737 self.elf_input.attack = ctx.pressed(Button::Attack);
738 self.elf_input.jump = ctx.pressed(Button::Jump);
739 self.elf_input.dance = ctx.pressed(Button::Dance);
740
741 self.elf_input.near_seat = self.elf_pos.distance(SEAT_POSITION) < SEAT_INTERACT_RADIUS;
742 self.elf_input.interact = ctx.pressed(Button::Interact);
743 if self.elf_input.interact && grounded && self.elf_input.near_seat {
744 self.elf_pos = SEAT_SPOT;
745 self.elf_yaw = SEAT_FACING;
746 }
747
748 let underfoot = self.patch_underfoot();
749 let entered_patch = underfoot.is_some() && !self.in_patch;
750 self.in_patch = underfoot.is_some();
751 self.hits += u32::from(entered_patch);
752 self.elf_input.hit = entered_patch && self.hits < FATAL_HITS;
753 self.elf_input.dying = entered_patch && self.hits >= FATAL_HITS;
754 if entered_patch {
755 self.last_event = match self.elf_input.dying {
756 true => "elf died",
757 false => "elf hit",
758 };
759 }
760 }
761
762 /// Starts a new [`Animator`] over the elf's own state, its position and
763 /// hit count reset with it.
764 fn restart_elf(&mut self) {
765 self.elf_animator = Animator::new();
766 self.elf_pos = ELF_START;
767 self.elf_prev = ELF_START;
768 self.elf_yaw = 0.0;
769 self.elf_height = 0.0;
770 self.elf_height_prev = 0.0;
771 self.jump_speed = 0.0;
772 self.elf_input = ElfInput::default();
773 self.hits = 0;
774 self.in_patch = false;
775 self.last_event = "new elf started";
776 }
777
778 fn panel(&self, ctx: &mut FrameContext<'_, Scene>) {
779 let state = match self.elf_animator.state() {
780 ElfState::Idle => "idle",
781 ElfState::Locomotion if self.elf_input.speed > WALK_CAP => "running",
782 ElfState::Locomotion => "walking",
783 ElfState::Attack => "attacking",
784 ElfState::Hit => "hit",
785 ElfState::Death => "dead",
786 ElfState::SitDown => "sitting down",
787 ElfState::Sit => "sitting",
788 ElfState::StandUp => "standing up",
789 ElfState::Jump => "jumping",
790 ElfState::Dance => "dancing",
791 };
792 ctx.ui(|ui| {
793 egui::Frame::new()
794 .fill(egui::Color32::from_black_alpha(PANEL_BACKDROP))
795 .inner_margin(PANEL_PADDING)
796 .corner_radius(f32::from(PANEL_PADDING))
797 .show(ui, |ui| {
798 ui.heading(panel_text(format!("elf is {state}")));
799 ui.label(panel_text(format!(
800 "hits taken {} of the {} red patches hurt for, {}",
801 self.hits, FATAL_HITS, self.last_event
802 )));
803 ui.label(panel_text(match self.elf_animator.transitioning() {
804 true => "fading between clips",
805 false => "one clip playing",
806 }));
807 ui.add_space(f32::from(PANEL_PADDING));
808 egui::Grid::new("controls").show(ui, |ui| {
809 for (key, does) in CONTROLS {
810 ui.label(panel_text(key));
811 ui.label(panel_text(does));
812 ui.end_row();
813 }
814 });
815 });
816 });
817 }
818
819 /// A prompt over the seat, each hurt patch, and the scrubbed elf,
820 /// naming what a player finds there; the seat's own prompt names the
821 /// live binding of `Button::Interact` by its own name, not one fixed
822 /// in the code, and is absent while the elf sits on it.
823 fn draw_prompts(&self, ctx: &mut FrameContext<'_, Scene>, camera: Camera) {
824 let sit_key = ctx
825 .bindings(Button::Interact)
826 .into_iter()
827 .next()
828 .map_or_else(|| "interact".to_owned(), |binding| binding.to_string());
829 let sit = ctx.text_layout(
830 &format!("{sit_key} sits"),
831 egui::FontId::proportional(PROMPT_SIZE),
832 );
833 let hurts = ctx.text_layout("hurts", egui::FontId::proportional(PROMPT_SIZE));
834 let walk_closer = ctx.text_layout("walk closer", egui::FontId::proportional(PROMPT_SIZE));
835
836 let mut prompts = vec![(
837 SCRUBBED_ELF_POSITION + Vec3::Y * (ELF_HEIGHT + PROMPT_LIFT),
838 walk_closer,
839 )];
840 if !self.elf_animator.state().seated() {
841 prompts.push((
842 SEAT_POSITION + Vec3::Y * (SEAT_HEAD_HEIGHT + PROMPT_LIFT),
843 sit,
844 ));
845 }
846 prompts.extend(HURT_PATCHES.map(|patch| (patch + Vec3::Y * PROMPT_LIFT, hurts.clone())));
847
848 let window_size = ctx.window_size();
849 let pixels_per_point = ctx.pixels_per_point();
850 ctx.ui(|ui| {
851 let painter = ui.painter();
852 for (point, galley) in prompts {
853 let Some(pixel) = camera.pixel_of(point, window_size) else {
854 continue;
855 };
856 let at = logical(pixel, pixels_per_point);
857 let ink = galley.mesh_bounds;
858 let pos = egui::pos2(at.x - ink.center().x, at.y - ink.center().y);
859 let backdrop = egui::Rect::from_center_size(
860 at,
861 ink.size() + egui::Vec2::splat(PROMPT_PADDING * 2.0),
862 );
863 painter.rect_filled(
864 backdrop,
865 PROMPT_PADDING,
866 egui::Color32::from_black_alpha(PANEL_BACKDROP),
867 );
868 painter.galley(pos, galley, PANEL_TEXT_COLOR);
869 }
870 });
871 }
872}
873
874impl Game for Scene {
875 type Meshes = Shape;
876 type Sounds = NoSounds;
877 type InputActions = Controls;
878 type Skyboxes = Sky;
879 type SurfaceStyles = NoSurfaceStyles;
880 type PostEffects = NoPostEffects;
881
882 fn tick(&mut self, ctx: &mut TickContext<'_, Scene>) {
883 self.elf_prev = self.elf_pos;
884 self.elf_height_prev = self.elf_height;
885
886 if ctx.pressed(Button::Restart) {
887 self.restart_elf();
888 }
889 if ctx.pressed(Button::Hold) {
890 self.holding = true;
891 }
892 if ctx.pressed(Button::Release) {
893 self.holding = false;
894 }
895
896 self.elf_input.landed = self.fall(ctx.dt().as_secs_f32());
897 self.tick_elf(ctx);
898 ctx.animate(Elf, &mut self.elf_animator, &self.elf_input);
899
900 if self.elf_animator.entered(ElfState::Jump) {
901 self.jump_speed = JUMP_LAUNCH_SPEED;
902 }
903 if self.elf_animator.left(ElfState::StandUp) {
904 self.last_event = "elf stood up";
905 }
906 if self.elf_animator.entered(ElfState::Sit) {
907 self.last_event = "elf sat down";
908 }
909 if self.elf_animator.entered(ElfState::Death) {
910 self.last_event = "elf died";
911 }
912
913 let scrubbed_input = ScrubbedInput {
914 settled: settled_at(self.elf_pos.distance(SCRUBBED_ELF_POSITION)),
915 };
916 ctx.animate(Elf, &mut self.scrubbed_animator, &scrubbed_input);
917 ctx.animate(Butterfly, &mut self.butterfly_animator, &());
918 }965 fn tick(&mut self, ctx: &mut TickContext<'_, Breakout>) {
966 if self.paused {
967 return;
968 }
969
970 let dt = ctx.dt().as_secs_f32();
971 self.paddle_flash = (self.paddle_flash - dt).max(0.0);
972 self.brick_flash = (self.brick_flash - dt).max(0.0);
973 self.life_lost_flash = (self.life_lost_flash - dt).max(0.0);
974 self.step_sparks(dt);
975
976 // Decay runs before the end-screen return below, so the last pulse and
977 // burst do not stay on screen.
978 if matches!(self.phase, Phase::Won | Phase::Lost) {
979 return;
980 }
981
982 let axis = if ctx.ui_wants_keyboard() {
983 0.0
984 } else {
985 ctx.axis(Move::Paddle)
986 };
987 self.step_paddle(axis, dt);
988
989 match self.phase {
990 Phase::Serving => self.hold_ball(ctx),
991 _ => self.step_ball(ctx, dt),
992 }
993 }Sourcepub fn set_tick_interval(&mut self, interval: Duration)
pub fn set_tick_interval(&mut self, interval: Duration)
Sets the simulated time every later tick covers; see
FrameContext::set_tick_interval.
Sourcepub fn close(&mut self)
pub fn close(&mut self)
Ends the run once the frame these ticks belong to is drawn; see
FrameContext::close.
Sourcepub fn ui_wants_pointer(&self) -> bool
pub fn ui_wants_pointer(&self) -> bool
Whether the UI took the pointer last frame. Always false without the
ui feature.
Examples found in repository?
797 fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
798 self.elapsed += ctx.dt();
799 self.orbit.yaw += AUTO_TURN_RATE * ctx.dt().as_secs_f32();
800
801 if let Some(dialogue) = &mut self.dialogue {
802 dialogue.tick();
803 }
804 if ctx.pressed(Trigger::Close) {
805 self.dialogue = None;
806 self.hailed = None;
807 }
808 if ctx.pressed(Trigger::Sheet) {
809 self.sheet_open = !self.sheet_open;
810 }
811 if ctx.pressed(Trigger::Hail) && !ctx.ui_wants_pointer() {
812 self.handle_hail(ctx);
813 }
814 }More examples
474 fn handle_drag(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
475 // Read before the check below for the UI's own claim on the
476 // pointer, so a release over it still frees a source a drag moved
477 // there.
478 if ctx.released(Button::Select) {
479 self.dragging = None;
480 }
481 if ctx.ui_wants_pointer() {
482 return;
483 }
484 let ray = ctx
485 .last_camera()
486 .ray_through(ctx.pointer(), ctx.window_size());
487
488 if ctx.pressed(Button::Select) {
489 self.dragging = self.sources.iter().position(|source| {
490 ray.hit_sphere(source.position, SOURCE_PICK_RADIUS)
491 .is_some()
492 });
493 }
494
495 let Some(index) = self.dragging else {
496 return;
497 };
498 let Some(distance) = ray.hit_plane(ray::Plane {
499 point: Vec3::ZERO,
500 normal: Vec3::Y,
501 }) else {
502 return;
503 };
504 let hit = ray.at(distance);
505 let dropped =
506 Vec2::new(hit.x, hit.z).clamp(Vec2::splat(-PLAY_BOUND), Vec2::splat(PLAY_BOUND));
507 self.sources[index].position = Vec3::new(dropped.x, SOURCE_HEIGHT, dropped.y);
508 }414 fn handle_click(&mut self, ctx: &mut TickContext<'_, Board>) {
415 if ctx.ui_wants_pointer() || !ctx.pressed(Button::Select) {
416 return;
417 }
418 let ray = ctx
419 .last_camera()
420 .ray_through(ctx.pointer(), ctx.window_size());
421 let (lift, half) = unit_geometry(self.turn);
422
423 if self.current().target.is_none() {
424 let center = self.current().position;
425 if ray.hit_aabb(center - half, center + half).is_some() {
426 self.selected = !self.selected;
427 return;
428 }
429 }
430 if !self.selected {
431 return;
432 }
433
434 let Some(distance) = ray.hit_plane(ray::Plane {
435 point: Vec3::ZERO,
436 normal: Vec3::Y,
437 }) else {
438 return;
439 };
440 let Some(tile) = tile_at(ray.at(distance)) else {
441 return;
442 };
443 if tile == self.current().tile || tile == self.other().tile {
444 return;
445 }
446
447 let destination = tile_center(tile) + Vec3::Y * lift;
448 let heading = destination.x - self.current().position.x;
449 let current = self.current_mut();
450 if heading.abs() > f32::EPSILON {
451 current.facing_right = heading > 0.0;
452 }
453 current.target = Some(destination);
454 self.selected = false;
455 ctx.play(Sound::Click);
456 }Sourcepub fn ui_wants_keyboard(&self) -> bool
pub fn ui_wants_keyboard(&self) -> bool
Whether the UI took the keyboard last frame. Always false without the
ui feature.
Examples found in repository?
399 fn hold_ball(&mut self, ctx: &mut TickContext<'_, Breakout>) {
400 self.ball_prev = self.ball_pos;
401 self.ball_pos.x = self.paddle_x;
402 self.ball_trail = [self.ball_pos; TRAIL_LEN + 1];
403
404 if !ctx.ui_wants_keyboard() && ctx.pressed(Button::Serve) {
405 self.launch();
406 ctx.play(Sound::Serve);
407 }
408 }
409
410 fn step_paddle(&mut self, axis: f32, dt: f32) {
411 self.paddle_prev_x = self.paddle_x;
412 self.paddle_x =
413 (self.paddle_x + axis * PADDLE_SPEED * dt).clamp(-PADDLE_LIMIT, PADDLE_LIMIT);
414 }
415
416 fn step_ball(&mut self, ctx: &mut TickContext<'_, Breakout>, dt: f32) {
417 self.ball_prev = self.ball_pos;
418 self.ball_pos += self.ball_vel * dt;
419
420 self.bounce_walls(ctx);
421 self.bounce_paddle(ctx);
422 self.bounce_bricks(ctx);
423 self.push_trail();
424
425 if self.ball_pos.z - BALL_RADIUS > COURT_HALF_DEPTH {
426 self.lose_life(ctx);
427 }
428 }
429
430 /// Shifts the ghost trail back one slot and records the ball's newly
431 /// resolved position at the front.
432 fn push_trail(&mut self) {
433 self.ball_trail.rotate_right(1);
434 self.ball_trail[0] = self.ball_pos;
435 }
436
437 fn bounce_walls(&mut self, ctx: &mut TickContext<'_, Breakout>) {
438 let left = -COURT_HALF_WIDTH + WALL_THICKNESS;
439 let right = COURT_HALF_WIDTH - WALL_THICKNESS;
440 let top = -COURT_HALF_DEPTH + WALL_THICKNESS;
441
442 let mut hit = false;
443 if self.ball_pos.x - BALL_RADIUS < left {
444 self.ball_pos.x = left + BALL_RADIUS;
445 self.ball_vel.x = self.ball_vel.x.abs();
446 hit = true;
447 } else if self.ball_pos.x + BALL_RADIUS > right {
448 self.ball_pos.x = right - BALL_RADIUS;
449 self.ball_vel.x = -self.ball_vel.x.abs();
450 hit = true;
451 }
452
453 if self.ball_pos.z - BALL_RADIUS < top {
454 self.ball_pos.z = top + BALL_RADIUS;
455 self.ball_vel.z = self.ball_vel.z.abs();
456 hit = true;
457 }
458
459 if hit {
460 ctx.play(Sound::Bounce.pitch(WALL_BOUNCE_PITCH));
461 }
462 }
463
464 /// Bounces the ball off the paddle, steering it by where it landed.
465 fn bounce_paddle(&mut self, ctx: &mut TickContext<'_, Breakout>) {
466 if self.ball_vel.z <= 0.0 {
467 return;
468 }
469 let reach_x = PADDLE_HALF_WIDTH + BALL_RADIUS;
470 let reach_z = PADDLE_HALF_DEPTH + BALL_RADIUS;
471 let dx = self.ball_pos.x - self.paddle_x;
472 let dz = self.ball_pos.z - PADDLE_Z;
473 if dx.abs() > reach_x || dz.abs() > reach_z {
474 return;
475 }
476
477 let offset = (dx / PADDLE_HALF_WIDTH).clamp(-1.0, 1.0);
478 self.ball_vel = Vec3::new(offset, 0.0, -1.0).normalize() * BALL_SPEED;
479 self.ball_pos.z = PADDLE_Z - reach_z;
480 self.paddle_flash = PADDLE_FLASH;
481 ctx.play(Sound::Bounce.pitch(PADDLE_BOUNCE_PITCH));
482 }
483
484 /// Bounces the ball off the nearest overlapping brick, damaging it.
485 fn bounce_bricks(&mut self, ctx: &mut TickContext<'_, Breakout>) {
486 let reach_x = BRICK_HALF_WIDTH + BALL_RADIUS;
487 let reach_z = BRICK_HALF_DEPTH + BALL_RADIUS;
488 let mut broken = None;
489
490 for brick in self
491 .bricks
492 .iter_mut()
493 .filter(|brick| brick.hits_remaining > 0)
494 {
495 let dx = self.ball_pos.x - brick.position.x;
496 let dz = self.ball_pos.z - brick.position.z;
497 if dx.abs() > reach_x || dz.abs() > reach_z {
498 continue;
499 }
500
501 if reach_x - dx.abs() < reach_z - dz.abs() {
502 self.ball_vel.x = if dx < 0.0 {
503 -self.ball_vel.x.abs()
504 } else {
505 self.ball_vel.x.abs()
506 };
507 } else {
508 self.ball_vel.z = if dz < 0.0 {
509 -self.ball_vel.z.abs()
510 } else {
511 self.ball_vel.z.abs()
512 };
513 }
514
515 brick.hits_remaining -= 1;
516 self.score += 10 * (BRICK_ROWS - brick.row) as u32;
517 ctx.play(Sound::Bounce.pitch(BRICK_BOUNCE_PITCH));
518 if brick.hits_remaining == 0 {
519 ctx.play(
520 Sound::BrickBreak
521 .at(brick.position)
522 .reference(BRICK_BREAK_REFERENCE),
523 );
524 self.brick_flash = BRICK_FLASH;
525 broken = Some((brick.position, BRICK_ROW_COLORS[brick.row]));
526 }
527 break;
528 }
529
530 if let Some((position, color)) = broken {
531 self.spawn_sparks(position, color);
532 }
533
534 if self.bricks.iter().all(|brick| brick.hits_remaining == 0) {
535 self.phase = Phase::Won;
536 ctx.play(Sound::LevelClear);
537 }
538 }
539
540 /// Sends [`SPARK_BURST_COUNT`] sparks outward and upward from a broken
541 /// brick's position, spread by index so no randomness is needed.
542 fn spawn_sparks(&mut self, position: Vec3, color: Color) {
543 for i in 0..SPARK_BURST_COUNT {
544 let t = i as f32 / SPARK_BURST_COUNT as f32;
545 let azimuth = t * TAU;
546 let rise = 0.6 + 0.4 * (t * 3.0).fract();
547 let speed = SPARK_SPEED_MIN.lerp(SPARK_SPEED_MAX, (t * 5.0).fract());
548 let direction = Vec3::new(azimuth.cos(), rise, azimuth.sin()).normalize();
549 self.sparks.push(Spark {
550 position,
551 velocity: direction * speed,
552 roll: azimuth,
553 age: 0.0,
554 color,
555 });
556 }
557 }
558
559 /// Integrates and ages the live sparks, dropping any past their
560 /// lifetime.
561 fn step_sparks(&mut self, dt: f32) {
562 for spark in &mut self.sparks {
563 spark.velocity.y -= SPARK_GRAVITY * dt;
564 spark.position += spark.velocity * dt;
565 spark.age += dt;
566 }
567 self.sparks.retain(|spark| spark.age < SPARK_LIFETIME);
568 }
569
570 fn lose_life(&mut self, ctx: &mut TickContext<'_, Breakout>) {
571 self.life_lost_flash = LIFE_LOST_FLASH;
572 self.lives = self.lives.saturating_sub(1);
573 if self.lives == 0 {
574 self.phase = Phase::Lost;
575 ctx.play(Sound::GameOver);
576 } else {
577 ctx.play(Sound::BallLost);
578 self.ready_serve();
579 }
580 }
581
582 fn camera() -> Camera {
583 Camera::new(
584 View::look_at(Vec3::new(0.0, 13.5, 12.5), Vec3::new(0.0, 0.0, 0.5)),
585 Projection::perspective(50.0),
586 )
587 }
588
589 /// The paddle's face material: fully lit by the court's own lights, its
590 /// base tone flashed with emissive light past `1.0` just after it last
591 /// hit the ball, and a small emissive kept under that so it stays
592 /// visible where the ball's own light does not reach.
593 fn paddle_face_material(&self) -> Material {
594 let t = (self.paddle_flash / PADDLE_FLASH).clamp(0.0, 1.0);
595 let flash = PADDLE_FLASH_EMISSIVE.dimmed(t);
596 let emissive = Color::rgb(
597 PADDLE_AMBIENT_EMISSIVE.red + flash.red,
598 PADDLE_AMBIENT_EMISSIVE.green + flash.green,
599 PADDLE_AMBIENT_EMISSIVE.blue + flash.blue,
600 );
601 Material::lit(PADDLE_BASE).emissive(emissive)
602 }
603
604 fn draw_court(&self, ctx: &mut FrameContext<'_, Breakout>) {
605 ctx.draw(
606 Plane
607 .at(Transform::from_scale(Vec3::new(
608 COURT_HALF_WIDTH * 2.0,
609 1.0,
610 COURT_HALF_DEPTH * 2.0,
611 )))
612 .material(Material::lit(FLOOR_COLOR)),
613 );
614
615 let side_half = Vec3::new(WALL_THICKNESS * 0.5, WALL_HEIGHT * 0.5, COURT_HALF_DEPTH);
616 for side in [-1.0, 1.0] {
617 let x = side * (COURT_HALF_WIDTH - WALL_THICKNESS * 0.5);
618 ctx.draw(
619 Cube.at(Transform::from_scale_rotation_translation(
620 side_half * 2.0,
621 Quat::IDENTITY,
622 Vec3::new(x, side_half.y, 0.0),
623 ))
624 .material(Material::lit(WALL_COLOR)),
625 );
626 }
627
628 let top_half = Vec3::new(COURT_HALF_WIDTH, WALL_HEIGHT * 0.5, WALL_THICKNESS * 0.5);
629 ctx.draw(
630 Cube.at(Transform::from_scale_rotation_translation(
631 top_half * 2.0,
632 Quat::IDENTITY,
633 Vec3::new(0.0, top_half.y, -COURT_HALF_DEPTH + WALL_THICKNESS * 0.5),
634 ))
635 .material(Material::lit(WALL_COLOR)),
636 );
637 }
638
639 fn draw_bricks(&self, ctx: &mut FrameContext<'_, Breakout>) {
640 let scale = Vec3::new(
641 BRICK_HALF_WIDTH * 2.0,
642 BRICK_HALF_HEIGHT * 2.0,
643 BRICK_HALF_DEPTH * 2.0,
644 );
645 for brick in self.bricks.iter().filter(|brick| brick.hits_remaining > 0) {
646 let health = f32::from(brick.hits_remaining) / f32::from(BRICK_HITS);
647 let color = BRICK_ROW_COLORS[brick.row].dimmed(0.4 + 0.6 * health);
648 ctx.draw(
649 Cube.at(Transform::from_scale_rotation_translation(
650 scale,
651 Quat::IDENTITY,
652 brick.position,
653 ))
654 .material(Material::shaded(color, health)),
655 );
656 }
657 }
658
659 /// Draws the live spark burst: additive, tumbling by roll as they age,
660 /// shrinking and fading out over their lifetime.
661 fn draw_sparks(&self, ctx: &mut FrameContext<'_, Breakout>) {
662 for spark in &self.sparks {
663 let age = (spark.age / SPARK_LIFETIME).clamp(0.0, 1.0);
664 let fade = 1.0 - age;
665 let size = SPARK_SIZE_START.lerp(SPARK_SIZE_END, age);
666 ctx.draw(
667 Quad.at(Transform::from_scale_rotation_translation(
668 Vec3::splat(size),
669 Quat::IDENTITY,
670 spark.position,
671 ))
672 .billboard()
673 .roll(spark.roll + spark.age * SPARK_SPIN_SPEED)
674 .material(
675 Material::color(spark.color.with_alpha(fade))
676 .emissive(spark.color.dimmed(SPARK_EMISSIVE_PEAK))
677 .additive(),
678 ),
679 );
680 }
681 }
682
683 /// Draws the ball's ghost trail, each ghost smaller and more transparent
684 /// than the one ahead of it; each ghost's position interpolates between
685 /// its own last two resolved ticks by the same `alpha` the ball itself
686 /// draws at, and its radius clamps to what the ball's own radius has
687 /// left over its distance from the head, so a ghost still close to the
688 /// ball never draws past its edge.
689 fn draw_trail(&self, ctx: &mut FrameContext<'_, Breakout>, alpha: f32) {
690 let head = self.ball_trail[1].lerp(self.ball_trail[0], alpha);
691 for i in 0..TRAIL_LEN {
692 let position = self.ball_trail[i + 1].lerp(self.ball_trail[i], alpha);
693 let age = (i + 1) as f32 / TRAIL_LEN as f32;
694 let fade = (1.0 - age).max(TRAIL_ALPHA_FLOOR);
695 let radius = (BALL_RADIUS * TRAIL_SCALE_MIN.lerp(TRAIL_SCALE_MAX, fade))
696 .min((BALL_RADIUS - head.distance(position)).max(0.0));
697 let scale = Vec3::splat(radius * 2.0);
698 ctx.draw(
699 Sphere { subdivisions: 2 }
700 .at(Transform::from_scale_rotation_translation(
701 scale,
702 Quat::IDENTITY,
703 position,
704 ))
705 .material(
706 Material::color(BALL_GLOW.with_alpha(fade))
707 .emissive(BALL_EMISSIVE.dimmed(TRAIL_EMISSIVE_PEAK)),
708 ),
709 );
710 }
711 }
712
713 /// Draws one held ball for every life past the one in play, set in a
714 /// row alongside the paddle's own path.
715 fn draw_lives(&self, ctx: &mut FrameContext<'_, Breakout>) {
716 let held_lives = self.lives.saturating_sub(1);
717 for slot in 0..held_lives {
718 let z = PADDLE_Z + (slot + 1) as f32 * LIFE_ROW_SPACING;
719 ctx.draw(
720 Sphere { subdivisions: 2 }
721 .at(Transform::from_scale_rotation_translation(
722 Vec3::splat(BALL_RADIUS * 2.0),
723 Quat::IDENTITY,
724 Vec3::new(LIFE_ROW_X, BALL_RADIUS, z),
725 ))
726 .material(
727 Material::color(BALL_GLOW)
728 .emissive(BALL_EMISSIVE)
729 .additive(),
730 ),
731 );
732 }
733 }
734
735 fn overlay(&mut self, ctx: &mut FrameContext<'_, Breakout>) {
736 let bricks_left = self
737 .bricks
738 .iter()
739 .filter(|brick| brick.hits_remaining > 0)
740 .count();
741 // Read before `ctx.ui` so a rebind changes what the hint reads this
742 // frame too.
743 let move_hint = bindings_text(ctx.bindings(Move::Paddle));
744 let pause_hint = bindings_text(ctx.bindings(Button::Pause));
745 let serve_hint = bindings_text(ctx.bindings(Button::Serve));
746 ctx.ui(|ui| {
747 ui.horizontal(|ui| {
748 ui.label(egui::RichText::new(format!("score {}", self.score)).size(32.0));
749 ui.label(format!("{bricks_left} bricks left"));
750 });
751 ui.label(format!("move: {move_hint} · {pause_hint} to pause"));
752 if self.phase == Phase::Serving {
753 ui.label(format!("{serve_hint} to serve"));
754 }
755 });
756
757 match self.phase {
758 Phase::Serving | Phase::Playing if self.paused => self.menu(ctx, "paused", false),
759 Phase::Won => self.menu(ctx, "you win", true),
760 Phase::Lost => self.menu(ctx, "game over", true),
761 _ => {}
762 }
763 }
764
765 fn menu(&mut self, ctx: &mut FrameContext<'_, Breakout>, title: &str, over: bool) {
766 let mut clicked = false;
767 let mut quit = false;
768
769 // `ctx.ui` cannot borrow `ctx`, so anything the controls list needs is
770 // read first and applied after.
771 let buttons: Vec<(Button, String)> = Button::all()
772 .into_iter()
773 .map(|action| (action, bindings_text(ctx.bindings(action))))
774 .collect();
775 let axes: Vec<(Move, String)> = Move::all()
776 .into_iter()
777 .map(|action| (action, bindings_text(ctx.bindings(action))))
778 .collect();
779 let listening = self.listening;
780 let actuated_button = (!ctx.ui_wants_keyboard())
781 .then(|| ctx.actuated_button())
782 .flatten();
783 let actuated_axis = (!ctx.ui_wants_keyboard())
784 .then(|| ctx.actuated_axis())
785 .flatten();
786 let mut reset = None;
787
788 ctx.ui(|ui| {
789 egui::Window::new(title)
790 .collapsible(false)
791 .resizable(false)
792 .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
793 .show(ui.ctx(), |ui| {
794 if over {
795 ui.label(format!("score {}", self.score));
796 }
797 if !over {
798 ui.add(
799 egui::Slider::new(&mut self.master_volume, 0.0..=1.0).text("volume"),
800 );
801 if ui.button("resume").clicked() {
802 self.paused = false;
803 clicked = true;
804 }
805 ui.separator();
806 ui.heading("controls");
807 for (action, text) in &buttons {
808 controls_row(
809 ui,
810 action.name(),
811 text,
812 listening == Some(Listening::Button(*action)),
813 &mut self.listening,
814 Listening::Button(*action),
815 &mut reset,
816 );
817 }
818 for (action, text) in &axes {
819 controls_row(
820 ui,
821 action.name(),
822 text,
823 listening == Some(Listening::Move(*action)),
824 &mut self.listening,
825 Listening::Move(*action),
826 &mut reset,
827 );
828 }
829 }
830 if ui.button("restart").clicked() {
831 self.restart();
832 clicked = true;
833 }
834 if ui.button("quit").clicked() {
835 quit = true;
836 }
837 });
838 });
839
840 match (self.listening, actuated_button, actuated_axis) {
841 (Some(Listening::Button(action)), Some(binding), _) => {
842 ctx.rebind(action, vec![binding]);
843 self.listening = None;
844 }
845 (Some(Listening::Move(action)), _, Some(binding)) => {
846 ctx.rebind(action, vec![binding]);
847 self.listening = None;
848 }
849 _ => {}
850 }
851 match reset {
852 Some(Listening::Button(action)) => ctx.rebind(action, action.bindings()),
853 Some(Listening::Move(action)) => ctx.rebind(action, action.bindings()),
854 None => {}
855 }
856
857 if clicked {
858 ctx.play(Sound::Click);
859 }
860 if quit {
861 ctx.close();
862 }
863 }
864
865 /// Sustains both tracks every frame, and the gain goes to whichever the
866 /// game calls for: gameplay music while a round is live, serving
867 /// included, and menu music whenever a menu covers it.
868 ///
869 /// Each fades in over [`MUSIC_CROSSFADE`] and slides every later gain
870 /// over it, which is the crossfade itself; the one at no gain costs no
871 /// voice while its playback goes on under the other.
872 fn sustain_music(&self, ctx: &mut FrameContext<'_, Breakout>) {
873 let playing = !self.paused && matches!(self.phase, Phase::Serving | Phase::Playing);
874 let gain = |wanted: bool| match wanted {
875 true => MUSIC_GAIN,
876 false => 0.0,
877 };
878
879 ctx.sustain(
880 Sound::Music
881 .gain(gain(playing))
882 .fade(MUSIC_CROSSFADE)
883 .glide(MUSIC_CROSSFADE)
884 .loop_from(MUSIC_LOOP_FROM),
885 );
886 ctx.sustain(
887 Sound::MenuMusic
888 .gain(gain(!playing))
889 .fade(MUSIC_CROSSFADE)
890 .glide(MUSIC_CROSSFADE)
891 .loop_from(MENU_MUSIC_LOOP_FROM),
892 );
893 }
894}
895
896/// One action's name, its live bindings, a rebind control that starts
897/// listening for a new one, and a reset to its defaults; cancel is a
898/// button rather than Escape, since Escape is itself a binding a listen
899/// could capture.
900fn controls_row(
901 ui: &mut egui::Ui,
902 name: &str,
903 bindings: &str,
904 listening: bool,
905 target: &mut Option<Listening>,
906 action: Listening,
907 reset: &mut Option<Listening>,
908) {
909 ui.horizontal(|ui| {
910 ui.label(format!("{name}: {bindings}"));
911 if listening {
912 ui.label("listening");
913 if ui.button("cancel").clicked() {
914 *target = None;
915 }
916 } else if ui.button("rebind").clicked() {
917 *target = Some(action);
918 }
919 if ui.button("reset").clicked() {
920 *reset = Some(action);
921 }
922 });
923}
924
925/// The controls-menu text for a live binding list: each alternative,
926/// separated, in the order the player can use them.
927fn bindings_text<B: Display>(bindings: Vec<B>) -> String {
928 bindings
929 .iter()
930 .map(ToString::to_string)
931 .collect::<Vec<_>>()
932 .join(", ")
933}
934
935fn spawn_bricks() -> Vec<Brick> {
936 let cell = BRICK_HALF_WIDTH * 2.0 + BRICK_GAP;
937 let row_span = BRICK_HALF_DEPTH * 2.0 + BRICK_ROW_GAP;
938 let grid_width = cell * BRICK_COLUMNS as f32 - BRICK_GAP;
939 let start_x = -grid_width * 0.5 + BRICK_HALF_WIDTH;
940 let start_z = -COURT_HALF_DEPTH + WALL_THICKNESS + BRICK_HALF_DEPTH + 0.6;
941
942 (0..BRICK_ROWS)
943 .flat_map(|row| {
944 (0..BRICK_COLUMNS).map(move |column| Brick {
945 row,
946 position: Vec3::new(
947 start_x + column as f32 * cell,
948 BRICK_HALF_HEIGHT,
949 start_z + row as f32 * row_span,
950 ),
951 hits_remaining: BRICK_HITS,
952 })
953 })
954 .collect()
955}
956
957impl Game for Breakout {
958 type Meshes = Shape;
959 type Sounds = Sound;
960 type InputActions = Controls;
961 type Skyboxes = NoSkyboxes;
962 type SurfaceStyles = NoSurfaceStyles;
963 type PostEffects = NoPostEffects;
964
965 fn tick(&mut self, ctx: &mut TickContext<'_, Breakout>) {
966 if self.paused {
967 return;
968 }
969
970 let dt = ctx.dt().as_secs_f32();
971 self.paddle_flash = (self.paddle_flash - dt).max(0.0);
972 self.brick_flash = (self.brick_flash - dt).max(0.0);
973 self.life_lost_flash = (self.life_lost_flash - dt).max(0.0);
974 self.step_sparks(dt);
975
976 // Decay runs before the end-screen return below, so the last pulse and
977 // burst do not stay on screen.
978 if matches!(self.phase, Phase::Won | Phase::Lost) {
979 return;
980 }
981
982 let axis = if ctx.ui_wants_keyboard() {
983 0.0
984 } else {
985 ctx.axis(Move::Paddle)
986 };
987 self.step_paddle(axis, dt);
988
989 match self.phase {
990 Phase::Serving => self.hold_ball(ctx),
991 _ => self.step_ball(ctx, dt),
992 }
993 }More examples
1801 fn tick(&mut self, ctx: &mut TickContext<'_, Keep>) {
1802 self.previous = self.position;
1803
1804 if self.reset_requested || ctx.pressed(Button::Reset) {
1805 self.reset_requested = false;
1806 self.reset(ctx);
1807 return;
1808 }
1809
1810 let heading = if ctx.ui_wants_keyboard() {
1811 Vec2::ZERO
1812 } else {
1813 ctx.axis2(Move::Walk)
1814 };
1815 match Facing::from_heading(heading) {
1816 Some(facing) => {
1817 self.facing = facing;
1818 self.walk_ticks += 1;
1819 }
1820 None => self.walk_ticks = 0,
1821 }
1822 let stride = Vec3::new(heading.x, 0.0, -heading.y) * WALK_SPEED * ctx.dt().as_secs_f32();
1823 self.position += stride;
1824 self.simulated += ctx.dt();
1825
1826 match self.area {
1827 Area::Overworld => self.tick_overworld(ctx),
1828 Area::Cave => self.tick_cave(ctx),
1829 }
1830 }Sourcepub fn window_size(&self) -> UVec2
pub fn window_size(&self) -> UVec2
The window’s drawing area, in physical pixels; zero while minimized.
Examples found in repository?
634 fn handle_hail(&mut self, ctx: &mut TickContext<'_, Self>) {
635 let ray = ctx
636 .last_camera()
637 .ray_through(ctx.pointer(), ctx.window_size());
638 let Some(station) = hit_station(ray) else {
639 return;
640 };
641
642 if self.hailed != Some(station) {
643 self.hailed = Some(station);
644 let look = station.look();
645 self.dialogue = Some(Dialogue::start(look.name, look.lines.map(str::to_owned)));
646 return;
647 }
648 let Some(dialogue) = &mut self.dialogue else {
649 return;
650 };
651 if !dialogue.advance() {
652 self.dialogue = None;
653 self.hailed = None;
654 }
655 }More examples
474 fn handle_drag(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
475 // Read before the check below for the UI's own claim on the
476 // pointer, so a release over it still frees a source a drag moved
477 // there.
478 if ctx.released(Button::Select) {
479 self.dragging = None;
480 }
481 if ctx.ui_wants_pointer() {
482 return;
483 }
484 let ray = ctx
485 .last_camera()
486 .ray_through(ctx.pointer(), ctx.window_size());
487
488 if ctx.pressed(Button::Select) {
489 self.dragging = self.sources.iter().position(|source| {
490 ray.hit_sphere(source.position, SOURCE_PICK_RADIUS)
491 .is_some()
492 });
493 }
494
495 let Some(index) = self.dragging else {
496 return;
497 };
498 let Some(distance) = ray.hit_plane(ray::Plane {
499 point: Vec3::ZERO,
500 normal: Vec3::Y,
501 }) else {
502 return;
503 };
504 let hit = ray.at(distance);
505 let dropped =
506 Vec2::new(hit.x, hit.z).clamp(Vec2::splat(-PLAY_BOUND), Vec2::splat(PLAY_BOUND));
507 self.sources[index].position = Vec3::new(dropped.x, SOURCE_HEIGHT, dropped.y);
508 }414 fn handle_click(&mut self, ctx: &mut TickContext<'_, Board>) {
415 if ctx.ui_wants_pointer() || !ctx.pressed(Button::Select) {
416 return;
417 }
418 let ray = ctx
419 .last_camera()
420 .ray_through(ctx.pointer(), ctx.window_size());
421 let (lift, half) = unit_geometry(self.turn);
422
423 if self.current().target.is_none() {
424 let center = self.current().position;
425 if ray.hit_aabb(center - half, center + half).is_some() {
426 self.selected = !self.selected;
427 return;
428 }
429 }
430 if !self.selected {
431 return;
432 }
433
434 let Some(distance) = ray.hit_plane(ray::Plane {
435 point: Vec3::ZERO,
436 normal: Vec3::Y,
437 }) else {
438 return;
439 };
440 let Some(tile) = tile_at(ray.at(distance)) else {
441 return;
442 };
443 if tile == self.current().tile || tile == self.other().tile {
444 return;
445 }
446
447 let destination = tile_center(tile) + Vec3::Y * lift;
448 let heading = destination.x - self.current().position.x;
449 let current = self.current_mut();
450 if heading.abs() > f32::EPSILON {
451 current.facing_right = heading > 0.0;
452 }
453 current.target = Some(destination);
454 self.selected = false;
455 ctx.play(Sound::Click);
456 }Sourcepub fn last_camera(&self) -> Camera
pub fn last_camera(&self) -> Camera
The camera the last drawn frame was viewed from; Camera::default
before the first frame.
Required if you want a ray through a pixel: the player points at what was last drawn.
Examples found in repository?
634 fn handle_hail(&mut self, ctx: &mut TickContext<'_, Self>) {
635 let ray = ctx
636 .last_camera()
637 .ray_through(ctx.pointer(), ctx.window_size());
638 let Some(station) = hit_station(ray) else {
639 return;
640 };
641
642 if self.hailed != Some(station) {
643 self.hailed = Some(station);
644 let look = station.look();
645 self.dialogue = Some(Dialogue::start(look.name, look.lines.map(str::to_owned)));
646 return;
647 }
648 let Some(dialogue) = &mut self.dialogue else {
649 return;
650 };
651 if !dialogue.advance() {
652 self.dialogue = None;
653 self.hailed = None;
654 }
655 }More examples
474 fn handle_drag(&mut self, ctx: &mut TickContext<'_, SoundCheck>) {
475 // Read before the check below for the UI's own claim on the
476 // pointer, so a release over it still frees a source a drag moved
477 // there.
478 if ctx.released(Button::Select) {
479 self.dragging = None;
480 }
481 if ctx.ui_wants_pointer() {
482 return;
483 }
484 let ray = ctx
485 .last_camera()
486 .ray_through(ctx.pointer(), ctx.window_size());
487
488 if ctx.pressed(Button::Select) {
489 self.dragging = self.sources.iter().position(|source| {
490 ray.hit_sphere(source.position, SOURCE_PICK_RADIUS)
491 .is_some()
492 });
493 }
494
495 let Some(index) = self.dragging else {
496 return;
497 };
498 let Some(distance) = ray.hit_plane(ray::Plane {
499 point: Vec3::ZERO,
500 normal: Vec3::Y,
501 }) else {
502 return;
503 };
504 let hit = ray.at(distance);
505 let dropped =
506 Vec2::new(hit.x, hit.z).clamp(Vec2::splat(-PLAY_BOUND), Vec2::splat(PLAY_BOUND));
507 self.sources[index].position = Vec3::new(dropped.x, SOURCE_HEIGHT, dropped.y);
508 }414 fn handle_click(&mut self, ctx: &mut TickContext<'_, Board>) {
415 if ctx.ui_wants_pointer() || !ctx.pressed(Button::Select) {
416 return;
417 }
418 let ray = ctx
419 .last_camera()
420 .ray_through(ctx.pointer(), ctx.window_size());
421 let (lift, half) = unit_geometry(self.turn);
422
423 if self.current().target.is_none() {
424 let center = self.current().position;
425 if ray.hit_aabb(center - half, center + half).is_some() {
426 self.selected = !self.selected;
427 return;
428 }
429 }
430 if !self.selected {
431 return;
432 }
433
434 let Some(distance) = ray.hit_plane(ray::Plane {
435 point: Vec3::ZERO,
436 normal: Vec3::Y,
437 }) else {
438 return;
439 };
440 let Some(tile) = tile_at(ray.at(distance)) else {
441 return;
442 };
443 if tile == self.current().tile || tile == self.other().tile {
444 return;
445 }
446
447 let destination = tile_center(tile) + Vec3::Y * lift;
448 let heading = destination.x - self.current().position.x;
449 let current = self.current_mut();
450 if heading.abs() > f32::EPSILON {
451 current.facing_right = heading > 0.0;
452 }
453 current.target = Some(destination);
454 self.selected = false;
455 ctx.play(Sound::Click);
456 }Auto Trait Implementations§
impl<'a, G> !UnwindSafe for TickContext<'a, G>
impl<'a, G> Freeze for TickContext<'a, G>
impl<'a, G> RefUnwindSafe for TickContext<'a, G>where
&'a mut MeshCatalog<<G as Game>::Meshes>: RefUnwindSafe,
&'a mut Sounding<<G as Game>::Sounds>: RefUnwindSafe,
impl<'a, G> Send for TickContext<'a, G>
impl<'a, G> Sync for TickContext<'a, G>
impl<'a, G> Unpin for TickContext<'a, G>
impl<'a, G> UnsafeUnpin for TickContext<'a, G>where
&'a mut MeshCatalog<<G as Game>::Meshes>: UnsafeUnpin,
&'a mut Sounding<<G as Game>::Sounds>: UnsafeUnpin,
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<S, T> Duplex<S> for Twhere
T: FromSample<S> + ToSample<S>,
Source§impl<S> FromSample<S> for S
impl<S> FromSample<S> for S
fn from_sample_(s: S) -> S
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more