[游戏开发]Unity红点系统_树实现

~ [目录] ~

  • 0. 前言
  • 1. 红点系统
    • (1)种类
    • (2)结构
    • (3)约定
  • 2. 红点树
    • (1)树节点
    • (2)树_增删查
    • (3)树_红点处理
  • 3. 封装、检查
    • (1)检查
    • (2)UGF封装为组件
  • 4. 结束咯

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做了一下封装。

using GameFramework;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityGameFramework.Runtime;
using System;
using EventArg;

namespace GDT
{
    /// 
    /// 自定义红点组件
    /// 
    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

你可能感兴趣的:(游戏开发,unity,c#)