Physics & Colliders Tutorial
This tutorial covers 2D physics in AxisPy: rigidbodies, colliders, collision handling, and physics queries (raycasts/overlaps). It is based on the actual engine components and the PhysicsSystem that the runtime attaches to every scene.
Overview
Rigidbody2D— motion, gravity, forces, damping.BoxCollider2D,CircleCollider2D,PolygonCollider2D— shapes for collision.PhysicsSystem— simulation, collision detection/response, queries.
The runtime adds a single PhysicsSystem per world. Default gravity is (0, 980) in pixels/s².
Editor workflow (no code)
Add physics components (Inspector) - Select an entity → Add Component → Physics →
Rigidbody 2D. - Add a collider:Box Collider 2D,Circle Collider 2D, orPolygon Collider 2D. - Box/Circle sizes can be inferred fromSpriteRenderer. Adjust Width/Height/Radius,Offset X/Y,Rotation, and toggleIs Trigger. - For Polygon, click “Start Adding New Point” in the Inspector to place points in the Scene viewport; per‑point X/Y editing and delete are available (3+ points required).Configure Rigidbody in Inspector -
Body Type: Dynamic / Kinematic / Static. -Use Gravity,Gravity Scale,Mass,Friction,Elasticity,Linear/Angular Damping,Freeze Rotation. - Optional initialVelocity X/YandAngular Velocity.Groups and Collision Matrix - Project → Project Settings → Layers/Groups. - Define logical groups (e.g., Player, Enemy, Environment). - In “Physics Collision Matrix”, tick which groups collide. The runtime loads this into
world.physics_group_orderandworld.physics_collision_matrixwhen the project opens. - Assign entities to groups via the Inspector (Groups) or the Groups dock.Visualize and edit colliders - In the Scene viewport toolbar, enable “Physics Debug Mode” (bug icon) to show collider outlines and handles. Drag handles to resize; polygon point add/remove integrates with this mode.
Getting Started: Rigidbody + Collider
from core.components import Transform, Rigidbody2D, BoxCollider2D
class PlayerSetup:
def on_start(self):
# Ensure a Transform exists
if not self.entity.get_component(Transform):
self.entity.add_component(Transform())
# Add a dynamic rigidbody
rb = self.entity.get_component(Rigidbody2D)
if not rb:
rb = Rigidbody2D(mass=1.0, use_gravity=True, linear_damping=0.05)
self.entity.add_component(rb)
# Add a box collider (auto-sizes from sprite if width/height=None)
col = self.entity.get_component(BoxCollider2D)
if not col:
col = BoxCollider2D(width=None, height=None, is_trigger=False)
self.entity.add_component(col)
Rigidbody2D Body Types
Rigidbody2D supports three body types via body_type:
"dynamic"— affected by forces and gravity (default)."kinematic"— moved by setting velocity directly; not affected by forces."static"— immovable; used for the environment.
rb = self.entity.get_component(Rigidbody2D)
rb.body_type = Rigidbody2D.BODY_TYPE_STATIC
# or kinematic
rb.body_type = Rigidbody2D.BODY_TYPE_KINEMATIC
Forces, Impulses, and Rotation
from core.components import Rigidbody2D
class Thrust:
def on_update(self, dt: float):
rb = self.entity.get_component(Rigidbody2D)
if not rb or not rb.is_dynamic:
return
# Continuous force (applied this frame only)
rb.apply_force(500.0, 0.0)
# One-shot velocity change
# rb.apply_impulse(50.0, 0.0)
# Spin
# rb.apply_torque(10.0)
# rb.apply_angular_impulse(1.0)
# Damping and friction
# rb.linear_damping = 0.05
# rb.angular_damping = 0.1
# rb.friction = 0.5
# Elasticity (alias: rb.elasticity)
# rb.restitution = 0.2
Colliders: Shapes, Offsets, Triggers, Masks
Box: BoxCollider2D(width=None, height=None, offset_x=0, offset_y=0, rotation=0)
Circle: CircleCollider2D(radius=None, offset_x=0, offset_y=0)
Polygon: PolygonCollider2D(points=[(x,y),…], offset_x=0, offset_y=0, rotation=0)
Notes:
If width/height (box) or radius (circle) is
None, sizes are inferred from a SpriteRenderer if present, else reasonable defaults are used.Set
is_trigger=Truefor overlap-only detection (no physical resolution).Use bitmasks to filter collisions per body:
category_mask(what I am) andcollision_mask(what I collide with).
Example:
from core.components import BoxCollider2D
# Player belongs to category bit 1, collides with bits 1 and 2
player_col = BoxCollider2D(
category_mask=(1 << 0),
collision_mask=(1 << 0) | (1 << 1)
)
self.entity.add_component(player_col)
Layered Filtering via World Groups
Instead of hardcoding bitmasks, you can derive effective masks from entity groups using world-level settings (read by the PhysicsSystem).
from core.systems.physics_system import PhysicsSystem
class GameManager:
def on_start(self):
# Define physics groups (order = bit position)
self.entity.world.physics_group_order = ["Player", "Enemy", "Environment"]
# Define which groups collide with which
self.entity.world.physics_collision_matrix = {
"Player": ["Environment", "Enemy"],
"Enemy": ["Environment", "Player"],
"Environment": ["Player", "Enemy"],
}
# Mark entities with groups; `PhysicsSystem` converts groups to masks
self.entity.add_group("Player")
Collision Handling
You can handle collisions either via script callbacks on the attached script class or by subscribing to events.
Script callbacks recognized by the engine: -
on_collision_enter(other, info)-on_collision_exit(other)
info is a lightweight object with normal (Vector2) and penetration (float).
class DamageOnHit:
def on_collision_enter(self, other, info):
self.logger.info("Hit", other=other.name, normal=(info.normal.x, info.normal.y))
# Or via events (immediate dispatch within the same frame):
class EventListener:
def on_start(self):
# Subscribe to this entity's local collision events
self.subscribe_to_event("collision_enter", self._on_enter, target_entity=self.entity)
self.subscribe_to_event("collision_exit", self._on_exit, target_entity=self.entity)
def _on_enter(self, other, info):
self.logger.debug("collision_enter", other=other.name)
def _on_exit(self, other):
self.logger.debug("collision_exit", other=other.name)
Physics Queries: Raycasts and Overlaps
Access the PhysicsSystem from the world and use queries for detection.
from core.vector import Vector2
from core.components import Transform
from core.systems.physics_system import PhysicsSystem
class Sensor:
def on_update(self, dt: float):
phys = self.entity.world.get_system(PhysicsSystem)
if not phys:
return
# Raycast forward 500 px from this entity's position
t = self.entity.get_component(Transform)
origin = t.position if t else Vector2(0, 0)
hit = phys.raycast_first(origin, Vector2(1, 0), 500)
if hit:
self.logger.info("Ray hit", entity=hit["entity"].name, dist=hit["distance"])
# Overlap box centered on entity (100x50 half extents)
center = origin
overlaps = phys.overlap_box(center, Vector2(100, 50))
for e in overlaps:
self.logger.debug("Overlap", entity=e.name)
Adjusting Gravity
Change global gravity via the PhysicsSystem.
from core.systems.physics_system import PhysicsSystem
from core.vector import Vector2
class GravitySetup:
def on_start(self):
phys = self.entity.world.get_system(PhysicsSystem)
if phys:
phys.gravity = Vector2(0, 1200) # stronger downward gravity
Disabling Physics Processing per Entity
Temporarily opt an entity out of physics (integration and collisions):
# Pause/resume physics on this entity and its children
self.entity.process_physics(False)
# ... later ...
self.entity.process_physics(True)
Script Editor snippets you may need
Apply a force while a key is held
import pygame
from core.input import Input
from core.components import Rigidbody2D
class Thruster:
def on_update(self, dt: float):
rb = self.entity.get_component(Rigidbody2D)
if rb and rb.is_dynamic and Input.get_key(pygame.K_RIGHT):
rb.apply_force(800.0, 0.0)
Raycast from the mouse
from core.input import Input
from core.systems.render_system import RenderSystem
from core.systems.physics_system import PhysicsSystem
from core.vector import Vector2
class MouseProbe:
def on_update(self, dt: float):
rs = self.entity.world.get_system(RenderSystem)
phys = self.entity.world.get_system(PhysicsSystem)
if not rs or not phys:
return
mx, my = Input.get_game_mouse_position()
wx, wy = rs.screen_to_world(mx, my, entities=self.entity.world.entities)
hit = phys.raycast_first(Vector2(wx, wy), Vector2(1, 0), 600)
if hit:
self.logger.info("hit", target=hit["entity"].name, dist=hit["distance"])
Toggle a collider’s trigger flag at runtime
from core.components import BoxCollider2D
class Toggle:
def on_update(self, dt: float):
col = self.entity.get_component(BoxCollider2D)
if col:
col.is_trigger = not col.is_trigger
self.logger.info("trigger", value=col.is_trigger)