在使用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)
再啓動遊戲就能看到行爲樹的打印了Log了
最基本的用法就這樣完成了!
那,實現代碼呢?
關於行爲樹的實現,從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