Tip: Navigation is recommended for enabling Scripted Avatar NPCs to reach their destinations. If you have a simple flat world without complex features, you may be able to avoid deploying navigation.
Note: Retain the Name value. This value is passed as a string through a TypeScript API call to locate the profile to your NPC(s).
Note: These settings are based on a scaling factor of(1,1,1)
for the size of the avatar in the NPC gizmo. You may need to adjust them if you are using a different scaling.
NPCs
.private navMesh!: NavMesh;
start() {
// Get reference to nav profile and assign to constant "navMesh"
const navMeshManager = NavMeshManager.getInstance(this.world);
const navMesh = await navMeshManager.getByName("NPCs");
if (navMesh == null) {
console.error("Could not find navMesh: NPCs");
return;
};
this.navMesh = navMesh;
// Get status of navigation mesh baking. Wait until complete.
const bake = this.navMesh.getStatus().currentBake;
if (bake != null) {
await bake;
};
};
navMesh
object associated with the NPCs
profile is completed, the navMesh
is ready for use with the Scripted Avatar NPC APIs.AvatarAIAgent
.moveToPosition()
and moveToPositions()
methods for the entity to move to those locations.from
location to a to
location, using the referenced navMesh
object that you baked:public getPathTo(from : hz.Vec3, to : hz.Vec3) : Array<hz.Vec3> {
let nextPath: NavMeshPath | null;
let getPathAttempts: number = 0;
do {
nextPath = this.manager.navMesh.getPath(from, to);
getPathAttempts++;
} while (nextPath == null && getPathAttempts < 20);
if (nextPath == null) {
console.warn("Couldn't find any valid paths from ", from.toString(), " to ", to.toString());
return new Array<hz.Vec3>();
} ;
if(!nextPath.pathReachesDestination) {
console.warn("Path incomplete");
};
console.log("Path size: " + nextPath.waypoints.length + " from ", from.toString(), " to ", to.toString(), " dest " + nextPath.waypoints[nextPath.waypoints.length - 1].toString());
return nextPath.waypoints;
};
getPathTo()
function returns an array of hz.Vec3
waypoints between from
and to
Vec3 positions, derived from the nextPath.waypoints
object.moveToPositions()
function. Alternatively, if you may need to perform other actions during the process or potentially interrupt at a waypoint, you may choose to iterate through the array, feeding individual waypoints to moveToPosition()
in sequential order.let npcTarget: hz.Vec3 = this.props.npc!
.as(AvatarAIAgent)
.locomotion.targetPosition.get();
Property | Description |
---|---|
targetPosition.get() | Returns the current target Vec3 position of the agent. Undefined if it is not moving. |
targetDirection.get() | Returns the current target Vec3 rotation of the agent. Undefined if it is not rotating to a target. |
isMoving.get() | Returns true if the agent is in motion. |
isGrounded.get() | Returns true if the agent is on the ground. |
isJumping.get() | Returns true if the agent is currently jumping. |
moveToPosition()
method, which returns a Promise that can be evaluated for success or failure.moveToPosition()
call and ignores the result.this.entity.as(AvatarAIAgent).locomotion.moveToPosition((3,2,1))
AgentLocomotionResult
, an enum with the following values:Value | Description |
---|---|
0 | Complete |
1 | Canceled |
2 | Error |
this.entity.as(AvatarAIAgent).locomotion.moveToPosition((3,2,1)).then((locomotionResult) => {
switch (spawnResult) {
case AgentLocomotionResult.Complete:
console.log("Movement complete!");
break;
case AgentLocomotionResult.Canceled:
console.log("Movement was canceled.");
break;
case AgentSpawnResult.Error:
console.error("Uh-oh! An error occurred while moving.");
break;
};
});
moveToPositions
method accepts an array of Vec3 values. This array is a sequence of waypoints through which the Scripted Avatar NPC moves.moveToPositions()
method. The AgentMovementOptions
object is defined to include a movement speed of 5
.import * as hz from 'horizon/core';
import { AgentLocomotionOptions, AvatarAIAgent } from 'horizon/avatar_ai_agent';
class testScript02 extends hz.Component<typeof testScript02> {
static propsDefinition = {
npc: { type: hz.PropTypes.Entity },
waypoint1: { type: hz.PropTypes.Vec3 },
waypoint2: { type: hz.PropTypes.Vec3 },
waypoint3: { type: hz.PropTypes.Vec3 },
};
start() {
console.log("testScript02: Running start().");
let arrWaypoints: hz.Vec3[] = new Array<hz.Vec3>();
// sets movement options for movement between waypoints
let movementOptions: AgentLocomotionOptions = {
movementSpeed: 5,
};
if (this.props.npc) {
if (this.props.waypoint1) {
arrWaypoints.push(this.props.waypoint1);
if (this.props.waypoint2) {
arrWaypoints.push(this.props.waypoint2);;
if (this.props.waypoint3) {
arrWaypoints.push(this.props.waypoint3);
};
};
};
let npcGizmo = this.props.npc.as(AvatarAIAgent);
if (arrWaypoints[0] != null ) {
npcGizmo.locomotion.moveToPositions(arrWaypoints, movementOptions);
} else {
console.error("No waypoints defined!");
};
} else {
console.error("No NPC defined!");
};
};
};
hz.Component.register(testScript02);
moveToPosition()
method when Trigger0 has been entered by the specified Scripted Avatar NPC entity.Tip: This approach is helpful for defining the NPC’s path as a set of physical entities in the world.moveToPositions()
works when the waypoints are a set of known coordinates.
import * as hz from "horizon/core";
import { AvatarAIAgent } from "horizon/avatar_ai_agent";
export class TriggerZone extends hz.Component<typeof TriggerZone> {
static propsDefinition = {
zoneName: { type: hz.PropTypes.String },
display: { type: hz.PropTypes.Entity },
npc: { type: hz.PropTypes.Entity },
};
static triggerEnteredEvent = new hz.NetworkEvent<{
player: hz.Player;
entered: boolean;
zoneName: string;
}>("triggerEnteredEvent");
start() {
this.connectCodeBlockEvent(
this.entity,
hz.CodeBlockEvents.OnPlayerEnterTrigger,
(player) => {
this.sendNetworkBroadcastEvent(TriggerZone.triggerEnteredEvent, {
player,
entered: true,
zoneName: this.props.zoneName,
});
this.props.npc
.as(AvatarAIAgent)
.locomotion.moveToPosition(hz.Vec3.zero)
.then((result) => console.log("move to position result: " + result));
};
);
this.connectCodeBlockEvent(
this.entity,
hz.CodeBlockEvents.OnPlayerExitTrigger,
(player) => {
this.sendNetworkBroadcastEvent(TriggerZone.triggerEnteredEvent, {
player,
entered: false,
zoneName: this.props.zoneName,
});
};
);
if (this.props.display.exists()) {
this.props.display.as(hz.TextGizmo).text.set(this.props.zoneName);
};
};
};
hz.Component.register(TriggerZone);