Skip to main content
EchoExclusive
Inspiring
April 9, 2025
Solved

Differentiating NPC and Player

Hi all,

I having a little issue with differentiating player and NPC.

Side note: I have the NPC gizmo in the editor and enabled avatar ai_agent. I named my NPC "NameNPC"

I have this script that was referenced from "Intro to desktop Editor and Typescript". I added some logic to check if it's an NPC. If it is an NPC it will print "Should be NPC". If it is a player it will print "Should be Player"

Since I only have one NPC and myself. I was expecting to see "Should be Player" and "Should be NPC" printed once

However, not only was Should be Player printed three tiems, but it thought NPC was a player, than deleted the NPC and than said NPC is a player again. See attached below

This would be a problem because I have other logic where enemies will check when player entered world and follow that player, but it will start following the NPC instead because it thinks the NPC is a player

How come the NPC is treated as a player at the beginning? How can I make sure the NPC is never treated as a player - I don't want to have to check the NPC's name to see if it is an NPC

uRocketLife
New Participant
April 10, 2025

Pigeon found that you have to add a 2 second delay to a PlayerEnterWorld before NPC is registered as NPC. There is a "GetPlayerType" method that recognizes the init NPC enter as being "departed" which you can use as a filter to prevent initial NPC 'fails' from registering

You would call if (getPlayerType(newPlayer, this.world) === "departed") return; Something like that
export function getPlayerType(player: Player, world: World): PlayerType {
  if (player === world.getServerPlayer()) {
    return "server";
  } else if (!world.getPlayers().includes(player)) {
    return "departed";
  } else if (player.isInBuildMode.get()) {
    return "builder";
  } else if (AvatarAIAgent.getGizmoFromPlayer(player) !== undefined) {
    return "npc";
  } else {
    return "human";
  }
}​

8 replies

SeeingBlue
Honored Contributor
April 9, 2025

This is a good question and a pain point I have also experienced. 

Meta's solution is documented in this article: https://developers.meta.com/horizon-worlds/learn/documentation/desktop-editor/npcs/scripted-avatar-npcs/spawning-for-scripted-avatar-npcs#npc-detection

    this.connectCodeBlockEvent(
      this.entity,
      hz.CodeBlockEvents.OnPlayerEnterWorld,
      (player: hz.Player) => {
        // isNPC == true -> NPC; isNPC == false -> player
        const isNpc = AvatarAIAgent.getGizmoFromPlayer(player) !== undefined;
      }
    );

NPCs have other issues as well, like spawning when world sim is turned off, which always fires onPlayerExitsWorld events when you turn sim on. Also, if you delay the spawning of an NPC, it will still fire onPlayerEntersWorld events before it arrives in the world.

Upcoming Events : Performance Pro - UI Unleashed - Building Responsive & Performant Interfaces Thursday June 19th 5:00 PM PT - Register June Programming Calendar
EchoExclusive
Inspiring
April 10, 2025

Interesting, so I original did have the const isNpc check, but I did it in a separate function

this.connectCodeBlockEvent(
this.entity,
hz.CodeBlockEvents.OnPlayerEnterWorld,
(player: hz.Player) => {
this.handleOnPlayerEnter(player);

 

where handleOnPlayerEnter is shown in my first post.

That will always say NPC is the player.

If moved it to like yours, it was first say NPC is a player but than say NPC is a NPC. So it still detects NPC as a player

 

SeeingBlue
Honored Contributor
April 10, 2025

I would just check the NPC's name against a string array of NPC names in your world. Not failproof if a player comes in with the same name, though.

Upcoming Events : Performance Pro - UI Unleashed - Building Responsive & Performant Interfaces Thursday June 19th 5:00 PM PT - Register June Programming Calendar
SeeingBlue
Honored Contributor
April 9, 2025

Updated my answer to reflect meta's solutions in their article.

Upcoming Events : Performance Pro - UI Unleashed - Building Responsive & Performant Interfaces Thursday June 19th 5:00 PM PT - Register June Programming Calendar
uRocketLife
New Participant
April 10, 2025

Pigeon found that you have to add a 2 second delay to a PlayerEnterWorld before NPC is registered as NPC. There is a "GetPlayerType" method that recognizes the init NPC enter as being "departed" which you can use as a filter to prevent initial NPC 'fails' from registering

You would call if (getPlayerType(newPlayer, this.world) === "departed") return; Something like that
export function getPlayerType(player: Player, world: World): PlayerType {
  if (player === world.getServerPlayer()) {
    return "server";
  } else if (!world.getPlayers().includes(player)) {
    return "departed";
  } else if (player.isInBuildMode.get()) {
    return "builder";
  } else if (AvatarAIAgent.getGizmoFromPlayer(player) !== undefined) {
    return "npc";
  } else {
    return "human";
  }
}​
EchoExclusive
Inspiring
April 11, 2025

Interesting, this seems to give me the best result. Thanks uRocketLife 

I did this

export function getPlayerType(player: Player, world: World): PlayerType {
  if (player === world.getServerPlayer()) {
    return PlayerType.server;
  } else if (!world.getPlayers().includes(player)) {
    return PlayerType.departed;
  } else if (player.isInBuildMode.get()) {
    return PlayerType.builder;
  } else if (AvatarAIAgent.getGizmoFromPlayer(player) !== undefined) {
    return PlayerType.npc;
  } else {
    return PlayerType.human;
  }
}

export enum PlayerType {
  'server',
  'departed',
  'builder',
  'npc',
  'human'
};

and in start

 start() {
    this.connectCodeBlockEvent(
      this.entity,
      CodeBlockEvents.OnPlayerEnterWorld,
      (player: Player) => {
        //this.handleOnPlayerEnter(player);
        if(getPlayerType(player,this.world) == PlayerType.server)
        {
           console.log("server is" + player.name.get());
        }
        else if(getPlayerType(player,this.world) == PlayerType.builder)
          {
             console.log("builder is" + player.name.get());
          }
          else if(getPlayerType(player,this.world) == PlayerType.departed)
            {
               console.log("departed is" + player.name.get());
            }
            else if(getPlayerType(player,this.world) == PlayerType.human)
              {
                 console.log("human is" + player.name.get());
              }
        else if(getPlayerType(player,this.world) == PlayerType.npc)
        {
           console.log("npc is" + player.name.get());
        }
        else
        {
          console.log("else is" + player.name.get());
        }
      }
    );
LintheMeta
Participant
April 11, 2025

I got some suggestion from Shards; tested and works perfect

Like PigeonOnPizza mentioned, a small delay seems to be needed; but from what I can also see, the NPC takes these 2 seconds (or some time) to register as an NPC

So, the `simple` solution is to set the NPC "spawn on world start" to false.
Then on the NPC set a script like this

import {
  Component,
} from "horizon/core";
import { AvatarAIAgent } from "horizon/avatar_ai_agent";

//////
// Constants
//////
const SPAWN_DELAY = 3;

//////
// Class: Pet
//////
class NPC extends Component<typeof NPC> {
  static propsDefinition = {};

  preStart() {}

  start() {
    this.async.setTimeout(() => {
      this.entity.as(AvatarAIAgent).spawnAgentPlayer();
    }, SPAWN_DELAY * 1000)
  }
}
Component.register(NPC);


This just delays the NPC from spawning for a small delay; 
when then the NPC EntersWorld, it will code

isNpc = AvatarAIAgent.getGizmoFromPlayer(player) !== undefined;

will work just fine