Movement & Pathfinding

Controlling movement or making entities move by pathfinding is critical to many types of games. The HYTOPIA SDK exposes a number of building block primitives you can use to create your own movement logic and pathfinding systems.

Important Building Blocks & Primitives

Below are the most frequently used building blocks and primitives when creating movement and pathfinding logic for entities.

SimpleCharacterController

The first building block that's best suited for relatively simple pathfinding is the #simpleentitycontroller. This entity controller implements basic movement and facing functions to create realistic movement behaviors with defined speeds.

RigidBody

All entities inherit from the RigidBody class. Rigid bodies have a number of physics properties and controls that are necessary for creating custom movement and pathfinding logic. This means, any property or method that the RigidBody class has, an Entity does as well.

We recommend you look through the RigidBody API Reference here to see a full list of available properties and methods.

ChunkLattice

If you're creating more complex pathfinding, you'll likely want to iterate the nearby terrain state of the world to determine the path an entity should take for it's movement. You can use a world's chunk lattice for this. The chunk lattice is useful for retrieving the chunks near an entity, or seeing if specific coordinates have a block set or not, or what kind of block is set. Learn more about the ChunkLattice.

Basic Waypoint Movement Example

We can create some basic movement behavior by providing a set of waypoints our entity will move through. The SimpleCharacterController used will take the most immediate path to the next target waypoint, colliding and potentially being stopped by any terrain or entities in the way. This is simple, dumb movement in this case.

// ... other code

let targetWaypointIndex = 0;

const WAYPOINT_COORDINATES = [
  { x: -5, y: 1, z: -7 },
  { x: 15, y: 1, z: 10 },
  { x: 17, y: 1, z: -12 },
];

const cow = new Entity({
  controller: new SimpleEntityController(),
  modelUri: 'models/npcs/cow.gltf',
  modelScale: 0.7,
  modelLoopedAnimations: [ 'walk' ],
  modelAnimationsPlaybackRate: 1.6, // roughly match the animation speed to the move speed we'll use
  rigidBodyOptions: {
    enabledRotations: { x: false, y: true, z: false }, // prevent flipping over when moving
  },
});

// We want to face towards the target each tick, since our relative position
// to the target may change as we move from a previous waypoint to the next.
cow.onTick = () => {
  if (targetWaypointIndex >= WAYPOINT_COORDINATES.length) {
    return; // reached final waypoint, no need to rotate
  }

  // continually face towards target as we move
  const controller = cow.controller as SimpleEntityController;
  const targetWaypoint = WAYPOINT_COORDINATES[targetWaypointIndex];
  controller.face(targetWaypoint, 5);
};

cow.spawn(world, { x: 0, y: 3, z: 0 });

// Pathfind to the next waypoint as we reach each waypoint
const pathfind = () => {
  if (targetWaypointIndex >= WAYPOINT_COORDINATES.length) {
    cow.stopModelAnimations(['walk']);
    cow.startModelLoopedAnimations(['idle']);

    return; // reached final waypoint, no need to pathfind
  }
      
  const controller = cow.controller as SimpleEntityController;
  const targetWaypoint = WAYPOINT_COORDINATES[targetWaypointIndex];
    
  // Makes the controlled entity, the cow, start moving towards the waypoint
  // It will automatically handle it's own internal tick for movement.
  controller.move(targetWaypoint, 3, {
    moveCompleteCallback: () => {
      // pathfind to next waypoint
      targetWaypointIndex++; 
      pathfind();
    },
    moveIgnoreAxes: { x: false, y: true, z: false }, // ignore our y position when considering if movement is complete 
  });
};

pathfind();

Here's what our movement for our cow looks like!

Last updated