// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Systems for Physics2D integration.
///|
fn entity_key(e : @core.Entity) -> (Int, Int) {
(e.id, e.generation)
}
///|
fn update_transform_from_iso2(
current : @math.Transform,
iso : @rapier_core.Isometry2,
) -> @math.Transform {
let angle = iso.rotation.angle()
@math.Transform::new(
@math.Vec3::new(iso.translation.x, iso.translation.y, current.translation.z),
@math.Quat::from_rotation_z(angle),
current.scale,
)
}
///|
fn make_collider_builder(shape : ColliderShape2d) -> @collision.ColliderBuilder {
match shape {
Ball(radius) => @collision.ColliderBuilder::ball(radius)
Cuboid(hx, hy) => @collision.ColliderBuilder::cuboid(hx, hy)
CapsuleX(half_height, radius) =>
@collision.ColliderBuilder::capsule_x(half_height, radius)
CapsuleY(half_height, radius) =>
@collision.ColliderBuilder::capsule_y(half_height, radius)
Segment(a, b) =>
@collision.ColliderBuilder::segment(
mg_vec2_to_rapier(a),
mg_vec2_to_rapier(b),
)
Polyline(vertices, indices) => {
let vs : Array[@rapier_core.Vec2] = []
for v in vertices {
vs.push(mg_vec2_to_rapier(v))
}
@collision.ColliderBuilder::polyline(vs, indices)
}
ConvexPolygon(vertices) => {
let vs : Array[@rapier_core.Vec2] = []
for v in vertices {
vs.push(mg_vec2_to_rapier(v))
}
@collision.ColliderBuilder::new(@collision.Shape::ConvexPolygon(vs))
}
TriMesh(vertices, indices) => {
let vs : Array[@rapier_core.Vec2] = []
for v in vertices {
vs.push(mg_vec2_to_rapier(v))
}
@collision.ColliderBuilder::new(@collision.Shape::TriMesh(vs, indices))
}
Compound(parts) =>
@collision.ColliderBuilder::new(
collider_shape2d_to_rapier(Compound(parts)),
)
Raw(shape) => @collision.ColliderBuilder::new(shape)
}
}
///|
fn rigid_body_type_eq(
a : @dynamics.RigidBodyType,
b : @dynamics.RigidBodyType,
) -> Bool {
match (a, b) {
(@dynamics.RigidBodyType::Dynamic, @dynamics.RigidBodyType::Dynamic) => true
(@dynamics.RigidBodyType::Fixed, @dynamics.RigidBodyType::Fixed) => true
(
@dynamics.RigidBodyType::KinematicPositionBased,
@dynamics.RigidBodyType::KinematicPositionBased,
) => true
(
@dynamics.RigidBodyType::KinematicVelocityBased,
@dynamics.RigidBodyType::KinematicVelocityBased,
) => true
_ => false
}
}
///|
fn collision_groups_to_rapier(
groups : CollisionGroups2d,
) -> @dynamics.InteractionGroups {
@dynamics.InteractionGroups::new(
@dynamics.Group::new(groups.memberships),
@dynamics.Group::new(groups.filter),
groups.test_mode,
)
}
///|
fn solver_groups_to_rapier(
groups : SolverGroups2d,
) -> @dynamics.InteractionGroups {
@dynamics.InteractionGroups::new(
@dynamics.Group::new(groups.memberships),
@dynamics.Group::new(groups.filter),
groups.test_mode,
)
}
///|
fn recompute_body_mass_properties(
ctx : Physics2dContext,
bodyh : @dynamics.RigidBodyHandle,
) -> Unit {
guard ctx.rigid_bodies.get_mut_internal_with_modification_tracking(bodyh)
is Some(body) else {
return
}
match body.body_type() {
@dynamics.RigidBodyType::Dynamic => ()
_ => return
}
let attached = ctx.colliders.colliders_with_parent(bodyh)
if attached.length() == 0 {
return
}
let mut m = @rapier_core.MassProperties::default()
for ch in attached {
if ctx.colliders.get(ch) is Some(col) {
let mut mp = col.mass_properties()
match col.position_wrt_parent() {
Some(rel) => mp = mp.transform_by(rel)
None => ()
}
m = m.add(mp)
}
}
body.set_local_mass_properties(m) |> ignore
}
///|
pub fn[
W : @core.ChangeTickWorld + HasRes_Physics2dConfig + HasRes_Physics2dContext + HasRes_Physics2dEvents,
] physics2d_init_resources_system(
world : W,
) -> Unit {
let tick = @core.ChangeTickWorld::read_change_tick(world)
let cfg_res = HasRes_Physics2dConfig::get_physics2d_config_resource(world)
if !cfg_res.is_present() {
cfg_res.insert(Physics2dConfig::default(), tick)
}
let ctx_res = HasRes_Physics2dContext::get_physics2d_context_resource(world)
if !ctx_res.is_present() {
ctx_res.insert(Physics2dContext::new(), tick)
}
let ev_res = HasRes_Physics2dEvents::get_physics2d_events_resource(world)
if !ev_res.is_present() {
ev_res.insert(Physics2dEvents::new(), tick)
}
}
///|
pub fn[W : @core.ChangeTickWorld + HasRes_Physics2dEvents] physics2d_clear_events_system(
world : W,
) -> Unit {
let tick = @core.ChangeTickWorld::read_change_tick(world)
let ev_res = HasRes_Physics2dEvents::get_physics2d_events_resource(world)
match ev_res.get_ref_mut(tick) {
Some(r) => r.val.clear()
None => ()
}
}
///|
pub fn[
W : @core.ChangeTickWorld + Has_RigidBody2d + Has_Collider2d + Has_ImpulseJoint2d + Has_MultibodyJoint2d + Has_RapierBodyHandle2d + Has_RapierColliderHandle2d + Has_RapierImpulseJointHandle2d + Has_RapierMultibodyJointHandle2d + HasRes_Physics2dContext,
] physics2d_cleanup_despawned_system(
world : W,
) -> Unit {
let tick = @core.ChangeTickWorld::read_change_tick(world)
let ctx_res = HasRes_Physics2dContext::get_physics2d_context_resource(world)
guard ctx_res.get_ref_mut(tick) is Some(ctx_ref) else { return }
let ctx = ctx_ref.val
let rb_store = Has_RigidBody2d::get_rigid_body2d_store(world)
let col_store = Has_Collider2d::get_collider2d_store(world)
let impulse_joint_store = Has_ImpulseJoint2d::get_impulse_joint2d_store(world)
let multibody_joint_store = Has_MultibodyJoint2d::get_multibody_joint2d_store(
world,
)
let body_handle_store = Has_RapierBodyHandle2d::get_rapier_body_handle2d_store(
world,
)
let collider_handle_store = Has_RapierColliderHandle2d::get_rapier_collider_handle2d_store(
world,
)
let impulse_joint_handle_store = Has_RapierImpulseJointHandle2d::get_rapier_impulse_joint_handle2d_store(
world,
)
let multibody_joint_handle_store = Has_RapierMultibodyJointHandle2d::get_rapier_multibody_joint_handle2d_store(
world,
)
// 1) Remove handles/rapier state when the authoring component is removed.
let remove_bodies : Array[@core.Entity] = []
body_handle_store.for_each(fn(e, _h) {
if !rb_store.contains(e) {
remove_bodies.push(e)
}
})
for e in remove_bodies {
if body_handle_store.get(e) is Some(h) {
// Remove and detach all colliders attached to this body (including
// colliders authored by other entities).
let attached = ctx.colliders.colliders_with_parent(h.handle)
for ch in attached {
let raw = ch.into_raw_parts()
if ctx.collider_to_entity.get(raw) is Some(owner) {
collider_handle_store.remove(owner) |> ignore
ctx.entity_to_collider.remove(entity_key(owner))
ctx.collider_to_entity.remove(raw)
}
}
@pipeline.remove_rigid_body(
ctx.rigid_bodies,
h.handle,
ctx.islands,
ctx.colliders,
ctx.impulse_joints,
ctx.multibody_joints,
true,
true,
)
|> ignore
body_handle_store.remove(e) |> ignore
ctx.entity_to_body.remove(entity_key(e))
// If this entity also authored a collider, the collider was removed by
// `remove_rigid_body`. Drop the handle so it can be reinserted as a
// standalone collider (if Collider2d still exists).
if collider_handle_store.get(e) is Some(ch) {
collider_handle_store.remove(e) |> ignore
ctx.entity_to_collider.remove(entity_key(e))
ctx.collider_to_entity.remove(ch.handle.into_raw_parts())
}
// Drop joint handles authored by this entity; `remove_rigid_body` removes
// all attached joints in Rapier.
if impulse_joint_handle_store.get(e) is Some(jh) {
impulse_joint_handle_store.remove(e) |> ignore
ctx.entity_to_impulse_joint.remove(entity_key(e))
// Best-effort remove: the joint may already be gone.
ctx.impulse_joints.remove(jh.handle, true) |> ignore
}
if multibody_joint_handle_store.get(e) is Some(jh) {
multibody_joint_handle_store.remove(e) |> ignore
ctx.entity_to_multibody_joint.remove(entity_key(e))
ctx.multibody_joints.remove(jh.handle, true)
}
}
}
let remove_colliders : Array[@core.Entity] = []
collider_handle_store.for_each(fn(e, _h) {
if !col_store.contains(e) {
remove_colliders.push(e)
}
})
for e in remove_colliders {
if collider_handle_store.get(e) is Some(h) {
ctx.colliders.remove(h.handle, ctx.islands, ctx.rigid_bodies, true)
|> ignore
collider_handle_store.remove(e) |> ignore
ctx.entity_to_collider.remove(entity_key(e))
ctx.collider_to_entity.remove(h.handle.into_raw_parts())
}
}
// Remove joints when the authoring component is removed or bodies are missing.
let remove_impulse_joints : Array[@core.Entity] = []
impulse_joint_handle_store.for_each(fn(e, _h) {
if !impulse_joint_store.contains(e) || !body_handle_store.contains(e) {
remove_impulse_joints.push(e)
}
})
for e in remove_impulse_joints {
if impulse_joint_handle_store.get(e) is Some(h) {
ctx.impulse_joints.remove(h.handle, true) |> ignore
impulse_joint_handle_store.remove(e) |> ignore
ctx.entity_to_impulse_joint.remove(entity_key(e))
}
}
let remove_multibody_joints : Array[@core.Entity] = []
multibody_joint_handle_store.for_each(fn(e, _h) {
if !multibody_joint_store.contains(e) || !body_handle_store.contains(e) {
remove_multibody_joints.push(e)
}
})
for e in remove_multibody_joints {
if multibody_joint_handle_store.get(e) is Some(h) {
ctx.multibody_joints.remove(h.handle, true)
multibody_joint_handle_store.remove(e) |> ignore
ctx.entity_to_multibody_joint.remove(entity_key(e))
}
}
// 2) Remove rapier state when entities are despawned (handle components gone).
let alive : @hashmap.HashMap[(Int, Int), Bool] = @hashmap.HashMap::new()
body_handle_store.for_each(fn(e, _h) { alive.set(entity_key(e), true) })
collider_handle_store.for_each(fn(e, _h) { alive.set(entity_key(e), true) })
impulse_joint_handle_store.for_each(fn(e, _h) {
alive.set(entity_key(e), true)
})
multibody_joint_handle_store.for_each(fn(e, _h) {
alive.set(entity_key(e), true)
})
let dead_body_keys : Array[(Int, Int)] = []
for entry in ctx.entity_to_body.iter() {
let (k, _h) = entry
if !alive.contains(k) {
dead_body_keys.push(k)
}
}
for k in dead_body_keys {
if ctx.entity_to_body.get(k) is Some(handle) {
// Remove and detach all colliders attached to this body (including
// colliders authored by other entities).
let attached = ctx.colliders.colliders_with_parent(handle)
for ch in attached {
let raw = ch.into_raw_parts()
if ctx.collider_to_entity.get(raw) is Some(owner) {
collider_handle_store.remove(owner) |> ignore
ctx.entity_to_collider.remove(entity_key(owner))
ctx.collider_to_entity.remove(raw)
}
}
@pipeline.remove_rigid_body(
ctx.rigid_bodies,
handle,
ctx.islands,
ctx.colliders,
ctx.impulse_joints,
ctx.multibody_joints,
true,
true,
)
|> ignore
ctx.entity_to_body.remove(k)
}
}
let dead_collider_keys : Array[(Int, Int)] = []
for entry in ctx.entity_to_collider.iter() {
let (k, _h) = entry
if !alive.contains(k) {
dead_collider_keys.push(k)
}
}
for k in dead_collider_keys {
if ctx.entity_to_collider.get(k) is Some(handle) {
ctx.colliders.remove(handle, ctx.islands, ctx.rigid_bodies, true)
|> ignore
ctx.entity_to_collider.remove(k)
ctx.collider_to_entity.remove(handle.into_raw_parts())
}
}
let dead_impulse_joint_keys : Array[(Int, Int)] = []
for entry in ctx.entity_to_impulse_joint.iter() {
let (k, _h) = entry
if !alive.contains(k) {
dead_impulse_joint_keys.push(k)
}
}
for k in dead_impulse_joint_keys {
if ctx.entity_to_impulse_joint.get(k) is Some(handle) {
ctx.impulse_joints.remove(handle, true) |> ignore
ctx.entity_to_impulse_joint.remove(k)
}
}
let dead_multibody_joint_keys : Array[(Int, Int)] = []
for entry in ctx.entity_to_multibody_joint.iter() {
let (k, _h) = entry
if !alive.contains(k) {
dead_multibody_joint_keys.push(k)
}
}
for k in dead_multibody_joint_keys {
if ctx.entity_to_multibody_joint.get(k) is Some(handle) {
ctx.multibody_joints.remove(handle, true)
ctx.entity_to_multibody_joint.remove(k)
}
}
// Keep query pipeline in sync with removals.
ctx.query_pipeline.update()
}
///|
pub fn[
W : @core.ChangeTickWorld + @math.Has_Transform + Has_RigidBody2d + Has_Collider2d + Has_ColliderParent2d + Has_Velocity2d + Has_Damping2d + Has_GravityScale2d + Has_LockedAxes2d + Has_Ccd2d + Has_Dominance2d + Has_Sleeping2d + Has_ExternalForce2d + Has_ExternalImpulse2d + Has_RigidBodyEnabled2d + Has_AdditionalMassProperties2d + Has_AdditionalSolverIterations2d + Has_ContactForceEventThreshold2d + Has_ColliderDensity2d + Has_Friction2d + Has_FrictionCombineRule2d + Has_Restitution2d + Has_RestitutionCombineRule2d + Has_CollisionGroups2d + Has_SolverGroups2d + Has_ColliderEnabled2d + Has_ActiveCollisionTypes2d + Has_ActiveEvents2d + Has_ActiveHooks2d + Has_ContactSkin2d + Has_ColliderMassProperties2d + Has_RapierBodyHandle2d + Has_RapierColliderHandle2d + HasRes_Physics2dContext,
] physics2d_sync_insertions_system(
world : W,
) -> Unit {
let tick = @core.ChangeTickWorld::read_change_tick(world)
let ctx_res = HasRes_Physics2dContext::get_physics2d_context_resource(world)
guard ctx_res.get_ref_mut(tick) is Some(ctx_ref) else { return }
let ctx = ctx_ref.val
let transform_store = @math.Has_Transform::get_transform_store(world)
let vel_store = Has_Velocity2d::get_velocity2d_store(world)
let damping_store = Has_Damping2d::get_damping2d_store(world)
let gravity_scale_store = Has_GravityScale2d::get_gravity_scale2d_store(world)
let locked_axes_store = Has_LockedAxes2d::get_locked_axes2d_store(world)
let ccd_store = Has_Ccd2d::get_ccd2d_store(world)
let dominance_store = Has_Dominance2d::get_dominance2d_store(world)
let sleeping_store = Has_Sleeping2d::get_sleeping2d_store(world)
let rb_enabled_store = Has_RigidBodyEnabled2d::get_rigid_body_enabled2d_store(
world,
)
let additional_mass_props_store = Has_AdditionalMassProperties2d::get_additional_mass_properties2d_store(
world,
)
let additional_solver_iter_store = Has_AdditionalSolverIterations2d::get_additional_solver_iterations2d_store(
world,
)
let contact_force_threshold_store = Has_ContactForceEventThreshold2d::get_contact_force_event_threshold2d_store(
world,
)
let density_store = Has_ColliderDensity2d::get_collider_density2d_store(world)
let friction_store = Has_Friction2d::get_friction2d_store(world)
let friction_rule_store = Has_FrictionCombineRule2d::get_friction_combine_rule2d_store(
world,
)
let restitution_store = Has_Restitution2d::get_restitution2d_store(world)
let restitution_rule_store = Has_RestitutionCombineRule2d::get_restitution_combine_rule2d_store(
world,
)
let collision_groups_store = Has_CollisionGroups2d::get_collision_groups2d_store(
world,
)
let solver_groups_store = Has_SolverGroups2d::get_solver_groups2d_store(world)
let collider_enabled_store = Has_ColliderEnabled2d::get_collider_enabled2d_store(
world,
)
let active_collision_types_store = Has_ActiveCollisionTypes2d::get_active_collision_types2d_store(
world,
)
let active_events_store = Has_ActiveEvents2d::get_active_events2d_store(world)
let active_hooks_store = Has_ActiveHooks2d::get_active_hooks2d_store(world)
let contact_skin_store = Has_ContactSkin2d::get_contact_skin2d_store(world)
let collider_mass_props_store = Has_ColliderMassProperties2d::get_collider_mass_properties2d_store(
world,
)
let rb_store = Has_RigidBody2d::get_rigid_body2d_store(world)
let col_store = Has_Collider2d::get_collider2d_store(world)
let col_parent_store = Has_ColliderParent2d::get_collider_parent2d_store(
world,
)
let body_handle_store = Has_RapierBodyHandle2d::get_rapier_body_handle2d_store(
world,
)
let collider_handle_store = Has_RapierColliderHandle2d::get_rapier_collider_handle2d_store(
world,
)
let touched_bodies : @hashmap.HashMap[(Int, Int), Bool] = @hashmap.HashMap::new()
// Insert rigid bodies.
rb_store.for_each(fn(entity, rb2d) {
if body_handle_store.contains(entity) {
return
}
let transform = transform_store
.get(entity)
.unwrap_or(@math.Transform::identity())
let iso = mg_transform_to_iso2(transform)
let builder = match rb2d.body_type {
@dynamics.RigidBodyType::Dynamic => @dynamics.RigidBodyBuilder::dynamic()
@dynamics.RigidBodyType::Fixed => @dynamics.RigidBodyBuilder::fixed()
@dynamics.RigidBodyType::KinematicPositionBased =>
@dynamics.RigidBodyBuilder::kinematic_position_based()
@dynamics.RigidBodyType::KinematicVelocityBased =>
@dynamics.RigidBodyBuilder::kinematic_velocity_based()
}
let mut builder = builder.position(iso)
if vel_store.get(entity) is Some(v) {
builder = builder.linvel(mg_vec2_to_rapier(v.linvel)).angvel(v.angvel)
}
if damping_store.get(entity) is Some(d) {
builder = builder.linear_damping(d.linear).angular_damping(d.angular)
}
if gravity_scale_store.get(entity) is Some(gs) {
builder = builder.gravity_scale(gs.scale)
}
if locked_axes_store.get(entity) is Some(la) {
if la.lock_translations {
builder = builder.lock_translations()
}
if la.lock_rotations {
builder = builder.lock_rotations()
}
}
if ccd_store.get(entity) is Some(ccd) {
builder = builder
.ccd_enabled(ccd.enabled)
.soft_ccd_prediction(ccd.soft_prediction)
}
if dominance_store.get(entity) is Some(dom) {
builder = builder.dominance_group(dom.group)
}
if sleeping_store.get(entity) is Some(s) {
builder = builder.sleeping(s.sleeping)
}
if rb_enabled_store.get(entity) is Some(e) {
builder = builder.enabled(e.enabled)
}
if additional_mass_props_store.get(entity) is Some(m) {
builder = builder.additional_mass_properties(m.props)
}
if additional_solver_iter_store.get(entity) is Some(it) {
builder = builder.additional_solver_iterations(it.iterations)
}
let rb = builder.build()
let handle = ctx.rigid_bodies.insert(rb)
body_handle_store.insert(entity, RapierBodyHandle2d::new(handle), tick)
ctx.entity_to_body.set(entity_key(entity), handle)
})
// Insert colliders.
col_store.for_each(fn(entity, col2d) {
if collider_handle_store.contains(entity) {
return
}
let mut builder = make_collider_builder(col2d.shape).sensor(col2d.sensor)
if collider_enabled_store.get(entity) is Some(e) {
builder = builder.enabled(e.enabled)
}
if active_collision_types_store.get(entity) is Some(t) {
builder = builder.active_collision_types(t.types)
}
if active_hooks_store.get(entity) is Some(h) {
builder = builder.active_hooks(h.hooks)
}
if contact_skin_store.get(entity) is Some(s) {
builder = builder.contact_skin(s.skin)
}
let active_events = match active_events_store.get(entity) {
Some(a) => a.events
None =>
if contact_force_threshold_store.get(entity) is Some(_) {
@collision.ActiveEvents::collision_events().insert(
@collision.ActiveEvents::contact_force_events(),
)
} else {
@collision.ActiveEvents::collision_events()
}
}
// No workaround policy: do NOT set a default density here.
// If moon_rapier defaults are incorrect, users must opt-in via
// `ColliderDensity2d` until upstream fixes the default behavior.
if density_store.get(entity) is Some(d) {
builder = builder.density(d.density)
}
if friction_store.get(entity) is Some(f) {
builder = builder.friction(f.coefficient)
}
if friction_rule_store.get(entity) is Some(r) {
builder = builder.friction_combine_rule(r.rule)
}
if restitution_store.get(entity) is Some(r) {
builder = builder.restitution(r.coefficient)
}
if restitution_rule_store.get(entity) is Some(r) {
builder = builder.restitution_combine_rule(r.rule)
}
if collision_groups_store.get(entity) is Some(g) {
builder = builder.collision_groups(collision_groups_to_rapier(g))
}
if solver_groups_store.get(entity) is Some(g) {
builder = builder.solver_groups(solver_groups_to_rapier(g))
}
if contact_force_threshold_store.get(entity) is Some(th) {
builder = builder.contact_force_event_threshold(th.threshold)
}
builder = builder.active_events(active_events).user_data(entity.id)
let handle = match body_handle_store.get(entity) {
Some(bodyh) => {
let mut collider = builder.build()
if collider_mass_props_store.get(entity) is Some(mp) {
collider = collider.set_mass_properties(mp.mass_properties)
}
let handle = ctx.colliders.insert_with_parent(
collider,
bodyh.handle,
ctx.rigid_bodies,
)
touched_bodies.set(bodyh.handle.into_raw_parts(), true)
handle
}
None =>
match col_parent_store.get(entity) {
Some(p) =>
match body_handle_store.get(p.parent) {
Some(parent_bh) => {
let transform = transform_store
.get(entity)
.unwrap_or(@math.Transform::identity())
let iso = mg_transform_to_iso2(transform)
let mut collider = builder
.translation(iso.translation)
.rotation(iso.rotation.angle())
.build()
if collider_mass_props_store.get(entity) is Some(mp) {
collider = collider.set_mass_properties(mp.mass_properties)
}
let handle = ctx.colliders.insert_with_parent(
collider,
parent_bh.handle,
ctx.rigid_bodies,
)
touched_bodies.set(parent_bh.handle.into_raw_parts(), true)
handle
}
None => {
let transform = transform_store
.get(entity)
.unwrap_or(@math.Transform::identity())
let iso = mg_transform_to_iso2(transform)
let mut collider = builder
.translation(iso.translation)
.rotation(iso.rotation.angle())
.build()
if collider_mass_props_store.get(entity) is Some(mp) {
collider = collider.set_mass_properties(mp.mass_properties)
}
ctx.colliders.insert(collider)
}
}
None => {
let transform = transform_store
.get(entity)
.unwrap_or(@math.Transform::identity())
let iso = mg_transform_to_iso2(transform)
let mut collider = builder
.translation(iso.translation)
.rotation(iso.rotation.angle())
.build()
if collider_mass_props_store.get(entity) is Some(mp) {
collider = collider.set_mass_properties(mp.mass_properties)
}
ctx.colliders.insert(collider)
}
}
}
collider_handle_store.insert(
entity,
RapierColliderHandle2d::new(handle),
tick,
)
ctx.entity_to_collider.set(entity_key(entity), handle)
ctx.collider_to_entity.set(handle.into_raw_parts(), entity)
})
// Keep query pipeline in sync with insertions.
ctx.query_pipeline.update()
// Recompute mass properties for bodies that gained colliders this frame.
for entry in touched_bodies.iter() {
let (k, _v) = entry
let (id, gen) = k
recompute_body_mass_properties(
ctx,
@dynamics.RigidBodyHandle::from_raw_parts(id, gen),
)
}
}
///|
pub fn[
W : @core.ChangeTickWorld + @math.Has_Transform + Has_RigidBody2d + Has_Collider2d + Has_ColliderParent2d + Has_Velocity2d + Has_Damping2d + Has_GravityScale2d + Has_LockedAxes2d + Has_Ccd2d + Has_Dominance2d + Has_Sleeping2d + Has_ExternalForce2d + Has_ExternalImpulse2d + Has_RigidBodyEnabled2d + Has_AdditionalMassProperties2d + Has_AdditionalSolverIterations2d + Has_ContactForceEventThreshold2d + Has_ColliderDensity2d + Has_Friction2d + Has_FrictionCombineRule2d + Has_Restitution2d + Has_RestitutionCombineRule2d + Has_CollisionGroups2d + Has_SolverGroups2d + Has_ColliderEnabled2d + Has_ActiveCollisionTypes2d + Has_ActiveEvents2d + Has_ActiveHooks2d + Has_ContactSkin2d + Has_ColliderMassProperties2d + Has_RapierBodyHandle2d + Has_RapierColliderHandle2d + HasRes_Physics2dConfig + HasRes_Physics2dContext,
] physics2d_sync_world_to_rapier_system(
world : W,
) -> Unit {
let tick = @core.ChangeTickWorld::read_change_tick(world)
let cfg_res = HasRes_Physics2dConfig::get_physics2d_config_resource(world)
let ctx_res = HasRes_Physics2dContext::get_physics2d_context_resource(world)
guard cfg_res.get_ref() is Some(cfg_ref) else { return }
guard ctx_res.get_ref_mut(tick) is Some(ctx_ref) else { return }
let cfg = cfg_ref.val
let ctx = ctx_ref.val
let dt = @app.fixed_time_timestep_seconds()
ctx.integration = cfg.integration.set_dt(dt)
physics2d_apply_global_hooks(ctx)
let transform_store = @math.Has_Transform::get_transform_store(world)
let rb2d_store = Has_RigidBody2d::get_rigid_body2d_store(world)
let col2d_store = Has_Collider2d::get_collider2d_store(world)
let col_parent_store = Has_ColliderParent2d::get_collider_parent2d_store(
world,
)
let vel_store = Has_Velocity2d::get_velocity2d_store(world)
let damping_store = Has_Damping2d::get_damping2d_store(world)
let gravity_scale_store = Has_GravityScale2d::get_gravity_scale2d_store(world)
let locked_axes_store = Has_LockedAxes2d::get_locked_axes2d_store(world)
let ccd_store = Has_Ccd2d::get_ccd2d_store(world)
let dominance_store = Has_Dominance2d::get_dominance2d_store(world)
let sleeping_store = Has_Sleeping2d::get_sleeping2d_store(world)
let rb_enabled_store = Has_RigidBodyEnabled2d::get_rigid_body_enabled2d_store(
world,
)
let additional_mass_props_store = Has_AdditionalMassProperties2d::get_additional_mass_properties2d_store(
world,
)
let additional_solver_iter_store = Has_AdditionalSolverIterations2d::get_additional_solver_iterations2d_store(
world,
)
let external_force_store = Has_ExternalForce2d::get_external_force2d_store(
world,
)
let external_impulse_store = Has_ExternalImpulse2d::get_external_impulse2d_store(
world,
)
let contact_force_threshold_store = Has_ContactForceEventThreshold2d::get_contact_force_event_threshold2d_store(
world,
)
let density_store = Has_ColliderDensity2d::get_collider_density2d_store(world)
let friction_store = Has_Friction2d::get_friction2d_store(world)
let friction_rule_store = Has_FrictionCombineRule2d::get_friction_combine_rule2d_store(
world,
)
let restitution_store = Has_Restitution2d::get_restitution2d_store(world)
let restitution_rule_store = Has_RestitutionCombineRule2d::get_restitution_combine_rule2d_store(
world,
)
let collision_groups_store = Has_CollisionGroups2d::get_collision_groups2d_store(
world,
)
let solver_groups_store = Has_SolverGroups2d::get_solver_groups2d_store(world)
let collider_enabled_store = Has_ColliderEnabled2d::get_collider_enabled2d_store(
world,
)
let active_collision_types_store = Has_ActiveCollisionTypes2d::get_active_collision_types2d_store(
world,
)
let active_events_store = Has_ActiveEvents2d::get_active_events2d_store(world)
let active_hooks_store = Has_ActiveHooks2d::get_active_hooks2d_store(world)
let contact_skin_store = Has_ContactSkin2d::get_contact_skin2d_store(world)
let collider_mass_props_store = Has_ColliderMassProperties2d::get_collider_mass_properties2d_store(
world,
)
let body_handle_store = Has_RapierBodyHandle2d::get_rapier_body_handle2d_store(
world,
)
let collider_handle_store = Has_RapierColliderHandle2d::get_rapier_collider_handle2d_store(
world,
)
// Colliders: update authoring properties and keep standalone colliders
// (no parent rigid body) following Transform.
collider_handle_store.for_each(fn(entity, ch) {
guard ctx.colliders.get_mut_internal_with_modification_tracking(ch.handle)
is Some(collider) else {
return
}
if col2d_store.get(entity) is Some(col2d) {
collider.set_sensor(col2d.sensor) |> ignore
collider.set_shape(collider_shape2d_to_rapier(col2d.shape)) |> ignore
}
collider.set_enabled(
collider_enabled_store
.get(entity)
.map(fn(e) { e.enabled })
.unwrap_or(true),
)
|> ignore
if density_store.get(entity) is Some(d) {
collider.set_density(d.density) |> ignore
}
if friction_store.get(entity) is Some(f) {
collider.set_friction(f.coefficient) |> ignore
}
if friction_rule_store.get(entity) is Some(r) {
collider.set_friction_combine_rule(r.rule) |> ignore
}
if restitution_store.get(entity) is Some(r) {
collider.set_restitution(r.coefficient) |> ignore
}
if restitution_rule_store.get(entity) is Some(r) {
collider.set_restitution_combine_rule(r.rule) |> ignore
}
if collision_groups_store.get(entity) is Some(g) {
collider.set_collision_groups(collision_groups_to_rapier(g)) |> ignore
}
if solver_groups_store.get(entity) is Some(g) {
collider.set_solver_groups(solver_groups_to_rapier(g)) |> ignore
}
if active_collision_types_store.get(entity) is Some(t) {
collider.set_active_collision_types(t.types) |> ignore
}
if active_hooks_store.get(entity) is Some(h) {
collider.set_active_hooks(h.hooks) |> ignore
}
if contact_skin_store.get(entity) is Some(s) {
collider.set_contact_skin(s.skin) |> ignore
}
if collider_mass_props_store.get(entity) is Some(mp) {
collider.set_mass_properties(mp.mass_properties) |> ignore
}
let active_events = match active_events_store.get(entity) {
Some(a) => a.events
None =>
if contact_force_threshold_store.get(entity) is Some(_) {
@collision.ActiveEvents::collision_events().insert(
@collision.ActiveEvents::contact_force_events(),
)
} else {
@collision.ActiveEvents::collision_events()
}
}
collider.set_active_events(active_events) |> ignore
if contact_force_threshold_store.get(entity) is Some(th) {
collider.set_contact_force_event_threshold(th.threshold) |> ignore
}
if body_handle_store.contains(entity) {
return
}
if col_parent_store.get(entity) is Some(p) {
if body_handle_store.get(p.parent) is Some(_parent_bh) {
let transform = transform_store
.get(entity)
.unwrap_or(@math.Transform::identity())
collider.set_position_wrt_parent(mg_transform_to_iso2(transform))
|> ignore
return
}
}
let transform = transform_store
.get(entity)
.unwrap_or(@math.Transform::identity())
collider.set_position(mg_transform_to_iso2(transform)) |> ignore
})
body_handle_store.for_each(fn(entity, bh) {
guard rb2d_store.get(entity) is Some(rb2d) else { return }
guard ctx.rigid_bodies.get_mut_internal_with_modification_tracking(
bh.handle,
)
is Some(body) else {
return
}
if !rigid_body_type_eq(body.body_type(), rb2d.body_type) {
body.set_body_type(rb2d.body_type, true) |> ignore
}
// Kinematic position-based bodies follow the ECS transform.
if rb2d.body_type is @dynamics.RigidBodyType::KinematicPositionBased {
let transform = transform_store
.get(entity)
.unwrap_or(@math.Transform::identity())
let iso = mg_transform_to_iso2(transform)
body.set_next_kinematic_translation(iso.translation) |> ignore
body.set_next_kinematic_rotation(iso.rotation) |> ignore
}
// Apply user-driven velocity updates.
if vel_store.get(entity) is Some(v) {
body.set_linvel(mg_vec2_to_rapier(v.linvel), true) |> ignore
body.set_angvel(v.angvel, true) |> ignore
}
// Bevy-like authoring components.
let damping = damping_store.get(entity)
body.set_linear_damping(damping.map(fn(d) { d.linear }).unwrap_or(0.0F))
|> ignore
body.set_angular_damping(damping.map(fn(d) { d.angular }).unwrap_or(0.0F))
|> ignore
body.set_gravity_scale(
gravity_scale_store.get(entity).map(fn(g) { g.scale }).unwrap_or(1.0F),
true,
)
|> ignore
let la = locked_axes_store.get(entity)
body.set_locked_translations(
la.map(fn(x) { x.lock_translations }).unwrap_or(false),
true,
)
|> ignore
body.set_locked_rotations(
la.map(fn(x) { x.lock_rotations }).unwrap_or(false),
true,
)
|> ignore
let ccd = ccd_store.get(entity)
body.enable_ccd(ccd.map(fn(c) { c.enabled }).unwrap_or(false)) |> ignore
body.set_soft_ccd_prediction(
ccd.map(fn(c) { c.soft_prediction }).unwrap_or(0.0F),
)
|> ignore
body.set_dominance_group(
dominance_store.get(entity).map(fn(d) { d.group }).unwrap_or(0),
)
|> ignore
body.set_enabled(
rb_enabled_store.get(entity).map(fn(e) { e.enabled }).unwrap_or(true),
)
|> ignore
if additional_mass_props_store.get(entity) is Some(m) {
body.set_additional_mass_properties(m.props, true) |> ignore
}
if additional_solver_iter_store.get(entity) is Some(it) {
body.set_additional_solver_iterations(it.iterations) |> ignore
}
match sleeping_store.get(entity) {
Some(s) =>
if s.sleeping {
body.sleep() |> ignore
} else {
body.wake_up(true) |> ignore
}
None => ()
}
// External forces (persistent, applied each fixed step).
if external_force_store.get(entity) is Some(f) {
body.add_force(mg_vec2_to_rapier(f.force), true) |> ignore
body.add_torque(f.torque, true) |> ignore
}
// External impulses (one-shot, cleared after application).
if external_impulse_store.get(entity) is Some(imp) {
if imp.impulse.x != 0.0F || imp.impulse.y != 0.0F {
body.apply_impulse(mg_vec2_to_rapier(imp.impulse), true) |> ignore
}
if imp.torque_impulse != 0.0F {
body.apply_torque_impulse(imp.torque_impulse, true) |> ignore
}
external_impulse_store.set(entity, ExternalImpulse2d::zero(), tick)
|> ignore
}
})
// Keep query pipeline in sync with any transform/authoring changes.
ctx.query_pipeline.update()
// Ensure mass properties stay correct for multi-collider bodies when collider
// authoring components (density, etc.) change.
body_handle_store.for_each(fn(_entity, bh) {
recompute_body_mass_properties(ctx, bh.handle)
})
}
///|
pub fn[
W : @core.ChangeTickWorld + HasRes_Physics2dConfig + HasRes_Physics2dContext + HasRes_Physics2dEvents,
] physics2d_step_system(
world : W,
) -> Unit {
let tick = @core.ChangeTickWorld::read_change_tick(world)
let cfg_res = HasRes_Physics2dConfig::get_physics2d_config_resource(world)
let ctx_res = HasRes_Physics2dContext::get_physics2d_context_resource(world)
let ev_res = HasRes_Physics2dEvents::get_physics2d_events_resource(world)
guard cfg_res.get_ref() is Some(cfg_ref) else { return }
guard ctx_res.get_ref_mut(tick) is Some(ctx_ref) else { return }
guard ev_res.get_ref_mut(tick) is Some(ev_ref) else { return }
let cfg = cfg_ref.val
let ctx = ctx_ref.val
let events = ev_ref.val
let gravity = mg_vec2_to_rapier(cfg.gravity)
if !cfg.physics_pipeline_active {
ctx.query_pipeline.update()
return
}
physics2d_hooks_set_current_collider_map(ctx.collider_to_entity)
ctx.pipeline.step(
gravity,
ctx.integration,
ctx.islands,
ctx.broad_phase,
ctx.narrow_phase,
ctx.rigid_bodies,
ctx.colliders,
ctx.impulse_joints,
ctx.multibody_joints,
ctx.ccd,
ctx.hooks,
ctx.event_handler,
)
let rapier_events = ctx.event_handler.take_collision_events()
for ev in rapier_events {
if ev.removed() {
continue
}
let h1 = ev.collider1()
let h2 = ev.collider2()
let e1 = ctx.collider_to_entity.get(h1.into_raw_parts())
let e2 = ctx.collider_to_entity.get(h2.into_raw_parts())
match (e1, e2) {
(Some(a), Some(b)) => {
let sensor = ev.sensor()
if ev.started() {
events.collisions.push(CollisionEvent2d::Started(a, b, sensor))
} else if ev.stopped() {
events.collisions.push(CollisionEvent2d::Stopped(a, b, sensor))
}
}
_ => ()
}
}
let rapier_contact_force_events = ctx.event_handler.take_contact_force_events()
for ev in rapier_contact_force_events {
let h1 = ev.collider1()
let h2 = ev.collider2()
let e1 = ctx.collider_to_entity.get(h1.into_raw_parts())
let e2 = ctx.collider_to_entity.get(h2.into_raw_parts())
match (e1, e2) {
(Some(a), Some(b)) =>
events.contact_forces.push(ContactForceEvent2d::{
entity1: a,
entity2: b,
total_force: rapier_vec2_to_mg(ev.total_force()),
total_force_magnitude: ev.total_force_magnitude(),
max_force_direction: rapier_vec2_to_mg(ev.max_force_direction()),
max_force_magnitude: ev.max_force_magnitude(),
})
_ => ()
}
}
// Update query pipeline after stepping so queries see current positions.
ctx.query_pipeline.update()
physics2d_hooks_clear_current_collider_map()
}
///|
pub fn[
W : @core.ChangeTickWorld + @math.Has_Transform + Has_Velocity2d + Has_RapierBodyHandle2d + HasRes_Physics2dContext,
] physics2d_writeback_system(
world : W,
) -> Unit {
let tick = @core.ChangeTickWorld::read_change_tick(world)
let ctx_res = HasRes_Physics2dContext::get_physics2d_context_resource(world)
guard ctx_res.get_ref() is Some(ctx_ref) else { return }
let ctx = ctx_ref.val
let transform_store = @math.Has_Transform::get_transform_store(world)
let vel_store = Has_Velocity2d::get_velocity2d_store(world)
let body_handle_store = Has_RapierBodyHandle2d::get_rapier_body_handle2d_store(
world,
)
body_handle_store.for_each(fn(entity, bh) {
guard ctx.rigid_bodies.get(bh.handle) is Some(body) else { return }
let pos = body.position()
let current = transform_store
.get(entity)
.unwrap_or(@math.Transform::identity())
let next = update_transform_from_iso2(current, pos)
if transform_store.contains(entity) {
transform_store.set(entity, next, tick) |> ignore
} else {
transform_store.insert(entity, next, tick)
}
let linvel = rapier_vec2_to_mg(body.linvel())
let angvel = body.angvel()
let next_vel = Velocity2d::{ linvel, angvel }
if vel_store.contains(entity) {
vel_store.set(entity, next_vel, tick) |> ignore
} else {
vel_store.insert(entity, next_vel, tick)
}
})
}