Terrain & Heightmaps

When physics is disabled, Altruist uses the HeightmapSpatialQueryProvider to give the character controller physics-quality movement (slopes, wall sliding, ground detection, jumping) using your game's terrain data instead of a physics engine.

How It Works

Loading diagram...

The provider checks two things during movement:

  1. Terrain walkability — via ITerrainProvider (you implement this)
  2. Entity colliders — via ColliderDescriptors on world objects (same shapes used by physics)

Setup

1. Disable physics

altruist:
  game:
    physics:
      enabled: false

2. Implement ITerrainProvider

This is where you feed your game's terrain data — walkability grids, heightmaps, elevation data:

[Service(typeof(ITerrainProvider))]
public class MyTerrainProvider : ITerrainProvider
{
    private readonly byte[,] _walkabilityGrid;
    private readonly float _cellSize;

    public MyTerrainProvider(/* inject your map data loader */)
    {
        // Load your walkability grid from file, database, etc.
        _cellSize = 100f; // Each grid cell = 100 game units
        _walkabilityGrid = LoadGrid();
    }

    public bool IsWalkable(float x, float y, float z)
    {
        int cx = (int)(x / _cellSize);
        int cy = (int)(y / _cellSize);
        if (cx < 0 || cy < 0) return false;
        if (cx >= _walkabilityGrid.GetLength(1) || cy >= _walkabilityGrid.GetLength(0)) return false;
        return _walkabilityGrid[cy, cx] == 0; // 0 = walkable, 1 = blocked
    }

    public float GetHeight(float x, float z)
    {
        // Return terrain elevation at (x, z)
        // Used for ground detection and gravity
        return 0f; // Flat terrain
    }

    public Vector3 GetNormal(float x, float z)
    {
        // Return surface normal for slope detection
        return Vector3.UnitY; // Flat ground
    }
}

3. Use the character controller (no changes needed)

public override void Step(float dt, IGameWorldManager3D world)
{
    Controller?.Step(dt, world);
    // KCC queries terrain via HeightmapSpatialQueryProvider automatically
}

Note:

That's it. The KinematicCharacterController3D works the same way it does with physics — capsule sweeps, ground probing, slope detection, wall sliding. The only difference is the backend answering those queries.

ITerrainProvider Interface

public interface ITerrainProvider
{
    bool IsWalkable(float x, float y, float z);
    float GetHeight(float x, float z);
    Vector3 GetNormal(float x, float z);
}
MethodUsed ForReturns
IsWalkableMovement blockingtrue if the position is walkable
GetHeightGround detection, gravityTerrain elevation at (x, z)
GetNormalSlope detection, slide directionSurface normal (default: Vector3.UnitY = flat)

Note:

ITerrainProvider is optional. If you don't register one, the heightmap provider still works — it just uses entity colliders for collision and assumes flat terrain at Y=0.

What Happens During Movement

CapsuleCast (movement sweep)

When the character controller moves, it does a capsule cast along the movement direction:

  1. Terrain check — samples IsWalkable along the path (every ~50 units). If a cell is blocked, returns a hit at the boundary and the controller slides along the wall.
  2. Entity collider check — tests sphere/box intersection against all entity ColliderDescriptors. Buildings, obstacles, and other entities with isTrigger: false block movement.

RayCast (ground probe)

When the controller checks for ground:

  1. Terrain height — calls GetHeight(x, z) to find the ground elevation
  2. Entity colliders — also checks for platforms or objects below the character

Entity Colliders as Obstacles

Entities with ColliderDescriptors act as obstacles even without physics:

// A building that blocks movement
[WorldObject("building")]
public class Building : WorldObject3D
{
    public Building(float x, float y, float z, float width, float depth)
        : base(Transform3D.From(new Vector3(x, y, z), Quaternion.Identity, Vector3.One))
    {
        ColliderDescriptors = [PhysxCollider3D.Create(
            PhysxColliderShape3D.Box3D,
            Transform3D.From(Vector3.Zero, Quaternion.Identity, new Vector3(width, 10, depth)),
            isTrigger: false)];  // Solid — blocks movement
    }
}

Note:

isTrigger: false means the collider blocks movement. isTrigger: true means it's detection-only (used for collision events but doesn't stop the character). Same as physics mode.

Heightmap with Elevation

For terrain with hills and slopes, return real elevation data from GetHeight:

[Service(typeof(ITerrainProvider))]
public class HeightmapTerrainProvider : ITerrainProvider
{
    private readonly float[,] _heightmap;
    private readonly float _cellSize;
    private readonly int _width;
    private readonly int _depth;

    public HeightmapTerrainProvider(/* load your heightmap data */)
    {
        _cellSize = 100f;
        _heightmap = LoadHeightmap(); // float[,] of elevation values
        _width = _heightmap.GetLength(1);
        _depth = _heightmap.GetLength(0);
    }

    public bool IsWalkable(float x, float y, float z)
    {
        // Check bounds and optionally check slope steepness
        int cx = (int)(x / _cellSize);
        int cz = (int)(z / _cellSize);
        return cx >= 0 && cz >= 0 && cx < _width && cz < _depth;
    }

    public float GetHeight(float x, float z)
    {
        // Bilinear interpolation for smooth terrain
        float fx = x / _cellSize;
        float fz = z / _cellSize;
        int x0 = Math.Clamp((int)fx, 0, _width - 2);
        int z0 = Math.Clamp((int)fz, 0, _depth - 2);
        float tx = fx - x0;
        float tz = fz - z0;

        float h00 = _heightmap[z0, x0];
        float h10 = _heightmap[z0, x0 + 1];
        float h01 = _heightmap[z0 + 1, x0];
        float h11 = _heightmap[z0 + 1, x0 + 1];

        return (1 - tx) * (1 - tz) * h00
             + tx * (1 - tz) * h10
             + (1 - tx) * tz * h01
             + tx * tz * h11;
    }

    public Vector3 GetNormal(float x, float z)
    {
        // Compute normal from heightmap gradient
        float h = GetHeight(x, z);
        float hx = GetHeight(x + _cellSize, z);
        float hz = GetHeight(x, z + _cellSize);
        var normal = Vector3.Normalize(new Vector3(h - hx, _cellSize, h - hz));
        return normal;
    }
}

The controller uses GetHeight for gravity (character settles to terrain elevation) and GetNormal for slope limits (character can't walk up cliffs steeper than MaxSlopeAngleDeg).

Switching to Physics Later

Change one config line — zero code changes:

altruist:
  game:
    physics:
      enabled: true   # BEPU handles everything now
Heightmap ProviderPhysics Provider
MovementWalkability grid + collider mathBEPU capsule casts
GroundGetHeight()BEPU ray casts
ObstaclesColliderDescriptors sphere checkBEPU narrow phase
AccuracyGood (sphere/box approximation)Precise (convex meshes)
PerformanceVery fast (no physics overhead)Heavier (full simulation)

Note:

Start with heightmap (simpler, faster, no physics dependency). Switch to BEPU later if you need precise mesh collision or rigid body dynamics. Your character controller code, entity shapes, and collision handlers all work the same either way.