cocos小游戏实战-05-NPC与角色攻击逻辑

添加NPC攻击逻辑与动画

assets/Scripts/WoodenSkeleton/WoodenSkeletonManager.ts

判断角色是否在NPC的上下左右四个格子中;当前X坐标相等,且Y坐标相距小于等于1格,或者Y坐标相等,X坐标小于等于1格


async init() {
    
    
	// 攻击检测,角色出现在NPC附近,NPC主动发起攻击
  EventManager.Instance.on(EVENT_ENUM.PLAYER_MOVE_END, this.onAttack, this)
}

// NPC攻击角色检测
onAttack() {
    
    
  const {
    
     x: playerX, y: playerY } = DataManager.Instance.player
  // 判断角色是否在NPC的上下左右四个格子中
  const disX = Math.abs(this.x - playerX)
  const disY = Math.abs(this.y - playerY)
  // 当前X坐标相等,且Y坐标相距小于等于1格,或者Y坐标相等,X坐标小于等于1格
  if ((this.x === playerX && disY <= 1) || (this.y === playerY && disX <= 1)) {
    
    
    this.state = ENTITY_STATE_ENUM.ATTACK
  } else {
    
    
    this.state = ENTITY_STATE_ENUM.IDLE
  }
}

assets/Scripts/WoodenSkeleton/AttackSubStateMachine.ts

添加NPC攻击动画

const BASE_URL = 'texture/woodenskeleton/attack'
@ccclass('AttackSubStateMachine')
export class AttackSubStateMachine extends DirectionSubStateMachine {
    
    
  constructor(fsm: StateMachine) {
    
    
    super(fsm)
    // NPC攻击动画
    this.stateMachines.set(DIRECTION_ENUM.TOP, new State(fsm, `${
      
      BASE_URL}/top`))
    this.stateMachines.set(DIRECTION_ENUM.BOTTOM, new State(fsm, `${
      
      BASE_URL}/bottom`))
    this.stateMachines.set(DIRECTION_ENUM.LEFT, new State(fsm, `${
      
      BASE_URL}/left`))
    this.stateMachines.set(DIRECTION_ENUM.RIGHT, new State(fsm, `${
      
      BASE_URL}/right`))
  }
}

assets/Scripts/WoodenSkeleton/WoodenSkeletonStateMachine.ts

注册状态机,将攻击状态加入状态机

// 初始化参数
initParams() {
    
    
	//...
	this.params.set(PARAMS_NAME_ENUM.ATTACK, initParamsTrigger())
}

// 初始化状态机
initStateMachine() {
    
    
	//...
	// NPC打架动画
	 this.stateMachines.set(PARAMS_NAME_ENUM.ATTACK, new AttackSubStateMachine(this))
}

// 初始化动画
  initAnimationEvent() {
    
    
    this.animationComponent.on(Animation.EventType.FINISHED, () => {
    
    
      // 执行完动画需要恢复默认idle动画的白名单
      const whiteList = ['attack']
      const name = this.animationComponent.defaultClip.name
      if (whiteList.some(v => name.includes(v))) {
    
    
        // 统一state入口
        this.node.getComponent(EntityManager).state = ENTITY_STATE_ENUM.IDLE
        // this.currentState = this.stateMachines.get(PARAMS_NAME_ENUM.IDLE)
      }
    })
  }

run() {
    
    
	//...
	case this.stateMachines.get(PARAMS_NAME_ENUM.ATTACK):
	if (this.params.get(PARAMS_NAME_ENUM.ATTACK).value) {
    
    
          this.currentState = this.stateMachines.get(PARAMS_NAME_ENUM.ATTACK)
        } else if...

}

添加角色死亡逻辑与动画

assets/Scripts/WoodenSkeleton/WoodenSkeletonManager.ts

在NPC攻击处通知角色死亡

// ...

// NPC攻击角色检测
  onAttack() {
    
    
    const {
    
     x: playerX, y: playerY, state: playState } = DataManager.Instance.player
    // 判断角色是否在NPC的上下左右四个格子中
    const disX = Math.abs(this.x - playerX)
    const disY = Math.abs(this.y - playerY)
    // 当前X坐标相等,且Y坐标相距小于等于1格,或者Y坐标相等,X坐标小于等于1格,且判断当前角色不是死亡状态,不然会出现鞭尸现象
    if (
      ((this.x === playerX && disY <= 1) || (this.y === playerY && disX <= 1)) &&
      playState !== ENTITY_STATE_ENUM.DEATH &&
      playState !== ENTITY_STATE_ENUM.AIRDEATH
    ) {
    
    
      this.state = ENTITY_STATE_ENUM.ATTACK
      // 通知角色,在地面死亡
      EventManager.Instance.emit(EVENT_ENUM.ATTACK_PLAYER, ENTITY_STATE_ENUM.DEATH)
    } else {
    
    
      this.state = ENTITY_STATE_ENUM.IDLE
    }
  }
// ...

assets/Scripts/Player/PlayerManager.ts

注册角色死亡事件

// ...
async init() {
    
    
	//...
	EventManager.Instance.on(EVENT_ENUM.ATTACK_PLAYER, this.onDead, this)
}

// 角色死亡,直接给状态即可
  onDead(type: ENTITY_STATE_ENUM) {
    
    
    this.state = type
  }

inputHandler(inputDirection: CONTROLLER_ENUM) {
    
    
    // 死亡之后不可移动
    if (this.state === ENTITY_STATE_ENUM.DEATH || this.state === ENTITY_STATE_ENUM.AIRDEATH) {
    
    
      return
    }
	// ...
}

assets/Scripts/Player/DeathSubStateMachine.ts

添加角色死亡动画

const BASE_URL = 'texture/player/death'

@ccclass('DeathSubStateMachine')
export class DeathSubStateMachine extends DirectionSubStateMachine {
    
    
  constructor(fsm: StateMachine) {
    
    
    super(fsm)
    // 角色死亡动画
    this.stateMachines.set(DIRECTION_ENUM.TOP, new State(fsm, `${
      
      BASE_URL}/top`))
    this.stateMachines.set(DIRECTION_ENUM.BOTTOM, new State(fsm, `${
      
      BASE_URL}/bottom`))
    this.stateMachines.set(DIRECTION_ENUM.LEFT, new State(fsm, `${
      
      BASE_URL}/left`))
    this.stateMachines.set(DIRECTION_ENUM.RIGHT, new State(fsm, `${
      
      BASE_URL}/right`))
  }
}

assets/Scripts/Player/PlayerStateMachine.ts

注册角色死亡动画

// ...
// 初始化参数
initParams() {
    
    
//...
	this.params.set(PARAMS_NAME_ENUM.DEATH, initParamsTrigger())
}

// 初始化状态机
initStateMachine() {
    
    
	//...
	// 死亡
  this.stateMachines.set(PARAMS_NAME_ENUM.DEATH, new DeathSubStateMachine(this))
}

run() {
    
    
	case this.stateMachines.get(PARAMS_NAME_ENUM.DEATH):
	if (this.params.get(PARAMS_NAME_ENUM.DEATH).value) {
    
    
	  this.currentState = this.stateMachines.get(PARAMS_NAME_ENUM.DEATH)
	} else if//...
}

https://web03-1252477692.cos.ap-guangzhou.myqcloud.com/blog/images/GIF%202022-7-21%2018-20-40.gif

添加人物攻击与动画

assets/Scripts/Player/PlayerManager.ts

攻击逻辑,当前方向与下一步方向相同,且前方一格是NPC,则发动攻击

//...
inputHandler{
    
    
		//...
		//当前是否可以攻击NPC
    if (this.willAttack(inputDirection)) {
    
    
      return
    }
		//...
}

// 判断攻击敌人,与碰撞检测类似
  willAttack(type: CONTROLLER_ENUM): boolean {
    
    
    // 所有NPC
    const enemies = DataManager.Instance.enemies
    for (let i = 0; i < enemies.length; i++) {
    
    
      const {
    
     x: enemyX, y: enemyY } = enemies[i]
      // 如果当前方向朝上,且下一步也是往上走,NPC的X坐标与角色X坐标相等,在同一条线上,且NPC的Y坐标在角色Y坐标上2格,也就是在兵器的前一格,那么触发攻击
      if (
        type === CONTROLLER_ENUM.TOP &&
        this.direction === DIRECTION_ENUM.TOP &&
        enemyX === this.x &&
        enemyY === this.targetY - 2
      ) {
    
    
        this.state = ENTITY_STATE_ENUM.ATTACK
        return true
      } else if (
        type === CONTROLLER_ENUM.BOTTOM &&
        this.direction === DIRECTION_ENUM.BOTTOM &&
        enemyX === this.x &&
        enemyY === this.targetY + 2
      ) {
    
    
        this.state = ENTITY_STATE_ENUM.ATTACK
        return true
      } else if (
        type === CONTROLLER_ENUM.LEFT &&
        this.direction === DIRECTION_ENUM.LEFT &&
        enemyY === this.targetY &&
        enemyX === this.targetX - 2
      ) {
    
    
        this.state = ENTITY_STATE_ENUM.ATTACK
        return true
      } else if (
        type === CONTROLLER_ENUM.RIGHT &&
        this.direction === DIRECTION_ENUM.RIGHT &&
        enemyY === this.targetY &&
        enemyX === this.targetX + 2
      ) {
    
    
        this.state = ENTITY_STATE_ENUM.ATTACK
        return true
      }
    }
    return false
  }

assets/Scripts/Player/AttackSubStateMachine.ts

添加人物攻击动画

const BASE_URL = 'texture/player/attack'
@ccclass('AttackSubStateMachinePlayer')
export class AttackSubStateMachinePlayer extends DirectionSubStateMachine {
    
    
  constructor(fsm: StateMachine) {
    
    
    super(fsm)
    // 人物攻击动画
    this.stateMachines.set(DIRECTION_ENUM.TOP, new State(fsm, `${
      
      BASE_URL}/top`))
    this.stateMachines.set(DIRECTION_ENUM.BOTTOM, new State(fsm, `${
      
      BASE_URL}/bottom`))
    this.stateMachines.set(DIRECTION_ENUM.LEFT, new State(fsm, `${
      
      BASE_URL}/left`))
    this.stateMachines.set(DIRECTION_ENUM.RIGHT, new State(fsm, `${
      
      BASE_URL}/right`))
  }
}

assets/Scripts/Player/PlayerStateMachine.ts

注册攻击动画

// ...
// 初始化参数
initParams() {
    
    
//...
	this.params.set(PARAMS_NAME_ENUM.ATTACK, initParamsTrigger())
}

// 初始化状态机
initStateMachine() {
    
    
	//...
	// 死亡
  this.stateMachines.set(PARAMS_NAME_ENUM.ATTACK, new AttackSubStateMachinePlayer(this))
}

run() {
    
    
	case this.stateMachines.get(PARAMS_NAME_ENUM.ATTACK):
	if (this.params.get(PARAMS_NAME_ENUM.ATTACK).value) {
    
    
	  this.currentState = this.stateMachines.get(PARAMS_NAME_ENUM.ATTACK)
	} else if//...
}

添加NPC死亡逻辑与动画

assets/Base/EntityManager.ts

在实体类中加上一个随机id,人物,NPC都将拥有一个id

///...
export class EntityManager extends Component {
    
    
  id: string = randomString(12)
	//...
}

// 生成随机字符串
export const randomString = (length: number): string => {
    
    
  const str = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
  let result = ''
  for (let i = length; i > 0; --i) result += str[Math.floor(Math.random() * str.length)]
  return result
}

assets/Scripts/Player/PlayerManager.ts

添加攻击后NPC死亡的逻辑

//...

inputHandler(inputDirection: CONTROLLER_ENUM) {
    
    
//...
//当前是否可以攻击NPC
    const attackId = this.willAttack(inputDirection)
    if (attackId) {
    
    
      // 通知NPC去世
      EventManager.Instance.emit(EVENT_ENUM.ATTACK_ENEMY, attackId)
      return
    }

    // 当前人物死亡||人物没渲染||当前状态是攻击
    if (
      this.state === ENTITY_STATE_ENUM.DEATH ||
      this.state === ENTITY_STATE_ENUM.AIRDEATH ||
      this.state === ENTITY_STATE_ENUM.ATTACK
    ) {
    
    
      return
    }
//...
}

willAttack(type: CONTROLLER_ENUM): string {
    
    
	//...
	for (let i = 0; i < enemies.length; i++) {
    
    
	      // 如果NPC已死亡,跳过攻击
	      if (enemies[i].state === ENTITY_STATE_ENUM.DEATH) {
    
    
	        continue
	      }
	//...
	const {
    
     x: enemyX, y: enemyY, id: enemyId } = enemies[i]
	//...
	//在每一个攻击情况下,都返回NPC的id
	return enemyId
	//...
	return ''
}

assets/Scripts/WoodenSkeleton/WoodenSkeletonManager.ts

注册NPC死亡动画

//...
async init() {
    
    
	//...
	// 死亡
    EventManager.Instance.on(EVENT_ENUM.ATTACK_ENEMY, this.onDead, this)
}

// NPC朝向角色检测
onChangeDirection(isInit?: boolean) {
    
    
    if (this.state === ENTITY_STATE_ENUM.DEATH || !DataManager.Instance.player) {
    
    
      // 已死亡
      return
    }
	//...
}

// NPC攻击角色检测
onAttack() {
    
    
  if (this.state === ENTITY_STATE_ENUM.DEATH || !DataManager.Instance.player) {
    
    
    // 已死亡
    return
  }
	//...
}

// NPC死亡
onDead(attackId: string) {
    
    
  if (this.state === ENTITY_STATE_ENUM.DEATH) {
    
    
    return
  }
  // 判断当前NPC死亡才死亡
  if (this.id === attackId) {
    
    
    this.state = ENTITY_STATE_ENUM.DEATH
  }
}

assets/Scripts/WoodenSkeleton/DeathSubStateMachineWooden.ts

添加NPC死亡动画

const BASE_URL = 'texture/woodenskeleton/death'

@ccclass('DeathSubStateMachineWooden')
export class DeathSubStateMachineWooden extends DirectionSubStateMachine {
    
    
  constructor(fsm: StateMachine) {
    
    
    super(fsm)
    // NPC死亡动画
    this.stateMachines.set(DIRECTION_ENUM.TOP, new State(fsm, `${
      
      BASE_URL}/top`))
    this.stateMachines.set(DIRECTION_ENUM.BOTTOM, new State(fsm, `${
      
      BASE_URL}/bottom`))
    this.stateMachines.set(DIRECTION_ENUM.LEFT, new State(fsm, `${
      
      BASE_URL}/left`))
    this.stateMachines.set(DIRECTION_ENUM.RIGHT, new State(fsm, `${
      
      BASE_URL}/right`))
  }
}

https://web03-1252477692.cos.ap-guangzhou.myqcloud.com/blog/images/GIF%202022-7-21%2018-22-11.gif

渲染门以及门打开逻辑

assets/Scripts/Door/DeathSubStateMachineDoor.ts

门打开动画-代码与其他动画代码几乎一样,改一个BASE_URL即可,代码略

assets/Scripts/Door/IdleSubStateMachineDoor.ts

门关闭动画-代码略

assets/Scripts/Door/DoorManager.ts

添加开门事件与逻辑,当所有NPC都死亡时,门打开

@ccclass('DoorManager')
export class DoorManager extends EntityManager {
    
    
  async init() {
    
    
    this.fsm = this.addComponent(DoorStateMachine)
    await this.fsm.init()

    super.init({
    
    
      x: 7,
      y: 8,
      type: ENTITY_TYPE_ENUM.PLAYER,
      direction: DIRECTION_ENUM.TOP, // 设置初始方向
      state: ENTITY_STATE_ENUM.IDLE, // 设置fsm化动画
    })
    EventManager.Instance.on(EVENT_ENUM.DOOR_OPEN, this.onOpen, this)
  }

  // 解绑事件
  onDestroy() {
    
    
    super.onDestroy()
    EventManager.Instance.off(EVENT_ENUM.DOOR_OPEN, this.onOpen)
  }
  onOpen() {
    
    
    // 门还在,所有NPC都已经死亡,就让门消失
    if (
      this.state !== ENTITY_STATE_ENUM.DEATH &&
      DataManager.Instance.enemies.every(enemy => enemy.state === ENTITY_STATE_ENUM.DEATH)
    )
      this.state = ENTITY_STATE_ENUM.DEATH
  }
}

assets/Scripts/Door/DoorStateMachine.ts

注册门打开关闭动画,与其他的注册方式一样

@ccclass('DoorStateMachine')
export class DoorStateMachine extends StateMachine {
    
    
  resetTrigger() {
    
    
    for (const [_, value] of this.params) {
    
    
      if (value.type === FSM_PARAMS_TYPE_ENUM.TRIGGER) {
    
    
        value.value = false
      }
    }
  }

  // 初始化参数
  initParams() {
    
    
    this.params.set(PARAMS_NAME_ENUM.IDLE, initParamsTrigger())
    this.params.set(PARAMS_NAME_ENUM.DIRECTION, initParamsNumber())
    this.params.set(PARAMS_NAME_ENUM.DEATH, initParamsTrigger())
  }

  // 初始化状态机
  initStateMachine() {
    
    
    // 门初始化
    this.stateMachines.set(PARAMS_NAME_ENUM.IDLE, new IdleSubStateMachineDoor(this))
    // 门消失
    this.stateMachines.set(PARAMS_NAME_ENUM.DEATH, new DeathSubStateMachineDoor(this))
  }

  // 初始化动画
  initAnimationEvent() {
    
    }

  async init() {
    
    
    // 添加动画组件
    this.animationComponent = this.addComponent(Animation)
    this.initParams()
    this.initStateMachine()
    this.initAnimationEvent()
    // 确保资源资源加载
    await Promise.all(this.waitingList)
  }
  run() {
    
    
    switch (this.currentState) {
    
    
      case this.stateMachines.get(PARAMS_NAME_ENUM.IDLE):
      case this.stateMachines.get(PARAMS_NAME_ENUM.DEATH):
        if (this.params.get(PARAMS_NAME_ENUM.DEATH).value) {
    
    
          this.currentState = this.stateMachines.get(PARAMS_NAME_ENUM.DEATH)
        } else if (this.params.get(PARAMS_NAME_ENUM.IDLE).value) {
    
    
          this.currentState = this.stateMachines.get(PARAMS_NAME_ENUM.IDLE)
        } else {
    
    
          // 为了触发子状态机的改变
          this.currentState = this.currentState
        }
        break
      default:
        this.currentState = this.stateMachines.get(PARAMS_NAME_ENUM.IDLE)
    }
  }
}

assets/Scripts/Player/PlayerManager.ts

触发事件,每当NPC去世时触发检测

//...
inputHandler(inputDirection: CONTROLLER_ENUM) {
    
    
    if (this.isMoving) {
    
    
      return
    }

    //当前是否可以攻击NPC
    const attackId = this.willAttack(inputDirection)
    if (attackId) {
    
    
      // 通知NPC去世
      EventManager.Instance.emit(EVENT_ENUM.ATTACK_ENEMY, attackId)
      // 通知判断门是否隐藏
      EventManager.Instance.emit(EVENT_ENUM.DOOR_OPEN)
      return
    }
	//...
}

在这里插入图片描述

本节源码地址:

https://gitee.com/yuan30/cramped-room-of-death/tree/day5/

猜你喜欢

转载自blog.csdn.net/weixin_43840202/article/details/125988574