Simple Entity Controller

The SimpleEntityController class provides simple movement controls for entities. It's a great starting point if you're building basic pathfinding of have simple movement needs of your entities. You can piece together it's .move()and .face()methods to create simple pathfinding and behaviors relatively quickly.

Using a SimpleEntityController with an Entity

You can assign a SimpleEntityController to your Entity when it spawns or at any point in its lifecycle. In this example, we'll assign the controller when our entity spawns.

import {
  Entity,
  SimpleEntityController,
  // ... other imports
} from 'hytopia';

// other code ...

// Create a spider entity instance with our entity controller
const spider = new Entity({
  controller: new SimpleEntityController(),
  modelUri: 'models/npcs/spider.gltf',
  modelScale: 2.5,
  modelLoopedAnimations: [ 'idle' ],
});

// Spawn the spider in the world.
spider.spawn(world, { x: 0, y: 10, z: -15 });

// have the spider face towards and move towards the target
// position, taking physics into account.
const target = { x: 0, y: 0, z: 0 };
const spiderEntityController = spider.controller as SimpleEntityController;
spiderEntityController.move(target, 3); // move towards target at a speed of 3 (approx. blocks per second)
spiderEntityController.face(target, 1); // face towards the target a speed of 1

SimpleEntityController Implementation

For the sake of demonstration and reference for creating your own controllers or modifying the existing SimpleEntityController, you can find the current internal implementation below:

/**
 * A callback function called when the entity associated with the
 * SimpleEntityController updates its rotation as it is
 * attempting to face a target coordinate.
 * @param currentRotation - The current rotation of the entity.
 * @param targetRotation - The target rotation of the entity.
 * @public
 */
export type FaceCallback = (currentRotation: QuaternionLike, targetRotation: QuaternionLike) => void;

/**
 * A callback function called when the entity associated with the
 * SimpleEntityController finishes rotating and is now facing 
 * a target coordinate.
 * @param endRotation - The rotation of the entity after it has finished rotating.
 * @public
 */
export type FaceCompleteCallback = (endRotation: QuaternionLike) => void;

/**
 * Options for the {@link SimpleEntityController.face} method.
 * @public
 */
export type FaceOptions = {
  faceCallback?: FaceCallback;
  faceCompleteCallback?: FaceCompleteCallback;
}

/**
 * A callback function called when the entity associated with the
 * SimpleEntityController updates its position as it is
 * attempting to move to a target coordinate.
 * @param currentPosition - The current position of the entity.
 * @param targetPosition - The target position of the entity.
 * @public
 */
export type MoveCallback = (currentPosition: Vector3Like, targetPosition: Vector3Like) => void;

/**
 * A callback function called when the entity associated with the
 * SimpleEntityController reaches the target coordinate. An entity
 * must reach the x,y,z coordinate for the callback to be called.
 * @param endPosition - The position of the entity after it has finished moving.
 * @public
 */
export type MoveCompleteCallback = (endPosition: Vector3Like) => void;

/**
 * Options for the {@link SimpleEntityController.move} method.
 * @public
 */
export type MoveOptions = {
  /** Callback called each tick movement of the entity controller's entity. */
  moveCallback?: MoveCallback;

  /** Callback called when the entity controller's entity has finished moving. */
  moveCompleteCallback?: MoveCompleteCallback;

  /** Axes to ignore when moving the entity controller's entity. Also ignored for determining completion. */
  moveIgnoreAxes?: { x?: boolean, y?: boolean, z?: boolean };
}

/**
 * A simple entity controller with basic movement functions.
 * 
 * @remarks
 * This class implements simple movement methods that serve
 * as a way to add realistic movement and rotational facing
 * functionality to an entity. This is also a great base to
 * extend for your own more complex entity controller
 * that implements things like pathfinding. Compatible with
 * entities that have kinematic or dynamic rigid body types.
 * 
 * @example 
 * ```typescript
 * // Create a custom entity controller for myEntity, prior to spawning it.
 * myEntity.setController(new SimpleEntityController());
 * 
 * // Spawn the entity in the world.
 * myEntity.spawn(world, { x: 53, y: 10, z: 23 });
 * 
 * // Move the entity at a speed of 4 blocks
 * // per second to the coordinate (10, 1, 10).
 * // console.log when we reach the target.
 * myEntity.controller.move({ x: 10, y: 1, z: 10 }, 4, {
 *   moveCompleteCallback: endPosition => {
 *     console.log('Finished moving to', endPosition);
 *   },
 * });
 * ```
 * 
 * @public
 */
export default class SimpleEntityController extends BaseEntityController {
  /** @internal */
  private _faceSpeed: number = 0;

  /** @internal */
  private _faceTarget: Vector3Like | undefined;

  /** @internal */
  private _moveSpeed: number = 0;

  /** @internal */
  private _moveTarget: Vector3Like | undefined;

  /** @internal */
  private _moveIgnoreAxes: { x?: boolean, y?: boolean, z?: boolean } = {};

  /** @internal */
  private _onFace: FaceCallback | undefined;

  /** @internal */
  private _onFaceComplete: FaceCompleteCallback | undefined;

  /** @internal */
  private _onMove: MoveCallback | undefined;

  /** @internal */
  private _onMoveComplete: MoveCompleteCallback | undefined;

  /**
   * Rotates the entity at a given speed to face a target coordinate.
   * 
   * @remarks
   * If this method is called while the entity is already attempting
   * to face another target, the previous target will be ignored and
   * the entity will start attempting to face the new target.
   * 
   * @param target - The target coordinate to face.
   * @param speed - The speed at which to rotate to the target coordinate.
   * @param options - Additional options for the face operation, such as callbacks.
   */
  public face(target: Vector3Like, speed: number, options?: FaceOptions): void {
    this._faceTarget = target;
    this._faceSpeed = speed;
    this._onFace = options?.faceCallback;
    this._onFaceComplete = options?.faceCompleteCallback;
  }

  /**
   * Moves the entity at a given speed in a straight line to a target coordinate.
   * 
   * @remarks
   * If this method is called while the entity is already attempting
   * to move to another target, the previous target will be ignored and
   * the entity will start attempting to move to the new target.
   * 
   * @param target - The target coordinate to move to.
   * @param speed - The speed at which to move to the target coordinate.
   * @param options - Additional options for the move operation, such as callbacks.
   */
  public move(target: Vector3Like, speed: number, options?: MoveOptions): void {
    this._moveTarget = target;
    this._moveSpeed = speed;
    this._moveIgnoreAxes = options?.moveIgnoreAxes ?? {};
    this._onMove = options?.moveCallback;
    this._onMoveComplete = options?.moveCompleteCallback;
  }

  /** @internal */
  public tick(entity: Entity, deltaTimeMs: number): void {
    super.tick(entity, deltaTimeMs);

    if (!this._moveTarget && !this._faceTarget) {
      return;
    }

    const deltaTimeSeconds = deltaTimeMs / 1000;
    const currentPos = entity.position;

    if (this._moveTarget) {
      const direction = {
        x: this._moveIgnoreAxes.x ? 0 : this._moveTarget.x - currentPos.x,
        y: this._moveIgnoreAxes.y ? 0 : this._moveTarget.y - currentPos.y,
        z: this._moveIgnoreAxes.z ? 0 : this._moveTarget.z - currentPos.z,
      };

      const distanceSquared = direction.x * direction.x + 
                              direction.y * direction.y + 
                              direction.z * direction.z;

      if (distanceSquared > 0.1) {
        const distance = Math.sqrt(distanceSquared);
        const maxMove = this._moveSpeed * deltaTimeSeconds;
        const moveDistance = Math.min(distance, maxMove);
        const moveScale = moveDistance / distance;
        const position = {
          x: currentPos.x + direction.x * moveScale,
          y: currentPos.y + direction.y * moveScale,
          z: currentPos.z + direction.z * moveScale,
        };

        entity.setPosition(position);

        if (this._onMove) {
          this._onMove(position, this._moveTarget);
        }
      } else {
        this._moveTarget = undefined;

        if (this._onMoveComplete) {
          this._onMoveComplete(currentPos);
          this._onMoveComplete = undefined;
        }
      }
    }

    if (this._faceTarget) {
      const direction = {
        x: this._faceTarget.x - currentPos.x,
        z: this._faceTarget.z - currentPos.z,
      };

      // Calculate yaw angle to face target (-z facing since -z is forward)
      const targetYaw = Math.atan2(-direction.x, -direction.z);
      
      const currentRotation = entity.rotation;
      const currentYaw = Math.atan2(
        2 * (currentRotation.w * currentRotation.y), 
        1 - 2 * (currentRotation.y * currentRotation.y),
      );

      // Calculate shortest angle difference
      let angleDiff = targetYaw - currentYaw;
      
      // Normalize angle difference to [-π, π] range for shortest path
      while (angleDiff > Math.PI) {
        angleDiff -= 2 * Math.PI;
      }
      
      while (angleDiff < -Math.PI) {
        angleDiff += 2 * Math.PI;
      }

      if (Math.abs(angleDiff) > 0.01) {
        // Smoothly rotate towards target using shortest path
        const maxRotation = this._faceSpeed * deltaTimeSeconds;
        const rotationAmount = Math.abs(angleDiff) < maxRotation ? angleDiff : Math.sign(angleDiff) * maxRotation;
        const newYaw = currentYaw + rotationAmount;
        const halfYaw = newYaw / 2;
        const rotation = {
          x: 0,
          y: Math.fround(Math.sin(halfYaw)),
          z: 0,
          w: Math.fround(Math.cos(halfYaw)),
        };

        entity.setRotation(rotation);

        if (this._onFace) {
          this._onFace(currentRotation, rotation);
        }
      } else {
        this._faceTarget = undefined;

        if (this._onFaceComplete) {
          this._onFaceComplete(entity.rotation);
          this._onFaceComplete = undefined;
        }
      }
    }
  }
}

Diving Deeper

The SimpleEntityController class is constantly evolving. You can find the latest SimpleEntityController API Reference here.

If there are features that we don't currently support for the SimpleEntityController that you'd like to see added to the HYTOPIA SDK, you can submit a feature request here.

Last updated