**
本篇旨在讲解如何在游戏项目中配合Lua代码正常使用NodeCanvans行为树框架
**
tips:本篇篇幅过长,主要是讲解NodeCanvas主要代码及接口
首先介绍几个主要的Lua脚本
一、BTConfig.lua
这个脚本主要用于存放全局的节点,包括NodeCanvas自带的组合节点、连接节点、动作节点以及自定义节点,还用于存放全局的节点状态枚举。
所有自定义的节点都要在这里申明。
local PathToClassMap = {
--组合节点
["NodeCanvas.BehaviourTrees.Selector"] = require "Game/LuaBT/BT/Core/Nodes/Composites/Selector",
["NodeCanvas.BehaviourTrees.Sequencer"] = require "Game/LuaBT/BT/Core/Nodes/Composites/Sequencer",
["NodeCanvas.BehaviourTrees.Parallel"] = require "Game/LuaBT/BT/Core/Nodes/Composites/Parallel",
--连接节点
["NodeCanvas.BehaviourTrees.BTConnection"] = require "Game/LuaBT/BT/Core/BTConnection",
--动作节点
["NodeCanvas.BehaviourTrees.ActionNode"] = require "Game/LuaBT/BT/Core/Nodes/Leafs/ActionNode",
["NodeCanvas.BehaviourTrees.ConditionNode"] = require "Game/LuaBT/BT/Core/Nodes/Leafs/ConditionNode",
["NodeCanvas.Framework.ActionList"] = require "Game/LuaBT/BT/Core/Tasks/ActionList",
--自定义节点
["Scenario.Wait"] = require "Game/LuaBT/Custom/Actions/Wait",--延时
}
function BTGetClass(path)
local realClass = PathToClassMap[path]
if not realClass then
print(string.format("%s对应的节点不存在", path))
end
return realClass
end
BTStatus = {
Failure = 0,
Success = 1,
Running = 2,
Resting = 3,
Error = 4,
Optional = 5
}
二、BehaviourTree.lua
这个脚本是NodeCanvas在Lua应用的主干脚本,用于管理整个行为树的行为逻辑,包括每个节点的执行调用、挂起、停止等等,还有一些必须的接口如加载行为树,加载子行为树等操作。
local BehaviourTree = class("BehaviourTree")
function BehaviourTree:ctor()
self.id = 0
self.version = false
self.type = false
self.name = "BehaviourTree"
self.primeNode = false
self.nodes = {}
self.nodesIndex = {}
self.subTrees = {}
self.debugList = {}
self.rootStatus = BTStatus.Resting
self.agent = {id = 1001}
self.agent.isBTDebug = false
self.blackboard = false
self.isRunning = false
self.isPaused = false
self.isRepeat = false
self.tickCount = 0
self.time = 0
end
-- 开始执行行为树
function BehaviourTree:start()
self.isRunning = true
self.rootStatus = self.primeNode.status
end
-- 行为树的每帧更新
function BehaviourTree:update()
if not self.isRunning then return end
self.time = Cache.serverTimeCache:getServerTimeMS()
if self:tick(self.agent, self.blackboard) ~= BTStatus.Running and not self.isRepeat then
self:stop(self.rootStatus == BTStatus.Success)
end
end
-- 每帧更新的方法,用于调用节点的execute函数
function BehaviourTree:tick(agent, blackboard)
if self.rootStatus ~= BTStatus.Running then
self.tickCount = self.tickCount + 1
self.primeNode:reset()
end
self.rootStatus = self.primeNode:execute(agent, blackboard)
return self.rootStatus
end
-- 停止运行行为树(行为树节点是否全部完成的停止)
function BehaviourTree:stop(success)
if not self.isRunning and not self.isPaused then
return
end
print("行为树运行完成")
self.isRunning = false
self.isPaused = false
for k, node in pairs(self.nodes) do
node:reset(false)
node:onGraphStoped()
end
end
-- 行为树的暂停
function BehaviourTree:pause()
if not self.isRunning then
return
end
self.isRunning = false
self.isPaused = true
for k, node in pairs(self.nodes) do
node:onGraphPaused()
end
end
-- 行为树销毁
function BehaviourTree:destroy()
for k, node in pairs(self.nodes) do
node:destroy()
node = false
end
self.nodes = false
self.nodesIndex = false
end
-- 获取行为树的节点数据
function BehaviourTree:getNodeInfo()
local tb = {}
tb[#tb+1] = "{"
for k,nodeId in pairs(self.nodesIndex) do
local node = self.nodes[nodeId]
tb[#tb+1] = "{"
tb[#tb+1] = node.status
tb[#tb+1] = ","
if #node.outConnections > 0 then
for i=1,#node.outConnections do
tb[#tb+1] = node.outConnections[i].status
tb[#tb+1] = ","
end
end
local info = table.concat( tb, "")
info = string.sub(info,1,string.len(info)-1)
tb = {info, "},"}
end
local info = table.concat( tb, "")
info = string.sub(info,1,string.len(info)-1)
info = info .. "}"
return info
end
-- 根据NodeCanvas导出的json名称加载行为树
function BehaviourTree:load(fileName)
-- local jsonData = CS.GYGame.ResManager.LoadRes("as/df/asdf", typeof(CS.UnityEngine.TextAsset))
local jsonData = io.readfile(fileName)
local data = json.decode(jsonData)
self.version = data.version
self.type = data.type
self.name = fileName
local spec, id, type, node, Cls
for i, v in pairs(data.nodes) do
spec = v
id = tonumber(spec["$id"])
if id then
-- 这里的spec["$type"]一般是自定义节点脚本的C#脚本名称
Cls = BTGetClass(spec["$type"])
node = Cls.new(self)
node.id = id
-- 这里做数据的初始化 将节点所有的json数据存入节点中
node:_init(spec)
self.nodes[id] = node
table.insert(self.nodesIndex, id)
end
end
--connections
for i, v in pairs(data.connections) do
spec = v
Cls = BTGetClass(spec["$type"])
local sourceNodeId = tonumber(spec["_sourceNode"]["$ref"])
local targetNodeId = tonumber(spec["_targetNode"]["$ref"])
local isDisabled = false
if spec["_isDisabled"] then
isDisabled = true
end
-- 这里凭借节点间的连接线来进行节点与节点的连接
-- 每个节点的targetNode就是它的子节点 也就是下一个节点
-- 每个节点的sourceNode就是它的父节点 也就是上一个节点 也就是来源节点
local sourceNode = self.nodes[sourceNodeId]
local targetNode = self.nodes[targetNodeId]
local connection = Cls.new()
connection:create(i, sourceNode, targetNode, isDisabled)
targetNode:addInConnection(connection)
sourceNode:addOutConnection(connection)
end
-- 这里将所有节点的索引以id的形式存入节点自身
for k,v in pairs(self.nodes)do
local task = v.action
if task then
task.id = v.id
end
end
self.primeNode = self.nodes[0]
end
-- 创建子树
function BehaviourTree:createSubTree(id,name)
local btree = BehaviourTree.new()
btree.id = id
btree.agent = self.agent
btree.blackboard = self.blackboard
btree:load(name)
self.subTrees[id] = btree
return btree
end
function BehaviourTree:debug(info)
if not self.agent.isBTDebug then return end
end
function BehaviourTree:addDebugger(tbSocket)
table.insert( self.debugList, tbSocket )
self.agent.isBTDebug = true
end
function BehaviourTree:delDebugger(tbSocket)
for i=1,#self.debugList do
if self.debugList[i] == tbSocket then
table.remove( self.debugList, i )
end
end
if #self.debugList > 0 then
self.agent.isBTDebug = false
end
end
function BehaviourTree:checkDebugger()
if not self.agent.isBTDebug then return end
local info = self:getNodeInfo()
for i,socket in pairs(self.debugList) do
print("check debug:",i,info)
s2c.btInfo(socket,info)
end
end
return BehaviourTree
三、举个自定义节点的例子
// C#脚本
-- 用于将节点显示在NodeCanvas插件面板
using NodeCanvas.Framework;
using ParadoxNotion.Design;
namespace Scenario
{
// Name 对应途中节点的名称
[Name("延时")]
// Category对应点击Action节点弹出的左上角面板后 再点击Assign Action Task弹出的面板的分类
[Category("剧情")]
// Description对应节点的简介
[Description("延时")]
public class Wait : ActionTask
{
// 申明想要在NodeCanvas面板输入的属性
// 这里的属性名必须与Lua脚本中M:_init(jsonData)传入的jsonData.WaitTimeInt64等一致
// 这里注意 NodeCanvas源码只能识别某些类型的字段 如Int64、double、bool、string、Dictionary<string, fsData>、List<fsData>
// 具体找NodeCanvas源码的fsDataType枚举 详情见fsData.cs
// 但可以自己写识别其他类型如Color、Vector等类型的方法 按照原有的fsDataType枚举照搬即可 在此不多做介绍
public Int64 WaitTimeInt64;
public double WaitTimeDouble;
public bool WaitTimeBool;
public string WaitTimeString;
}
}
-- Wait.lua
-- 这个脚本是做了个延时的功能 多少s后才继续执行下面的节点
-- ActionTask节点脚本路径
local ActionTask = require("Game/LuaBT/BT/Core/Tasks/ActionTask")
-- 继承自ActionTask节点
local M = class("Wait", ActionTask)
-- 定义节点属性的函数 在加载行为树的时候所有节点都会调用
function M:ctor()
self._WaitTimeInt64 = false
self._WaitTimeDouble = false
self._WaitTimeBool= false
self._WaitTimeString = false
end
-- 节点初始化json存入的数据
function M:_init(jsonData)
self._WaitTimeInt64 = jsonData.WaitTimeInt64 or 0
self._WaitTimeDouble = jsonData.WaitTimeDoubleor 0
self._WaitTimeBool = jsonData.WaitTimeBool or 0
self._WaitTimeString = jsonData.WaitTimeString or 0
end
-- 开始执行该节点的函数 类似C#中的Start()
function M:onExecute()
end
-- 执行该节点时每帧调用的函数 类似C#中的Update()
function M:onUpdate()
-- self:getElapsedTime()获取当前节点从开始执行到现在的持续时间
if self:getElapsedTime() > self._WaitTimeDouble then
-- self:endAction(true)函数是每个节点结束时必须调用的函数 表示该节点已完成
-- 此时节点状态为BTStatus.Success
self:endAction(true)
end
end
-- 执行完毕该节点时调用的函数
function M:onStop()
end
-- 暂停该节点时调用的函数
function M:onPause()
end
return M
四、节点状态以及行为树执行逻辑
BTStatus = {
Failure = 0,
Success = 1,
Running = 2,
Resting = 3,
Error = 4,
Optional = 5
}
当节点状态为BTStatus .Running的时候,节点的M:onExecute()每帧都会调用,直到节点的状态为BTStatus.Success或者BTStatus.Failure。
行为树执行逻辑首先一开始当然是被指定为开头,即节点右键设置了Set Start的节点,当该节点状态为BTStatus.Success时,开始按顺序从左到右执行子节点,按照树的逻辑执行下去,直到其中某个子节点返回Failure,或者所有节点执行完毕,则继续执行第二个子节点,以此类推。
当然,如果是一些特殊的节点如Selector选择器,那就会根据不同的实际情况决定树的执行逻辑。
五、Lua源码以及NodeCanvas插件资源
链接: https://pan.baidu.com/s/1sPr2rLI1N_qVp30BOUR8hg
提取码: wvg4
欢迎指点错误以及不足