Unity lua行爲樹實現(可實現rpg掛機自動戰鬥)

在使用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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章