原创

cocos小游戏实战-04-碰撞检测与NPC渲染


碰撞检测

https://web03-1252477692.cos.ap-guangzhou.myqcloud.com/blog/images/QQ%E6%88%AA%E5%9B%BE20220719101335.png

assets/Scripts/Tile/TileManager.ts

在设置人物移动的地方加上判断当前位置是否可移动与转向的逻辑

import { _decorator, Component, Sprite, SpriteFrame, UITransform } from 'cc'
import { TILE_TYPE_ENUM } from '../../Enum'

const { ccclass, property } = _decorator

export const TILE_WIDTH = 55
export const TILE_HEIGHT = 55

@ccclass('TileManager')
export class TileManager extends Component {
  type: TILE_TYPE_ENUM
  moveable: boolean //可走
  turnable: boolean //可转向
  async init(type: TILE_TYPE_ENUM, spriteFrame: SpriteFrame, i: number, j: number) {
    this.type = type
    // 墙壁
    const wallet: Array<TILE_TYPE_ENUM> = [
      TILE_TYPE_ENUM.WALL_COLUMN,
      TILE_TYPE_ENUM.WALL_ROW,
      TILE_TYPE_ENUM.WALL_LEFT_TOP,
      TILE_TYPE_ENUM.WALL_RIGHT_TOP,
      TILE_TYPE_ENUM.WALL_LEFT_BOTTOM,
      TILE_TYPE_ENUM.WALL_RIGHT_BOTTOM,
    ]
    // 悬崖
    const cliff: Array<TILE_TYPE_ENUM> = [
      TILE_TYPE_ENUM.CLIFF_CENTER,
      TILE_TYPE_ENUM.CLIFF_LEFT,
      TILE_TYPE_ENUM.CLIFF_RIGHT,
    ]
    if (wallet.indexOf(this.type) !== -1) {
      // 当前是墙壁,不可走也不可旋转
      this.moveable = false
      this.turnable = false
    } else if (cliff.indexOf(this.type) !== -1) {
      // 当前是悬崖 不可走 可转
      this.moveable = false
      this.turnable = true
    } else if (this.type === TILE_TYPE_ENUM.FLOOR) {
      // 当前是地板 可走 可转
      this.moveable = true
      this.turnable = true
    }
    const sprite = this.addComponent(Sprite)
    sprite.spriteFrame = spriteFrame
    const transform = this.getComponent(UITransform)
    transform.setContentSize(TILE_WIDTH, TILE_HEIGHT)
    this.node.setPosition(i * TILE_WIDTH, -j * TILE_HEIGHT)
  }
}

assets/Runtime/DataManager.ts

给数据中心单例加上位置信息,位置类型,包括是否可转向的信息

import Singleton from '../Base/Singleton'
import { ITile } from '../Levels'
import { TileManager } from '../Scripts/Tile/TileManager'
/**
 * 单例模式
 * 当前渲染的地图数据
 */
export default class DataManager extends Singleton {
  static get Instance() {
    return super.GetInstance<DataManager>()
  }
  mapInfo: Array<Array<ITile>> = [] // 地图数据
  tileInfo: Array<Array<TileManager>> //当前位置信息,当前位置类型,是否可走可转
  mapRowCount: number = 0 //行数
  mapColumnCount: number = 0 //列数
  levelIndex: number = 1 // 当前关卡
  reset() {
    this.mapInfo = []
    this.tileInfo = []
    this.mapColumnCount = 0
    this.mapRowCount = 0
  }
}
export const DataManagerInstance = new DataManager()

assets/Scripts/Tile/TileMapManager.ts

在渲染地图时,将瓦片信息存储到数据中心中的瓦片信息中,DataManager.Instance.tileInfo

@ccclass('TileMapManager')
export class TileMapManager extends Component {
  async init() {
    // 从数据中心取出
    const { mapInfo } = DataManager.Instance
    // 加载资源
    const spriteFrames = await ResourceManager.Instance.loadDir('texture/tile/tile')
    DataManager.Instance.tileInfo = []
    for (let i = 0; i < mapInfo.length; i++) {
      DataManager.Instance.tileInfo[i] = []
      for (let j = 0; j < mapInfo[i].length; j++) {
        const item = mapInfo[i][j]
        if (item.src === null || item.type === null) {
          continue
        }
        const node = createUINode()
        let srcNumber = item.src
        // 指定渲染随机瓦片,并且加条件,偶数的瓦片才随机
        if ((srcNumber === 1 || srcNumber === 5 || srcNumber === 9) && i % 2 === 0 && j % 2 === 0) {
          srcNumber += randomByRange(0, 4)
        }
        const spriteFrame = spriteFrames.find(v => v.name === `tile (${srcNumber})`) || spriteFrames[0]
        const tileManager = node.addComponent(TileManager)
        const type = item.type
        tileManager.init(type, spriteFrame, i, j)
        DataManager.Instance.tileInfo[i][j] = tileManager
        node.setParent(this.node)
      }
    }
  }
}

assets/Scripts/Player/PlayerManager.ts

在人物移动的地方加上碰撞检测,碰撞包括了人物碰撞,兵器碰撞,左右转向碰撞

并在碰撞的时候加上碰撞动画,碰撞动画与转向动画用法一样

// 碰撞检测
  willBlock(inputDirection: CONTROLLER_ENUM): boolean {
    /**
     * 移动:需要判断当前所处4个方向,且判断下一步四个移动方向
     * 转向:左右两边需要判断当前所处方向,且判断下一步转向方向
     */

    const { targetX, targetY, direction } = this
    const { tileInfo } = DataManager.Instance
    // 输入方向向上
    if (inputDirection === CONTROLLER_ENUM.TOP) {
      // 人物下一个位置
      let playerNextY = targetY
      const playerNextX = targetX
      // 枪的下一个位置
      let weaponNextY = targetY
      let weaponNextX = targetX

      // 预测下一个位置
      if (direction === DIRECTION_ENUM.TOP) {
        // 当前方向向上
        playerNextY = targetY - 1
        weaponNextY = targetY - 2
      } else if (direction === DIRECTION_ENUM.LEFT) {
        // 当前方向向左
        playerNextY = targetY - 1
        weaponNextY = targetY - 1
        weaponNextX = targetX - 1
      } else if (direction === DIRECTION_ENUM.RIGHT) {
        // 当前方向向右
        playerNextY = targetY - 1
        weaponNextY = targetY - 1
        weaponNextX = targetX + 1
      } else if (direction === DIRECTION_ENUM.BOTTOM) {
        // 当前方向向下
        playerNextY = targetY - 1
      }
      // 判断走出地图
      if (playerNextY < 0) {
        this.state = ENTITY_STATE_ENUM.BLOCK_FRONT
        return true
      }
      const playerTile = tileInfo[playerNextX][playerNextY]
      const weaponTile = tileInfo[weaponNextX][weaponNextY]
      // 人物不可以移动&不可以转向
      if (!(playerTile && playerTile.moveable && (!weaponTile || weaponTile.turnable))) {
        this.state = ENTITY_STATE_ENUM.BLOCK_FRONT
        return true
      }
    }
    if (inputDirection === CONTROLLER_ENUM.BOTTOM) {
      let playerNextY = targetY
      let playerNextX = targetX
      let weaponNextY = targetY
      let weaponNextX = targetX

      if (direction === DIRECTION_ENUM.TOP) {
        playerNextY = targetY + 1
      } else if (direction === DIRECTION_ENUM.LEFT) {
        playerNextY = targetY + 1
        weaponNextY = targetY + 1
        weaponNextX = targetX - 1
      } else if (direction === DIRECTION_ENUM.RIGHT) {
        playerNextX = targetX + 1
        weaponNextY = targetY + 1
        weaponNextX = targetX + 1
      } else if (direction === DIRECTION_ENUM.BOTTOM) {
        playerNextY = targetY + 1
        weaponNextY = targetY + 2
      }
      if (playerNextY > tileInfo.length) {
        this.state = ENTITY_STATE_ENUM.BLOCK_FRONT
        return true
      }
      const playerTile = tileInfo[playerNextX][playerNextY]
      const weaponTile = tileInfo[weaponNextX][weaponNextY]
      // 人物不可以移动&不可以转向
      if (!(playerTile && playerTile.moveable && (!weaponTile || weaponTile.turnable))) {
        this.state = ENTITY_STATE_ENUM.BLOCK_FRONT
        return true
      }
    }
    if (inputDirection === CONTROLLER_ENUM.LEFT) {
      const playerNextY = targetY
      let playerNextX = targetX
      let weaponNextY = targetY
      let weaponNextX = targetX

      if (direction === DIRECTION_ENUM.TOP) {
        weaponNextY = targetY - 1
        weaponNextX = targetX - 1
        playerNextX = targetX - 1
      } else if (direction === DIRECTION_ENUM.LEFT) {
        weaponNextX = targetX - 2
        playerNextX = targetX - 1
      } else if (direction === DIRECTION_ENUM.RIGHT) {
        playerNextX = targetX - 1
      } else if (direction === DIRECTION_ENUM.BOTTOM) {
        weaponNextY = targetY + 1
        weaponNextX = targetX - 1
        playerNextX = targetX - 1
      }
      if (playerNextX < 0) {
        this.state = ENTITY_STATE_ENUM.BLOCK_TURN_LEFT
        return true
      }
      const playerTile = tileInfo[playerNextX][playerNextY]
      const weaponTile = tileInfo[weaponNextX][weaponNextY]
      if (!(playerTile && playerTile.moveable && (!weaponTile || weaponTile.turnable))) {
        this.state = ENTITY_STATE_ENUM.BLOCK_TURN_LEFT
        return true
      }
    }
    if (inputDirection === CONTROLLER_ENUM.RIGHT) {
      let playerNextY = targetY
      let playerNextX = targetX
      let weaponNextY = targetY
      let weaponNextX = targetX

      if (direction === DIRECTION_ENUM.TOP) {
        weaponNextY = targetY - 1
        weaponNextX = targetX + 1
        playerNextY = targetY - 1
      } else if (direction === DIRECTION_ENUM.LEFT) {
        weaponNextX = targetX + 1
      } else if (direction === DIRECTION_ENUM.RIGHT) {
        weaponNextX = targetX + 2
        playerNextX = targetX + 1
      } else if (direction === DIRECTION_ENUM.BOTTOM) {
        playerNextX = targetX + 1
        weaponNextX = targetY + 1
        weaponNextY = targetY + 1
      }
      if (playerNextX > tileInfo[0].length) {
        this.state = ENTITY_STATE_ENUM.BLOCK_TURN_LEFT
        return true
      }
      const playerTile = tileInfo[playerNextX][playerNextY]
      const weaponTile = tileInfo[weaponNextX][weaponNextY]
      if (!(playerTile && playerTile.moveable && (!weaponTile || weaponTile.turnable))) {
        this.state = ENTITY_STATE_ENUM.BLOCK_TURN_LEFT
        return true
      }
    }
    if (inputDirection === CONTROLLER_ENUM.TURN_LEFT) {
      // 方向左转,判断方向对角位置和方向位置是否可转向
      // 对角
      let nextX1 = targetX
      let nextY1 = targetY
      // 侧边
      let nextX2 = targetX
      let nextY2 = targetY
      if (direction === DIRECTION_ENUM.TOP) {
        // 如果当前角色面朝上,需要获取左边和左上角两块位置
        nextX1 = targetX - 1
        nextY1 = targetY - 1
        nextX2 = targetX - 1
      } else if (direction === DIRECTION_ENUM.BOTTOM) {
        nextX1 = targetX + 1
        nextY1 = targetY + 1
        nextX2 = targetX + 1
      } else if (direction === DIRECTION_ENUM.LEFT) {
        nextX1 = targetX - 1
        nextY1 = targetY + 1
        nextY2 = targetY + 1
      } else if (direction === DIRECTION_ENUM.RIGHT) {
        nextX1 = targetX + 1
        nextY1 = targetY - 1
        nextY2 = targetY - 1
      }
      // 没有瓦片或者可以转弯
      if (
        (!tileInfo[nextX1][nextY1] || tileInfo[nextX1][nextY1].turnable) &&
        (!tileInfo[nextX2][nextY2] || tileInfo[nextX2][nextY2].turnable)
      ) {
        //
      } else {
        this.state = ENTITY_STATE_ENUM.BLOCK_FRONT
        return true
      }
    }
    if (inputDirection === CONTROLLER_ENUM.TURN_RIGHT) {
      // 方向右转,判断方向对角位置和方向位置是否可转向
      // 对角
      let nextX1 = targetX
      let nextY1 = targetY
      // 侧边
      let nextX2 = targetX
      let nextY2 = targetY
      if (direction === DIRECTION_ENUM.TOP) {
        // 如果当前角色面朝上,需要获取左边和左上角两块位置
        nextX1 = targetX + 1
        nextY1 = targetY - 1
        nextX2 = targetX + 1
      } else if (direction === DIRECTION_ENUM.BOTTOM) {
        nextX1 = targetX - 1
        nextY1 = targetY + 1
        nextX2 = targetX - 1
      } else if (direction === DIRECTION_ENUM.LEFT) {
        nextY1 = targetY - 1
        nextX1 = targetX - 1
        nextY2 = targetY - 1
      } else if (direction === DIRECTION_ENUM.RIGHT) {
        nextX1 = targetX + 1
        nextY1 = targetY + 1
        nextY2 = targetY + 1
      }
      // 没有瓦片或者可以转弯
      if (
        (!tileInfo[nextX1][nextY1] || tileInfo[nextX1][nextY1].turnable) &&
        (!tileInfo[nextX2][nextY2] || tileInfo[nextX2][nextY2].turnable)
      ) {
        //
      } else {
        this.state = ENTITY_STATE_ENUM.BLOCK_FRONT
        return true
      }
    }
    return false
  }

assets/Scripts/Player/PlayerManager.ts

移动的地方判断碰撞

inputHandler(inputDirection: CONTROLLER_ENUM) {
    if (this.willBlock(inputDirection)) {
      return
    }
    this.move(inputDirection)
  }

碰撞检测总结

1、将瓦片信息在初始化时存储每一块瓦片是否可转向,可移动

2、在人物移动的地方判断移动方向和当前朝向,再通过判断下一动作的瓦片在瓦片信息中是否可以移动,并在判断的时候播放碰撞动画

实现NPC渲染

assets/Scripts/WoodenSkeleton/WoodenSkeletonStateMachine.ts

添加一个状态机,与player几乎一样,将移动动画删除即可

@ccclass('WoodenSkeletonStateMachine')
export class WoodenSkeletonStateMachine 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())
  }

  // 初始化状态机
  initStateMachine() {
    // NPC动画,无限播放
    this.stateMachines.set(PARAMS_NAME_ENUM.IDLE, new IdleSubStateMachineWooden(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):
        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/WoodenSkeleton/IdleSubStateMachineWooden.ts

添加NPC资源动画,所有IdleSubStateMachine 都类似,只是加载了不同的路径

const BASE_URL = 'texture/woodenskeleton/idle'
@ccclass('IdleSubStateMachineWooden')
export class IdleSubStateMachineWooden extends DirectionSubStateMachine {
  constructor(fsm: StateMachine) {
    super(fsm)
    // NPC动画,无限播放
    this.stateMachines.set(DIRECTION_ENUM.TOP, new State(fsm, `${BASE_URL}/top`, AnimationClip.WrapMode.Loop))
    this.stateMachines.set(DIRECTION_ENUM.BOTTOM, new State(fsm, `${BASE_URL}/bottom`, AnimationClip.WrapMode.Loop))
    this.stateMachines.set(DIRECTION_ENUM.LEFT, new State(fsm, `${BASE_URL}/left`, AnimationClip.WrapMode.Loop))
    this.stateMachines.set(DIRECTION_ENUM.RIGHT, new State(fsm, `${BASE_URL}/right`, AnimationClip.WrapMode.Loop))
  }
}

assets/Scripts/WoodenSkeleton/WoodenSkeletonManager.ts

加载NPC入口,与player也是基本一样的、NPC目前不能移动,将移动与碰撞检测干掉就可以了

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

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

assets/Scripts/Scene/BattleManager.ts

将NPC添加到地图上

start() {
	this.generateEnemies()
}

// 创建NPC
generateEnemies() {
  const woodenSkeleton = createUINode()
  woodenSkeleton.setParent(this.stage)
  const woodenSkeletonManager = woodenSkeleton.addComponent(WoodenSkeletonStateManager)
  woodenSkeletonManager.init()
}

https://web03-1252477692.cos.ap-guangzhou.myqcloud.com/blog/images/QQ%E6%88%AA%E5%9B%BE20220720152805.png

实现NPC朝向人物

https://web03-1252477692.cos.ap-guangzhou.myqcloud.com/blog/images/QQ%E6%88%AA%E5%9B%BE20220721113929.png

assets/Runtime/DataManager.ts

在数据中心加上人物以及NPC的信息

/**
 * 单例模式
 * 当前渲染的地图数据
 */
export default class DataManager extends Singleton {
  static get Instance() {
    return super.GetInstance<DataManager>()
  }
  mapInfo: Array<Array<ITile>> = [] // 地图数据
  tileInfo: Array<Array<TileManager>> //当前位置信息,当前位置类型,是否可走可转
  mapRowCount: number = 0 //行数
  mapColumnCount: number = 0 //列数
  levelIndex: number = 1 // 当前关卡
  player: PlayerManager // 当前人物信息
  enemies: WoodenSkeletonStateManager[] // NPC信息
  reset() {
    this.mapInfo = []
    this.tileInfo = []
    this.mapColumnCount = 0
    this.mapRowCount = 0
    this.player = null
    this.enemies = []
  }
}
export const DataManagerInstance = new DataManager()

assets/Scripts/WoodenSkeleton/WoodenSkeletonManager.ts

NPC朝向逻辑

取人物位置与NPC位置,以NPC为原点判断人物所在象限,根据靠近+-XY轴判断NPC朝向不同的角度

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

    super.init({
      x: 7,
      y: 7,
      type: ENTITY_TYPE_ENUM.PLAYER,
      direction: DIRECTION_ENUM.TOP, // 设置初始方向
      state: ENTITY_STATE_ENUM.IDLE, // 设置fsm化动画
    })
    // 角色创建完成 或者 角色移动 触发更新NPC方向
    EventManager.Instance.on(EVENT_ENUM.PLAYER_MOVE_END, this.onChangeDirection, this)
    EventManager.Instance.on(EVENT_ENUM.PLAYER_BORN, this.onChangeDirection, this)
  }
  onChangeDirection(isInit?: boolean) {
    if (!DataManager.Instance.player) {
      return
    }
    const { x: playerX, y: playerY } = DataManager.Instance.player
    const disX = Math.abs(this.x - playerX)
    const disY = Math.abs(this.y - playerY)
    // 如果disY = disX,表示在某个象限夹角处,如果 disY > disX 靠近Y轴,如果 disY < disX 则靠近X轴

    // 当人物在移动为对角线的时候,NPC不做转向操作
    if (disX === disY && !isInit) {
      return
    }

    if (playerX >= this.x && playerY <= this.y) {
      // 在第一象限
      if (disY > disX) {
        // 在第一象限0~45°夹角中 靠近Y轴,朝上
        this.direction = DIRECTION_ENUM.TOP
      } else {
        // 在第一象限45~90°夹角中 靠近X轴,朝右
        this.direction = DIRECTION_ENUM.RIGHT
      }
    } else if (playerX <= this.x && playerY <= this.y) {
      // 在第二象限
      if (disY > disX) {
        // 第二象限靠近Y轴,向上
        this.direction = DIRECTION_ENUM.TOP
      } else {
        // 第二象限靠近X轴,向左
        this.direction = DIRECTION_ENUM.LEFT
      }
    } else if (playerX <= this.x && playerY >= this.y) {
      // 在第三象限
      this.direction = disY > disX ? DIRECTION_ENUM.BOTTOM : DIRECTION_ENUM.LEFT
    } else if (playerX >= this.x && playerY >= this.y) {
      // 在第四象限
      this.direction = disY > disX ? DIRECTION_ENUM.BOTTOM : DIRECTION_ENUM.RIGHT
    }
  }
}

assets/Scripts/Scene/BattleManager.ts

在人物初始化时触发PLAYER_BORN事件,

// 创建人物
  async generatePlayer() {
    const player = createUINode()
    player.setParent(this.stage)
    const playerManager = player.addComponent(PlayerManager)
    await playerManager.init()
    DataManager.Instance.player = playerManager
    EventManager.Instance.emit(EVENT_ENUM.PLAYER_BORN, true)
  }
  // 创建NPC
  async generateEnemies() {
    const woodenSkeleton = createUINode()
    woodenSkeleton.setParent(this.stage)
    const woodenSkeletonManager = woodenSkeleton.addComponent(WoodenSkeletonStateManager)
    await woodenSkeletonManager.init()
    DataManager.Instance.enemies.push(woodenSkeletonManager)
  }

assets/Scripts/Player/PlayerManager.ts

人物移动的时候触发事件

move(){
	//...
	EventManager.Instance.emit(EVENT_ENUM.PLAYER_MOVE_END)
	//...
}

https://web03-1252477692.cos.ap-guangzhou.myqcloud.com/blog/images/QQ%E6%88%AA%E5%9B%BE20220721103805.png

https://web03-1252477692.cos.ap-guangzhou.myqcloud.com/blog/images/QQ%E6%88%AA%E5%9B%BE20220721103707.png 本节源码地址:

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

Cocos
  • 作者:零三(联系作者)
  • 最后更新时间:2022-07-21 13:40
  • 版权声明:自由转载-非商用-非衍生-保持署名
  • 转载声明:来源地址 https://web03.cn