【无标题】

文章详细介绍了游戏开发中设计系统的过程,包括红点系统的优化、对象池的实现以降低GC压力、事件系统的高效设计以及无限列表滚动窗口的原理和应用。同时,文章还讨论了背包系统、状态系统和寻路系统等,分享了在性能和可维护性方面的经验与思考。
摘要由CSDN通过智能技术生成

前言

设计系统的过程

以前:有没有思路,有思路就直接去实现,没有思路就网上找参考再实现。

现在:无论有没有思路,先记下来,然后去网上参考,然后对比自己的思路,进行拓展和修改,最后实现。

等级评判

我会对每个系统设计好坏、每个知识掌握程度,都会给一个主观评价,评判等级如下

  • 好评如潮
  • 特别好评
  • 多半好评
  • 褒贬不一
  • 多半差评
  • 差评如潮

面试前研究的系统

红点系统(好评如潮)

参考

树的应用(一)- 基于TrieTree(前缀树)的红点系统 - 知乎 (zhihu.com)

在这里插入图片描述

一种树状结构,其实节点间类似于继承的关系,我思来想去觉得觉得不够灵活,俗话说得好,组合大于继承。那么如果继承+组合岂不是大于组合。

类图

在这里插入图片描述

集合可以关注集合,也可以直接关注模块。若策划更改需求,某个子集合关注的某个模块父集不感兴趣了,只需要让父集合直接关注子集合的模块即可。

或者使用更加简单粗暴方法就是,所有集合都关注自己感兴趣的模块,集合之间互不相干,这是纯组合的方式。

Mgr,负责模块和集合之间值的更新。

需求1 避免重复调用

当某个模块红点发生变化,就会标记相应集合,并且将脏的模块集合加入的mgr的相应list中

    /// <summary>
    /// 新增了红点进行调用。未激活状态,SetDirty不会导致数据变化
    /// </summary>
    public void SetDirty(bool force = false)
    {
        if (!_isDirty &&(force || _active))
        {
            _isDirty = true;
            SetAllRedCollectionDirty();
            _redMgr.AddDirtyRedModule(this);
        }
    }

Mgr负责让更新脏模块和脏集合(做了分帧处理)

        if (FramingTool.Check(EFraming.Framing1) && _dirtyRMs.Count != 0)
        {
            foreach(var rm in _dirtyRMs)
            {
                rm.CalculationValue();
            }
            _dirtyRMs.Clear();
            foreach (var rc in _dirtyRCs)
            {
                rc.CalculationValue();
            }
            _dirtyRCs.Clear();
        }

需求2 动态规划的计算相应的值(提高了不可感知的性能)

下面代码会从子节点开始计算,然后递归计算值。

    public void CalculationValue()
    {
        _value = 0;
        foreach (var rm in _rms)
        {
            _value += rm.GetValue();
        }
        foreach(var rc in _rcs)
        {
            if (rc.IsDirty)
            {
                rc.CalculationValue();
            }
            _value += rc.GetValue();
        }
        _action(_value);
        IsDirty = false;
    }

需求3 避免集合之间互相依赖以及避免重复关注模块

    public void AddRedCollection(IRedCollection iRedCollection)
    {
        if (_rcs == null)
        {
            _rcs = new List<IRedCollection>();
        }
        _rcs.Add(iRedCollection);
        iRedCollection.AddParentRedCollection(this);
        //发现逻辑错误,可以避免rc互相认爹和重复性添加,比直接用HashSet更好
#if UNITY_EDITOR
        if (_testRMs == null)
        {
            _testRMs = new List<IRedModule>();
        }
        foreach(IRedModule rm in iRedCollection.GetRedModules())
        {
            _testRMs.AddCheckRepeat(rm);
        }
#endif
    }

需求4模块支持开关

采用位枚举的方式,存档系统只需要保存一个long即可。

public enum ERedModel
{
    ERedModel1 = 1<<1,
    ERedModel2 = 1<<2,
}

模块的激活变动会导致变脏

    public int GetValue()
    {
        if (!_active)
        {
            return 0;
        }
        return _value;
    }

    public void SetModuleActive(bool active)
    {
        if (_active != active)
        {
            _active = active;
            SetDirty(true);
        }
    }

需求5 不只是红点

如果某个任务系统,需要用红点显示有新任务,用完成标志显示任务完成。

红点系统和完成标志系统等等功能上是否类似,所以我让mgr抽象化,只要实现InitRedMgr方法即可搞出两套独立的mgr

    /// <summary>
    ///根据需求init,因为可能存在红点/新物品等多种红点,而他们其实要分开管理的,所有
    ///1.创建需要的红点模块
    ///2.创建红点集合,并且将其关注相应的模块
    ///3.将所有module模块SetDirty(true),重置UI的红点
    /// </summary>
    /// <param name="eSwitch">模块开关,位枚举</param>
    protected abstract void InitRedMgr(ERedModel eSwitch);

而模块会以一个变量的形式依托于成就系统,成就系统只需要给模块一个计算红点的方法,以及合适时机调用SetDirty即可。而成就系统若需要红点,完成,新成就等等,只需要多一个变量存相应模块即可。

对象池(好评如潮)

总结

有效降低GC压力,甚至一些List这些集合类也可以放入对象池中,避免List扩容导致GC

回收对象的生命周期

  1. 创建Create(存在默认值,非必须)
  2. 回收中出来的Init(由于Init需要各种参数,无法必须)
  3. OnRecycle(必须)
  4. 执行销毁(非必须)

所以接口定义为

public interface IPoolable
{
    public bool IsRecycled { get; set; }
    public void OnRecycle();
}

其他非必须的采用委托代替

    public Func<T> Create;
    public Action<T> InitFun;
    public Action<T> ToDostory;

实现线程安全的对象池

对象池都需要有一个池子进行保存对象,我使用的是Stack,但是普通的Stack不保证线程安全,而ConcurrentStack与Stack又没有公共的接口,所以需要自己定义一个公共接口

public interface IKStack<T>
{
    public int Count { get; }
    public bool TryPop(out T t);
    public void Push(T t);
    public void Clear();
}
public class KStack<T> : Stack<T>, IKStack<T>{}
public class KSafeStack<T> : ConcurrentStack<T>, IKStack<T>{}

对象池抽象类的池子使用接口IKStack即可,对象池实现类赋予具体的IKStack

public abstract class AbsPool<T> where T : IPoolable,new()
{
    protected IKStack<T> _stack;
    public int Count { get => _stack.Count; }
    private int _maxCount;
    private Func<T> CreateFun;
    private Action<T> InitFun;
    private Action<T> DostoryFun;
    
    public void Init(int addCache, int maxCount, Func<T> create, Action<T> initFun, Action<T> dostoryFun);
    //事先创建一些缓存
    public void AddCache(int initCount);
    public void Recycle(T t);
    public T Allocate();
    public void Clear();
}


public class Pool<T> : AbsPool<T> where T : IPoolable, new()
{
    public readonly static Pool<T> Inst = new Pool<T>();
    public Pool() 
    {
        _stack = new KStack<T>();
    }
}

public class SafePool<T> : AbsPool<T> where T : IPoolable, new()
{
    public readonly static SafePool<T> Inst = new SafePool<T>();
    public SafePool()
    {
        _stack = new KSafeStack<T>();
    }
}

Inst采用字段赋值,避免多线程问题,不会浪费空间,赋值是发生在对象第一次调用的时候。

集合类扩容问题

public class ListPoolAble<T> : List<T>, IPoolable
{
    public bool IsRecycled { get; set; }

    public void OnRecycle()
    {
        Clear();
    }
}

事件系统(好评如潮)

总结

支持泛型、泛型+枚举、泛型+字符串等作为Key作为索引。

使用结构体作为事件参数,可以0GC。

主要功能

注册,注销,注销调用,触发

Key

  • 泛型
  • 泛型+枚举
  • 泛型+字符串

注销调用

public interface IUnRegister<T>
{
    public KEventDelegate<T> Event { get; set; }
    public KEventDelegate<T> OnEvent { get; set; }
    public void UnReg(ref IUnRegister<T> t);
}
public class UnRegister<T> :IUnRegister<T>
{
    public KEventDelegate<T> Event { get; set; }
    public KEventDelegate<T> OnEvent { get; set; }

    public UnRegister(KEventDelegate<T> e, KEventDelegate<T> onE)
    {
        Event = e;
        OnEvent = onE;
    }
    public void UnReg(ref IUnRegister<T> t)
    {
        Event -= OnEvent;
        t = null;
    }
}

实现

在这里插入图片描述

池子

由于KEvent是泛型,避免多个事件池,所以额外搞了一个静态类。

public enum EKOrder
{

}
public static class EventStr
{
    public const string Bag = "Bag";
}
public static class KEventMap
{
    public readonly static Dictionary<EKOrder,object> EKOrderData;
    public readonly static Dictionary<string, object> StringData;
    static KEventMap()
    {
        var c = typeof(EKOrder).GetEnumValues().Length;
        EKOrderData = new Dictionary<EKOrder, object>(c);
        c = typeof(EventStr).GetFields().Length;
        StringData = new Dictionary<string, object>(c);
    }
}

struct优于class

aaa是struct,bbb是class,this是MonoBehaviour

            var ab1 = this;
            var ab2 = this;
            var ab3 = this;
            var ab4 = this;
            var ab5 = this;
            if (a == 0)
            {
                for (int i = 0; i < 100000; i++)
                {
                    KEvent<aaa>.Trigger(new aaa(1, 1, 1, 1, 1, ab1, ab2, ab3, ab4, ab5));
                }
            }
            if (a == 1)
            {
                for (int i = 0; i < 100000; i++)
                {
                    KEvent<bbb>.Trigger(new bbb(1, 1, 1, 1, 1, ab1, ab2, ab3, ab4, ab5));
                }
            }
            //struct inStruct  class   classGC
            //结构复杂度:5个int 5个引用 
            //11ms    13ms      26ms     7.6mb
            //(我估测)struct越简单,引用越少,速度越明显
            //我认为struct上限和下限区别很大,但是速度下限是class,况且GC吊打class
            //有in更快因为避免了值拷贝
            //以上测试是基于struct很快就会被释放,如果struct不是很快被释放的化,可能其他方面性能不如class吧。

避免struct值拷贝

使用in修饰符避免值拷贝

由于Action无法使用in/ref修饰符,所以我用了我不熟悉的delegate。

    public delegate void KEventDelegate<T>(in T t);

当然in可以换成ref,但是实际上应该不存在事件中修改值的情况吧。

无限列表的滚动窗口(好评如潮)

原理

通过监听窗口的滚动,改变Item的位置,和Item里面的元素重新刷新。

仅仅实例化少量的Item,即可展示无限个Item。

作用

不知道无限列表之前,我采用有限列表,打开和关闭页面都会卡到飞起。用了携程也好不到哪里去。

有了无限列表,10000000000000000个Item也不怕。

实现

1.Content根据Item数量扩容

2.Content的描点要设置好,避免扩容后跑中间去了。

public class InfiniteList : MonoBehaviour
{
    private float _itemHeight;
    //Content扩容前高度
    private float _contentHeight;
    public GameObject itemGO;
    public RectTransform Content;
    //间隔距离
    public float Spacing;
    //第一个开始的地方
    private Vector3 _firstPos;
    //间隔的距离 = _itemHeight+Spacing
    private Vector3 _spacingPos;
    //实例化的数量
    private int _itemCount;
    //实例化列表
    private Item[] _list;
    //列表数量
    public int Count;
    // Start is called before the first frame update
    void Start()
    {
        var a = new Item(itemGO);
        if (Count == 0)
        {
            a.Cg.alpha= 0;
            return;
        }
        else
        {
            a.Cg.alpha = 1;
        }
        _firstPos = a.RectTransform.anchoredPosition;

        _contentHeight = Content.sizeDelta.y;
        _itemHeight = Spacing + a.RectTransform.sizeDelta.y;

        _itemCount = Math.Min((int)(_contentHeight / _itemHeight + 2),Count);

        _spacingPos = new Vector3(0, _itemHeight);
        Content.sizeDelta = new Vector2(Content.sizeDelta.x, Count * _itemHeight);
        _list = new Item[_itemCount];
        _list[0] = a;
        for(int i = 1; i < _itemCount; i++)
        {
            var b =Instantiate(itemGO, Content.transform);
            _list[i] = new Item(b);
        }
        for (int i = 0; i < _itemCount; i++)
        {
            _list[i].ListIndex.text = i.ToString();
        }
        OnUpdate(Vector2.zero);
    }
    public void OnUpdate(Vector2 pos)
    {
        //最前面的index
        int index = (int)(Content.anchoredPosition.y / _itemHeight);
        //当前轮次
        int lun = (int)(Content.anchoredPosition.y / (_itemHeight * (_itemCount)));
        //最前面是当前轮的第几个
        int lunIndex = index % _itemCount;

        //更新当前轮的
        for (int i = lunIndex; i < _list.Length; i++)
        {
            RefreshItem(_list[i], i + lun * _itemCount);
        }

        //更新下一轮的
        for (int i = 0; i < lunIndex; i++)
        {
            RefreshItem(_list[i], i + (lun + 1) * _itemCount);
        }
    }
    public void RefreshItem(Item item,int index)
    {
        if (item.CurIndex != index)
        {
            item.CurIndex = index;
            item.RectTransform.anchoredPosition = _firstPos - _spacingPos * item.CurIndex;
            item.Index.text = item.CurIndex.ToString();
        }
    }
}
public class Item
{
    public int CurIndex = -1;
    public RectTransform RectTransform;
    public Text ListIndex;
    public Text Index;
    public CanvasGroup Cg;

    public Item(GameObject gameObject)
    {
        RectTransform = gameObject.GetComponent<RectTransform>();
        ListIndex = gameObject.transform.Find("ListIndex").GetComponent<Text>();
        Index = gameObject.transform.Find("Index").GetComponent<Text>();
        Cg = gameObject.GetComponent<CanvasGroup>();
    }
}

可能存在的问题

由于Item里面的元素如果加载很耗时间的化,可能会导致刷新不及时。甚至可能因为加载资源的异步问题导致新的内容被旧内容替换。

背包系统(褒贬不一)

Model

public class ItemData
{
    public readonly string Id;
    public int Count;
}
//死数据,存在配置表中,
public class ItemDieData
{
    public string Id;
    public string Name;
    public string Asset;
    public EItem EItem;
    public string Description;
}

Bag–对ItemData进行统一管理操作

public class Bag
{
    private Dictionary<string, ItemData> _data;
    public readonly string _bagId;
    public readonly bool _needSave;
}
增删查改
  • 增删 Add和Del,
  • 查 GetReadOnlyData
实现原子交易
  • CheckDel 检查是否满足数量
  • UnSafeDel 不做任何检查处理,若数量<=0,则删除数据,检查Del是否数量充足
  • SafeDel 内部调用CheckCount ,满足数量则会进行UnSafeDel
  • 外部若进行Del,可以先调用CheckCount,然后调用UnSafeDel
  • CheckAdd检查背包是否放的下(根据实际情况进行拓展)
BagsModel – 所有Bag的的数据和管理
public class BagsModel
{
    //玩家背包  根据玩家存档信息生成数据
    //Npc背包   根据配置表生成数据,可能随机,可能固定,可能从存档加载,等等根据需求而定
    public Bag PlayerBag { get; private set; }
    private Dictionary<string,Bag> _bags;
    private Dictionary<string, Bag> _savebags;
    public BagsModel()
    {
        //加载玩家背包信息

        //尝试加载_saveBags信息

        _bags = new Dictionary<string, Bag>();
        _bags.AddRange(_savebags);
    }
    public Bag GetBag(string id)
    {
        if(_bags.TryGetValue(id,out var result))
        {
            return result;
        }
        else
        {
            //根据ID加载配置表中信息

            //创建出来后如果是needSave的,放入_savebags
            return null;
        }
    }
    public void Save()
    {
        //将_savebags保存
    }
}


知识点

事件队列(无评价)

不知道应用场景,而且找不到一个好的详细的参考的设计思路,自己开脑洞和网上差不多。

MVC(多半差评)

多半差评意思是我对这方面掌握和理解,目前感觉有点迷茫

参考QFreamework,但是我不太理解的是命令模式

在这里插入图片描述

UniTask

可以让异步方法不是以回调的方式,注册一个事件,西注册一个事件。

而是可以让多个异步在写在一个代码当中。

比如,法术释放的代码

{

await 法术准备(每帧)

await 法术飞向敌人(每帧)

await 外部一个触发(这需要某个系统进行触发,起到流程控制的作用)

await 对敌人造成伤害()

}

以上代码统统写在一起,没有一堆回调,整个流程取消应该也很方便。

好吧思来想去还是不合适。

功能

1.消耗小

2.支持取消和内部完成

3.WhenAll,WhenAny,WhenUnti

4.支持在Unity各个生命周期的await。

5.自定义PlayerLoop

6.Forget是什么没搞懂

.NET 类型UniTask 类型
Task/ ValueTaskUniTask
Task/ ValueTaskUniTask
async voidasync UniTaskVoid
+= async () => { }UniTask.Void, UniTask.Action, UniTask.UnityAction
UniTaskCompletionSource
TaskCompletionSourceUniTaskCompletionSource/ AutoResetUniTaskCompletionSource
ManualResetValueTaskSourceCoreUniTaskCompletionSourceCore
IValueTaskSourceIUniTaskSource
IValueTaskSourceIUniTaskSource
ValueTask.IsCompletedUniTask.Status.IsCompleted()
ValueTask.IsCompletedUniTask.Status.IsCompleted()
new ProgressProgress.Create
CancellationToken.Register(UnsafeRegister)CancellationToken.RegisterWithoutCaptureExecutionContext
CancellationTokenSource.CancelAfterCancellationTokenSource.CancelAfterSlim
Channel.CreateUnbounded(false){ SingleReader = true}Channel.CreateSingleConsumerUnbounded
IAsyncEnumerableIUniTaskAsyncEnumerable
IAsyncEnumeratorIUniTaskAsyncEnumerator
IAsyncDisposableIUniTaskAsyncDisposable
Task.DelayUniTask.Delay
Task.YieldUniTask.Yield
Task.RunUniTask.RunOnThreadPool
Task.WhenAllUniTask.WhenAll
Task.WhenAnyUniTask.WhenAny
Task.CompletedTaskUniTask.CompletedTask
Task.FromExceptionUniTask.FromException
Task.FromResultUniTask.FromResult
Task.FromCanceledUniTask.FromCanceled
Task.ContinueWithUniTask.ContinueWith
TaskScheduler.UnobservedTaskExceptionUniTaskScheduler.UnobservedTaskException

其他

1.迭代器中的MoveNext,(比用WaitUnitil好吧,因为WaitUntil应该是基于循环不可控制,而Onclick应该是基于事件的)

在这里插入图片描述

2.这个有点难搞,目前我觉得,只是迭代器实现不了,需要自己在Movenext前加判断,所以有点意思

在这里插入图片描述

自己游戏中的系统

配置表系统(多半好评–屎山但能用)

基于微软Json插件和NPOI的xlxs插件和反射

实现功能

  1. 生成对应配置表(类型和注解的生成)
  2. 配置表一对多(但是不支持互相引用)
  3. 校验(其实只是完成了设计)

ECS多线程方案(褒贬不一)

实现多线程方式

Task(高GC,速度漫)

UniTask(低GC,速度一般,其他功能好像不错)

Job System(0GC,速度快,其他功能未知)

无锁多线程模型

1.准备需要用到的参数,比如transform.position这些参数是不能在其他线程调用的,先在主线程准备好,通过一个args数组储存

2.开启多线程,将结果存储results数组中

3.多线程结束后,拿到结果,在主线程修改和赋值

(Job System和我想得差不多,抄袭我的,没错)

异步模型

1.甩手掌柜型

就是丢个另外一个线程,然后就不管了的类型。

性能:高

复杂度:低

问题:应用场景少

2.回调型

由于调用时机不可控,所以回调方法不好搞。

第一帧我需要攻击范围内所有敌人,所有我发起多线程,注册回调是有结果后更新,问题是第二帧了,我被击晕了,不能攻击了,第三针回调执行了,在晕眩状态下进行攻击, 这就出问题了。所以回调执行的时候还要判断一次状态是否合适,这样代码复杂度提高了。

性能:中/高(无委托GC且回调判断简单)

复杂度:高(微观上)

3.等到你搞定型

一帧内循环等待所有线程,直到全部搞定。

比如

  1. 系统1寻找敌人,
  2. 系统2进行画面渲染逻辑
  3. 系统3进行ICheckTime,将一些过期的东西干掉
  4. 系统4寻路(不移动)
  5. 系统5 GC(如果能判断上面系统目前花费时间,而且花费时间较少的话,是不是可以尝试进行一下GC,这是我瞎猜的)
  6. 系统6根据找到敌人进行攻击(这里进行等待,由于中间穿插了其他系统,所以性能发挥比较好)
  7. 系统7赋予敌人伤害和buff,由于这个系统是会影响系统6敌人的逻辑的,比如击晕等等,所以要在系统6后面,若1和6中穿插了很多其他系统,多线程结果可能早早就搞定了。

性能:中(等待时间长)或高(无等待)

复杂度:高(宏观上)

4.协程等待型

就是协程+多线程。情况和回调型类似。

空间分区碰撞系统(褒贬不一)

待优化

四叉树

多线程方案改为JobSystem

二分空间

使用Filter过滤器替换委托,实现0GC和距离精确检测。

原理

根据格子进行碰撞。人物每次移动都会更新对应格子。检测时,只需要检测对应范围的格子,从格子中取出目标即可。检测范围越大,消耗越大。

消耗点

1.更新地图,若人物移动时更新格子没有改变,不更新,反之更新格子数据,并且移除缓存(低消耗,高调用)

2.获取人物攻击范围内格子,for遍历出来,会缓存(中消耗,但少调用)

3.从格子取出敌人(超高消耗,中调用)

速度测试

cpu是12400F,6核12线程,Debug模式

测试1(消耗点3)

1000个人10范围的格子

Debug模式总gcGCAvg消耗1%Max消耗0.1%Max消耗
单线程几乎019.2ms19.2ms19.2ms
UniTask144kb左右7.15ms14ms28ms
Task844kb左右9.3ms23ms60ms
Job
Release模式gcGCAvg消耗1%Max消耗0.1%Max消耗
单线程几乎07.5ms7.5ms7.5ms
UniTask144kb左右5.5ms9ms17ms
Task844kb左右6.8ms21ms26ms
Job

如果打包会更快,但是就不测打包了。奇怪的是,ILCpp的打包,多线程比Mono卡。

Dbg系统(多半好评)

微软的Json进行序列化,使输出支持普通对象,

支持颜色输出

寻路系统(褒贬不一)

总结:够用就行。

残缺速度可能还慢,一个简单的寻路。

性能:由于没有做网格划分,和多线程,所以性能上可能不太行。

功能:不支持动态避障。

RTS的框选系统(多半好评)

魔改商店里面的插件的。

状态系统(多半差评)

总结:一塌糊涂,讲究用

主要问题是和状态机之间的配合,由于对状态机事件的不了解,连生命周期都不清楚,搞出了很多问题。

本想用状态模式,但是实际上还是离不开不快判断条件.

Moba属性系统(差评如潮)

后知后觉太烂了,失败,准备重构

MobaBuff系统(差评如潮)

后知后觉太烂了,失败,准备重构

数学方面

闭卷:向量方面70分,矩阵35分,四元数0分

开卷:向量80分,矩阵60分,四元数0分

shader

能力

  • 实现过:边缘光,保护罩(扰动,接触面光),消融,阴影。
  • 可能能实现:比如水流动,后期处理
  • 有一丢丢希望能实现:水流动,雪地
  • 但是如果涉及粒子系统就不行了。粒子系统接触不多。

知识面

冯乐乐的书看了12章左右。learnOpengl网站学到了高级OpenGl篇。

在这里插入图片描述

12章内容能理解百分之70左右,但是一些变换矩阵的推理就有点难搞,还有切线空间的法线贴图,也不太理解。

对于shader语法有个大概了解。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值