前言
设计系统的过程
以前:有没有思路,有思路就直接去实现,没有思路就网上找参考再实现。
现在:无论有没有思路,先记下来,然后去网上参考,然后对比自己的思路,进行拓展和修改,最后实现。
等级评判
我会对每个系统设计好坏、每个知识掌握程度,都会给一个主观评价,评判等级如下
- 好评如潮
- 特别好评
- 多半好评
- 褒贬不一
- 多半差评
- 差评如潮
面试前研究的系统
红点系统(好评如潮)
参考
树的应用(一)- 基于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
回收对象的生命周期
- 创建Create(存在默认值,非必须)
- 回收中出来的Init(由于Init需要各种参数,无法必须)
- OnRecycle(必须)
- 执行销毁(非必须)
所以接口定义为
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/ ValueTask | UniTask |
Task/ ValueTask | UniTask |
async void | async UniTaskVoid |
+= async () => { } | UniTask.Void, UniTask.Action, UniTask.UnityAction |
— | UniTaskCompletionSource |
TaskCompletionSource | UniTaskCompletionSource/ AutoResetUniTaskCompletionSource |
ManualResetValueTaskSourceCore | UniTaskCompletionSourceCore |
IValueTaskSource | IUniTaskSource |
IValueTaskSource | IUniTaskSource |
ValueTask.IsCompleted | UniTask.Status.IsCompleted() |
ValueTask.IsCompleted | UniTask.Status.IsCompleted() |
new Progress | Progress.Create |
CancellationToken.Register(UnsafeRegister) | CancellationToken.RegisterWithoutCaptureExecutionContext |
CancellationTokenSource.CancelAfter | CancellationTokenSource.CancelAfterSlim |
Channel.CreateUnbounded(false){ SingleReader = true} | Channel.CreateSingleConsumerUnbounded |
IAsyncEnumerable | IUniTaskAsyncEnumerable |
IAsyncEnumerator | IUniTaskAsyncEnumerator |
IAsyncDisposable | IUniTaskAsyncDisposable |
Task.Delay | UniTask.Delay |
Task.Yield | UniTask.Yield |
Task.Run | UniTask.RunOnThreadPool |
Task.WhenAll | UniTask.WhenAll |
Task.WhenAny | UniTask.WhenAny |
Task.CompletedTask | UniTask.CompletedTask |
Task.FromException | UniTask.FromException |
Task.FromResult | UniTask.FromResult |
Task.FromCanceled | UniTask.FromCanceled |
Task.ContinueWith | UniTask.ContinueWith |
TaskScheduler.UnobservedTaskException | UniTaskScheduler.UnobservedTaskException |
其他
1.迭代器中的MoveNext,(比用WaitUnitil好吧,因为WaitUntil应该是基于循环不可控制,而Onclick应该是基于事件的)
2.这个有点难搞,目前我觉得,只是迭代器实现不了,需要自己在Movenext前加判断,所以有点意思
自己游戏中的系统
配置表系统(多半好评–屎山但能用)
基于微软Json插件和NPOI的xlxs插件和反射
实现功能
- 生成对应配置表(类型和注解的生成)
- 配置表一对多(但是不支持互相引用)
- 校验(其实只是完成了设计)
ECS多线程方案(褒贬不一)
实现多线程方式
Task(高GC,速度漫)
UniTask(低GC,速度一般,其他功能好像不错)
Job System(0GC,速度快,其他功能未知)
无锁多线程模型
1.准备需要用到的参数,比如transform.position这些参数是不能在其他线程调用的,先在主线程准备好,通过一个args数组储存
2.开启多线程,将结果存储results数组中
3.多线程结束后,拿到结果,在主线程修改和赋值
(Job System和我想得差不多,抄袭我的,没错)
异步模型
1.甩手掌柜型
就是丢个另外一个线程,然后就不管了的类型。
性能:高
复杂度:低
问题:应用场景少
2.回调型
由于调用时机不可控,所以回调方法不好搞。
第一帧我需要攻击范围内所有敌人,所有我发起多线程,注册回调是有结果后更新,问题是第二帧了,我被击晕了,不能攻击了,第三针回调执行了,在晕眩状态下进行攻击, 这就出问题了。所以回调执行的时候还要判断一次状态是否合适,这样代码复杂度提高了。
性能:中/高(无委托GC且回调判断简单)
复杂度:高(微观上)
3.等到你搞定型
一帧内循环等待所有线程,直到全部搞定。
比如
- 系统1寻找敌人,
- 系统2进行画面渲染逻辑
- 系统3进行ICheckTime,将一些过期的东西干掉
- 系统4寻路(不移动)
- 系统5 GC(如果能判断上面系统目前花费时间,而且花费时间较少的话,是不是可以尝试进行一下GC,这是我瞎猜的)
- 系统6根据找到敌人进行攻击(这里进行等待,由于中间穿插了其他系统,所以性能发挥比较好)
- 系统7赋予敌人伤害和buff,由于这个系统是会影响系统6敌人的逻辑的,比如击晕等等,所以要在系统6后面,若1和6中穿插了很多其他系统,多线程结果可能早早就搞定了。
性能:中(等待时间长)或高(无等待)
复杂度:高(宏观上)
4.协程等待型
就是协程+多线程。情况和回调型类似。
空间分区碰撞系统(褒贬不一)
待优化
四叉树
多线程方案改为JobSystem
二分空间
使用Filter过滤器替换委托,实现0GC和距离精确检测。
原理
根据格子进行碰撞。人物每次移动都会更新对应格子。检测时,只需要检测对应范围的格子,从格子中取出目标即可。检测范围越大,消耗越大。
消耗点
1.更新地图,若人物移动时更新格子没有改变,不更新,反之更新格子数据,并且移除缓存(低消耗,高调用)
2.获取人物攻击范围内格子,for遍历出来,会缓存(中消耗,但少调用)
3.从格子取出敌人(超高消耗,中调用)
速度测试
cpu是12400F,6核12线程,Debug模式
测试1(消耗点3)
1000个人10范围的格子
Debug模式 | 总gcGC | Avg消耗 | 1%Max消耗 | 0.1%Max消耗 |
---|---|---|---|---|
单线程 | 几乎0 | 19.2ms | 19.2ms | 19.2ms |
UniTask | 144kb左右 | 7.15ms | 14ms | 28ms |
Task | 844kb左右 | 9.3ms | 23ms | 60ms |
Job |
Release模式 | gcGC | Avg消耗 | 1%Max消耗 | 0.1%Max消耗 |
---|---|---|---|---|
单线程 | 几乎0 | 7.5ms | 7.5ms | 7.5ms |
UniTask | 144kb左右 | 5.5ms | 9ms | 17ms |
Task | 844kb左右 | 6.8ms | 21ms | 26ms |
Job |
如果打包会更快,但是就不测打包了。奇怪的是,ILCpp的打包,多线程比Mono卡。
Dbg系统(多半好评)
微软的Json进行序列化,使输出支持普通对象,
支持颜色输出
寻路系统(褒贬不一)
总结:够用就行。
残缺速度可能还慢,一个简单的寻路。
性能:由于没有做网格划分,和多线程,所以性能上可能不太行。
功能:不支持动态避障。
RTS的框选系统(多半好评)
魔改商店里面的插件的。
状态系统(多半差评)
总结:一塌糊涂,讲究用
主要问题是和状态机之间的配合,由于对状态机事件的不了解,连生命周期都不清楚,搞出了很多问题。
本想用状态模式,但是实际上还是离不开不快判断条件.
Moba属性系统(差评如潮)
后知后觉太烂了,失败,准备重构
MobaBuff系统(差评如潮)
后知后觉太烂了,失败,准备重构
数学方面
闭卷:向量方面70分,矩阵35分,四元数0分
开卷:向量80分,矩阵60分,四元数0分
shader
能力
- 实现过:边缘光,保护罩(扰动,接触面光),消融,阴影。
- 可能能实现:比如水流动,后期处理
- 有一丢丢希望能实现:水流动,雪地
- 但是如果涉及粒子系统就不行了。粒子系统接触不多。
知识面
冯乐乐的书看了12章左右。learnOpengl网站学到了高级OpenGl篇。
12章内容能理解百分之70左右,但是一些变换矩阵的推理就有点难搞,还有切线空间的法线贴图,也不太理解。
对于shader语法有个大概了解。