0. 前言
刚好处理到红点系统的问题,就写个文章记录一下。本文的红点系统为一个树结构,UI实现需要和红点运行逻辑剥离,防止过度耦合,现在就暂时不提及,后续在讲述。
1. 红点系统
红点是游戏中一种常见且重要的提醒方式,通常涉及到很多方面信息和界面的显示,如果不做成独立的系统的话,那么游戏逻辑将会杂乱散落在各个角落不便于开发维护。
(1)种类
红点通常是指有UI上那种带数字的小红标,或者单纯一个小红点。当然也不止这些,比如金矿建筑有无产出的信息,也可以用红点系统来处理,有的话显示个小金币图标在建筑头上。当然这个产出信息通常是和金矿建筑高度关联的,也会让金矿建筑自己来处理。这个可能要先考虑一下?
(2)结构
对于红点的信息来说,可能如下图:
主菜单--背包--道具页--新道具1
--新道具2
--任务--新任务1
--新任务2
通常会呈现一个树状,的结构。
这个时候,子节点有一个点亮,父节应点亮。比如有”新道具1”,”主菜单”、”背包”、”道具页面”、”新道具1”应该都点亮,去引导玩家往去查看这个新道具。查看完之后,”新道具1”子节熄灭,其他父节点也应该重新检查是否点亮。
需要考虑的另一点是,叶节点可以自由控制点亮,父节点是否点亮由子节点决定,所有子节点熄灭,父节点应熄灭。(叶节点:树的末端节点,“新道具1”、“新道具2”、“新任务1”、"新任务2"为叶节点)。”道具页”是否点亮,是由有没有新道具来决定的。如果没有新道具,道具页自己亮了,只会给玩家提供错误的信息。若有需求是只让玩家看看这个道具页,比如道具页面更新了,那么应该另外设置一个子节点来处理。
(3)约定
这里我们约定一下红点由路径节点的值组成,并由“|”分隔,方便后续处理。
比如主菜单–任务–新任务1,这个红点查询时,应查询"menu|Task|newTask1"
2. 红点树
我们可以用树的数据结构来实现这个红点系统,那先来实现一个树结构
(1)树节点
所以是树节点的基础结构,值 + 子节点;
public class RedHitNode
{
public int value;
public List<RedHitNode> children = new List<RedHitNode>();
}
这里采用了List而不是Dictionary主要是考虑到Dictionary开销更大,而按照树结构,子节点通常不会太多,所以用List即可。另外作为红点的节点,我还希望:
- 节点自己控制节点的增加和删除
- 节点是否为叶子节点
- 节点通过一个只读的key来互相区分(在同节点下)
所以给RedHitNode加上Key,以及Add,Remove,Find的功能,那么就得到
public class RedHitNode
{
public readonly string Key;
private List<RedHitNode> children = new List<RedHitNode>();
public int Value = 0;
public bool IsLeaf => children.Count == 0;
public RedHitNode(string key)
{
this.Key = key;
}
public RedHitNode Add(string key)
{
RedHitNode node = null;
int index = IndexOf(key);
if (index == -1)
{
node = new RedHitNode(key);
children.Add(node);
}
return node;
}
public RedHitNode Remove(string key)
{
RedHitNode node = null;
int index = IndexOf(key);
if (index != -1)
{
// 删除对应节点
// 因为无序,所以做了位置互换,防止数据过多复制
children[index] = children[children.Count - 1];
children.RemoveAt(children.Count - 1);
}
return node;
}
public RedHitNode Find(string key)
{
RedHitNode node = null;
int index = IndexOf(key);
if (index != -1)
{
node = children[index];
}
return node;
}
protected int IndexOf(string key)
{
for (int i = 0; i < children.Count; i++)
{
if (children[i].Key == key)
{
return i;
}
}
return -1;
}
}
Remove作为和其他List移除略有不同的是,先将数据移动到末端再删除。因为作为红点数据,其子节点是无序的,所以可以通过这种方式,防止数据删除后,后面的数据往前复制。
(2)树_增删查
作为树,应该具备基本的根节点Root,以及Insert,Delete,Search处理。而且因为作为红点树,其节点值应该完全由自己控制,所以节点的插入删除不对外暴露。
public class RedHitNode
{
private RedHitNode root;
private RedHitNode Insert(string[] keys){}
private RedHitNode Delete(string[] keys){}
private RedHitNode Search(string[] keys){}
}
首先Search就不断查找子节点直到找到,Delete也差不多,找到了就删除。实现如下
private RedHitNode Delete(string[] keys)
{
RedHitNode cur = null;
if (keys != null && keys.Length != 0)
{
// 查找父节点
cur = root;
int last = keys.Length - 1;
for (int i = 0; i < last && cur != null; i++)
{
cur = cur.Find(keys[i]);
}
// 如果找到父节点,尝试删除叶节点
if (cur != null)
{
cur = cur.Remove(keys[last]);
}
}
return cur;
}
private RedHitNode Search(string[] keys)
{
RedHitNode cur = null;
if (keys != null && keys.Length != 0)
{
// 查找节点
cur = root;
for (int i = 0; i < keys.Length && cur != null; i++)
{
cur = cur.Find(keys[i]);
}
}
return cur;
}
对于插入处理Insert,我们插入时可能和常规的插入操作有点不同。
- 根节点外的叶子节点不应该再可以加入子节点。比如“主菜单–任务–新任务1”,那么“新任务1”的节点后面不应该再有子节点,才能符合红点树的基础规则。
- 另外对于已经有的节点,再次插入失败时,直接返回原有节点,方便操作。
所以做了两个而外参数,普通插入直接调用Insert,而有额外需求的再传参限制。实现如下
private RedHitNode Insert(string[] keys)
{
return Insert(keys, false, false);
}
private RedHitNode Insert(string[] keys, bool OnlyInsertOnBranchNote, bool getNoteExist)
{
RedHitNode cur = null;
if (keys != null && keys.Length != 0)
{
// 查找节点
RedHitNode parent = root;
cur = root;
int i;
for (i = 0; i < keys.Length && cur != null; i++)
{
parent = cur;
cur = cur.Find(keys[i]);
}
if (cur == null)
{
// 节点未找到
if (!OnlyInsertOnBranchNote || cur != root || !parent.IsLeaf)
{
cur = parent;
for (i = i - 1; i < keys.Length; i++)
{
cur = cur.Add(keys[i]);
}
}
}
else
{
// 节点已存在,
if (!getNoteExist)
{
cur = null;
}
}
}
return cur;
}
这样的话,树的基础结构就实现了。后面做一下红点树的功能。
(3)树_红点处理
作为红点树,打开关闭红点,查看红点是必须功能。另外当红点信息有改变的时候,是需要播报出来的,这里单纯做一个回调在方便处理。也就是可以表现为
public class RedHitTree
{
private Action<string, int> changeNoteCallBack;
private RedHitNode root;
private char splitChar = '|';
public RedHitTree(Action<string, int> changeNoteCallBack)
{
this.root = new RedHitNode("");
this.changeNoteCallBack = changeNoteCallBack;
}
public bool HasRedHit(string redHit){}
public int GetRedHitValue(string redHit){}
public bool TurnOnRedHit(string redHit){}
public bool TurnOffRedHit(string redHit){}
private RedHitNode Insert(string[] keys){}
private RedHitNode Delete(string[] keys){}
private RedHitNode Search(string[] keys){}
}
接下来依次实现如下
public bool HasRedHit(string redHit)
{
return GetRedHitValue(redHit) > 0;
}
public int GetRedHitValue(string redHit)
{
RedHitNode node = Search(redHit.Split(splitChar));
int result = 0;
if (node != null)
{
result = node.Value;
}
return result;
}
public bool TurnOnRedHit(string redHit)
{
string[] keys = redHit.Split(splitChar);
bool flag = false;
RedHitNode node = Insert(keys, true, true);
if (node != null && node.IsLeaf)
{
if (node.Value == 0)
{
ChangeNotesValue(keys, 1);
}
flag = true;
}
return flag;
}
public bool TurnOffRedHit(string redHit)
{
string[] keys = redHit.Split(splitChar);
bool flag = false;
RedHitNode node = Search(keys);
if (node != null && node.IsLeaf && node.Value > 0)
{
if (node.Value == 1)
{
ChangeNotesValue(keys, -1);
}
flag = true;
}
return flag;
}
上面我们在更改红点数据的时候使用到了ChangeNotesValue,这个需要从根节点一直改到叶子节点,并每次更改时做一个回调。如下:
private void ChangeNotesValue(string[] keys, int value)
{
RedHitNode cur = root;
string str = "";
for (int i = 0; i < keys.Length; i++)
{
cur = cur.Find(keys[i]);
if (cur == null)
{
break;
}
cur.Value += value;
// 改变
str += keys[i];
changeNoteCallBack(str, cur.Value);
}
}
那么到这里的时候,树就完成了!!
3. 封装、检查
(1)检查
检查一下,树能否按预期正常运行
- 红点开启是否正常
TurnOnRedHit("menu|Task|newTask1"); //True
HasRedHit("menu|Task|newTask1"); //True
HasRedHit("menu|Task|newTask2"); //False
HasRedHit("menu|Task"); //True
HasRedHit("menu|Knapsack"); //False
- 子节点有一个点亮时,父节点点亮,子节点全熄灭时,父节点熄灭
TurnOnRedHit("menu|Knapsack|Prop|newProp1"); //True
TurnOnRedHit("menu|Knapsack|Prop|newProp2"); //True
HasRedHit("menu|Knapsack"); //True
TurnOnRedHit("menu|Knapsack|Prop|newProp1"); //True
HasRedHit("menu|Knapsack"); //True
TurnOnRedHit("menu|Knapsack|Prop|newProp2"); //True
HasRedHit("menu|Knapsack"); //False
- 父节点是否可以自由控制
TurnOnRedHit("menu|Task|newTask1"); //True
TurnOffRedHit("menu|Task"); //False
TurnOffRedHit("menu|Task|newTask1"); //True
ok,符合预期咯。
(2)UGF封装为组件
因为用的是UGF的框架,所以用UGF做了一下封装。文章来源:https://www.toymoban.com/news/detail-739902.html
using GameFramework;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityGameFramework.Runtime;
using System;
using EventArg;
namespace GDT
{
/// <summary>
/// 自定义红点组件
/// </summary>
public class RedHitComponent : GameFrameworkComponent
{
private RedHitTree tree;
private RedHitChangeEventArgs arg;
protected override void Awake()
{
base.Awake();
tree = new RedHitTree(OnRedHitChange);
arg = new RedHitChangeEventArgs();
}
public bool HasRedHit(string redHit)
{
return tree.HasRedHit(redHit);
}
public int GetRedHitValue(string redHit)
{
return tree.GetRedHitValue(redHit);
}
public void TurnOnRedHit(string redHit)
{
if (!tree.TurnOnRedHit(redHit))
{
Debug.LogError("Failed to turn on the redHit, redHit:" + redHit);
}
}
public void TurnOffRedHit(string redHit)
{
if (!tree.TurnOffRedHit(redHit))
{
Debug.LogError("Failed to turn off the redHit, redHit:" + redHit);
}
}
public void OnRedHitChange(string key, int value)
{
arg.Key = key;
arg.Value = value;
GameEntry.Event.Fire(this, arg);
}
}
}
4. 结束咯
这样的话我们的红点系统就完成了。另外我在考虑用字典的方式来做这个红点系统,后面有做的话,可能再处理吧。最后,参考文章:https://linxinfa.blog.csdn.net/article/details/121899276文章来源地址https://www.toymoban.com/news/detail-739902.html
到了这里,关于[游戏开发]Unity红点系统_树实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!