Forum Discussion

Darth.Thanos's avatar
9 months ago

Requesting some simple help

I’m working on just playing around with different assets, and I’m having a devil of a time with this. 

I’ve made a basic world. Literally nothing in it except a ground plane and a interactive npc (in this case a zombie) I’ve managed to get it to wander around aimlessly, (like a chicken) but I can not get it to identify the player and then initiate an attack - as it should being an enemy class and all my scripts telling it to do so. It just wanders around. 

I’ve put in a npc manager script, and a npc monster script both of which tell it to wander until it notices a player (45 minimum sight distance) then upon sight of target player play its taunt animation then move and attack. I’ve also got the nav mesh set up, and my player is inside said mesh. (The mesh is the full size of the ground plane) 

But it does not do this. It just wanders. Any help on this would be much appreciated. Thanks! 

 

 

edit:

 

here is the code I'm working with the "NPCMonster"  - in this case, all I'm wanting to do is have the zombie wander around the area until it notices a player, at which point it will do its "taunt" animation, then move towards the player and try to attack them.

 

i will be adding details to track the attack hits and make them matter (after 5 hits the player is sent back to spawn point), and ways for the player to fight back - but that's all a later addition. trying to take this step by step as i teach myself from basically zero knowledge.

 

import * as hz from "horizon/core";
import { NPCAgent, NPCAgentEmote } from "NPCAgent";

enum NPCMonsterState {
  Idle,
  Walking,
  Wandering,
  Taunting, // New state for taunting
}

class NPCMonster extends NPCAgent<typeof NPCMonster> {
  static propsDefinition = {
    ...NPCAgent.propsDefinition,
    maxWanderDistance: { type: hz.PropTypes.Number, default: 20 },
  };

  state: NPCMonsterState = NPCMonsterState.Idle;
  stateTimer: number = 0;
  startLocation!: hz.Vec3;
  targetPlayer: hz.Player | undefined = undefined;

  start() {
    super.start();
    this.setState(NPCMonsterState.Idle);
    this.startLocation = this.entity.position.get();
  }

  update(deltaTime: number) {
    super.update(deltaTime);
    this.updateTarget();
    this.updateStateMachine(deltaTime);
  }

  private updateTarget() {
    if (this.targetPlayer === undefined) {
      const players = hz.World.getPlayers() as hz.Player[];
      const monsterPosition = this.entity.position.get();
      for (const player of players) {
        const playerPosition = player.position.get();
        const distanceSq = monsterPosition.distanceSquared(playerPosition);
        if (distanceSq < this.props.maxWanderDistance * this.props.maxWanderDistance) {
          this.targetPlayer = player;
          console.log("Player detected:", player);
          this.setState(NPCMonsterState.Taunting);
          break;
        }
      }
    }
  }

  private updateStateMachine(deltaTime: number) {
    switch (this.state) {
      case NPCMonsterState.Idle:
        if (Math.random() < 0.1) {
          this.setState(NPCMonsterState.Wandering);
        }
        break;
      case NPCMonsterState.Walking:
        this.updateWalkingState(deltaTime);
        break;
      case NPCMonsterState.Wandering:
        this.updateWanderingState(deltaTime);
        break;
      case NPCMonsterState.Taunting:
        this.updateTauntingState(deltaTime);
        break;
    }
  }

  private onEnterState(state: NPCMonsterState) {
    console.log("Entering state:", NPCMonsterState[state]);
    switch (state) {
      case NPCMonsterState.Idle:
        this.navMeshAgent?.isImmobile.set(true);
        this.navMeshAgent?.destination.set(this.entity.position.get());
        break;
      case NPCMonsterState.Walking:
        this.navMeshAgent?.isImmobile.set(false);
        this.setMaxSpeedToWalkSpeed();
        break;
      case NPCMonsterState.Wandering:
        this.navMeshAgent?.isImmobile.set(false);
        this.setMaxSpeedToWalkSpeed();
        this.setRandomDestination();
        break;
      case NPCMonsterState.Taunting:
        this.navMeshAgent?.isImmobile.set(true);
        this.triggerEmoteAnimation(NPCAgentEmote.Taunt);
        this.stateTimer = 2.8; // Duration of the taunt animation
        break;
    }
  }

  private onLeaveState(state: NPCMonsterState) {
    console.log("Leaving state:", NPCMonsterState[state]);
    if (state === NPCMonsterState.Taunting) {
      this.targetPlayer = undefined;
    }
  }

  private setState(state: NPCMonsterState) {
    if (this.state !== state) {
      this.onLeaveState(this.state);
      this.state = state;
      this.onEnterState(this.state);
    }
  }

  private updateWalkingState(deltaTime: number) {
    if (this.navMeshAgent && this.navMeshAgent.remainingDistance.get() < 0.5) {
      this.setState(NPCMonsterState.Idle);
    }
  }

  private updateWanderingState(deltaTime: number) {
    this.stateTimer -= deltaTime;
    if (this.stateTimer <= 0) {
      this.setRandomDestination();
    }
  }

  private updateTauntingState(deltaTime: number) {
    this.stateTimer -= deltaTime;
    if (this.stateTimer <= 0) {
      this.setState(NPCMonsterState.Idle);
    }
  }

  private setRandomDestination() {
    const randomOffset = new hz.Vec3(
      (Math.random() - 0.5) * this.props.maxWanderDistance,
      0,
      (Math.random() - 0.5) * this.props.maxWanderDistance
    );
    const randomDestination = this.startLocation.add(randomOffset);
    this.navMeshAgent?.destination.set(randomDestination);
    this.stateTimer = Math.random() * 5 + 2; // Wander for 2 to 7 seconds
    this.setState(NPCMonsterState.Walking);
  }

  public setMaxSpeedToWalkSpeed() {
    if (this.navMeshAgent) {
      this.navMeshAgent.maxSpeed.set(this.props.walkSpeed);
    }
  }

  public triggerEmoteAnimation(emote: NPCAgentEmote) {
    console.log("Triggering emote animation:", emote);
    super.triggerEmoteAnimation(emote);
  }
}

hz.Component.register(NPCMonster);

4 Replies

  • Can you share the script that you are using to make the NPC move and detect the player?

    Generally speaking, the trick is to implement a loop system checking if a player should be chased, commonly by using one of these methods:

    * Subscribe to when a player enters the world

    * Subscribe to when a player enters a perimeter (enters a trigger)

    * When a player grabs an object (I believe this is how the template is configured, the zombie starts chasing when the player grabs a sword)

    * When the player attacks first

    These are a few examples. If you share the code, me or others should be able to point what has to change in your code in order to make it work.

    • Darth.Thanos's avatar
      Darth.Thanos
      Member

      Oh sure! I’m on mobile and away from the computer but I’ll throw that in here later tonight. Thanks!!

  • At first glance, it looks like this script only supports taunting. It doesn't do anything else after that, no chasing or attacking.

    There is a script at the bottom of this article that may help you. It is called NPCMonster.ts but it includes more states like Running, Hit, & Dead.

    • Darth.Thanos's avatar
      Darth.Thanos
      Member

      Unfortunately the npc does not even taunt at this point. It just wanders. 

      i am curious, if its a good idea to build the script slowly, like make it wander first, test it, then add small features like taunting then the attack, then the other stuff, or just do it all in one shot?