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
)");
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 BlockEach 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:
...
poseInitial world-space pose: [x, y, theta].
x, y — position in physics unitstheta — orientation in degreespose: [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)init_velocity: [-5.0, 0.0, 0.0] # moving left at 5 units/s
physics BlockControls 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 setsinv_mass = 0) andgravity_scale: 0.0. The body will participate in collision detection but receive no correction from the solver.
Inertia: if
inertiais 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. Specifyinertiaexplicitly 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 wherephysics:appears in the file. An entity with novisual:block has nothing to measure and gets zero inertia; so does one withmass: 0, and so does anedge, which has no area.
visual BlockControls 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 BlockDefines 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
visualandcollisionshapes 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).
Used inside both visual and collision blocks.
geometry:
circle: 0.5 # radius
geometry:
rectangle: [1.0, 0.5] # [width, height]
The rectangle is axis-aligned in local space and centred at the shape’s pose offset.
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
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.
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.
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.
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 torectangleandpolygon.circleandcapsulecarry their own radius in their own value, andedgeis zero-thickness by definition, so aradius:alongside any of those three is silently ignored.
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.
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 BlockEach 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 |
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:
control_mode: positioncontrol_mode: velocitycontrol_mode: effortjoints:
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]
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:
control_mode: positioncontrol_mode: velocitycontrol_mode: effortjoints:
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.
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.