// 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.
///|
/// Kinematic character controller integration (2D).
///|
pub struct CharacterCollision2d {
/// The entity that owns the hit collider (if known).
other : @core.Entity?
toi : Float
point : @math.Vec2
normal : @math.Vec2
translation_applied : @math.Vec2
translation_remaining : @math.Vec2
}
///|
pub fn CharacterCollision2d::from_rapier(
ctx : Physics2dContext,
c : @control.CharacterCollision,
) -> CharacterCollision2d {
let h = c.handle()
let other = ctx.collider_to_entity.get(h.into_raw_parts())
let hit = c.hit()
{
other,
toi: hit.toi(),
point: rapier_vec2_to_mg(hit.point()),
normal: rapier_vec2_to_mg(hit.normal()),
translation_applied: rapier_vec2_to_mg(c.translation_applied()),
translation_remaining: rapier_vec2_to_mg(c.translation_remaining()),
}
}
///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct KinematicCharacterController2d {
/// Desired movement (one-shot). The system resets this to zero each fixed step.
translation : @math.Vec2
controller : @control.KinematicCharacterController
}
///|
pub fn KinematicCharacterController2d::default() -> KinematicCharacterController2d {
{
translation: @math.Vec2::new(0.0F, 0.0F),
controller: @control.KinematicCharacterController::default(),
}
}
///|
pub fn KinematicCharacterController2d::with_translation(
self : KinematicCharacterController2d,
translation : @math.Vec2,
) -> KinematicCharacterController2d {
KinematicCharacterController2d::{ ..self, translation, }
}
///|
/// Adds to the current one-shot translation (useful when multiple systems contribute to movement).
pub fn KinematicCharacterController2d::add_translation(
self : KinematicCharacterController2d,
delta : @math.Vec2,
) -> KinematicCharacterController2d {
KinematicCharacterController2d::{
..self,
translation: @math.Vec2::new(
self.translation.x + delta.x,
self.translation.y + delta.y,
),
}
}
///|
pub fn KinematicCharacterController2d::reset_translation(
self : KinematicCharacterController2d,
) -> KinematicCharacterController2d {
KinematicCharacterController2d::{
..self,
translation: @math.Vec2::new(0.0F, 0.0F),
}
}
///|
pub fn KinematicCharacterController2d::with_up(
self : KinematicCharacterController2d,
up : @math.Vec2,
) -> KinematicCharacterController2d {
let controller = self.controller.with_up(mg_vec2_to_rapier(up))
KinematicCharacterController2d::{ ..self, controller, }
}
///|
pub fn KinematicCharacterController2d::with_slide(
self : KinematicCharacterController2d,
slide : Bool,
) -> KinematicCharacterController2d {
let controller = self.controller.with_slide(slide)
KinematicCharacterController2d::{ ..self, controller, }
}
///|
pub fn KinematicCharacterController2d::with_offset(
self : KinematicCharacterController2d,
offset : @control.CharacterLength,
) -> KinematicCharacterController2d {
let controller = self.controller.with_offset(offset)
KinematicCharacterController2d::{ ..self, controller, }
}
///|
pub fn KinematicCharacterController2d::with_snap_to_ground(
self : KinematicCharacterController2d,
snap : @control.CharacterLength?,
) -> KinematicCharacterController2d {
let controller = self.controller.with_snap_to_ground(snap)
KinematicCharacterController2d::{ ..self, controller, }
}
///|
pub fn KinematicCharacterController2d::with_autostep(
self : KinematicCharacterController2d,
autostep : @control.CharacterAutostep?,
) -> KinematicCharacterController2d {
let controller = self.controller.with_autostep(autostep)
KinematicCharacterController2d::{ ..self, controller, }
}
///|
pub fn KinematicCharacterController2d::with_max_slope_climb_angle(
self : KinematicCharacterController2d,
angle : Float,
) -> KinematicCharacterController2d {
let controller = self.controller.with_max_slope_climb_angle(angle)
KinematicCharacterController2d::{ ..self, controller, }
}
///|
pub fn KinematicCharacterController2d::with_min_slope_slide_angle(
self : KinematicCharacterController2d,
angle : Float,
) -> KinematicCharacterController2d {
let controller = self.controller.with_min_slope_slide_angle(angle)
KinematicCharacterController2d::{ ..self, controller, }
}
///|
pub fn KinematicCharacterController2d::with_normal_nudge_factor(
self : KinematicCharacterController2d,
factor : Float,
) -> KinematicCharacterController2d {
let controller = self.controller.with_normal_nudge_factor(factor)
KinematicCharacterController2d::{ ..self, controller, }
}
///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct KinematicCharacterControllerOutput2d {
effective_translation : @math.Vec2
grounded : Bool
sliding_down_slope : Bool
mut collisions : Array[CharacterCollision2d]
}
///|
pub fn KinematicCharacterControllerOutput2d::new() -> KinematicCharacterControllerOutput2d {
{
effective_translation: @math.Vec2::new(0.0F, 0.0F),
grounded: false,
sliding_down_slope: false,
collisions: [],
}
}
///|
pub fn KinematicCharacterControllerOutput2d::without_collisions(
self : KinematicCharacterControllerOutput2d,
) -> KinematicCharacterControllerOutput2d {
KinematicCharacterControllerOutput2d::{ ..self, collisions: [] }
}
///|
pub fn[
W : @core.ChangeTickWorld + @math.Has_Transform + Has_KinematicCharacterController2d + Has_KinematicCharacterControllerOutput2d + Has_RapierBodyHandle2d + Has_RapierColliderHandle2d + HasRes_Physics2dContext,
] physics2d_character_controller_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
// Ensure queries see the latest collider positions (e.g. standalone colliders synced from Transform).
ctx.query_pipeline.update()
let dt = @app.fixed_time_timestep_seconds()
let transform_store = @math.Has_Transform::get_transform_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 controller_store = Has_KinematicCharacterController2d::get_kinematic_character_controller2d_store(
world,
)
let output_store = Has_KinematicCharacterControllerOutput2d::get_kinematic_character_controller_output2d_store(
world,
)
controller_store.for_each(fn(entity, kcc) {
// Avoid conflicting with rigid-body driven motion for now.
if body_handle_store.contains(entity) {
return
}
if kcc.translation.x == 0.0F && kcc.translation.y == 0.0F {
return
}
guard collider_handle_store.get(entity) is Some(ch) else { return }
guard ctx.colliders.get(ch.handle) is Some(col) else { return }
if col.parent() is Some(_) {
return
}
let shape = col.shape()
let character_pos = col.position()
let collisions : Array[CharacterCollision2d] = []
let query = ctx
.query()
.with_filter(@collision.QueryFilter::new().exclude_collider(ch.handle))
let movement = kcc.controller.move_shape(
dt,
query,
shape,
character_pos,
mg_vec2_to_rapier(kcc.translation),
fn(c) { collisions.push(CharacterCollision2d::from_rapier(ctx, c)) },
)
let eff = rapier_vec2_to_mg(movement.translation())
// Apply to ECS transform.
let mut t = transform_store
.get(entity)
.unwrap_or(@math.Transform::identity())
t = @math.Transform::new(
@math.Vec3::new(
t.translation.x + eff.x,
t.translation.y + eff.y,
t.translation.z,
),
t.rotation,
t.scale,
)
if transform_store.contains(entity) {
transform_store.set(entity, t, tick) |> ignore
} else {
transform_store.insert(entity, t, tick)
}
// Apply to Rapier collider immediately so the next physics step sees it.
if ctx.colliders.get_mut_internal_with_modification_tracking(ch.handle)
is Some(col_mut) {
let next_pos = mg_transform_to_iso2(t)
col_mut.set_position(next_pos) |> ignore
}
// Write output and reset one-shot input.
let out = KinematicCharacterControllerOutput2d::{
effective_translation: eff,
grounded: movement.is_grounded(),
sliding_down_slope: movement.is_sliding_down_slope(),
collisions,
}
if output_store.contains(entity) {
output_store.set(entity, out, tick) |> ignore
} else {
output_store.insert(entity, out, tick)
}
controller_store.set(
entity,
KinematicCharacterController2d::{ ..kcc }.reset_translation(),
tick,
)
|> ignore
})
// Colliders moved; keep query pipeline consistent.
ctx.query_pipeline.update()
}