碰撞检测
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()
}
实现NPC朝向人物
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)
//...
}
本节源码地址: