Unity游戏开发中的树形结构——红点系统
Posted _WuWu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity游戏开发中的树形结构——红点系统相关的知识,希望对你有一定的参考价值。
游戏开发中最常见的用到树形结构的功能就是红点系统和行为树。
我今天先写一下红点系统的开发。
1.需求分析
红点的作用就是给玩家提示,例如:玩家有未读邮则主界面邮件功能出现红点,玩家看到红点后点击邮件功能入口,进入邮件功能主界面后又看到邮件标签页显示红点于是又点击邮件标签进入邮件列表,在众多邮件中找到某一封显示红点的未读邮件。
整个提示流程是:主界面邮件入口→邮件界面邮件页签→邮件列表中的未读邮件。直观的看,就是从外到内逐层进行提示。
然而在实现红点功能的时候,需要在主界面,邮件界面,邮件列表界面分别写红点提示代码吗?
别说,我还真见过有人这么搞,一个功能的红点提示要到处写,可想而知他弄的功能要涉及多少代码,尤其是主界面,那代码已经不能看了,一个界面调用十几二十个模块的方法就为了判断红点显不显示。
所以优雅代码的诞生就是从偷懒开始,我不想为了一个红点提示在其他地方加入一句本不该出现的XXX.IsShowRedPoint(),我希望只要在邮件列表的刷新逻辑加上一句:RedPointMgr.ShowPoint(RedPointType.Mail,true)就完成上面整个提示流程。
2.功能实现
于是红点树应运而生,当调用SetState的时候就会从当前节点逐层向前遍历,即:触发红点功能的节点的所有父节点全部进行一次红点提示即可。
PS:为什么不是从根节点向子节点遍历?显然,每个节点只有一个父节点,有众多子节点,无法得知当前节点在哪个节点下,只能先遍历一次找到当前节点,再从当前节点反向遍历。
那么到这里就很清晰了,首先要有一个红点类,它是一个树形结构。
using System;
using System.Collections.Generic;
using UnityEngine.UI;
public enum RedPointType
None,
Enternal,//一直存在
Once,//点击一次就消失
public enum RedPointState
None,
Show,
Hide,
public class RedPoint
/// <summary>
/// 主关键字(属于哪一个根节点)
/// </summary>
public string key
get
return m_Key;
/// <summary>
/// 自己的关键字
/// </summary>
public string subKey
get
return m_SubKey;
/// <summary>
/// 是否是根节点
/// </summary>
public bool isRoot
get
return m_IsRoot;
/// <summary>
/// 红点类型
/// </summary>
public RedPointType type
get
return m_Type;
/// <summary>
/// 当前状态
/// </summary>
public RedPointState state
get
return m_State;
/// <summary>
/// 数据
/// </summary>
public int data
get
return m_Data;
/// <summary>
/// 父节点
/// </summary>
public RedPoint parent
get
return m_Parent;
/// <summary>
/// 子节点
/// </summary>
public List<RedPoint> children
get
return m_Children;
public RedPoint(string key, string subKey, bool isRoot, RedPointType type)
m_Key = key;
m_SubKey = subKey;
m_IsRoot = isRoot;
m_Type = type;
m_State = RedPointState.Hide;
m_Data = 0;
m_Children = new List<RedPoint>();
public void Init(Action<RedPointState, int> showEvent, Button btn)
m_ShowEvent = showEvent;
if (btn != null)
m_Btn = btn;
m_Btn.onClick.AddListener(OnClick);
m_ShowEvent?.Invoke(m_State, m_Data);
public void AddChild(RedPoint node, string parentKey)
if (m_SubKey.Equals(parentKey))
node.SetParent(this);
m_Children.Add(node);
return;
for (int i = 0; i < m_Children.Count; i++)
m_Children[i].AddChild(node, parentKey);
public RedPoint GetChild(string subKey)
if (m_SubKey.Equals(subKey))
return this;
if (m_Children == null)
return null;
for (int i = 0; i < m_Children.Count; i++)
RedPoint node = m_Children[i].GetChild(subKey);
if (node != null)
return node;
return null;
public void RemoveChild(string subKey)
if (m_SubKey.Equals(subKey))
m_Parent.children.Remove(this);
Dispose();
return;
if (m_Children == null)
return;
for (int i = 0; i < m_Children.Count; i++)
m_Children[i].RemoveChild(subKey);
public void SetParent(RedPoint parent)
m_Parent = parent;
public void SetState(string subKey, RedPointState state, int data)
RedPoint node = GetChild(subKey);
if (node == null)
return;
node.SetTreeState(subKey, state, data);
m_Data = 0;
for (int i = 0; i < m_Children.Count; i++)
m_Data += m_Children[i].m_Data;
m_ShowEvent?.Invoke(m_State, m_Data);
private void SetTreeState(string subKey, RedPointState state, int data)
m_State = state;
if (m_SubKey.Equals(subKey))
m_Data = data;
else
m_Data = 0;
for (int i = 0; i < m_Children.Count; i++)
if (m_Children[i].state == RedPointState.Show)
m_State = RedPointState.Show;
m_Data += m_Children[i].data;
if (m_Parent != null)
m_Parent.SetTreeState(subKey, state, data);
m_ShowEvent?.Invoke(m_State, m_Data);
private void OnClick()
if (m_Type == RedPointType.Once)
HideChildren();
SetState(m_SubKey, RedPointState.Hide, m_Data);
private void HideChildren()
m_State = RedPointState.Hide;
for (int i = 0; i < m_Children.Count; i++)
m_Children[i].HideChildren();
m_ShowEvent?.Invoke(m_State, m_Data);
public void Dispose()
for (int i = 0; i < m_Children.Count; i++)
m_Children[i].Dispose();
m_Children.Clear();
m_Children = null;
if (m_Btn != null)
m_Btn.onClick.RemoveListener(OnClick);
m_Btn = null;
m_Parent = null;
m_Key = null;
m_SubKey = null;
m_ShowEvent = null;
m_Type = RedPointType.None;
m_State = RedPointState.None;
private string m_Key = string.Empty;
private string m_SubKey = string.Empty;
private bool m_IsRoot = false;
private int m_Data = 0;
private RedPointType m_Type = RedPointType.None;
private RedPointState m_State = RedPointState.None;
private Action<RedPointState, int> m_ShowEvent = null;
private Button m_Btn;
private RedPoint m_Parent = null;
private List<RedPoint> m_Children = null;
这里面key就是归属,也就是属于哪一个大功能,subKey是自己的关键字。
然后需要一个管理器来管理游戏中所有红点的根节点,需要的时候通过关键字找到红点的根节点,再向其中的某个节点插入子节点。
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class RedPointMgr : IDisposable
public static RedPointMgr instance
get
if (s_Instance == null)
s_Instance = new RedPointMgr();
return s_Instance;
public RedPointMgr()
m_ListRedPointTrees = new List<RedPoint>();
public void Add(string key, string subKey, string parentKey, RedPointType type)
RedPoint root = GetRoot(key);
if (string.IsNullOrEmpty(subKey) || key.Equals(subKey))
if (root != null)
Debug.LogError("The red point root [" + key + "] is already exist!");
return;
root = new RedPoint(key, key, true, type);
m_ListRedPointTrees.Add(root);
else
if (root == null)
Debug.LogError("The red point root [" + key + "] is invalid,please add it first");
return;
RedPoint node = new RedPoint(key, subKey, false, type);
root.AddChild(node, parentKey);
public void Remove(string key, string subKey)
if (string.IsNullOrEmpty(subKey) || key.Equals(subKey))
for (int i = m_ListRedPointTrees.Count - 1; i >= 0; i--)
if (m_ListRedPointTrees[i].key.Equals(key))
m_ListRedPointTrees[i].Dispose();
m_ListRedPointTrees.RemoveAt(i);
return;
return;
RedPoint root = GetRoot(key);
if (root == null)
return;
root.RemoveChild(subKey);
public void Init(string key, string subKey, Action<RedPointState, int> showEvent, Button btn = null)
RedPoint root = GetRoot(key);
if (root == null)
Debug.LogError("The red point root [" + key + "] is invalid,please add it first");
return;
RedPoint node = root.GetChild(subKey);
if (node == null)
Debug.LogError("The red point node [" + subKey + "] is invalid,please add it first");
return;
node.Init(showEvent, btn);
public void SetState(string key, string subKey, RedPointState state, int data = 0)
RedPoint root = GetRoot(key);
if (root == null)
Debug.LogError("The red point root [" + key + "] is invalid,please add it first");
return;
root.SetState(subKey, state, data);
private RedPoint GetRoot(string key)
if (string.IsNullOrEmpty(key))
return null;
for (int i = 0; i < m_ListRedPointTrees.Count; i++)
if (m_ListRedPointTrees[i].key.Equals(key))
return m_ListRedPointTrees[i];
return null;
public void Dispose()
for (int i = m_ListRedPointTrees.Count - 1; i >= 0; i--)
m_ListRedPointTrees[i].Dispose();
m_ListRedPointTrees.Clear(); ;
private static RedPointMgr s_Instance = null;
private List<RedPoint> m_ListRedPointTrees = null;
使用时,先在游戏初始化的时候调用Add方法声明有哪些红点构建红点树;然后在UI界面初始化时调用Init方法加入红点的显示和点击的回调;最后在功能逻辑处调用SetState方法。
3.实例测试
例如:现在有mail1、mail2、mail3、mail4、mail5、mail6需要红点提示,mail4、mail5、mail6是具体业务管理的且都是mail3的子节点,mial1、mail2、mail3要随着他们的子节点变化。
先构建,再初始化,最后写显示回调。(一顿操作猛如虎,一看工资2k5)
using UnityEngine;
using UnityEngine.UI;
public class Test : MonoBehaviour
public GameObject mail1RedPoint;
public GameObject mail2RedPoint;
public GameObject mail3RedPoint;
public GameObject mail4RedPoint;
public GameObject mail5RedPoint;
public GameObject mail6RedPoint;
public Text txtMail1;
public Text txtMail2;
public Text txtMail3;
public Text txtMail4;
public Text txtMail5;
public Text txtMail6;
public Button mail4Btn;
public Button mail5Btn;
public Button mail6Btn;
public Button btnSet1;
public Button btnSet2;
public Button btnSet3;
public int count1 = 5;
public int count2 = 6;
public int count3 = 7;
string mail1 = "mail1";
string mail2 = "mail2";
string mail3 = "mail3";
string mail4 = "mail4";
string mail5 = "mail5";
string mail6 = "mail6";
private void Awake()
RedPointMgr.Init(gameObject);
//在实际开发中,整个游戏的红点树要在游戏初始化时全部构建出来
//声明mail1根节点,它的主key是mail1,无subKey,无父节点,红点类型是随着子节点变化
RedPointMgr.instance.Add(mail1, null, null, RedPointType.Enternal);
//声明mail2节点,它的主key是mail1,subKey是mail2,父节点是mail1,红点类型是随着子节点变化
RedPointMgr.instance.Add(mail1, mail2, mail1, RedPointType.Enternal);
//声明mail3节点,它的主key是mail1,subKey是mail3,父节点是mail2,红点类型是随着子节点变化
RedPointMgr.instance.Add(mail1, mail3, mail2, RedPointType.Enternal);
//声明mai4节点,它的主key是mail1,subKey是mail4,父节点是mail3,红点类型是点击即消失
RedPointMgr.instance.Add(mail1, mail4, mail3, RedPointType.Once);
//声明mai5节点,它的主key是mail1,subKey是mail5,父节点是mail3,红点类型是点击即消失
RedPointMgr.instance.Add(mail1, mail5, mail3, RedPointType.Once);
//声明mai5节点,它的主key是mail1,subKey是mail6,父节点是mail3,红点类型是点击即消失
RedPointMgr.instance.Add(mail1, mail6, mail3, RedPointType.Once);
//在实际开发中,初始化代码要写在对应UI界面的初始化函数中
RedPointMgr.instance.Init(mail1, mail1, OnMail1Show);
RedPointMgr.instance.Init(mail1, mail2, OnMail2Show);
RedPointMgr.instance.Init(mail1, mail3, OnMail3Show);
RedPointMgr.instance.Init(mail1, mail4, OnMail4Show, mail4Btn);
RedPointMgr.instance.Init(mail1, mail5, OnMail5Show, mail5Btn);
RedPointMgr.instance.Init(mail1, mail6, OnMail6Show, mail6Btn);
btnSet1.onClick.AddListener(OnBtnSet1Click);
btnSet2.onClick.AddListener(OnBtnSet2Click);
btnSet3.onClick.AddListener(OnBtnSet3Click);
private void OnMail1Show(RedPointState state, int data)
mail1RedPoint.SetActive(state == RedPointState.Show);
txtMail1.text = data.ToString();
private void OnMail2Show(RedPointState state, int data)
mail2RedPoint.SetActive(state == RedPointState.Show);
txtMail2.text = data.ToString();
private void OnMail3Show(RedPointState state, int data)
mail3RedPoint.SetActive(state == RedPointState.Show);
txtMail3.text = data.ToString();
private void OnMail4Show(RedPointState state, int data)
mail4RedPoint.SetActive(state == RedPointState.Show);
txtMail4.text = data.ToString();
private void OnMail5Show(RedPointState state, int data)
mail5RedPoint.SetActive(state == RedPointState.Show);
txtMail5.text = data.ToString();
private void OnMail6Show(RedPointState state, int data)
mail6RedPoint.SetActive(state == RedPointState.Show);
txtMail6.text = data.ToString();
private void OnBtnSet1Click()
RedPointMgr.instance.SetState(mail1, mail4, count1 == 0 ? RedPointState.Hide : RedPointState.Show, count1);
private void OnBtnSet2Click()
RedPointMgr.instance.SetState(mail1, mail5, count2 == 0 ? RedPointState.Hide : RedPointState.Show, count2);
private void OnBtnSet3Click()
RedPointMgr.instance.SetState(mail1, mail6, count3 == 0 ? RedPointState.Hide : RedPointState.Show, count3);
RedPointMgr.instance.SetState方法的第三个参数是显示在红点上的数字,也是业务需要统计的数据,如果只显示红点不显示数字就不用传了。
运行Test脚本,效果如下
4.结语
以上是红点系统的简单实现,写这篇东西也是因为见到我的大神同事竟然手写每一个红点,这实在是令我惊叹不已。
我可不想写那么多的cv代码。
再次PS:
前几天用hexo搭建了一个个人博客,欢迎来找我玩。Ming-ehttps://ming-e.space/
unity游戏开发知识检测
1.红点系统设计
2.快速排序
参考:快速排序详解
3.点乘,叉乘,投影的数学意义以及几何意义
点乘
数学意义:
向量点乘结果是标量,是两个向量在一个方向的累计结果,结果只保留大小属性,抹去方向属性,就相等于降维;
点乘运算:
#####几何意义
点积是两个向量的长度与它们夹角余弦的积。点乘的结果表示向量A在向量B方向上的投影与向量B模的的乘积,点乘的意义就是两个向量在一个向量方向的共同积累的结果,但是这种结果只保留的大小属性,抹去了方向这个属性;同时反映了两个向量在方向上的相似度,结果越大越相似。基于结果可以判断这两个向量是否是同一方向,是否正交垂直,具体对应关系为:
1.结果>0:夹角在0°到90°之间,则方向基本相同,
2.结果=0:相互垂直,正交,
3.结果<0:夹角在90°到180°之间,方向基本相反
叉乘
数学意义:
向量叉乘,是这这两个向量平面上,垂直生成新的向量,大小是两个向量构成四边形的面积。相等于生维。
参考:向量点乘与叉乘的概念及几何意义
4.拼ui如何减少DC(DrawCall)
1.合理的分配图集,合理的分配图集可以降低drawcall和资源加载速度
具体细节如下:
同一个UI界面的图片尽可能放到一个图集中,这样可以尽可能的降低drawcall。
1.共用的图片放到一个或几共享的图集中,例如通用的弹框和按钮等;
2.相同功能的图片放到一个图集中,例如装备图标和英雄头像等;这样可以降低切换界面的加载速度。
3.不同格式的图片分别放到不同的图集中,例如透明(带Alpha)和不透明〈不带Alpha)的图片,这样可以减少图片的存储空间和占用内存。(UGUI的sprite packer会自动处理这种情况)
合理的资源目录
resources目录中应该只保存prefab文件,其它非prefab文件(例如动画,贴图,材质等)应放到resource目录之外;因为随着项目的迭代,可能会导致部分资源(动画,贴图)等失效,如果这些文件放在resource目录下,在打包时,unity会将resource目录下文本全部打成一个大的AssetBundle包(非resouce目录下的文件只有在引用到时才会被打到包里),从而出现冗余,增加不必要的存储空间和内存占用。
基于以上UGUI的网格更新原理,我们可以做以下优化:
1.使用尽可能少的U元素;在制作U时,一定要仔细查检U层级,删除不不必要的U元素,这样可以减少深度排序的时间以及Rebuild的时间 。
2.减少Rebuild的频率,将动态UI元素(频繁改变例如顶点、alpha、坐标和大小等的元素)与静态U元素分离出来,放到特定的Canvas中。
3.减少重建的频率,将动态UI元素(频繁改变例如顶点、alpha、坐标和大小等的元素)与静态U元素分离出来,放到特定的Canvas中。
4.谨慎使用UI元素的enable与disable,因为它们会触发耗时较高的rebuild,替代方案之一是enable和disableUl元素的canvasrender或者Canvas。
5.谨慎使用Text的Best Fit选项,虽然这个选项可以动态的调整字体大小以适应UI布局而不会超框,但其代价是很高的,Unity会为用到的该元素所用到的所有字号生成图元保存在atlas里,不但增加额外的生成时间,还会使得字体对应的atlas变大。
6.谨慎使用Canvas的Pixel Perfect选项,该选项会使得ui元素在发生位置变化时,造成layout Rebuild。(比如ScrollRect滚动时,如果开启了Canvas的pixel Perfect,会使得Canvas.SendWillRenderCanvas消耗较高)
7.使用缓存池来保存ScrollView中的ltem,对于移出或移进View外的的元素,不要调用disable或enable,而是把它们放到缓存池里或从缓存池中取出复用。
8.除了rebuild过程之外,UGUI的touch处理消耗也可能会成为性能热点。因为UGUI在默认情况下会对所有可见的Graphic组件调用raycast。对于不需要接收touch事件的grahic,一定要禁用raycast。对于unity5以上的可以关闭graphic的Raycast Target。而对于unity4.6,可以给不需要接收touch的UI元素加上canvasgroup组件。
5. lua为什么能热更
热更新问题的本质是代码更新而不是资源更新。
LUA是解释型语言,并不需要事先编译成块,而是运行时动态解释执行的。
C#为什么不能热更新?准确的说,C#在安卓上可以实现热更新,但在苹果上却不能。在安卓上可以通过C#的语言特性-反射机制实现动态代码加载从而实现热更新。具体做法是:将需要频繁更改的逻辑部分独立出来做成DLL,在主模块调用这些DLL,主模块代码是不修改的,只有作为业务(逻辑)模块的DLL部分需要修改。游戏运行时通过反射机制加载这些DLL就实现了热更新。
6 堆,栈的理解,我们写的代码是堆还是栈
堆
堆(Heap):是应用程序在运行的时候请求操作系统分配给自己内存,一般是申请/给予的过程。
堆的相关特性:
1,堆是由操作系统管理的一片空间,事先是没有在进程空间里分配的(比如你在没有分配堆的时候就访问堆空间会报一个内存访问错误),一般是由程序动态的分配出来,一旦分配了以后,一般需要程序去释放自己的堆空间.
2,堆的空间较大,但访问速度没有栈快
3,堆受垃圾处理器GC管理(GC会去找那些很久没有引用地址指向的内存块,把它们清理掉)
栈
栈(Stack):是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域。
栈的相关特性:
1,栈上是向下填充的,数据只能从栈的顶端插入和删除(先进后出原则)。把数据放入栈顶称为入栈(push),从栈顶删除数据称为出栈(pop)。
2,栈的空间较小,但访问速度快。
3,栈的生长方向是有高地址向低地址生长的。
4,栈的清理是由系统自动完成的。
分配
值类型的实例通常是在线程栈上分配的(静态分配)。
引用类型声明时并没有为其分配堆上的内存空间。引用类型的对象总是在进程堆中分配(动态分配)。
值类型(value type):byte,short,int,long,float,double,decimal,char,bool 和 struct
引用类型:string 和 class
7.lua如何实现继承
function base:New()
local obj =
setmetatable(obj, self)
self.__index = self
return obj
end
继承使用:
local test = base:new()
8.3d游戏的攻击范围计算
Unity3D实现攻击范围检测
Unity3D-游戏中的技能碰撞检测
9.tcp
以上是关于Unity游戏开发中的树形结构——红点系统的主要内容,如果未能解决你的问题,请参考以下文章
游戏开发实战手把手教你在Unity中使用lua实现红点系统(前缀树 | 数据结构 | 设计模式 | 算法 | 含工程源码)