1use super::World;
14use crate::broad_phase::update_broad_phase_pairs;
15use crate::constants::{speculative_distance, GRAPH_COLOR_COUNT};
16use crate::contact::{contact_flags, update_contact, Contact, ContactSim, ContactUpdateContext};
17use crate::core::NULL_INDEX;
18use crate::events::{ContactBeginTouchEvent, ContactEndTouchEvent};
19use crate::id::{ContactId, ShapeId};
20use crate::math_functions::{
21 aabb_overlaps, abs_float, distance, dot, inv_mul_rot, inv_mul_world_transforms, invert_rot,
22 is_valid_float, max_float, min_float, mul_rot, rotate_vector, sub, sub_pos, Rot,
23};
24use crate::sensor::overlap_sensors;
25use crate::solver::{make_soft, solve, StepContext};
26use crate::solver_set::{AWAKE_SET, FIRST_SLEEPING_SET, STATIC_SET};
27use crate::timer::{get_milliseconds, get_ticks};
28use crate::types::BodyType;
29
30fn relative_cos(a: Rot, b: Rot) -> f32 {
32 a.c * b.c + a.s * b.s
33}
34
35#[derive(Clone, Copy)]
39struct ContactSimLocation {
40 color_index: i32,
41 local_index: i32,
42}
43
44fn get_located_sim(world: &World, location: ContactSimLocation) -> ContactSim {
45 if location.color_index != NULL_INDEX {
46 world.constraint_graph.colors[location.color_index as usize].contact_sims
47 [location.local_index as usize]
48 } else {
49 world.solver_sets[AWAKE_SET as usize].contact_sims[location.local_index as usize]
50 }
51}
52
53fn put_located_sim(world: &mut World, location: ContactSimLocation, sim: ContactSim) {
54 if location.color_index != NULL_INDEX {
55 world.constraint_graph.colors[location.color_index as usize].contact_sims
56 [location.local_index as usize] = sim;
57 } else {
58 world.solver_sets[AWAKE_SET as usize].contact_sims[location.local_index as usize] = sim;
59 }
60}
61
62fn collide_contact(
64 world: &mut World,
65 location: ContactSimLocation,
66 update_context: &ContactUpdateContext,
67) {
68 let recycle_distance = world.contact_recycle_distance;
69 let speculative_distance_ = speculative_distance();
70 let recycle_distance_non_touching = min_float(recycle_distance, speculative_distance_);
71
72 let mut contact_sim = get_located_sim(world, location);
73
74 let contact_id = contact_sim.contact_id;
75
76 let overlap = {
78 let shape_a = &world.shapes[contact_sim.shape_id_a as usize];
79 let shape_b = &world.shapes[contact_sim.shape_id_b as usize];
80 aabb_overlaps(shape_a.fat_aabb, shape_b.fat_aabb)
81 };
82
83 if !overlap {
84 contact_sim.sim_flags |= contact_flags::SIM_DISJOINT;
85 contact_sim.sim_flags &= !contact_flags::SIM_TOUCHING;
86 put_located_sim(world, location, contact_sim);
87 world.task_contexts[0]
88 .contact_state_bit_set
89 .set_bit(contact_id as u32);
90 return;
91 }
92
93 let was_touching = contact_sim.sim_flags & contact_flags::SIM_TOUCHING != 0;
94
95 let (shape_body_id_a, shape_body_id_b) = {
97 let shape_a = &world.shapes[contact_sim.shape_id_a as usize];
98 let shape_b = &world.shapes[contact_sim.shape_id_b as usize];
99 (shape_a.body_id, shape_b.body_id)
100 };
101
102 let body_a = &world.bodies[shape_body_id_a as usize];
103 let body_b = &world.bodies[shape_body_id_b as usize];
104 let body_sim_a =
105 world.solver_sets[body_a.set_index as usize].body_sims[body_a.local_index as usize];
106 let body_sim_b =
107 world.solver_sets[body_b.set_index as usize].body_sims[body_b.local_index as usize];
108 let transform_a = body_sim_a.transform;
109 let transform_b = body_sim_b.transform;
110
111 contact_sim.body_sim_index_a = if body_a.set_index == AWAKE_SET {
113 body_a.local_index
114 } else {
115 NULL_INDEX
116 };
117 contact_sim.inv_mass_a = body_sim_a.inv_mass;
118 contact_sim.inv_i_a = body_sim_a.inv_inertia;
119
120 contact_sim.body_sim_index_b = if body_b.set_index == AWAKE_SET {
121 body_b.local_index
122 } else {
123 NULL_INDEX
124 };
125 contact_sim.inv_mass_b = body_sim_b.inv_mass;
126 contact_sim.inv_i_b = body_sim_b.inv_inertia;
127
128 let type_a = body_a.type_;
129 let type_b = body_b.type_;
130
131 if recycle_distance > 0.0
136 && contact_sim.sim_flags & contact_flags::SIM_RELATIVE_TRANSFORM_VALID != 0
137 && contact_sim.sim_flags & contact_flags::RECYCLE != 0
138 {
139 let cached_q_a = contact_sim.cached_rotation_a;
140 let cached_q_b = contact_sim.cached_rotation_b;
141 let xfc = contact_sim.cached_relative_pose;
142 let xf = inv_mul_world_transforms(transform_a, transform_b);
143
144 let cos_a = relative_cos(transform_a.q, cached_q_a);
145 let cos_b = relative_cos(transform_b.q, cached_q_b);
146 let min_cos = min_float(cos_a, cos_b);
147
148 let max_extent_a = if type_a == BodyType::Static {
149 0.0
150 } else {
151 body_sim_a.max_extent
152 };
153 let max_extent_b = if type_b == BodyType::Static {
154 0.0
155 } else {
156 body_sim_b.max_extent
157 };
158 let max_extent = max_float(max_extent_a, max_extent_b);
159 let distance_ = distance(xf.p, xfc.p);
160 let qr = inv_mul_rot(xf.q, xfc.q);
161
162 let tolerance = if was_touching {
167 recycle_distance
168 } else {
169 recycle_distance_non_touching
170 };
171
172 if min_cos > crate::constants::CONTACT_RECYCLE_COS_ANGLE
173 && distance_ + max_extent * abs_float(qr.s) < tolerance
174 {
175 let dq_a = mul_rot(transform_a.q, invert_rot(cached_q_a));
176 let dq_b = mul_rot(transform_b.q, invert_rot(cached_q_b));
177 let normal = contact_sim.manifold.normal;
178
179 let dc = sub_pos(body_sim_b.center, body_sim_a.center);
181
182 for i in 0..contact_sim.manifold.point_count as usize {
183 let mp = &mut contact_sim.manifold.points[i];
186 let r_a = rotate_vector(dq_a, mp.anchor_a);
187 let r_b = rotate_vector(dq_b, mp.anchor_b);
188 let dp = crate::math_functions::add(dc, sub(r_b, r_a));
189 mp.separation = mp.base_separation + dot(dp, normal);
190 mp.persisted = true;
191 }
192
193 world.task_contexts[0].recycled_contact_count += 1;
194
195 put_located_sim(world, location, contact_sim);
198 return;
199 }
200 }
201
202 contact_sim.cached_rotation_a = transform_a.q;
204 contact_sim.cached_rotation_b = transform_b.q;
205 contact_sim.cached_relative_pose = inv_mul_world_transforms(transform_a, transform_b);
206 contact_sim.sim_flags |= contact_flags::SIM_RELATIVE_TRANSFORM_VALID;
207
208 let center_offset_a = rotate_vector(transform_a.q, body_sim_a.local_center);
209 let center_offset_b = rotate_vector(transform_b.q, body_sim_b.local_center);
210
211 let touching = {
213 let shape_a = &world.shapes[contact_sim.shape_id_a as usize];
214 let shape_b = &world.shapes[contact_sim.shape_id_b as usize];
215 update_contact(
216 update_context,
217 &mut contact_sim,
218 shape_a,
219 transform_a,
220 center_offset_a,
221 shape_b,
222 transform_b,
223 center_offset_b,
224 )
225 };
226
227 if touching && !was_touching {
230 contact_sim.sim_flags |= contact_flags::SIM_STARTED_TOUCHING;
231 world.task_contexts[0]
232 .contact_state_bit_set
233 .set_bit(contact_id as u32);
234 } else if !touching && was_touching {
235 contact_sim.sim_flags |= contact_flags::SIM_STOPPED_TOUCHING;
236 world.task_contexts[0]
237 .contact_state_bit_set
238 .set_bit(contact_id as u32);
239 }
240
241 for i in 0..contact_sim.manifold.point_count as usize {
242 let mp = &mut contact_sim.manifold.points[i];
243 mp.base_separation = mp.separation;
244 }
245
246 put_located_sim(world, location, contact_sim);
247}
248
249fn add_non_touching_contact(world: &mut World, contact_id: i32, contact_sim: ContactSim) {
251 debug_assert!(world.contacts[contact_id as usize].set_index == AWAKE_SET);
252 let local_index = world.solver_sets[AWAKE_SET as usize].contact_sims.len() as i32;
253 {
254 let contact = &mut world.contacts[contact_id as usize];
255 contact.color_index = NULL_INDEX;
256 contact.local_index = local_index;
257 }
258 world.solver_sets[AWAKE_SET as usize]
259 .contact_sims
260 .push(contact_sim);
261}
262
263fn remove_non_touching_contact(world: &mut World, set_index: i32, local_index: i32) {
265 let set = &mut world.solver_sets[set_index as usize];
266 let moved_index = set.contact_sims.len() as i32 - 1;
267 set.contact_sims.swap_remove(local_index as usize);
268 if moved_index != local_index {
269 let moved_contact_id =
270 world.solver_sets[set_index as usize].contact_sims[local_index as usize].contact_id;
271 let moved_contact = &mut world.contacts[moved_contact_id as usize];
272 debug_assert!(moved_contact.set_index == set_index);
273 debug_assert!(moved_contact.local_index == moved_index);
274 debug_assert!(moved_contact.color_index == NULL_INDEX);
275 moved_contact.local_index = local_index;
276 }
277}
278
279fn collide(world: &mut World, _context: &StepContext) {
281 let mut locations: Vec<ContactSimLocation> = Vec::new();
284 for color_index in 0..GRAPH_COLOR_COUNT {
285 let count = world.constraint_graph.colors[color_index as usize]
286 .contact_sims
287 .len();
288 for local_index in 0..count as i32 {
289 locations.push(ContactSimLocation {
290 color_index,
291 local_index,
292 });
293 }
294 }
295 {
296 let count = world.solver_sets[AWAKE_SET as usize].contact_sims.len();
297 for local_index in 0..count as i32 {
298 locations.push(ContactSimLocation {
299 color_index: NULL_INDEX,
300 local_index,
301 });
302 }
303 }
304
305 if locations.is_empty() {
306 return;
307 }
308
309 let contact_id_capacity = world.contact_id_pool.id_capacity();
312 {
313 let task_context = &mut world.task_contexts[0];
314 task_context
315 .contact_state_bit_set
316 .set_bit_count_and_clear(contact_id_capacity as u32);
317 task_context.recycled_contact_count = 0;
318 }
319
320 let update_context = ContactUpdateContext::new(world);
322 for &location in &locations {
323 collide_contact(world, location, &update_context);
324 }
325
326 let end_event_array_index = world.end_event_array_index as usize;
329 let world_id = world.world_id;
330
331 let block_count = world.task_contexts[0].contact_state_bit_set.block_count();
332 for k in 0..block_count {
333 let mut bits = world.task_contexts[0].contact_state_bit_set.block(k);
334 while bits != 0 {
335 let ctz = bits.trailing_zeros();
336 let contact_id = (64 * k + ctz) as i32;
337
338 let (color_index, local_index, flags, generation, shape_id_a, shape_id_b) = {
339 let contact: &Contact = &world.contacts[contact_id as usize];
340 debug_assert!(contact.set_index == AWAKE_SET);
341 (
342 contact.color_index,
343 contact.local_index,
344 contact.flags,
345 contact.generation,
346 contact.shape_id_a,
347 contact.shape_id_b,
348 )
349 };
350
351 let sim_flags = if color_index != NULL_INDEX {
352 debug_assert!((0..GRAPH_COLOR_COUNT).contains(&color_index));
354 world.constraint_graph.colors[color_index as usize].contact_sims
355 [local_index as usize]
356 .sim_flags
357 } else {
358 world.solver_sets[AWAKE_SET as usize].contact_sims[local_index as usize].sim_flags
359 };
360
361 let (shape_id_a_full, shape_id_b_full) = {
362 let shape_a = &world.shapes[shape_id_a as usize];
363 let shape_b = &world.shapes[shape_id_b as usize];
364 (
365 ShapeId {
366 index1: shape_a.id + 1,
367 world0: world_id,
368 generation: shape_a.generation,
369 },
370 ShapeId {
371 index1: shape_b.id + 1,
372 world0: world_id,
373 generation: shape_b.generation,
374 },
375 )
376 };
377 let contact_full_id = ContactId {
378 index1: contact_id + 1,
379 world0: world_id,
380 padding: 0,
381 generation,
382 };
383
384 if sim_flags & contact_flags::SIM_DISJOINT != 0 {
385 crate::contact::destroy_contact(world, contact_id, false);
387 } else if sim_flags & contact_flags::SIM_STARTED_TOUCHING != 0 {
388 debug_assert!(world.contacts[contact_id as usize].island_id == NULL_INDEX);
389
390 if flags & contact_flags::ENABLE_CONTACT_EVENTS != 0 {
391 world.contact_begin_events.push(ContactBeginTouchEvent {
392 shape_id_a: shape_id_a_full,
393 shape_id_b: shape_id_b_full,
394 contact_id: contact_full_id,
395 });
396 }
397
398 debug_assert!(color_index == NULL_INDEX);
399 debug_assert!(
400 world.solver_sets[AWAKE_SET as usize].contact_sims[local_index as usize]
401 .manifold
402 .point_count
403 > 0
404 );
405
406 world.contacts[contact_id as usize].flags |= contact_flags::TOUCHING;
409 crate::island::link_contact(world, contact_id);
410
411 debug_assert!(world.contacts[contact_id as usize].color_index == NULL_INDEX);
413 debug_assert!(world.contacts[contact_id as usize].local_index == local_index);
414
415 let mut contact_sim =
417 world.solver_sets[AWAKE_SET as usize].contact_sims[local_index as usize];
418 contact_sim.sim_flags &= !contact_flags::SIM_STARTED_TOUCHING;
419
420 crate::constraint_graph::add_contact_to_graph(world, contact_sim, contact_id);
422
423 remove_non_touching_contact(world, AWAKE_SET, local_index);
425 } else if sim_flags & contact_flags::SIM_STOPPED_TOUCHING != 0 {
426 debug_assert!(color_index != NULL_INDEX);
427 let contact_sim = {
428 let sim = &mut world.constraint_graph.colors[color_index as usize].contact_sims
429 [local_index as usize];
430 sim.sim_flags &= !contact_flags::SIM_STOPPED_TOUCHING;
431 *sim
432 };
433 world.contacts[contact_id as usize].flags &= !contact_flags::TOUCHING;
434
435 if flags & contact_flags::ENABLE_CONTACT_EVENTS != 0 {
436 world.contact_end_events[end_event_array_index].push(ContactEndTouchEvent {
437 shape_id_a: shape_id_a_full,
438 shape_id_b: shape_id_b_full,
439 contact_id: contact_full_id,
440 });
441 }
442
443 debug_assert!(contact_sim.manifold.point_count == 0);
444
445 crate::island::unlink_contact(world, contact_id);
446 let body_id_a = world.contacts[contact_id as usize].edges[0].body_id;
447 let body_id_b = world.contacts[contact_id as usize].edges[1].body_id;
448
449 add_non_touching_contact(world, contact_id, contact_sim);
451 crate::constraint_graph::remove_contact_from_graph(
452 world,
453 body_id_a,
454 body_id_b,
455 color_index,
456 local_index,
457 );
458 }
459
460 bits &= bits - 1;
462 }
463 }
464
465 world.validate_solver_sets();
466 world.validate_contacts();
467}
468
469impl World {
470 pub fn validate_contacts(&self) {
473 if !cfg!(debug_assertions) {
474 return;
475 }
476
477 let contact_count = self.contacts.len() as i32;
478 debug_assert!(contact_count == self.contact_id_pool.id_capacity());
479 let mut allocated_contact_count = 0;
480
481 for contact_index in 0..contact_count {
482 let contact = &self.contacts[contact_index as usize];
483 if contact.contact_id == NULL_INDEX {
484 continue;
485 }
486
487 debug_assert!(contact.contact_id == contact_index);
488
489 allocated_contact_count += 1;
490
491 let touching = contact.flags & contact_flags::TOUCHING != 0;
492
493 let set_id = contact.set_index;
494
495 if set_id == AWAKE_SET {
496 if touching {
497 debug_assert!(
498 0 <= contact.color_index && contact.color_index < GRAPH_COLOR_COUNT
499 );
500 } else {
501 debug_assert!(contact.color_index == NULL_INDEX);
502 }
503 } else if set_id >= FIRST_SLEEPING_SET {
504 debug_assert!(touching);
506 } else {
507 debug_assert!(!touching && set_id == crate::solver_set::DISABLED_SET);
509 }
510
511 let contact_sim = if set_id == AWAKE_SET && contact.color_index != NULL_INDEX {
512 &self.constraint_graph.colors[contact.color_index as usize].contact_sims
513 [contact.local_index as usize]
514 } else {
515 &self.solver_sets[set_id as usize].contact_sims[contact.local_index as usize]
516 };
517 debug_assert!(contact_sim.contact_id == contact_index);
518 debug_assert!(contact_sim.body_id_a == contact.edges[0].body_id);
519 debug_assert!(contact_sim.body_id_b == contact.edges[1].body_id);
520
521 let sim_touching = contact_sim.sim_flags & contact_flags::SIM_TOUCHING != 0;
522 debug_assert!(touching == sim_touching);
523
524 debug_assert!(
525 0 <= contact_sim.manifold.point_count && contact_sim.manifold.point_count <= 2
526 );
527 }
528
529 let contact_id_count = self.contact_id_pool.id_count();
530 debug_assert!(allocated_contact_count == contact_id_count);
531 let _ = allocated_contact_count;
532 }
533}
534
535pub fn world_step(world: &mut World, time_step: f32, sub_step_count: i32) {
538 debug_assert!(is_valid_float(time_step));
539 debug_assert!(0 < sub_step_count);
540
541 debug_assert!(!world.locked);
542 if world.locked {
543 return;
544 }
545
546 if let Some(mut rec) = world.recording.take() {
548 let world_id = crate::id::WorldId {
549 index1: world.world_id + 1,
550 generation: world.generation,
551 };
552 crate::recording::write_step(&mut rec, world_id, time_step, sub_step_count);
553 world.recording = Some(rec);
554 }
555
556 world.body_move_events.clear();
559 world.sensor_begin_events.clear();
560 world.contact_begin_events.clear();
561 world.contact_hit_events.clear();
562 world.joint_events.clear();
563
564 world.profile = super::Profile::default();
565
566 world.locked = true;
567
568 let step_ticks = get_ticks();
569
570 {
571 let static_shape_count = world.broad_phase.trees[BodyType::Static as usize].proxy_count();
572 let dynamic_shape_count = world.broad_phase.trees[BodyType::Dynamic as usize].proxy_count();
573 let static_body_count = world.solver_sets[STATIC_SET as usize].body_sims.len() as i32;
574 let total_body_count = world.body_id_pool.id_count();
576 let total_contact_count = world.contact_id_pool.id_count();
577
578 let c = &mut world.max_capacity;
579 c.static_shape_count =
580 crate::math_functions::max_int(c.static_shape_count, static_shape_count);
581 c.dynamic_shape_count =
582 crate::math_functions::max_int(c.dynamic_shape_count, dynamic_shape_count);
583 c.static_body_count =
584 crate::math_functions::max_int(c.static_body_count, static_body_count);
585 c.dynamic_body_count = crate::math_functions::max_int(
586 c.dynamic_body_count,
587 total_body_count - static_body_count,
588 );
589 c.contact_count = crate::math_functions::max_int(c.contact_count, total_contact_count);
590 }
591
592 {
594 let pair_ticks = get_ticks();
595 update_broad_phase_pairs(world);
596 world.profile.pairs = get_milliseconds(pair_ticks);
597 }
598
599 let sub_step_count = crate::math_functions::max_int(1, sub_step_count);
600 let mut context = StepContext {
601 dt: time_step,
602 sub_step_count,
603 ..StepContext::default()
604 };
605
606 if time_step > 0.0 {
607 context.inv_dt = 1.0 / time_step;
608 context.h = time_step / sub_step_count as f32;
609 context.inv_h = sub_step_count as f32 * context.inv_dt;
610 } else {
611 context.inv_dt = 0.0;
612 context.h = 0.0;
613 context.inv_h = 0.0;
614 }
615
616 world.inv_h = context.inv_h;
617 world.inv_dt = context.inv_dt;
618
619 let contact_hertz = min_float(world.contact_hertz, 0.125 * context.inv_h);
621 context.contact_softness = make_soft(contact_hertz, world.contact_damping_ratio, context.h);
622 context.static_softness =
623 make_soft(2.0 * contact_hertz, world.contact_damping_ratio, context.h);
624
625 context.restitution_threshold = world.restitution_threshold;
626 context.max_linear_velocity = world.max_linear_speed;
627 context.contact_speed = world.contact_speed;
628 context.enable_warm_starting = world.enable_warm_starting;
629
630 {
632 let collide_ticks = get_ticks();
633 collide(world, &context);
634 world.profile.collide = get_milliseconds(collide_ticks);
635 }
636
637 if time_step > 0.0 {
640 let solve_ticks = get_ticks();
641 solve(world, &context);
642 world.profile.solve = get_milliseconds(solve_ticks);
643 }
644
645 {
647 let sensor_ticks = get_ticks();
648 overlap_sensors(world);
649 world.profile.sensors = get_milliseconds(sensor_ticks);
650 }
651
652 world.profile.step = get_milliseconds(step_ticks);
653
654 world.locked = false;
655
656 world.end_event_array_index = 1 - world.end_event_array_index;
658 world.sensor_end_events[world.end_event_array_index as usize].clear();
659 world.contact_end_events[world.end_event_array_index as usize].clear();
660
661 crate::recording::record_step_end(world);
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668 use crate::body::{create_body, get_body_full_id, get_body_transform};
669 use crate::geometry::make_box;
670 use crate::math_functions::{rot_get_angle, to_pos, Vec2};
671 use crate::shape::create_polygon_shape;
672 use crate::types::{default_body_def, default_shape_def, default_world_def};
673
674 #[test]
677 fn hello_world() {
678 let mut world_def = default_world_def();
681 world_def.gravity = Vec2 { x: 0.0, y: -10.0 };
682
683 let mut world = World::new(&world_def);
684
685 let mut ground_body_def = default_body_def();
687 ground_body_def.position = to_pos(Vec2 { x: 0.0, y: -10.0 });
688
689 let ground_id = create_body(&mut world, &ground_body_def);
690
691 let ground_box = make_box(50.0, 10.0);
694
695 let ground_shape_def = default_shape_def();
697 create_polygon_shape(&mut world, ground_id, &ground_shape_def, &ground_box);
698
699 let mut body_def = default_body_def();
702 body_def.type_ = crate::types::BodyType::Dynamic;
703 body_def.position = to_pos(Vec2 { x: 0.0, y: 4.0 });
704
705 let body_id = create_body(&mut world, &body_def);
706 let body_index = get_body_full_id(&world, body_id);
707
708 let dynamic_box = make_box(1.0, 1.0);
710
711 let mut shape_def = default_shape_def();
714 shape_def.density = 1.0;
715 shape_def.material.friction = 0.3;
716
717 create_polygon_shape(&mut world, body_id, &shape_def, &dynamic_box);
718
719 let time_step = 1.0 / 60.0;
723 let sub_step_count = 4;
724
725 for _ in 0..90 {
727 world_step(&mut world, time_step, sub_step_count);
730 }
731
732 let transform = get_body_transform(&world, body_index);
733 let position = transform.p;
734 let rotation = transform.q;
735
736 assert!(abs_float(position.x as f32) < 0.01);
737 assert!(abs_float(position.y as f32 - 1.00) < 0.01);
738 assert!(abs_float(rot_get_angle(rotation)) < 0.01);
739
740 let profile = crate::world::world_get_profile(&world);
742 assert!(profile.step > 0.0);
743 assert!(profile.pairs >= 0.0);
744 assert!(profile.collide >= 0.0);
745 assert!(profile.solve >= 0.0);
746 assert!(profile.sensors >= 0.0);
747 }
748
749 #[test]
751 fn empty_world() {
752 let world_def = default_world_def();
753 let mut world = World::new(&world_def);
754
755 let time_step = 1.0 / 60.0;
756 let sub_step_count = 1;
757
758 for _ in 0..60 {
759 world_step(&mut world, time_step, sub_step_count);
760 }
761
762 assert_eq!(world.step_index, 60);
764 }
765}