在使用Unity开发手游项目中,用Lua作为热更脚本时,也许有的RPG项目会有连战斗也要求热更,对于角色挂机自动战斗,Unity有行为树插件Behavior Designer可以实现,但不能实现战斗逻辑热更,所以我用Lua对着Behavior Designer重新实现了部分基础功能,这样,使用Lua版的行为树实现挂机自动战斗,就可以热更啦!
前提说明:
1,本文假设读者对树插件Behavior Designer有些了解,因为我是对着它思路来实现的,不了解可以去看一下,这里可能不打算介绍行为树知识。
2,我使用的时ulua的LuaFramework_UGUI来实现的,如果你使用xLua也不影响移植。
3,这当然只是实现比较简单的基础功能,不能像Behavior Designer那样有丰富的配置,但也可以继续拓展呀,如遍历行为树时间间隔为每帧,不服可以改成0.02s的配置。
行为树启动后,每帧tick一次,检测行为树的Task(行为树的每个节点都是一个Task)。
然后基础Task大致可以分为几大类Composites、Decorator、Action等
然后得出代码结构:
文件名和类(表)名尽量跟Behavior Designer一样。
关于Lua行为树实现基础代码都在 “LuaFramework\Lua\BehaviorTree” 文件夹下
大体代码结构如下:
系不系有点相似。
看实现之前,不如先到过来看,完成了怎么使用,再去了解它的实现。
使用方法,如,我们要完成Behavior Designer中这样的一颗行为树
行为树框架之外需要新建3个lua文件,2个自定义节点xxx.lua文件和一个拼接操作Test.lua文件,最后在游戏入口处Game.lua调用,3个文件即
:
TestConditional.lua:
TestConditionalTask = BehTree.IConditional:New()
local this = TestConditionalTask
this.name = 'TestConditionalTask'
testt = {}
idnex = 1
--
function this:OnUpdate()
log('----------TestConditionalTask---------Running')
log(self:ToString())
--模拟Behavior Designer IsNullOrEmpty节点
--IsNullOrEmpty == false
return BehTree.TaskStatus.Failure
end
ActionLogTask.lua
ActionLogTask = BehTree.IAction:New()
local this = ActionLogTask
this.name = 'ActionLogTask'
-- 模拟Behavior Designer Log节点
function this:OnUpdate()
log('-----------ActionLogTask Success')
return BehTree.TaskStatus.Success
end
Test.lua
require 'BehaviorTree/Test/TestConditionalTask'
require 'BehaviorTree/Test/ActionLogTask'
--[[
代码拼接行为树有代码结构顺序要求,
代码顺序也遵从行为树的图示,上到下,从左到右拼接
上层或者本节点的前一个节点完成才能进行下一个
]]
local function BuildTree()
local root = BehTree.TaskRoot:New()
--这里直接使用Repeater作为入口并且检测,相当于Entry
local entry = BehTree.Repeater:New()
entry.name = '第0个复合节点repeat == Entry '
--根节点添加layer:1
root:PushTask(entry)
--------layer:2
local selector1 = BehTree.Selector:New()
selector1.name = '第1个复合节点selector == Selector '
entry:AddChild(selector1)
-----layer3
local sequence2 = BehTree.Sequence:New()
sequence2.name = '第2个复合节点sequence == Sequence'
selector1:AddChild(sequence2)
--layer:4,并行
local testConditionalTask = TestConditionalTask:New()
testConditionalTask.name = '并行第3个叶子节点 == Is Null Or Empty'
local actionLogTask = ActionLogTask:New()
actionLogTask.name = '并行第3个叶子节点 == Log'
--添加
sequence2:AddChild(testConditionalTask)--child:1
sequence2:AddChild(actionLogTask)--child:2
return root
end
return BuildTree()
最后启动游戏时调用,在Game.lua中加入这3行代码,初始化和启动行为树
require 'BehaviorTree/BehaviorTreeManager'
local tree = require 'BehaviorTree/Test/Test'
BehTree.BehaviorTreeManager.RunTree(tree)
最基本的用法就这样完成了!
那,实现代码呢?
关于行为树的实现,从BehaviorTreeManager.lua看起,看到Gmae.lua中启动的方法BehTree.BehaviorTreeManager.RunTree(tree)
BehaviorTreeManager.lua
BehTree={}
require 'BehaviorTree/Base/Enum'
require 'BehaviorTree/Base/StackList'
require 'BehaviorTree/Base/TaskRoot'
require 'BehaviorTree/Base/ITask'
require 'BehaviorTree/Base/IParent'
require 'BehaviorTree/Base/IAction'
require 'BehaviorTree/Base/IComposite'
require 'BehaviorTree/Base/IConditional'
require 'BehaviorTree/Base/IDecorator'
--复合节点()
require 'BehaviorTree/Composite/Selector'
require 'BehaviorTree/Composite/Sequence'
--修饰节点
require 'BehaviorTree/Decorator/Repeater'
require 'BehaviorTree/Decorator/ReturnFailure'
require 'BehaviorTree/Decorator/ReturnSuccess'
require 'BehaviorTree/Decorator/UntilFailure'
require 'BehaviorTree/Decorator/Inverter'
--Action节点
require 'BehaviorTree/Action/Wait'
BehTree.BehaviorTreeManager={}
local this = BehTree.BehaviorTreeManager
function this.Init()
end
--从这里开始启动一颗行为树的入口跟节点
function this.RunTree(enter)
this.bhTree =enter
coroutine.start(this.OnUpdate)
end
--重置树下所有Action
function this.ResetTreeActions()
local treeRoot = this.GetCurTreeRoot()
treeRoot:ResetAllActionsState()
end
function this.OnUpdate()
while true do
coroutine.step()
this.UpdateTask()
end
end
function this.UpdateTask()
local status = this.bhTree:OnUpdate()
if status ~= BehTree.TaskStatus.Running then
table.remove(this.curTrees, key)
end
end
总的核心思想就这样,不停的每帧去遍历自己拼装好的行为树节点,剩下的也就是节点之间的层级等关系的实现。
回到最初说的,每个节点都是一个Task,所以上面看到的Selector.lua、Sequence.lua、IComposite.lua等都是ITask.lua的子类,如此思路,举例Sequence.lua:基类->IComposite.lua:基类->IParent.lua:基类->ITask.lua
BehTree.Sequence = BehTree.IComposite:New()
local this = BehTree.Sequence
--初始默认未激活
this.curReturnStatus = BehTree.TaskStatus.Inactive
this.name = 'Sequence'
function this:OnUpdate()
if self:HasChildren() == false then
logError(self.name..'父节点类型没有子节点!!')
return BehTree.TaskStatus.Failure
end
if self.curRunTask == nil then
--选择(or)节点肯定是去找子节点
self.curRunTask = self:GetNextChild()
--如下不该发生
if self.curRunTask == nil then
--如果没有子节点
logError('错误的节点配置!:没有子节点或已越界!!'..self.name..'子节点长度:'..self:GetChildCount()..' 尝试访问:'..self:GetCurChildIndex()+1)
return BehTree.TaskStatus.Failure
end
end
return self:RunChildByAnd()
end
--and:遇到一个false就中断执行
--序列组合节点:AND逻辑,所有子节点Success才返回Success
function this:RunChildByAnd()
while self.curRunTask ~= nil do
self.curReturnStatus = self.curRunTask:OnUpdate()
self.curRunTask:ResetTaskStatus()
--找到false或者running直接返回,就中断执行,这一帧到此结束
if self.curReturnStatus == BehTree.TaskStatus.Failure then
--返回Failure说明这次Sequence走完了,重置等下一轮
self:Reset()
return BehTree.TaskStatus.Failure
elseif self.curReturnStatus == BehTree.TaskStatus.Running then
return BehTree.TaskStatus.Running
else
--没找到false就一直执行下去
self.curRunTask = self:GetNextChild()
end
end
--找完了所有节点没有false,那么success
--说明这次Sequence走完了,重置等下一轮
self.curReturnStatus = BehTree.TaskStatus.Success
self:Reset()
return BehTree.TaskStatus.Success
end
--重置
function this:Reset()
self:ResetChildren()
end
IComposite.lua
--[[
常用于Sequence的第一个节点判断
]]
BehTree.IComposite = BehTree.IParent:New()
local this = BehTree.IComposite
this.taskType = BehTree.TaskType.Composite
IParent.lua
--[[
父任务 Parent Tasks
behavior tree 行为树中的父任务 task
包括:composite(复合),decorator(修饰符)!
虽然 Monobehaviour 没有类似的 API,但是并不难去理解这些功能:
]]
BehTree.IParent = BehTree.ITask:New({})
local this = BehTree.IParent
--此时this把ITask设为元表的表
--提供共有函数
function this:New(o)
o = o or {}
o.curChilIndex = 0
o.curRunTask = nil
o.childTasks={}
--o把BehTree.IParentTask设为元表,
--而BehTree.IParentTask把ITask设为元表
--从而保持类的属性独立,不共用
setmetatable(o, self)
self.__index = self
return o
end
--重置当前访问的子节点位置为第一个
function this:ResetChildren()
self.curRunTask = nil
self.curChilIndex = 0
end
function this:GetCurChildIndex()
return self.curChilIndex
end
--对于ReaterTask等只能有一个子节点的
function this:GetOnlyOneChild()
if self:GetChildCount() ~= 1 then
logError('---------'..self.name..'应该有且只有一个子节点!but:childCount:'..self:GetChildCount())
return nil
end
return self.childTasks[1]
end
--添加子节点有顺序要求
function this:AddChild(task)
log('------------------'..self.name..' 添加子节点 : '..task.name)
if task == nil then
logError('---------------------add task is nil !!')
return
end
local index = #self.childTasks+1
task.index = index
task.layer = self.layer + 1
task.parent = self
task.root = self.root
self.childTasks[index] = task
self.root:AddGlobalTask(task.tag, task)
return self
end
function this:ClearChildTasks()
self.curIndex = 0
self.childTasks = nil
self.childTasks = {}
end
function this:HasChildren()
if #self.childTasks <= 0 then
return false
else
return true
end
end
function this:GetChildCount()
return #self.childTasks
end
function this:GetNextChild()
if #self.childTasks >= (self.curChilIndex+1) then
--指向當前正執行的
self.curChilIndex = self.curChilIndex + 1
local nextChild = self.childTasks[self.curChilIndex]
return nextChild
else
return nil
end
end
--获取前一个子节点,不移动指针
function this:GetCurPrivousTask()
if self.curChilIndex <=1 then
logError(self.name..' GetCurPrivousTask : 已经是最前的Task或childtask为空')
return nil
else
return self.childTasks[self.curChilIndex-1]
end
end
--获取下一个子节点,不移动指针
function this:GetCurNextTask()
if self.curChilIndex >= #self.childTasks then
--logError(self.name..' GetCurNextTask : 已经是最后的Task或childtask为空')
return nil
else
return self.childTasks[self.curChilIndex+1]
end
end
ITask.lua
--[[
所有task基础
]]
BehTree.ITask={
--不需要主动设置参数
--由树结构的机制驱动的参数,
taskStatus = BehTree.TaskStatus.Running,
curReturnStatus = BehTree.TaskStatus.Inactive,
taskType = BehTree.TaskType.UnKnow,
root = nil,
index = 1,
parent = nil,
layer = 1,
--主动设置参数
name = '暂未设置名称',
tag = 'UnTag',--用于搜索
desc = '暂无描述'
}
local this = BehTree.ITask
function this:New(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
function this:ResetTaskStatus()
end
--获取同一层layer的上一个节点
function this:GetPriviousTask()
if self.parent == nil then
logError(self.name..' 找不到父节点 try call GetPriviousTask')
return nil
end
if self.layer <= 1 then
logError(self.name..' GetPriviousTask已经是最顶层,单独Task')
return nil
end
local priviousTask = self.parent:GetCurPrivousTask()
return priviousTask
end
--获取同一层layer下一个task
function this:GetNextTask()
if self.parent == nil then
logError(self.name..' 找不到父节点 try call GetNextTask')
return nil
end
if self.layer <= 1 then
logError(self.name..' GetNextTask已经是最顶层,单独Task')
return nil
end
local nextTask = self.parent:GetCurNextTask()
return nextTask
end
function this:ToString()
local name = '名称 : '..self.name..'\n'
local layer = '所处层次 :'..self.layer..'\n'
local parent = '父节点 : '..self.parent.name..'\n'
local index = '作为子节点顺序 : '..self.index..'\n'
local desc = '描述 : '..self.desc..'\n'
local status = 'UnKnow'
if self.curReturnStatus == 1 then
status = 'Inactive'
elseif self.curReturnStatus == 2 then
status = 'Failure'
elseif self.curReturnStatus == 3 then
status = 'Success'
elseif self.curReturnStatus == 4 then
status = 'Running'
end
local curReturnStatus = '运行返回结果:'..status..'\n'
return name..desc..layer..parent..index..curReturnStatus
end
关于Sequence部分差不多这样,其他代码略多我就不贴完了,我传上去,可以下载来看看,
但也只有LuaFramework中的LuaFramework\Lua\BehaviorTree部分代码,而不是整个ulua工程,
记住:调用时记得在Game.lua等游戏启动入口写上这3行来启动行为树。
require 'BehaviorTree/BehaviorTreeManager'
local tree = require 'BehaviorTree/Test/Test'
BehTree.BehaviorTreeManager.RunTree(tree)
下载地址:
https://github.com/HengyuanLee/LuaBehaviorTree