- 寻路
-- 角色移动
--[[
一般过程:1)lua层发起寻路;2)c++层利用AStar计算路线,run每帧执行一步
]]
--------------- lua ----------------
function moveToDes(targetx,targety)
local result = self.m_char:requestPath(targetx,targety) -- 在C++层寻路
return result
end
--------------- lua ----------------
--------------- c++层 ----------------
function cChar:requestPath(targetx,targety,sync) -- 请求路径 结果存于AStar
if isChar() then -- 先判断是否为主角
local targetPos = cc.p(targetx,targety)
AStar.break() -- 重新寻路前设置为停止状态
AStar.FreePath() -- 清空之前缓存的路径
local srcPos = self.m_char:getCurPosition() -- 起点
if sync then -- 不使用线程寻路 适用于短距离反复点击屏幕时使用
AStar.findPath(srcPos,targetPos) -- 手动执行路径搜索
else -- 使用线程寻路
AStar.isOpenSyncfind = true -- AStar线程开始执行路径搜索
end
return true
end
return false
end
function cChar:run() -- 角色移动时调用 每帧执行
self:walkPath()
-- 其他移动时需要做的操作
end
function cChar:walkPath() -- 角色执行路径
if AStar.isOpenSyncfind then -- 还在搜索路径
return
end
if self.m_char:isRun() then
return
end
local prePos = self:getCurPosition() -- 当前位置
local preAStarNodePos = AStar.getPathPreNode() -- 寻路所记录的前一个位置
if prePos != preAStarNodePos then -- 检校位置发生是否发生瞬移,如发生中途服务器强制拉人 重新寻路
-- 现在g_AStar path中找看看有没有prePos 有的话直接使用
while AStar.isHasPath() do -- 如果当前有寻路缓存结果,先从已有结果中查找
local curNodePos = AStar.NodeGetPos()
AStar.PathNextNode()
if curNodePos == prePos then -- 找到 后面直接使用
break
end
end
if not AStar.isHasPath() then -- 缓存路径中已经没有节点数据 说明上面的while内未找到匹配的位置节点
local nodeDestPos = AStar.realDstPos() -- 目标位置
if math.abs(nodeDestPos - prePos) <= 0 then -- 正好被拉到目标点 如传送类型的道具
self:onPathEnd() -- 寻路结束
else
self:onPathCancel()
requestPath(nodeDestPos.x,nodeDestPos.y) -- 重新寻路
end
end
end
local destPos = AStar.PathNextNode()
self:renderMoveTo(destPos) -- 渲染层移动主角
self:setSendMoveCmd(destPos) -- 同步给服务器
end
--------------- c++ ----------------
- 角色动作状态
------状态类------
ActionState = class("ActionState") -- 角色动作状态
function ActionState:ctor(obj) -- 做一些初始化工作
self.m_Obj = obj -- 状态对象
end
function ActionState:enter() -- 进入状态
-- body
end
function ActionState:begin() -- 开始执行
-- body
end
function ActionState:end() -- 结束执行
-- body
end
function ActionState:exit() -- 退出状态
-- body
end
AttackState = class("AttackState")
function AttackState:ctor(obj)
ActionState.ctor(self,obj)
end
function AttackState:enter( targetObj ) -- 状态切换时触发
lookAt(targetObj) -- 调整方向
self.m_Obj:playAni(Enum.ActionState.AttackState) -- 播放动作
-- 其他逻辑
end
function AttackState:begin() -- 在播放开始动作时调用
doAttack()
end
function AttackState:end() -- 在播放结束动作时调用
stopAttack()
local nextState = Enum.ActionState.Rest
self.m_Obj:setActionState(nextState) -- 退出时切换至下一个状态
end
function AttackState:exit() -- 状态切换时触发
end
------状态使用------
function NPC:ctor()
self.m_stateLst = {} -- 生成NPC时 创建所有状态
self.m_stateLst[Enum.ActionState.Attack] = AttackState.new(self)
self.m_curState = Enum.ActionState.NULL
end
function NPC:setActionState(newState)
if newState == self.m_curState then
return
end
-- 退出当前状态
if self.m_curState ~= Enum.ActionState.NULL and self.m_stateLst[self.m_curState] then
self.m_stateLst[self.m_curState]:exit() -- 退出当前状态
end
self.m_curState = newState
-- 进入新状态
if self.m_curState ~= Enum.ActionState.NULL and self.m_stateLst[self.m_curState] then
self.m_stateLst[self.m_curState]:enter() -- 进入新状态
end
end
function NPC:playAni(aniType)
-- 执行一个序列帧动作 char里包含一个每帧都调用的run函数
-- 函数体内判断当前动作是否为开始或结束帧,并相应触发begin或end
self.char:doAction(aniType)
end
- 跨区跨地图 自动寻路
autoWalk = function (regionid, mapid, posx, posy, endCallback) -- 寻路至某区某张地图的某位置
local curRegionId = getCurRegionId()
targetPos = cc.p(posx,posy)
if regionid ~= curRegionId then -- 不在同一区
if curMapId == transMapId then -- 跨区传送位置正好位于当前地图中
eventQueue.push(moveToTransPointEvent) -- 移动至传送位置(所有事件均存至队列中 每帧处理)
eventQueue.push(transEvent) -- 移至传送位置后发起传送
else -- 传送位置不位于当前地图 首先移至当前地图的跨场景位置
eventQueue.push(moveToCurMapTransPointEvent) -- 移至跨场景位置
eventQueue.push(reqTransEvent) -- 到了相关位置 向服务器发起转移请求
end
-- 经过上面的过程 角色总会发生一次场景转移 但是还未到达目的地 所以要递归执行autoWalk
autoWalk(regionid,mapid,posx,posy,endCallbackEvent)
else -- 目的场景与当前场景在同一个区
if curMapId == mapid then
-- 待寻路地图正是本地图
eventQueue.push(moveToTargetPointEvent) -- 直接移至目的地
eventQueue.push(endCallbackEvent) -- 寻路结束 执行一个回调函数
else
-- 同一个国家不同地图
eventQueue.push(moveToCurMapTransPointEvent) -- 移至跨场景位置
eventQueue.push(reqTransEvent) -- 到了相关位置 向服务器发起转移请求
autoWalk(regionid,mapid,posx,posy,endCallbackEvent)
end
end
end
总结来说,当区不同时首先移至当前地图跨场景位置,如果该位置是一个跨区传送点,则执行一个跨区操作(请求服务器完成);如果是普通跨场景点,则执行一个跨场景操作。这两种情形执行完一定还未到达目标位置,所以都需要执行寻路的递归操作。当在同一区时,如果也在同一地图,则直接寻路至目标位置,并执行一个最终寻路结束的回调,如果地图不同,则进行跨场景并递归寻路。理论上,所有递归的最后一步都会执行至第三种情形。
- 游戏引导
实现引导,需要解决几个问题:如何设计引导数据结构;如果检测并触发引导;引导后如何响应操作。一般引导与任务配搭,任务有开始、执行、结束三个过程,同时任务有不同的子步骤组成,因此引导数据结构如下:
self.guideStruct = {
[taskid] = { -- 以任务id作为key
isStart = false,
isFinish = false,
childTask = { -- 子任务
[id] = {state}, -- 记录每个子任务的状态等信息
}
}
}
检测并触发引导的过程,通常放在任务状态下发的相关消息协议中,我们需要一个检测函数检测当前任务是否需要出发引导,有时除了需要判断上面提及的引导是否开启结束的一般状态外,还需要执行其他特殊判断逻辑,此时,为了保证引导系统的统一性,可以将所有判断逻辑conditionFunc放于配置当中。
当开始引导后,从配置中读取当前任务及其状态需要引导的相关界面及按钮元素,当然,实际的配置将会更加复杂,可能包含具体的引导表现方式,比如是上下提示引导还是左右提示引导,矩形引导还是椭圆形引导等。读到所有需要的引导信息后,将其传送至引导panel以显示引导效果。
function checkGuide(taskid,otherData)
-- 每个任务都配备了是否需要执行引导的判断函数
local needCheck = (not guideStruct[taskid].isStart) or (guideStruct[taskid].isStart and not guideStruct[taskid].isFinish)
if not needCheck then return end -- 如果任务已经结束 则不再引导
local conditionFunc = GuideConf[taskid].conditionFunc -- 每个引导都定义了各自的判断函数
needCheck = conditionFunc and conditionFunc(otherData)
if not needCheck then return end -- 如果任务不满足既定条件 则不再引导
-- 满足引导条件 开启引导
local stateid = GuideConf[taskid].stateidLst[1] -- 取第一个stateid
startGuide(taskid,stateid,otherData) -- 开始引导
saveGuide(taskid,stateid) -- 将引导进度保存至本地
end
function startGuide(taskid,stateid,otherData)
local ui,spEle = getGuideUI(taskid,stateid) -- 获取当前需要引导的界面元素
createGuidePanel(ui,spEle) -- 在需要引导的对象上构建引导界面(层级最高 使用浙江将touch的有效区域设置为spEle区域)
end
上面完成了引导的触发,那么引导的响应如何处理。最粗暴的实现方式是对于每个可能响应引导的界面元素,都进行引导处理检查。如某英雄角色的升级按钮在升级时会被引导,那么就在按钮的触控世界上进行引导响应检查。这种做法会使得引导处理的逻辑结构过于分散。另一种做法可能更为简单且更统一,即直接重写Button的触控事件,然后在事件内部判断当前的引导过程是否是针对该button。如果是的话,则标记该引导为已完成状态并关闭引导界面。