Skip to content

Scene YAML Reference ​

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

cpp
#include "Fx2D/Core.h"
cpp
// 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:

yaml
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 ​

KeyTypeRequiredDescription
size[width, height]YesWorld dimensions in physics units
gravity[gx, gy]NoGravity acceleration vector (default: [0, 0])
background[R, G, B, A] or path stringNoBackground fill colour (0–255) or image file path
mouse_dragtrue / falseNoLet the left mouse button click-drag dynamic bodies through the mouse joint (default: false)
yaml
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.

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

pose ​

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

  • x, y — position in physics units
  • theta — orientation in degrees
yaml
pose: [6.0, 4.0, 45.0]     # x=6, y=4, rotated 45°

init_velocity (optional) ​

Initial velocity: [vx, vy, omega].

  • vx, vy — linear velocity (units/s)
  • omega — angular velocity (degrees/s)
yaml
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.

KeyTypeDefaultDescription
massfloat1.0Mass in kg. Set to 0 for a static (immovable) body
inertiafloatcomputedRotational inertia about the centre of mass. If omitted, computed from the visual geometry and mass
gravity_scalefloat1.0Multiplier on scene gravity. 0.0 disables gravity for this entity
vel_dampingfloat0.0Linear/angular velocity damping applied each step
elasticityfloat0.1Coefficient 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_frictionfloat0.0Static friction coefficient. A contact takes the smaller of the two values, so the slipperier surface wins
dynamic_frictionfloat0.0Kinetic friction coefficient. A contact takes the smaller of the two values
external_forces_enabledbooltrueIf false, the entity ignores external forces and collisions (gravity still applies)
ccdboolfalseIf true, enables speculative contacts for this entity to prevent tunneling at high speeds
sensorboolfalseIf 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
yaml
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.

KeyTypeDefaultDescription
pose[x, y, theta][0, 0, 0]Offset from body frame origin (local space)
geometrymap—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_thicknessfloat—Outline thickness in screen pixels
yaml
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.

KeyTypeDefaultDescription
pose[x, y, theta][0, 0, 0]Offset from body frame origin (local space)
geometrymap—Shape definition (see below)
yaml
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 ​

yaml
geometry:
    circle: 0.5     # radius

Rectangle ​

yaml
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 ​

yaml
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:

yaml
geometry:
    capsule:
        length: 2.0
        radius: 0.3

Edge ​

yaml
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 ​

yaml
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 ​

yaml
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):

yaml
geometry:
    rectangle: [2.0, 2.0]
    radius: 0.25            # rounded corners
yaml
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.

KeyValueProduces
circle0.5circle 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
polygonlist of [x, y] verticesconvex polygon, rounded if radius: is given
edge[[x1, y1], [x2, y2]]zero-thickness segment
chainlist of >= 3 [x, y] pointsopen polyline of zero-thickness segments
radius0.25modifier 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 ​

yaml
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.

KeyTypeRequiredDescription
typestringYesJoint type: revolute or prismatic
parentstringYesName of the parent entity
childstringYesName of the child entity
pid[p, i, d]NoPID gains, defaults to [1, 0, 0]
control_modestringNoposition, velocity, or effort
targetfloatNoTarget value interpreted by the chosen control mode
max_effortfloatNoShared effort limit for motor output
entities_collideboolNoParsed from YAML, but joint-linked bodies are currently kept non-colliding in the scene

Revolute Joint ​

Additional revolute fields:

KeyTypeDefaultDescription
anchor[x, y][0, 0]Anchor point in the parent body's local frame
angle_minfloat-piLower angular limit in radians
angle_maxfloatpiUpper angular limit in radians
max_torquefloat-Backward-compatible alias for max_effort

target is interpreted as:

  • angle in radians when control_mode: position
  • angular velocity in radians/s when control_mode: velocity
  • torque effort when control_mode: effort
yaml
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:

KeyTypeDefaultDescription
axis[x, y][1, 0]Motion axis in the parent body's local frame
position_minfloat-1000Lower translation limit
position_maxfloat1000Upper translation limit
max_forcefloat-Backward-compatible alias for max_effort

target is interpreted as:

  • translation along the axis when control_mode: position
  • linear velocity along the axis when control_mode: velocity
  • force effort when control_mode: effort
yaml
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 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.

Released under the BSD-3-Clause License.