fx2d-physics-engine

Scene YAML Reference

Fx2D scenes can be described entirely in YAML and loaded at runtime using FxYAML::buildScene(). Include via:

#include "Fx2D/Core.h"
// Load from file
auto scene = FxYAML::buildScene("./Scene.yml");

// Or load from an inline YAML string
auto scene = FxYAML::buildScene(R"(
    scene:
        size: [12, 8]
        gravity: [0, -10]
    entities:
        ball:
            pose: [6, 4, 0]
            physics:
                mass: 1.0
            visual:
                geometry:
                    circle: 0.5
                texture: [255, 100, 0, 255]
            collision:
                geometry:
                    circle: 0.5
)");

Top-level Structure

A valid YAML file must have a scene block and may also have entities and joints blocks:

scene:
    size: [12, 8]
    gravity: [0, -10]
    background: [255, 255, 255, 255]

entities:
    my_entity:
        pose: [6, 4, 0]
        ...

joints:
    my_joint:
        type: revolute
        parent: body
        child: wheel
        ...

scene Block

Key Type Required Description
size [width, height] Yes World dimensions in physics units
gravity [gx, gy] No Gravity acceleration vector (default: [0, 0])
background [R, G, B, A] or path string No Background fill colour (0–255) or image file path
scene:
    size: [12, 8]
    gravity: [0, -10]
    background: [230, 230, 230, 255]     # solid light-grey
    # background: ./examples/truck/assets/bricks.png  # or an image file

entities Block

Each key under entities becomes the entity’s unique name. The parser iterates keys in order, so declaration order is preserved.

entities:
    ground:     # entity name
        pose: ...
        physics: ...
        visual: ...
        collision: ...
    ball:
        ...

pose

Initial world-space pose: [x, y, theta].

pose: [6.0, 4.0, 45.0]     # x=6, y=4, rotated 45°

init_velocity (optional)

Initial velocity: [vx, vy, omega].

init_velocity: [-5.0, 0.0, 0.0]    # moving left at 5 units/s

physics Block

Controls how the entity responds to forces, gravity, and collisions.

Key Type Default Description
mass float 1.0 Mass in kg. Set to 0 for a static (immovable) body
inertia float computed Rotational inertia about the centre of mass. If omitted, computed from the visual geometry and mass
gravity_scale float 1.0 Multiplier on scene gravity. 0.0 disables gravity for this entity
vel_damping float 0.0 Linear/angular velocity damping applied each step
elasticity float 0.1 Coefficient of restitution (0 = inelastic, 1 = perfectly elastic). A contact takes the larger of the two bodies’ values, so a bouncy body bounces off anything
static_friction float 0.0 Static friction coefficient. A contact takes the smaller of the two values, so the slipperier surface wins
dynamic_friction float 0.0 Kinetic friction coefficient. A contact takes the smaller of the two values
external_forces_enabled bool true If false, the entity ignores external forces and collisions (gravity still applies)
ccd bool false If true, enables speculative contacts for this entity to prevent tunneling at high speeds
sensor bool false If true, the entity detects overlaps but applies no impulses. Bodies pass straight through it, and its contacts are reported only through FxScene::contacts() and the begin/end contact events
physics:
    mass: 5.0
    gravity_scale: 1.0
    vel_damping: 0.01
    elasticity: 0.7
    static_friction: 0.4
    dynamic_friction: 0.3
    external_forces_enabled: true
    ccd: false
    sensor: false

Static bodies: set mass: 0 (which sets inv_mass = 0) and gravity_scale: 0.0. The body will participate in collision detection but receive no correction from the solver.

Inertia: if inertia is omitted, it is computed from the visual geometry and the mass — not from the collision shape. Where the two differ (a simplified hitbox, or an oversized invisible collider), the visual shape is what determines how the body rotates. Specify inertia explicitly only when you need a custom value; an explicit value always wins over the computed one.

Because the calculation needs the shape, it runs after the visual: block is read, regardless of where physics: appears in the file. An entity with no visual: block has nothing to measure and gets zero inertia; so does one with mass: 0, and so does an edge, which has no area.


visual Block

Controls how the entity is drawn. The shape is defined by geometry.

Key Type Default Description
pose [x, y, theta] [0, 0, 0] Offset from body frame origin (local space)
geometry map Shape definition (see below)
texture [R, G, B, A] or path Fill colour or image file path
border_color [R, G, B, A] Outline colour (only active when border_thickness > 0)
border_thickness float Outline thickness in screen pixels
visual:
    pose: [0, 0, 0]
    geometry:
        rectangle: [1.0, 0.5]
    texture: ./examples/stacked_boxes/assets/rock.png
    border_color: [0, 0, 0, 255]
    border_thickness: 2.0

collision Block

Defines the shape used for physics collision detection. May differ from the visual shape.

Key Type Default Description
pose [x, y, theta] [0, 0, 0] Offset from body frame origin (local space)
geometry map Shape definition (see below)
collision:
    pose: [0, 0, 0]
    geometry:
        rectangle: [1.0, 0.5]

Having separate visual and collision shapes is useful when you want a simplified hitbox (e.g. a circle collision shape for a roughly round sprite) or an invisible static collider larger than the visual (e.g. a wider ground plane).


Geometry Types

Used inside both visual and collision blocks.

Circle

geometry:
    circle: 0.5     # radius

Rectangle

geometry:
    rectangle: [1.0, 0.5]   # [width, height]

The rectangle is axis-aligned in local space and centred at the shape’s pose offset.

Capsule

geometry:
    capsule: [2.0, 0.3]     # [segment_length, end_cap_radius]

A capsule is a line segment of the given length (oriented along the local x-axis) inflated uniformly by the end-cap radius — a rectangle with two semicircular caps. Use capsules for characters, wheels, projectiles, and other rounded primitives. A length of 0 degenerates to a circle; a radius of 0 degenerates to a line segment.

The map form is also accepted:

geometry:
    capsule:
        length: 2.0
        radius: 0.3

Edge

geometry:
    edge: [[-2.0, 0.0], [2.0, 0.0]]   # [endpoint_a, endpoint_b]

An edge is a zero-thickness line segment defined by two endpoints in local (body) space, intended for static level geometry such as floors, walls, and ramps. It has zero area and zero inertia, so pair it with mass: 0 and gravity_scale: 0.0; a dynamic body given an edge shape has no inertia and may behave unexpectedly.

Internally an edge is a capsule with a zero skin radius, and unlike other shapes its endpoints are kept exactly as authored rather than recentred on the centroid. Edge-vs-edge pairs never generate contacts, and edges are skipped by CCD.

Chain

geometry:
    chain: [[0.0, 0.0], [4.0, 1.0], [8.0, 0.5], [12.0, 2.0]]

An open polyline of at least 3 points, forming a run of zero-thickness segments authored as one entity. Use it for static level geometry — terrain, ramps, cave walls — that a convex polygon approximates badly. Like an edge it has no interior, so no area and no inertia: pair it with mass: 0 and gravity_scale: 0.0.

A chain inherits the edge limitations. Chain-vs-chain and chain-vs-edge pairs never generate contacts, since neither side has volume to resolve, and chains are skipped by speculative-contact CCD, so a fast enough body can pass through one.

Polygon

geometry:
    polygon:
        - [0.0, 0.0]
        - [2.0, 0.0]
        - [2.0, 1.0]
        - [0.0, 1.0]

Each entry is a 2D vertex [x, y] in local space. Vertices are specified in order (clockwise or counter-clockwise). For collision, convex polygons give the most reliable SAT results.

Rounded rectangles and polygons (skin radius)

Both rectangle and polygon accept an optional radius: modifier that adds a uniform rounding/skin radius around the shape’s boundary (Minkowski sum of the polygon with a disc of that radius):

geometry:
    rectangle: [2.0, 2.0]
    radius: 0.25            # rounded corners
geometry:
    polygon:
        - [-1.0, -0.5]
        - [ 1.0, -0.5]
        - [ 1.0,  0.5]
        - [-1.0,  0.5]
    radius: 0.1

A radius: 0 (or omitted) is the legacy sharp-corner behaviour. Internally, circle / capsule / rounded-polygon all share the same skin-radius mechanism, so contact normals, AABBs, and inertia are computed consistently.

The top-level radius: key applies only to rectangle and polygon. circle and capsule carry their own radius in their own value, and edge is zero-thickness by definition, so a radius: alongside any of those three is silently ignored.

Geometry key summary

Exactly one geometry key is read per geometry: block, in this order — the first one present wins and the rest are ignored.

Key Value Produces
circle 0.5 circle of that radius
capsule [length, radius], or a map with length: and radius: capsule along the local x-axis
rectangle [width, height] 4-vertex polygon, rounded if radius: is given
polygon list of [x, y] vertices convex polygon, rounded if radius: is given
edge [[x1, y1], [x2, y2]] zero-thickness segment
chain list of >= 3 [x, y] points open polyline of zero-thickness segments
radius 0.25 modifier for rectangle / polygon only

Anything else throws Unknown geometry type in shape config. Note the key is rectangle, not box, and the rounding modifier is radius, not skin_radius.


Complete Entity Example

entities:
    ball:
        pose: [10.0, 6.0, 0.0]
        init_velocity: [-6.0, 0.0, 0.0]
        physics:
            mass: 5.0
            gravity_scale: 1.0
            vel_damping: 0.0
            elasticity: 0.8
            static_friction: 0.3
            dynamic_friction: 0.2
            external_forces_enabled: true
        visual:
            pose: [0, 0, 0]
            geometry:
                circle: 0.5
            texture: ./examples/stacked_boxes/assets/ball.png
            border_color: [0, 0, 0, 255]
            border_thickness: 5.0
        collision:
            pose: [0, 0, 0]
            geometry:
                circle: 0.5

    ground:
        pose: [6.0, 0.5, 0.0]
        physics:
            gravity_scale: 0.0
            external_forces_enabled: false
            elasticity: 0.6
            static_friction: 0.3
            dynamic_friction: 0.2
        visual:
            geometry:
                rectangle: [12.0, 1.0]
            texture: [50, 50, 50, 255]
            border_thickness: 3.0
        collision:
            geometry:
                rectangle: [12.0, 1.0]

joints Block

Each key under joints becomes the joint’s unique name.

Key Type Required Description
type string Yes Joint type: revolute or prismatic
parent string Yes Name of the parent entity
child string Yes Name of the child entity
pid [p, i, d] No PID gains, defaults to [1, 0, 0]
control_mode string No position, velocity, or effort
target float No Target value interpreted by the chosen control mode
max_effort float No Shared effort limit for motor output
entities_collide bool No Parsed from YAML, but joint-linked bodies are currently kept non-colliding in the scene

Revolute Joint

Additional revolute fields:

Key Type Default Description
anchor [x, y] [0, 0] Anchor point in the parent body’s local frame
angle_min float -pi Lower angular limit in radians
angle_max float pi Upper angular limit in radians
max_torque float - Backward-compatible alias for max_effort

target is interpreted as:

joints:
    wheel_hinge:
        type: revolute
        parent: chassis
        child: wheel
        anchor: [0.0, -0.5]
        angle_min: -0.6
        angle_max: 0.6
        control_mode: effort
        target: 12.0
        max_effort: 20.0
        pid: [5.0, 0.2, 0.1]

Prismatic Joint

Additional prismatic fields:

Key Type Default Description
axis [x, y] [1, 0] Motion axis in the parent body’s local frame
position_min float -1000 Lower translation limit
position_max float 1000 Upper translation limit
max_force float - Backward-compatible alias for max_effort

target is interpreted as:

joints:
    slider:
        type: prismatic
        parent: rail
        child: carriage
        axis: [1.0, 0.0]
        position_min: -2.0
        position_max: 2.0
        control_mode: velocity
        target: 1.5
        max_force: 8.0
        pid: [4.0, 0.0, 0.2]

For a runnable scene that uses a joints: block, see examples/joint_control_demo/Scene.yml. It declares both joint types against static parents: arm_motor (revolute, with anchor, angle_min/angle_max, pid, control_mode, target, and max_effort) and slider_motor (prismatic, with axis, position_min/position_max, and the same control fields). The companion main.cpp drives both through all three control modes; see joint_control.md for the motor API and gain-tuning notes.


Notes

Joint units — entity pose angles in YAML remain in degrees, but joint limits and joint position targets are currently parsed exactly as provided by the joint loader. Revolute limits and revolute position targets should therefore be written in radians.

Inline YAML strings are de-indented automatically, so they can be written with natural indentation inside C++ raw string literals without alignment issues.

Field defaults — all physics fields are optional; the parser substitutes sensible defaults if missing. Missing visual or collision blocks mean the entity has no visual/collision geometry respectively.

Unit system — positions and sizes are in physics units as defined by scene.size. Angles are in degrees in YAML (converted internally to radians). Velocities are in units/s and degrees/s.