Bunyip a game engine in Go GitHub

Package github.com/matjam/bunyip/phys

phys

Package phys simulates rigid bodies in 2D and 3D on the entity component system. Shapes attached to entities collide, bounce, slide and stack under gravity, and the game reads what touched what.

A 2D entity carries a gfx.Transform2, a Body2 and a Collider2; a 3D entity a gfx.Transform, a Body3 and a Collider3. Register System2 or System3 (or both) on the world and set a Settings2 or Settings3 resource for gravity and solver quality. Each update the system integrates velocities, finds overlapping shapes with a sweep over bounding boxes, generates contact points, resolves them with sequential impulses (restitution, friction, positional correction) and emits Collision and Trigger events. Times are seconds, distances use the game's world units, and angles are radians unless a field says otherwise. Transform scale does not resize colliders; size the shapes themselves. A zero gravity vector means no gravity. World systems and queries require serialized access.

w.SetResource(phys.Settings3{Gravity: lin.V3(0, -9.8, 0)})
w.SpawnWith(gfx.At(0, 5, 0), phys.Dynamic3(1), phys.Collider3{Shape: phys.Sphere{Radius: 0.5}})
w.SpawnWith(gfx.Transform{}, phys.Collider3{Shape: phys.Box3{Half: lin.V3(10, 0.5, 10)}}) // static floor
w.AddSystem("physics", phys.System3)

Names carry their dimension as a suffix where a type exists in both (Body2 and Body3, Box2 and Box3, Collider2 and Collider3, the events, the systems and the settings); shapes that exist in one dimension only are plain words (Circle, Polygon2 for the 2D polygon, Sphere).

Shapes in 3D are Sphere, Box3, Capsule, ConvexHull, Compound3 (parts placed on one body) and MeshShape (a static triangle mesh for terrain and levels, with a triangle tree built by NewMeshShape). Sphere and box pairs have exact tests, as does a capsule lying along a box face; every other pair collides through support functions (GJK for distance, EPA for penetration) with face manifolds clipped the same way as the box test's. Shapes in 2D are Circle, Box2, Polygon2, Capsule2, and for terrain Edge2 and Chain2.

Queries inspect the world between updates. Raycast2 and Raycast3 return the nearest collider along a ray and RaycastAll2 and RaycastAll3 every one in order; OverlapShape2 and OverlapShape3 (with OverlapCircle2, OverlapBox2, OverlapSphere3 and OverlapBox3) return everything a placed shape touches; ShapeCast2 and ShapeCast3 sweep a shape and return the first thing in its way; Nearest2 and Nearest3 find the closest collider to a point. SignedDistance2 and SignedDistance3 measure a point against one placed shape without touching the world, for code that pushes points out of solids, such as the soft bodies in phys/soft. The queries that return a slice have an Into form (RaycastAll2Into, RaycastAll3Into, OverlapShape2Into, OverlapShape3Into) that appends to a slice the caller reuses, so a game can reuse result and scratch storage after their buffers grow. Each collider's placement (its rotation, position and bounds) is kept between steps and queries, and queries search a tree of those bounds, so a ray or a sweep tests only the colliders near it, in the order a walk over every collider would, with the same results. A query still walks the collider components once to notice transforms and shapes the game changed since the last step and places again only those; a character controller's move walks once for all its sweeps. In-place hull and compound edits are noticed even when bounds stay unchanged. MeshShape geometry must remain immutable.

Joints are components on their own entities that name the bodies they connect: DistanceJoint2 and DistanceJoint3 (rods and ropes), RevoluteJoint2 and HingeJoint3 (pins, with angle limits and a motor), BallJoint3 (a shoulder or hip, with cone and twist limits), PrismaticJoint2 and PrismaticJoint3 (sliders, with travel limits, a motor and a spring), WheelJoint2 (a wheel on a sprung suspension with a motor on its spin), SpringJoint2 and SpringJoint3 (damped springs) and FixedJoint2 and FixedJoint3 (welds). They are solved with the contacts, in entity order. NewRagdoll3 spawns a humanoid of capsules on limited joints from a RagdollSpec, and Ragdoll3.Pose places it from an animated character's bones. A body with CCD set is swept against static geometry every substep so it cannot tunnel, and its bounding sphere is swept against the other moving bodies. With Settings.SleepTime set, bodies that rest for that long sleep until touched or pushed (Body.Asleep, Body.Wake).

CharacterController2 and CharacterController3 move an upright capsule by sweeps rather than dynamics. The capsule slides along walls, climbs steps up to StepHeight, walks slopes up to MaxSlope and reports Grounded.

For orbits and spaceflight, see the orbit package. It works with the same transforms at astronomical scale.

Index

Examples

Example
package main

import (
	"fmt"

	"github.com/matjam/bunyip/ecs"
	"github.com/matjam/bunyip/gfx"
	"github.com/matjam/bunyip/lin"
	"github.com/matjam/bunyip/phys"
)

func main() {
	w := ecs.NewWorld()
	w.SetResource(phys.Settings3{Gravity: lin.V3(0, -9.8, 0)})
	w.AddSystem("physics", phys.System3)

	// A static floor (collider, no body) and a ball dropped onto it.
	w.SpawnWith(gfx.Transform{}, phys.Collider3{Shape: phys.Box3{Half: lin.V3(10, 0.5, 10)}})
	ball := w.SpawnWith(gfx.At(0, 5, 0), phys.Dynamic3(1), phys.Collider3{Shape: phys.Sphere{Radius: 0.5}})
	for range 180 {
		w.Update(1.0 / 60)
	}
	t, _ := w.Get[gfx.Transform](ball)
	fmt.Printf("ball rests at y = %.1f\n", t.Position.Y)
}
Output
ball rests at y = 1.0

Constants

const (
	RagdollPelvis    = "pelvis"
	RagdollSpine     = "spine"
	RagdollHead      = "head"
	RagdollUpperArmL = "upper_arm_l"
	RagdollForearmL  = "forearm_l"
	RagdollUpperArmR = "upper_arm_r"
	RagdollForearmR  = "forearm_r"
	RagdollThighL    = "thigh_l"
	RagdollShinL     = "shin_l"
	RagdollThighR    = "thigh_r"
	RagdollShinR     = "shin_r"
)

Ragdoll part names, in the order NewRagdoll3 spawns them.

Variables

var RagdollParts = []string{ /* … */ }

RagdollParts lists the part names in the order NewRagdoll3 spawns them, the pelvis first.

Functions

DrawColliders2 source

func DrawColliders2(g *gfx.Graphics, w *ecs.World)

DrawColliders2 outlines every 2D collider in the world as stroked paths in world units, and draws the normal of each contact the last update reported. Call it from Draw, under the same 2D camera the world is drawn with.

DrawColliders3 source

func DrawColliders3(g *gfx.Graphics, w *ecs.World)

DrawColliders3 outlines every 3D collider in the world as debug lines over the scene, and draws the normal of each contact the last update reported. Call it from Draw, with the same camera the scene is drawn with. Awake bodies, sleeping bodies and static colliders are told apart by colour; DrawCollidersColors3 chooses the colours.

DrawCollidersColors2 source

func DrawCollidersColors2(g *gfx.Graphics, w *ecs.World, colors DebugColors)

DrawCollidersColors2 is DrawColliders2 with the colours chosen.

DrawCollidersColors3 source

func DrawCollidersColors3(g *gfx.Graphics, w *ecs.World, colors DebugColors)

DrawCollidersColors3 is DrawColliders3 with the colours chosen.

DrawShape2 source

func DrawShape2(g *gfx.Graphics, s Shape2, t gfx.Transform2, c gfx.Color)

DrawShape2 outlines one 2D shape placed by a transform, as a stroked path in world units: a circle, a box, a polygon, a capsule as two circles and their sides, an edge or chain as its segments.

DrawShape3 source

func DrawShape3(g *gfx.Graphics, s Shape3, t gfx.Transform, c gfx.Color)

DrawShape3 outlines one 3D shape placed by a transform, as debug lines: a sphere as three rings, a box as its edges, a capsule as two spheres and the lines between them, a hull as every edge between its points, a compound as each of its parts. A mesh shape is left out, since a terrain mesh is already drawn.

SignedDistance2 source

func SignedDistance2(s Shape2, pos lin.Vec2, rot float32, point lin.Vec2) (dist float32, normal lin.Vec2, ok bool)

SignedDistance2 measures a point against a shape placed at pos with rotation rot. It returns the distance from the point to the nearest outline, negative when the point is inside, and the unit normal there, pointing out of the shape. Use it to push a point out of a solid: move it along the normal until the distance reaches the clearance wanted.

The shapes it understands are Circle, Box2, Polygon2 and Capsule2; ok is false for Edge2, Chain2 and a nil shape, which have no inside. It allocates nothing for polygons of up to sixteen points, so it suits a per-particle inner loop.

SignedDistance3 source

func SignedDistance3(s Shape3, pos lin.Vec3, rot lin.Quat, point lin.Vec3) (dist float32, normal lin.Vec3, ok bool)

SignedDistance3 measures a point against a shape placed at pos with rotation rot. It returns the distance from the point to the nearest surface, negative when the point is inside, and the unit normal there, pointing out of the shape. Use it to push a point out of a solid: move it along the normal until the distance reaches the clearance wanted.

The shapes it understands are Sphere, Box3, Capsule and a Compound3 of those; ok is false for ConvexHull, MeshShape and a nil shape, which have no cheap signed distance. It allocates nothing, so it suits a per-particle inner loop.

System2 source

func System2(w *ecs.World, dt float64)

System2 advances every 2D body by dt seconds.

System3 source

func System3(w *ecs.World, dt float64)

System3 advances every 3D body by dt seconds.

Types

type BallJoint3 source

type BallJoint3 struct {
	A, B             ecs.Entity
	AnchorA, AnchorB lin.Vec3
	AxisA, AxisB     lin.Vec3
	ConeAngle        float32
	TwistAngle       float32
	// contains filtered or unexported fields
}

BallJoint3 pins two bodies at an anchor and lets them turn freely about it: a shoulder, a hip, a neck. AxisB is the limb's axis in B's frame (zero means local Y) and AxisA the centre of its cone in A's frame (zero means where AxisB pointed on the first step). ConeAngle limits how far AxisB may swing from AxisA and TwistAngle how far B may turn about AxisB either way, both in radians; zero means unlimited. A side set to ecs.None fixes that anchor and axis in the world.

Angles source

func (j *BallJoint3) Angles(w *ecs.World) (cone, twist float32)

Angles returns the swing of AxisB away from AxisA and the twist of B about AxisB, in radians. The first call captures the reference pose and default cone axis if the solver has not measured them yet.

type Body2 source

type Body2 struct {
	Vel    lin.Vec2
	AngVel float32 // radians per second; positive rotates +X toward +Y (clockwise on screen)
	Mass   float32
	// Restitution is bounciness, 0 to 1; Friction is the Coulomb
	// coefficient, 0 slides freely.
	Restitution, Friction float32
	// Damping removes a fraction of velocity per second.
	LinearDamping, AngularDamping float32
	// GravityScale multiplies the world gravity; zero means 1.
	GravityScale float32
	Kinematic    bool
	LockRotation bool
	Sleeping     bool // set by the game to freeze a body
	// CCD sweeps the body against static colliders every substep and
	// stops it at the first one, so a fast small body cannot pass
	// through a thin wall.
	CCD bool
	// contains filtered or unexported fields
}

Body2 makes a 2D entity move. Mass zero is static; Kinematic bodies move by their velocity and push others without being pushed.

Dynamic2 source

func Dynamic2(mass float32) Body2

Dynamic2 returns a body with the given mass, restitution 0.1, friction 0.5 and gravity scale 1. A positive mass makes it dynamic.

Kinematic2 source

func Kinematic2() Body2

Kinematic2 returns a body moved by its velocity that others cannot push.

AddForce source

func (b *Body2) AddForce(f lin.Vec2)

AddForce accumulates a force (units of mass·distance/s²) until the next update.

AddImpulse source

func (b *Body2) AddImpulse(i lin.Vec2)

AddImpulse changes velocity at once by impulse/mass.

AddTorque source

func (b *Body2) AddTorque(t float32)

AddTorque accumulates a torque until the next update.

Asleep source

func (b *Body2) Asleep() bool

Asleep reports that the body has come to rest and is skipped by the simulation until something touches or pushes it.

Wake source

func (b *Body2) Wake()

Wake clears automatic sleep and its timer. It does not clear Sleeping, which remains under the game's control.

type Body3 source

type Body3 struct {
	Vel    lin.Vec3
	AngVel lin.Vec3 // radians per second about each world axis
	Mass   float32
	// Restitution is bounciness, 0 to 1; Friction is the Coulomb
	// coefficient, 0 slides freely.
	Restitution, Friction float32
	// Damping removes a fraction of velocity per second.
	LinearDamping, AngularDamping float32
	// GravityScale multiplies the world gravity; zero means 1.
	GravityScale float32
	Kinematic    bool
	LockRotation bool
	Sleeping     bool // set by the game to freeze a body
	// CCD sweeps the body against static colliders every substep and
	// stops it at the first one, so a fast small body cannot pass
	// through a thin wall.
	CCD bool
	// contains filtered or unexported fields
}

Body3 makes a 3D entity move. Mass zero is static; Kinematic bodies move by their velocity and push others without being pushed.

Dynamic3 source

func Dynamic3(mass float32) Body3

Dynamic3 returns a body with the given mass, restitution 0.1, friction 0.5 and gravity scale 1. A positive mass makes it dynamic.

Kinematic3 source

func Kinematic3() Body3

Kinematic3 returns a body moved by its velocity that others cannot push.

AddForce source

func (b *Body3) AddForce(f lin.Vec3)

AddForce accumulates a force until the next update.

AddImpulse source

func (b *Body3) AddImpulse(i lin.Vec3)

AddImpulse changes velocity at once by impulse/mass.

AddTorque source

func (b *Body3) AddTorque(t lin.Vec3)

AddTorque accumulates a torque until the next update.

Asleep source

func (b *Body3) Asleep() bool

Asleep reports that the body has come to rest and is skipped by the simulation until something touches or pushes it.

Wake source

func (b *Body3) Wake()

Wake clears automatic sleep and its timer. It does not clear Sleeping, which remains under the game's control.

type Box2 source

type Box2 struct{ HalfW, HalfH float32 }

Box2 is a rectangle centred on the body with the given half extents.

type Box3 source

type Box3 struct{ Half lin.Vec3 }

Box3 is a box centred on the body with the given half extents.

type Capsule source

type Capsule struct{ Radius, HalfHeight float32 }

Capsule is a cylinder along the body's local Y axis with hemispherical ends: Radius around the axis, HalfHeight from the centre to the centre of each cap, so the whole is 2·(HalfHeight+Radius) tall.

type Capsule2 source

type Capsule2 struct{ Radius, HalfHeight float32 }

Capsule2 is a segment along the body's local Y axis grown by Radius: HalfHeight from the centre to the centre of each round end.

type Chain2 source

type Chain2 struct {
	Points []lin.Vec2
	Loop   bool
}

Chain2 is a run of edges through Points, for terrain outlines; Loop joins the last point back to the first.

type CharacterController2 source

type CharacterController2 struct {
	Radius     float32 // capsule radius; zero means 0.4
	HalfHeight float32 // centre to the centre of each cap; zero means 0.5
	StepHeight float32 // tallest ledge it climbs; zero means none
	MaxSlope   float32 // steepest walkable slope in degrees; zero means 45
	Skin       float32 // gap kept from surfaces; zero means 0.02
	Mask       uint32  // which collider layers block it; zero means all

	// Set by Move.
	Grounded     bool
	GroundNormal lin.Vec2
	// contains filtered or unexported fields
}

CharacterController2 is the 2D CharacterController3: an upright capsule moved by sweeps that slide along walls, climb ledges no taller than StepHeight, walk slopes up to MaxSlope and report ground contact. The entity needs a gfx.Transform2, which Move updates; a Collider2 on the same entity is ignored by its own sweeps.

Move source

func (c *CharacterController2) Move(w *ecs.World, e ecs.Entity, velocity lin.Vec2, dt float32)

Move advances the character by velocity·dt: horizontally with sliding and stepping, then vertically, then out of anything it still overlaps. dt is seconds and velocity is world units per second. It applies no gravity or impulses; the game supplies vertical velocity. A missing transform leaves the controller unchanged. Use nonnegative dt; even a zero timestep may resolve overlaps and update ground contact.

type CharacterController3 source

type CharacterController3 struct {
	Radius     float32 // capsule radius; zero means 0.4
	HalfHeight float32 // centre to the centre of each cap; zero means 0.5
	StepHeight float32 // tallest ledge it climbs; zero means none
	MaxSlope   float32 // steepest walkable slope in degrees; zero means 45
	Skin       float32 // gap kept from surfaces; zero means 0.02
	Mask       uint32  // which collider layers block it; zero means all

	// Set by Move.
	Grounded     bool
	GroundNormal lin.Vec3
	// contains filtered or unexported fields
}

CharacterController3 moves an upright capsule through the world the way a player expects, without rigid-body dynamics: Move sweeps it along a velocity, slides it along whatever it hits, climbs ledges no taller than StepHeight, walks slopes up to MaxSlope and reports whether it stands on ground. The entity needs a gfx.Transform, which Move updates; a Collider3 on the same entity is ignored by its own sweeps, so other bodies can still bump into it.

Move source

func (c *CharacterController3) Move(w *ecs.World, e ecs.Entity, velocity lin.Vec3, dt float32)

Move advances the character by velocity·dt: horizontally with sliding and stepping, then vertically, then out of anything it still overlaps. dt is seconds and velocity is world units per second. It applies no gravity or impulses; the game supplies vertical velocity. A missing transform leaves the controller unchanged. Use nonnegative dt; even a zero timestep may resolve overlaps and update ground contact.

type Circle source

type Circle struct{ Radius float32 }

Circle is a disc centred on the body.

type Collider2 source

type Collider2 struct {
	Shape   Shape2
	Offset  lin.Vec2 // shape centre relative to the transform
	Trigger bool     // overlaps are reported but not resolved
	Layers
}

Collider2 gives an entity a shape. An entity with a Collider2 and no Body2 is a static obstacle. It also needs gfx.Transform2. A nil Shape is ignored; transform scale does not resize the shape.

type Collider3 source

type Collider3 struct {
	Shape   Shape3
	Offset  lin.Vec3 // local offset, rotated by the transform's rotation
	Trigger bool     // overlaps emit events without applying collision impulses
	Layers
}

Collider3 gives an entity a shape. An entity with a Collider3 and no Body3 is a static obstacle. It also needs gfx.Transform. A nil Shape is ignored; transform scale does not resize the shape.

type Collision2 source

type Collision2 struct {
	A, B    ecs.Entity
	Point   lin.Vec2
	Normal  lin.Vec2 // from A to B
	Depth   float32
	Impulse float32
}

Collision2 is emitted for each pair of colliders that touched this update; A and B are ordered by entity id. Impulse is the total normal impulse the contact applied in the first substep it was seen, a measure of how hard the hit was.

type Collision3 source

type Collision3 struct {
	A, B    ecs.Entity
	Point   lin.Vec3
	Normal  lin.Vec3 // from A to B
	Depth   float32
	Impulse float32
}

Collision3 is emitted for each pair of colliders that touched this update; A and B are ordered by entity id. Impulse is the total normal impulse the contact applied in the first substep it was seen, a measure of how hard the hit was.

type Compound3 source

type Compound3 struct{ Parts []Part3 }

Compound3 is several shapes moving as one body, for things a single convex shape cannot describe: a table, an L-shaped wall, a hammer. Parts may overlap each other.

type ConvexHull source

type ConvexHull struct{ Points []lin.Vec3 }

ConvexHull is the convex volume around a set of points in the body's frame. Only the extreme points matter; interior ones are ignored by collision and may be left out. The points should surround the body's origin, which is its centre of mass.

type DebugColors source

type DebugColors struct {
	Awake    gfx.Color // bodies that are simulating
	Asleep   gfx.Color // bodies that have gone to sleep
	Static   gfx.Color // colliders with no body
	Contacts gfx.Color // contact points and their normals
}

DebugColors are the colours the debug drawing uses. A zero colour where one is expected is left at the default noted, so DebugColors{} draws awake bodies orange, sleeping bodies grey, static colliders blue and contact normals red.

type DistanceJoint2 source

type DistanceJoint2 struct {
	A, B             ecs.Entity
	AnchorA, AnchorB lin.Vec2
	Length           float32
	Min, Max         float32
	// contains filtered or unexported fields
}

DistanceJoint2 keeps two anchors a fixed distance apart, like a rod, or within a range, like a rope. AnchorA and AnchorB are in each body's frame; with B set to ecs.None, AnchorB is a point in the world. Length zero measures the distance on the first step. When Max is above zero the joint only acts outside [Min, Max].

type DistanceJoint3 source

type DistanceJoint3 struct {
	A, B             ecs.Entity
	AnchorA, AnchorB lin.Vec3
	Length           float32
	Min, Max         float32
	// contains filtered or unexported fields
}

DistanceJoint3 keeps two anchors a fixed distance apart, like a rod, or within a range, like a rope. AnchorA and AnchorB are in each body's frame; with B set to ecs.None, AnchorB is a point in the world. Length zero measures the distance on the first step. When Max is above zero the joint only acts outside [Min, Max].

type Edge2 source

type Edge2 struct{ A, B lin.Vec2 }

Edge2 is a line segment in the body's frame with no inside: a piece of ground or wall that shapes collide with from either side.

type FixedJoint2 source

type FixedJoint2 struct {
	A, B             ecs.Entity
	AnchorA, AnchorB lin.Vec2
	// contains filtered or unexported fields
}

FixedJoint2 welds two bodies together at an anchor, keeping the angle between them as it was on the first step.

type FixedJoint3 source

type FixedJoint3 struct {
	A, B             ecs.Entity
	AnchorA, AnchorB lin.Vec3
	// contains filtered or unexported fields
}

FixedJoint3 welds two bodies together at an anchor, keeping the rotation between them as it was on the first step.

type HingeJoint3 source

type HingeJoint3 struct {
	A, B               ecs.Entity
	AnchorA, AnchorB   lin.Vec3
	AxisA, AxisB       lin.Vec3
	MinAngle, MaxAngle float32
	MotorSpeed         float32
	MaxMotorTorque     float32
	// contains filtered or unexported fields
}

HingeJoint3 pins two bodies at an anchor and lets them turn about one axis, given in each body's frame; a zero axis means local Y. A side set to ecs.None fixes that anchor and axis in the world.

The angle is how far B has turned about the axis relative to A since the first step, by the right-hand rule, in (-π, π]. MinAngle and MaxAngle limit it; both zero means unlimited. A motor drives the angle at MotorSpeed radians per second with up to MaxMotorTorque; zero torque means no motor.

Angle source

func (j *HingeJoint3) Angle(w *ecs.World) float32

Angle is the hinge angle: how far B has turned about the axis relative to A since the reference pose, in radians. The first call captures that pose if the solver has not measured it yet.

type Hit2 source

type Hit2 struct {
	Entity   ecs.Entity
	Point    lin.Vec2
	Normal   lin.Vec2
	Distance float32
}

Hit2 is what a query found. Distance is the fraction along the ray or sweep for casts, the penetration depth for overlaps and the gap for Nearest2.

Nearest2 source

func Nearest2(w *ecs.World, point lin.Vec2, radius float32, mask uint32) (Hit2, bool)

Nearest2 finds the collider closest to a point within radius: Point is the nearest point on its outline, Normal points from there toward the query point (zero when the point is inside) and Distance is how far.

OverlapBox2 source

func OverlapBox2(w *ecs.World, center lin.Vec2, halfW, halfH, rot float32, mask uint32) []Hit2

OverlapBox2 returns every collider a rotated rectangle overlaps.

OverlapCircle2 source

func OverlapCircle2(w *ecs.World, center lin.Vec2, radius float32, mask uint32) []Hit2

OverlapCircle2 returns every collider a circle overlaps.

OverlapShape2 source

func OverlapShape2(w *ecs.World, s Shape2, pos lin.Vec2, rot float32, mask uint32) []Hit2

OverlapShape2 returns every collider the shape overlaps when placed at pos with rotation rot. Each hit carries the deepest contact: Point on the collider, Normal pointing from the collider back toward the shape and Distance the penetration depth. Triggers are included. s must be non-nil. rot is radians; mask zero includes every collider layer.

OverlapShape2Into source

func OverlapShape2Into(out []Hit2, w *ecs.World, s Shape2, pos lin.Vec2, rot float32, mask uint32) []Hit2

OverlapShape2Into appends every collider the shape overlaps to out and returns out. Pass the previous result truncated with [:0] to reuse its storage; pass nil for a fresh slice. Result and scratch buffers may allocate on initial use or growth. OverlapShape2's contracts apply.

Raycast2 source

func Raycast2(w *ecs.World, r Ray2, mask uint32) (Hit2, bool)

Raycast2 finds the nearest collider along the ray, ignoring triggers and colliders the mask excludes.

Example
package main

import (
	"fmt"

	"github.com/matjam/bunyip/ecs"
	"github.com/matjam/bunyip/gfx"
	"github.com/matjam/bunyip/lin"
	"github.com/matjam/bunyip/phys"
)

func main() {
	w := ecs.NewWorld()
	w.AddSystem("physics", phys.System2)
	w.SpawnWith(gfx.At2(100, 0), phys.Collider2{Shape: phys.Box2{HalfW: 10, HalfH: 10}})
	w.Update(1.0 / 60)
	hit, ok := phys.Raycast2(w, phys.Ray2{Origin: lin.V2(0, 0), Dir: lin.V2(200, 0)}, 0)
	fmt.Printf("%v at (%.0f, %.0f), normal x %.0f\n", ok, hit.Point.X, hit.Point.Y, hit.Normal.X)
}
Output
true at (90, 0), normal x -1

RaycastAll2 source

func RaycastAll2(w *ecs.World, r Ray2, mask uint32) []Hit2

RaycastAll2 returns every collider along the ray, nearest first, ignoring triggers and colliders the mask excludes. To cast repeatedly without allocating a result each time, call RaycastAll2Into.

RaycastAll2Into source

func RaycastAll2Into(out []Hit2, w *ecs.World, r Ray2, mask uint32) []Hit2

RaycastAll2Into appends every collider along the ray to out, nearest first, and returns out. Pass the previous result truncated with [:0] to reuse its storage; pass nil for a fresh slice. The appended hits are sorted among themselves, not against what out already held.

ShapeCast2 source

func ShapeCast2(w *ecs.World, s Shape2, pos lin.Vec2, rot float32, delta lin.Vec2, mask uint32) (Hit2, bool)

ShapeCast2 sweeps a shape from pos along delta and returns the first collider it touches: Distance is the fraction of delta travelled, Point where the surfaces meet and Normal the collider's surface normal there. Colliders already overlapping the shape at the start and triggers are ignored.

type Hit3 source

type Hit3 struct {
	Entity   ecs.Entity
	Point    lin.Vec3
	Normal   lin.Vec3
	Distance float32
}

Hit3 is what a query found. Distance is the fraction along the ray or sweep for casts, the penetration depth for overlaps and the gap for Nearest3.

Nearest3 source

func Nearest3(w *ecs.World, point lin.Vec3, radius float32, mask uint32) (Hit3, bool)

Nearest3 finds the collider closest to a point within radius: Point is the nearest point on its surface, Normal points from there toward the query point (zero when the point is inside) and Distance is how far.

OverlapBox3 source

func OverlapBox3(w *ecs.World, center, half lin.Vec3, rot lin.Quat, mask uint32) []Hit3

OverlapBox3 returns every collider an oriented box overlaps.

OverlapShape3 source

func OverlapShape3(w *ecs.World, s Shape3, pos lin.Vec3, rot lin.Quat, mask uint32) []Hit3

OverlapShape3 returns every collider the shape overlaps when placed at pos with rotation rot. Each hit carries the deepest contact: Point on the collider, Normal pointing from the collider back toward the shape and Distance the penetration depth. Triggers are included. s must be non-nil. A zero rot is identity; mask zero includes every collider layer.

OverlapShape3Into source

func OverlapShape3Into(out []Hit3, w *ecs.World, s Shape3, pos lin.Vec3, rot lin.Quat, mask uint32) []Hit3

OverlapShape3Into appends every collider the shape overlaps to out and returns out. Pass the previous result truncated with [:0] to reuse its storage; pass nil for a fresh slice. Result and scratch buffers may allocate on initial use or growth. OverlapShape3's contracts apply.

OverlapSphere3 source

func OverlapSphere3(w *ecs.World, center lin.Vec3, radius float32, mask uint32) []Hit3

OverlapSphere3 returns every collider a sphere overlaps.

Raycast3 source

func Raycast3(w *ecs.World, r Ray3, mask uint32) (Hit3, bool)

Raycast3 finds the nearest collider along the ray, ignoring triggers and colliders the mask excludes.

RaycastAll3 source

func RaycastAll3(w *ecs.World, r Ray3, mask uint32) []Hit3

RaycastAll3 returns every collider along the ray, nearest first, ignoring triggers and colliders the mask excludes. To cast repeatedly without allocating a result each time, call RaycastAll3Into.

RaycastAll3Into source

func RaycastAll3Into(out []Hit3, w *ecs.World, r Ray3, mask uint32) []Hit3

RaycastAll3Into appends every collider along the ray to out, nearest first, and returns out. Pass the previous result truncated with [:0] to reuse its storage; pass nil for a fresh slice. The appended hits are sorted among themselves, not against what out already held.

ShapeCast3 source

func ShapeCast3(w *ecs.World, s Shape3, pos lin.Vec3, rot lin.Quat, delta lin.Vec3, mask uint32) (Hit3, bool)

ShapeCast3 sweeps a shape from pos along delta and returns the first collider it touches: Distance is the fraction of delta travelled, Point where the surfaces meet and Normal the collider's surface normal there. Colliders already overlapping the shape at the start and triggers are ignored.

type Layers source

type Layers struct {
	Layer uint32
	Mask  uint32
}

Layers restrict which colliders meet: two colliders collide when each one's Layer bits appear in the other's Mask. Zero means "all".

type MeshShape source

type MeshShape struct {
	Vertices []lin.Vec3
	Indices  []uint32
	// contains filtered or unexported fields
}

MeshShape is a triangle mesh for static geometry such as terrain and level walls: Indices are triples into Vertices, and either winding is fine since triangles collide from both sides. Bodies do not carry meshes; dynamic shapes collide against them. NewMeshShape builds the triangle tree once. A literal MeshShape builds it on first use and keeps it by the identity of the slices, so do not change them after.

NewMeshShape source

func NewMeshShape(vertices []lin.Vec3, indices []uint32) MeshShape

NewMeshShape makes a mesh collider with its triangle tree built. It retains vertices and indices without copying. Treat both slices as immutable after construction; build a new MeshShape to change geometry.

type Part3 source

type Part3 struct {
	Shape    Shape3
	Offset   lin.Vec3
	Rotation lin.Quat // zero means none
}

Part3 is one shape of a Compound3, placed in the body's frame.

type PlacedShape2 source

type PlacedShape2 struct {
	// contains filtered or unexported fields
}

PlacedShape2 is a 2D shape placed in the world once, so that many points can be measured against it without placing it again for each one: a polygon's world points and edge normals and a capsule's segment are worked out when it is placed. The zero value measures nothing.

PlaceShape2 source

func PlaceShape2(s Shape2, pos lin.Vec2, rot float32) PlacedShape2

PlaceShape2 places a shape at pos with rotation rot, in radians, for measuring. To measure many points against one collider, such as every particle of a fluid against a wall, call it once and call SignedDistance for each point. A polygon of more than sixteen points allocates when placed.

Bounds source

func (p *PlacedShape2) Bounds() (lo, hi lin.Vec2)

Bounds returns the world box around the placed shape.

SignedDistance source

func (p *PlacedShape2) SignedDistance(point lin.Vec2) (dist float32, normal lin.Vec2, ok bool)

SignedDistance measures a point against the placed shape. It returns exactly what SignedDistance2 returns for the shape, position and rotation the shape was placed with, and ok false for the same shapes.

type PlacedShape3 source

type PlacedShape3 struct {
	// contains filtered or unexported fields
}

PlacedShape3 is a shape placed in the world once, so that many points can be measured against it without placing it again for each one: the rotation, a box's inverse rotation and a capsule's segment are worked out when it is placed. The zero value measures nothing.

PlaceShape3 source

func PlaceShape3(s Shape3, pos lin.Vec3, rot lin.Quat) PlacedShape3

PlaceShape3 places a shape at pos with rotation rot for measuring. A zero rot is the identity. To measure many points against one collider, such as every particle of a cloth against a solid, call it once and call SignedDistance for each point. A compound still places its parts on each call.

Bounds source

func (p *PlacedShape3) Bounds() (lo, hi lin.Vec3)

Bounds returns the world box around the placed shape, the region outside which a point is at least as far from the shape as it is from the box. A nil shape has empty bounds at the origin.

SignedDistance source

func (p *PlacedShape3) SignedDistance(point lin.Vec3) (dist float32, normal lin.Vec3, ok bool)

SignedDistance measures a point against the placed shape. It returns exactly what SignedDistance3 returns for the shape, position and rotation the shape was placed with, and ok false for the same shapes.

type Polygon2 source

type Polygon2 struct{ Points []lin.Vec2 }

Polygon2 is a convex polygon with points in the body's frame, in any winding order.

type PrismaticJoint2 source

type PrismaticJoint2 struct {
	A, B             ecs.Entity
	AnchorA, AnchorB lin.Vec2
	Axis             lin.Vec2
	Min, Max         float32
	MotorSpeed       float32
	MaxMotorForce    float32
	Stiffness        float32
	Damping          float32
	// contains filtered or unexported fields
}

PrismaticJoint2 is a slider: two bodies may only move along one axis relative to each other, and keep the angle they had on the first step. Axis is the slide direction in A's frame and a zero axis means local X. AnchorA and AnchorB are in each body's frame; a side set to ecs.None fixes that anchor and the axis in the world.

The translation is how far B's anchor sits from A's along the axis, so it is zero when the anchors meet. Min and Max limit it; both zero means unlimited. A motor drives the translation at MotorSpeed units per second with up to MaxMotorForce; zero force means no motor. A spring pulls the translation back toward zero with Stiffness as the force per unit of travel and Damping as the force per unit of speed; zero stiffness means no spring.

Translation source

func (j *PrismaticJoint2) Translation(w *ecs.World) float32

Translation is how far B's anchor has slid from A's along the axis, in world units.

type PrismaticJoint3 source

type PrismaticJoint3 struct {
	A, B             ecs.Entity
	AnchorA, AnchorB lin.Vec3
	Axis             lin.Vec3
	Min, Max         float32
	MotorSpeed       float32
	MaxMotorForce    float32
	Stiffness        float32
	Damping          float32
	// contains filtered or unexported fields
}

PrismaticJoint3 is a slider: two bodies may only move along one axis relative to each other, and keep the rotation they had on the first step. Axis is the slide direction in A's frame and a zero axis means local X. AnchorA and AnchorB are in each body's frame; a side set to ecs.None fixes that anchor and the axis in the world.

The translation is how far B's anchor sits from A's along the axis, so it is zero when the anchors meet. Min and Max limit it; both zero means unlimited. A motor drives the translation at MotorSpeed units per second with up to MaxMotorForce; zero force means no motor. A spring pulls the translation back toward zero with Stiffness as the force per unit of travel and Damping as the force per unit of speed; zero stiffness means no spring.

Translation source

func (j *PrismaticJoint3) Translation(w *ecs.World) float32

Translation is how far B's anchor has slid from A's along the axis, in world units.

type Ragdoll3 source

type Ragdoll3 struct {
	// Parts maps a part name to its entity.
	Parts map[string]ecs.Entity
	// Joints maps a part name to the joint entity tying it to its
	// parent; the pelvis has none. Waist, neck, shoulder and hip joints
	// are BallJoint3, elbows and knees HingeJoint3.
	Joints map[string]ecs.Entity
	// Bones records each part's size as built, so a game can place the
	// centre of a part from the position of its joint.
	Bones map[string]RagdollBone
}

Ragdoll3 is a spawned humanoid: capsule bodies for the parts, joined by ball joints at the waist, neck, shoulders and hips and limited hinges at the elbows and knees. Every part's capsule runs along its local Y with the parent joint at the top, and every part starts with the spec's rotation, so a standing figure has all its parts upright.

NewRagdoll3 source

func NewRagdoll3(w *ecs.World, spec RagdollSpec) *Ragdoll3

NewRagdoll3 spawns the parts and joints of a humanoid ragdoll and returns them by name. Each part carries a gfx.Transform, a Body3 and a Collider3 with a Capsule.

Despawn source

func (r *Ragdoll3) Despawn(w *ecs.World)

Despawn removes every part and joint.

Entities source

func (r *Ragdoll3) Entities() []ecs.Entity

Entities returns every part and joint entity, parts first.

Pose source

func (r *Ragdoll3) Pose(w *ecs.World, positions map[string]lin.Vec3, rotations map[string]lin.Quat)

Pose places the parts at once and brings them to rest, to hand an animated character over to physics: positions are part centres and rotations part rotations in the world, by part name; a part missing from a map keeps what it has. A game with a bone's world position and rotation finds the centre of the part hanging from it as the bone position plus the rotation applied to (0, -Length/2, 0), with Length from Bones.

type RagdollBone source

type RagdollBone struct{ Length, Radius float32 }

RagdollBone sizes one part of a ragdoll: the distance from one end of its capsule to the other and the capsule's radius.

type RagdollSpec source

type RagdollSpec struct {
	// Position is the point on the ground between the feet and Rotation
	// turns the whole figure, which stands facing -Z with +X on its
	// right.
	Position lin.Vec3
	Rotation lin.Quat
	// Height scales the default bone sizes; zero means 1.8.
	Height float32
	// Mass is the total mass, shared between the parts in human
	// proportions; zero means 70.
	Mass float32
	// Friction and Damping apply to every part; zero friction means 0.6
	// and zero damping means 0.5 per second.
	Friction float32
	Damping  float32
	// Layers apply to every part. Zero puts the ragdoll on a layer of
	// its own that meets everything except other ragdoll parts, so
	// neighbouring limbs do not fight their joints.
	Layers Layers
	// Bone sizes; a zero field takes the default scaled by Height.
	Pelvis, Spine, Head, UpperArm, Forearm, Thigh, Shin RagdollBone
}

RagdollSpec describes a humanoid ragdoll for NewRagdoll3. Zero fields take the defaults of a figure 1.8 units tall.

type Ray2 source

type Ray2 struct {
	Origin, Dir lin.Vec2 // Dir need not be unit length
}

Ray2 describes a finite cast from Origin to Origin+Dir. Dir is the full displacement; a hit's Distance is its fraction in [0, 1].

type Ray3 source

type Ray3 struct {
	Origin, Dir lin.Vec3 // Dir need not be unit length
}

Ray3 describes a finite cast from Origin to Origin+Dir. Dir is the full displacement; a hit's Distance is its fraction in [0, 1].

type RevoluteJoint2 source

type RevoluteJoint2 struct {
	A, B               ecs.Entity
	AnchorA, AnchorB   lin.Vec2
	MinAngle, MaxAngle float32
	MotorSpeed         float32
	MaxMotorTorque     float32
	// contains filtered or unexported fields
}

RevoluteJoint2 pins two bodies together at an anchor so they turn freely about it. A side set to ecs.None fixes that anchor in the world.

The angle is how far B has turned relative to A since the first step or first Angle call, positive from +X toward +Y (clockwise on screen), in (-π, π]. MinAngle and MaxAngle limit it; both zero means unlimited. A motor drives the angle at MotorSpeed radians per second with up to MaxMotorTorque; zero torque means no motor.

Angle source

func (j *RevoluteJoint2) Angle(w *ecs.World) float32

Angle is the joint angle: how far B has turned relative to A since the reference pose, in radians. The first call captures that pose if the solver has not measured it yet.

type Settings2 source

type Settings2 struct {
	Gravity    lin.Vec2 // world units per second squared; zero disables gravity
	Substeps   int      // integration steps per update; more is stabler
	Iterations int      // solver passes per step; more is stiffer
	// SleepTime is how long a body and everything touching it must rest
	// before they sleep and drop out of the simulation until touched;
	// zero means bodies never sleep. Half a second suits most games.
	SleepTime float32
	// SleepThreshold is the speed (units and radians per second) below
	// which a body counts as resting; zero means 0.05.
	SleepThreshold float32
}

Settings2 is the world resource that tunes 2D simulation. Zero substeps or iterations mean 4 and 8.

type Settings3 source

type Settings3 struct {
	Gravity    lin.Vec3 // world units per second squared; zero disables gravity
	Substeps   int      // integration steps per update; nonpositive means 4
	Iterations int      // solver passes per substep; nonpositive means 8
	// SleepTime is how long a body and everything touching it must rest
	// before they sleep and drop out of the simulation until touched;
	// zero means bodies never sleep. Half a second suits most games.
	SleepTime float32
	// SleepThreshold is the speed, in units per second, below which a
	// body counts as resting: both its own speed and the speed its turning
	// gives the point of its collider farthest from its centre must be
	// slower. Zero means 0.05.
	SleepThreshold float32
}

Settings3 is the world resource that tunes 3D simulation. Zero substeps or iterations mean 4 and 8.

type Shape2 source

type Shape2 interface {
	// contains filtered or unexported methods
}

Shape2 is a collider outline in the body's local frame.

type Shape3 source

type Shape3 interface {
	// contains filtered or unexported methods
}

Shape3 is a collider volume in the body's local frame.

type Sphere source

type Sphere struct{ Radius float32 }

Sphere is a ball centred on the body.

type SpringJoint2 source

type SpringJoint2 struct {
	A, B             ecs.Entity
	AnchorA, AnchorB lin.Vec2
	RestLength       float32
	Stiffness        float32
	Damping          float32
	// contains filtered or unexported fields
}

SpringJoint2 pulls two anchors toward a rest length with a damped spring force. RestLength zero measures it on the first step; zero Stiffness means 10 and Damping is the velocity coefficient.

type SpringJoint3 source

type SpringJoint3 struct {
	A, B             ecs.Entity
	AnchorA, AnchorB lin.Vec3
	RestLength       float32
	Stiffness        float32
	Damping          float32
	// contains filtered or unexported fields
}

SpringJoint3 pulls two anchors toward a rest length with a damped spring force. RestLength zero measures it on the first step; zero Stiffness means 10 and Damping is the velocity coefficient.

type Trigger2 source

type Trigger2 struct {
	Trigger, Other ecs.Entity
}

Trigger2 is emitted while a trigger collider overlaps another collider. A pair is reported in its first overlapping substep of the update; when both colliders are triggers, each receives its own event.

type Trigger3 source

type Trigger3 struct {
	Trigger, Other ecs.Entity
}

Trigger3 is emitted while a trigger collider overlaps another collider. A pair is reported in its first overlapping substep of the update; when both colliders are triggers, each receives its own event.

type WheelJoint2 source

type WheelJoint2 struct {
	A, B             ecs.Entity
	AnchorA, AnchorB lin.Vec2
	Axis             lin.Vec2
	Frequency        float32
	DampingRatio     float32
	Min, Max         float32
	MotorSpeed       float32
	MaxMotorTorque   float32
}

WheelJoint2 is a wheel on a suspension: B may spin freely and slide along one axis of A, and is held on that line in every other direction. A is the chassis and B the wheel. Axis is the suspension direction in A's frame and a zero axis means local Y. AnchorA and AnchorB are in each body's frame, so AnchorA is where the wheel sits when the suspension is at rest; a side set to ecs.None fixes that anchor and the axis in the world.

The spring along the axis is tuned by Frequency in hertz and DampingRatio, where 1 is critically damped; zero frequency means no spring and zero DampingRatio means 0.7. Min and Max limit the travel along the axis; both zero means unlimited. A motor drives B's spin at MotorSpeed radians per second with up to MaxMotorTorque; zero torque means no motor.

Translation source

func (j *WheelJoint2) Translation(w *ecs.World) float32

Translation is how far the suspension has moved from its rest position, in world units. It is negative while the spring is compressed toward A.

Source files

alloc_test.go axis.go bench_test.go cache3.go cache3_test.go character2.go character3.go compound3.go convex3.go debugdraw.go example_test.go features_test.go gjk.go index2.go index2_test.go index3.go index_test.go joint2.go joint3.go ledge_test.go mesh3.go phys.go phys_test.go query2.go query3.go ragdoll3.go ragdoll_test.go review_test.go scaling_bench_test.go scratch2.go scratch3.go sdf.go sdf_test.go shape2.go shape3.go shapes_test.go slider_test.go system2.go system3.go terrain2.go tree.go