Unity NodeCanvas 可视化行为编辑框架(二)在Lua中使用NodeCanvas框架

**

本篇旨在讲解如何在游戏项目中配合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

欢迎指点错误以及不足


版权声明:本文为weixin_39315127原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。