moveCharacter method

  1. @override
void moveCharacter(
  1. int character,
  2. double dt,
  3. Vector3 velocity,
  4. CharacterState3D out, {
  5. required Vector3 stepDown,
  6. required Vector3 stepUp,
})
override

Moves character for one update at velocity: it slides along walls, keeps to the floor (stepDown, pointing down; zero to turn off) and climbs steps up to stepUp (pointing up; zero to turn off). Writes where it ended up into out.

Implementation

@override
void moveCharacter(
  int character,
  double dt,
  Vector3 velocity,
  CharacterState3D out, {
  required Vector3 stepDown,
  required Vector3 stepUp,
}) {
  final c = characters[character];
  if (c == null) {
    _clearState(out);
    return;
  }
  c.moves++;
  final r = c.settings.radius, h = c._halfHeight;
  final p = c.position;
  final start = Vector3.copy(p);
  final wasGrounded = c.groundState == GroundState3D.onGround;
  final solids = [
    for (final b in bodies.values)
      if (b._isSolid && _collide(b.layer, c.settings.layer)) b,
  ];
  // The capsule as a box: x and z ± r, y from the feet up 2h.
  bool overlaps(FakeBody3D b) =>
      p.x + r > b.minX &&
      p.x - r < b.maxX &&
      p.z + r > b.minZ &&
      p.z - r < b.maxZ &&
      p.y > b.minY + 1e-6 &&
      p.y - 2 * h < b.maxY - 1e-6;

  final climb = stepUp.length;
  void moveAxis(int axis, double d) {
    if (d == 0) return;
    final before = Vector3.copy(p);
    if (axis == 0) {
      p.x += d;
    } else {
      p.z += d;
    }
    for (final b in solids) {
      if (!overlaps(b)) continue;
      // A step low enough: up onto it.
      final rise = p.y - b.minY;
      if (rise > 0 && rise <= climb + 1e-6) {
        final feet = p.y;
        p.y = b.minY;
        if (!solids.any(overlaps)) continue;
        p.y = feet;
      }
      p.setFrom(before);
      if (axis == 0) {
        p.x = d > 0
            ? math.min(p.x + d, b.minX - r)
            : math.max(p.x + d, b.maxX + r);
      } else {
        p.z = d > 0
            ? math.min(p.z + d, b.minZ - r)
            : math.max(p.z + d, b.maxZ + r);
      }
    }
  }

  moveAxis(0, velocity.x * dt);
  moveAxis(2, velocity.z * dt);
  var vy = velocity.y;
  p.y += vy * dt;
  for (final b in solids) {
    if (!overlaps(b)) continue;
    // Out on the side its middle is on, so a platform rising into the
    // feet lifts the character instead of swallowing it.
    if (p.y - h <= (b.minY + b.maxY) / 2) {
      p.y = b.minY; // On top (+Y is down).
      if (vy > 0) vy = 0;
    } else {
      p.y = b.maxY + 2 * h; // Head on a ceiling.
      if (vy < 0) vy = 0;
    }
  }

  // Ground: something just under the feet; within stepDown when it was
  // on the ground already, so it keeps to stairs going down.
  final reach = wasGrounded ? math.max(stepDown.length, 0.02) : 0.02;
  FakeBody3D? ground;
  for (final b in solids) {
    final below = b.minY - p.y;
    if (below < -1e-6 || below > reach) continue;
    if (p.x + r <= b.minX ||
        p.x - r >= b.maxX ||
        p.z + r <= b.minZ ||
        p.z - r >= b.maxZ) {
      continue;
    }
    if (ground == null || b.minY < ground.minY) ground = b;
  }
  if (ground != null && vy >= 0) {
    p.y = ground.minY;
    c.groundState = GroundState3D.onGround;
    c.groundBody = ground.handle;
  } else {
    c.groundState = GroundState3D.inAir;
    c.groundBody = 0;
  }
  c.velocity
    ..setFrom(p)
    ..sub(start)
    ..scale(dt > 0 ? 1 / dt : 0);
  if (c.groundState == GroundState3D.onGround) c.velocity.y = 0;
  if (c.innerBody != 0) {
    bodies[c.innerBody]?.position.setValues(p.x, p.y - h, p.z);
  }
  characterState(character, out);
}