Walking on terrain
Open the live demo · Read the source · View on GitHub
The other four collision shapes are each one convex solid. Ground is not: two
triangles meeting along a ridge make a shape with a dent in it, and no set
of planes describes a dent. CollisionHeightfield answers a different
question from the others — which triangles are near this query — and that is
what lets a character controller walk across it exactly the same way it
walks across a box.
Step 1: A field of samples #
Rolling hills, a sample to the metre. Every triangle is extended downward into a solid prism, and where two prisms meet the shared face is a seam rather than a wall: nothing here stops a body crossing from one triangle to the next.
// Rolling hills, one height sample to the metre. `CollisionHeightfield`
// is the fifth shape and the first that is not one convex solid — it
// hands a query the triangles near it.
_field = CollisionHeightfield(
columns: _columns,
rows: _rows,
cellSize: _cellSize,
heights: heights,
);
_world.add(Collider(shape: _field, position: Vector3.zero()));
Step 2: Walk across it #
Nothing about CharacterController changes for a heightfield. It sweeps and
probes the ground exactly as it does against a box; the shape underneath it
just happens to answer with a different set of planes each time.
_controller = CharacterController(
world: _world,
position: Vector3(-_ring, 1.6, 0.0),
);
for (var i = 0; i < _steps; i++) {
_controller.step(_step, wishDirection: Vector3(0.0, 0.0, 1.0));
}
On the page the walker keeps going: it chases a point a little ahead of itself on a circle round the field, and the controller carries it up every hill and down again with its feet on the surface. Drag to turn the view; the camera follows the walker.
// Chase a point that keeps a little ahead of the walker on the circle;
// the controller does the rest, up every hill and down again.
final Vector3 at = _controller.position;
final double ahead = math.atan2(at.z, at.x) + 0.35;
final Vector3 wish = Vector3(
_ring * math.cos(ahead) - at.x,
0.0,
_ring * math.sin(ahead) - at.z,
)..normalize();
_controller.step(_step, wishDirection: wish);
Step 3: What the feet should be doing #
A body standing on sloped ground should have its feet at the ground's own height under it, not at some average across the field or at the height it started falling from.
if (!_controller.isGrounded) {
throw StateError('the walker should have landed on the slope');
}
final Vector3 at = _controller.position;
final double ground = _field.heightAt(Vector3.zero(), at.x, at.z);
final double feet = at.y - _controller.halfExtents.y;
if ((feet - ground).abs() > 0.15) {
throw StateError(
'the walker\'s feet should track the slope: feet $feet, ground $ground',
);
}
if (at.z <= 0.5) {
throw StateError('the walker should have walked across the hills');
}
Note. This package draws nothing. The mesh under the walker on this page is built by hand from the same height samples the collision shape was given, which is also why the two agree.