本文最终效果
嗨,大家好,我是新发。
有粉丝问了我这个问题,
我以为他是拿到一个纯文本数据,他不知道如何去解析,如果单纯是想解析数据,完全不需要使用树。
看格式的话是lua
的table
,我们如果拿到一个纯文本数据,并且格式是lua
的table
的格式的话,可以使用loadstring
方法去执行纯文本得到一个table
对象,例:
local data_str = '{ a = 1, b = 2, { c=3, d = 4}, { e = { x = 5 }}}'
local func = loadstring('return ' .. data_str)
local tb = func()
-- TODO 解析tb这个table
这里需要注意,loadstring
方法在lua 5.2
及以上版本改为了load
,所以如果你的lua
版本是5.2
及以上版本需要注意,我们可以查看lua
源码的lbaselib.c
文件,
事实上,我理解错他的意思了,他给我发了另一段数据
然后说要以这种形式展示,
这格式不是lua
的table
呢,到这里我以为他是做网页,所以问了下是不是用javascript
,
经过确认,是用lua
,他已经转化成了table
了,只是不知道怎么用树去构造出来递归遍历,
终于,我明白他遇到的问题了,好了,好人帮到底,现在就来讲讲具体实现吧~
注:上面提到的
红点
是指我之前写的另一篇文章:【游戏开发实战】手把手教你在Unity中使用lua实现红点系统(前缀树 | 数据结构 | 设计模式 | 算法 | 含工程源码)
因为要使用lua
,而Unity
默认用的是C#
,所以我们得搞个lua
框架进来,正好,我之前自己搭建了一个游戏框架UnityXFramework
,里面集成了tolua
框架,详细可以查看我之前写的这篇博客:【游戏开发框架】自制Unity通用游戏框架UnityXFramework,详细教程(Unity3D技能树 | tolua | 框架 | 热更新)
框架开源地址:https://gitcode.net/linxinfa/UnityXFramework
这里我就在框架的环境中去写lua
逻辑吧~
先在LuaFramework/Lua/Logic
目录中新建一个Tree
目录,
在Tree
目录中新建一个TreeNode.lua
脚本,
先思考一下,一个节点需要的信息,画个图,
现在我们写下代码,TreeNode.lua
脚本代码如下,
-- TreeNode.lua 树节点
TreeNode = TreeNode or {}
TreeNode.__index = TreeNode
function TreeNode.New(name)
local self = {}
-- 节点名
self.name = name
-- 值
self.value = nil
-- 父节点
self.parent = nil
-- 子节点
self.child = nil
-- 缩进
self.tab = 0
-- 是否展开
self.isopen = true
-- UI对象
self.uiObj = nil
setmetatable(self, TreeNode)
return self
end
我们再在Tree
目录中新建一个TreeLogic.lua
脚本,
先写个Init
方法,留个TODO
,如下
-- TreeLogic.lua 树逻辑
TreeLogic = TreeLogic or {}
local this = TreeLogic
-- 根节点
this.root = nil
-- 初始化
function TreeLogic.Init()
-- TODO
end
我们要构造一棵树,得先有数据,根据需求,数据就是一个table
,简单写一个,
-- 测试数据
local data_table = {
name = "林新发",
university = "华南理工大学",
major = '信息工程',
job = 'Unity3D游戏开发工程师',
blog = 'https://blog.csdn.net/linxinfa',
hobby = {'吉他', '钢琴', '画画', '撸猫'},
dream = {
developer = {
target = '成为一名优秀的独立游戏开发者',
style = {'ARPG', 'FPS', 'SLG', 'MOBA'}
},
painter = {
target = '成为一个独立画家',
magnum_opus = {'暴走柯南', '皮皮猫', '光'}
},
musician = {
target = '成为一个独立音乐人',
magnum_opus = {'尘土', '树与风'}
}
}
}
接着我们用测试数据去构造一棵树,我们封装一个MakeTree
方法,其实构造过程我们只需要把注意力放在一个节点的构造上即可,设置节点的名称、值,设置节点的父子节点关系,然后递归执行。
代码如下
-- TreeLogic.lua
-- 构造树
-- tb: 数据table
-- parent: 父节点
function TreeLogic.MakeTree(tb, parent)
-- 遍历table
for k, v in pairs(tb) do
-- 新建一个节点
local node = TreeNode.New(k)
node.value = v
-- 设置父节点
node.parent = parent
-- 子节点缩进+1
node.tab = parent.tab + 1
-- 父节点的child塞入node
if nil == parent.child then
parent.child = {}
end
parent.child[k] = node
-- 如果v是table,则递归遍历
if type(v) == 'table' then
-- 有子节点,默认不展开
node.isopen = false
this.MakeTree(v, node)
end
end
return parent
end
接着,我们在Init
方法中调用MakeTree
方法,
-- 初始化
function TreeLogic.Init()
-- 测试数据 data_table = {}
-- ...
-- 根节点
this.root = TreeNode.New("Root")
-- 构造树
this.root = this.MakeTree(data_table, this.root)
end
到这里,我用了30
行左右的代码完成了节点的封装和树的构造,接下来就是树的UI
显示了。
在做UI
显示之前,我们不妨封装一个打印树的方法,验证一下我们的树结构,
-- 把树转为字符串
function TreeLogic.TreeToString(node, str)
if nil ~= node.value then
local tabspace = ''
for i = 1, node.tab do
tabspace = tabspace .. ' '
end
if 'table' == type(node.value) then
str = str .. string.format('%s▼ %s :\n', tabspace, node.name)
else
str = str .. string.format('%s● %s : %s\n', tabspace, node.name, tostring(node.value))
end
end
if nil ~= node.child then
for _, child_node in pairs(node.child) do
-- 递归
str = this.TreeToString(child_node, str)
end
end
return str
end
我们调用一下
-- 打印树
local str = ''
str = this.TreeToString(this.root, str)
log(str)
制作一个TreePanel.prefab
界面预设,
如下
界面层级结构如下,其中我用了VerticalLayoutGroup
组件来做垂直布局,这样添加子节点的时候就会自动垂直排列了,
其中item
上挂一个Button
用于监听点击,子节点是一个Text
,缩进就通过Text
的坐标右移来实现即可,
在LuaFramework/Lua/View
目录中创建一个Tree
文件夹,然后创建一个TreePanel.lua
脚本,编写界面代码,
界面代码我做了模板,可以参见我之前写的框架教程:【游戏开发框架】自制Unity通用游戏框架UnityXFramework,详细教程(Unity3D技能树 | tolua | 框架 | 热更新) 的8.2
小节:
下面我重点写下递归展开树节点和关闭节点的逻辑~
封装一个张开节点的方法ExpanNode
,里面我用到了递归,代码我写了注释,这里就不多解释啦,
-- TreePanel.lua
-- 展开节点
function TreePanel.ExpanNode(node)
if nil == node.child then
return
end
local index = 1
for _, child_node in pairs(node.child) do
-- 创建节点的UI对象
local uiObj = LuaUtil.CloneObj(this.tiemForClone)
local text = uiObj.transform:GetChild(0):GetComponent("Text")
child_node.uiObj = uiObj
if not LuaUtil.IsNilOrNull(node.uiObj) then
-- 子节点塞在父节点下面
local siblingIndex = node.uiObj:GetComponent("RectTransform"):GetSiblingIndex()
child_node.uiObj:GetComponent("RectTransform"):SetSiblingIndex(siblingIndex + index)
index = index + 1
end
if type(child_node.value) == 'table' then
text.text = (child_node.isopen and '▼ ' or '► ') .. child_node.name
else
text.text = '● ' .. child_node.name .. ': ' .. child_node.value
end
-- 坐标缩进
text.transform.localPosition = text.transform.localPosition + Vector3.New((child_node.tab-1)*50, 0,0)
uiObj:GetComponent("Button").onClick:AddListener(function()
if not child_node.isopen then
child_node.isopen = true
-- 递归, 展开子节点
this.ExpanNode(child_node)
else
-- 关闭子节点
this.CloseNode(child_node)
end
if type(child_node.value) == 'table' then
text.text = ( child_node.isopen and '▼ ' or '► ') .. child_node.name
end
end)
end
end
我们需要传入树的根节点,我们给TreeLogic.lua
添加一个GetTree
方法,
-- TreeLogic.lua
function TreeLogic.GetTree()
return this.root
end
接着我们去调用ExpanNode
方法,如下
local tree = TreeLogic.GetTree()
this.ExpanNode(tree)
关闭节点也封装一个方法CloseNode
,依然使用了递归,如下
-- 关闭子节点
function TreePanel.CloseNode(node)
if LuaUtil.IsNilOrNull(node.child) then
return
end
node.isopen = false
for _, child in pairs(node.child) do
child.isopen = false
LuaUtil.SafeDestroyObj(child.uiObj)
if nil ~= child.child then
-- 递归关闭子节点
this.CloseNode(child)
end
end
end
好啦,现在我们测试一下效果吧,
完美,收工~
代码我已经提交到框架上了,可下载工程进行查阅,https://gitcode.net/linxinfa/UnityXFramework
我是林新发,https://blog.csdn.net/linxinfa
一个在小公司默默奋斗的Unity
开发者,希望可以帮助更多想学Unity
的人,共勉~