游戏开发中最常见的用到树形结构的功能就是红点系统和行为树。
我今天先写一下红点系统的开发。
1.需求分析
红点的作用就是给玩家提示,例如:玩家有未读邮则主界面邮件功能出现红点,玩家看到红点后点击邮件功能入口,进入邮件功能主界面后又看到邮件标签页显示红点于是又点击邮件标签进入邮件列表,在众多邮件中找到某一封显示红点的未读邮件。
整个提示流程是:主界面邮件入口→邮件界面邮件页签→邮件列表中的未读邮件。直观的看,就是从外到内逐层进行提示。
然而在实现红点功能的时候,需要在主界面,邮件界面,邮件列表界面分别写红点提示代码吗?
别说,我还真见过有人这么搞,一个功能的红点提示要到处写,可想而知他弄的功能要涉及多少代码,尤其是主界面,那代码已经不能看了,一个界面调用十几二十个模块的方法就为了判断红点显不显示。
所以优雅代码的诞生就是从偷懒开始,我不想为了一个红点提示在其他地方加入一句本不该出现的XXX.IsShowRedPoint(),我希望只要在邮件列表的刷新逻辑加上一句:RedPointMgr.ShowPoint(RedPointType.Mail,true)就完成上面整个提示流程。
2.功能实现
于是红点树应运而生,当调用SetState的时候首先根据主Key找到红点树的根节点,然后逐层向下遍历直到找到目标子节点,接着从该子节点的父节点逐层向上依次计算红点状态,最后派发红点事件。即:触发红点功能的节点的所有父节点全部进行一次红点提示即可。
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()
{
//在实际开发中,整个游戏的红点树要在游戏初始化时全部构建出来
//声明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/