今年的新改变是打算偶尔写一点点技术向的文章,不再是只写流水帐了,单纯记录一下一些用过的东西和框架。
本来想着整一套口播 +whisper+Cursor 的自动出视频教程文案的工作流的,奈何有点失败,为此还花了 20 刀买了一个月的 CursorPro。这篇暂时还是先用自己手写了。
因为最近想要花点时间,在重写自己的 GameFramework,而且有开源的意向(虽然估计也没人看就是了)
去年也写了不少的屎山代码。最夸张的时候两个月就能造 9800 多行的 C#屎山代码,这么多的屎山代码里总有一些是能够存留下来,接着为后面的屎山做出巨大贡献的。所以今年可能会有这么一个系列,来统述我逐渐堆积屎山的过程。
现在大概想到的能够放进屎山工具箱的内容可能有:
希望新的一年能够真的实打实地输出一些东西吧
今天来介绍两个我在做自己的 GameFramework 的时候接触到的两个框架:
Unity 游戏引擎的超快 DI(依赖项注入)。“V”表示使 Unity 的初始“U”更细、更坚固..!
Zenject 太棒了。但是,VContainer 具有以下优点:
MessagePipe 是适用于 .NET 和 Unity 的高性能内存/分布式消息传递管道。它支持所有使用 Pub/Sub 的情况、CQRS 的 mediator 模式、Prism 的 EventAggregator(V-VM 解耦)、IPC(进程间通信)-RPC 等。
项目地址:https://github.com/hadashiA/VContainer
如果你对 DI/Ioc 还不是很了解,可以自己去看一下相关的视频或者讲解文章,这里就不再赘述了。
这里只写一个最简单的需求来展示一下基础的写法,如果我的理解和解释还有问题的还请指出。
我们想要这样一个功能:
一个简单的切换 Tab,我们希望点击 tab 页面的时候能够切换,然后如果点击到某些页面的时候(比如空白页面),显示一个错误的小 Tip
我们大概需要两个功能模块:一个负责处理点 Tab 相关的 UI/数据操作,另一个需要一个 MessageLayerMgr 类作为单例,来处理显示错误小 Tip 的功能。
这个代码直接用 C#在 Monobehaviour 里写是很简单的,但是现在我们有了伟大的 VContainer,所有的代码都可以写在
非 MonoBehaviour 中了
这个类的声明很简单,代码如下
1namespace Project.Script.UIFramework
2{
3 public class MessageLayerMgr
4 {
5 public void ShowErrorTip(string message)
6 {
7 Debug.Log($"Error: {message}");
8 }
9 }
10}
没什么特别的,就只有一个方法,展示错误 Tip(具体实现就先不写了)
创建这个代码,并且将之挂到一个场景里的 GameObject 上
1namespace Project.Script.UIFramework
2{
3 public class GameLiftetimeScope : LifetimeScope
4 {
5 protected override void Configure(IContainerBuilder builder)
6 {
7 builder.Register<MessageLayerMgr>(Lifetime.Singleton);
8 }
9
10 }
11}
注意要手动指定 Inject 的 GameObject.当然这个 GameObject 只要是一个父节点即可,VContainer 会自动遍历所有的子节点
然后在使用的时候,只需要在 Canvas 下面的任意子节点的 MonoBehaviour 中
1 [Inject] MessageLayerMgr messageLayerMgr;
2
3 private void OnClickTab(int index)
4 {
5 messageLayerMgr.ShowErrorTip("Error");
6 }
7}
这里简单说下 UI 的文件,UI 功能上分离为这么几个文件
具体的一些思路和写法可以看 https://vcontainer.hadashikick.jp/getting-started/hello-world **6. Inversion of Control (IoC) **的部分,可以根据那个形式写出自己的代码。
这里只说一下怎么和刚才的注入结合起来。
我的方案是自己定义一个新的**UILiftetimeScope,**内容大概就是注入 EasyTabPresenter,EasyTabService,UIEasyTab,并且将 EasyTabPresenter 加入到 EntryPoint
1namespace Project.Script.UIFramework
2{
3 public class UILifeTimeScope : LifetimeScope
4 {
5 protected override void Configure(IContainerBuilder builder)
6 {
7 builder.RegisterEntryPoint<EasyTabPresenter>();
8 builder.Register<EasyTabService>(Lifetime.Scoped);
9 builder.RegisterComponent(GetComponentInChildren<UIEasyTab>());
10 }
11 }
12}
然后只需要将这个 UILifeTimeScope 挂到 UI 上,并且指定一下 LifeTimeScope 的父对象
这样即使有很多个 EasyTab,最后的效果也是正确的
项目地址:https://github.com/Cysharp/MessagePipe
这里只说几个我觉得用得上的功能
最大的卖点。可以非常优雅地写 Publishe 和 subscribe
说实在话我确实想不出来非单例的订阅和发布有啥意义,可能某些特殊的需求下需要将不同种类的订阅发布隔离开来把(比如网络消息和其他应用中消息)
首先注册
1// RegisterMessagePipe returns options.
2var options = builder.RegisterMessagePipe(/* configure option */);
3
4// Setup GlobalMessagePipe to enable diagnostics window and global function
5builder.RegisterBuildCallback(c => GlobalMessagePipe.SetProvider(c.AsServiceProvider()));
6
7// RegisterMessageBroker: Register for IPublisher<T>/ISubscriber<T>, includes async and buffered.
8builder.RegisterMessageBroker<int>(options);
这里注册了一个 Action<int> 的 Puslisher/subscriber(以下简称 PB),需要注意的是,所有的该类型的订阅都会走同一个管线.也就是所有你这个类型下发布的消息都会被收到,这点特别要注意。
用起来很简单
发布端:
1public class MessageLayerMgr
2{
3 [Inject] IPublisher<int> publisher;
4
5 public void ShowErrorTip(string message)
6 {
7 Debug.Log($"Error: {message}");
8 publisher.Publish(10086);
9 }
10}
订阅端稍微麻烦一点,因为要注意 DIspose 对象,不然的话会有订阅泄露,不过 Message Pipe 提供了一个比较好的写法
1[Inject]
2ISubscriber<int> _subscriber;
3
4private IDisposable _disposable;
5
6public void Start()
7{
8 AutoBind();
9
10 OnClickTab(view.DefaultIndex);
11
12 var bag = DisposableBag.CreateBuilder();
13 _subscriber.Subscribe((input)=> Debug.Log($"Received {input}")).AddTo(bag);
14 _disposable = bag.Build();
15}
16
17public void Dispose()
18{
19 _disposable.Dispose();
20}
这样多个对象也可以交给一个_disposable 对象来管理
因为刚才的注意的问题
这里注册了一个 Action<int> 的 Puslisher/subscriber(以下简称 PB),需要注意的是,所有的该类型的订阅都会走同一个管线.也就是所有你这个类型下发布的消息都会被收到,这点特别要注意。
因此很多时候更常用的其实是带 Key 的 PB,写法也很类似,在泛型里输入两个参数即可,第一个参数便是 Key
1public class GameLiftetimeScope : LifetimeScope
2{
3 protected override void Configure(IContainerBuilder builder)
4 {
5 ...
6 builder.RegisterMessageBroker<GameObject, int>(options);
7 ...
8 }
9
10}
11
其他的用法一样,唯一的区别就是在 Publish 的时候需要传一个 GameObject 作为 Key 进去
花里胡哨,但是现在还没想到有什么用的功能。
简单来说就是能够筛选一些消息管道中的消息,通过在 Subscribe 中指定 Filter 来实现一些过滤或者附加的操作(比如打 Log)
这是 HandlerFilter 的声明
1public class MessageFilter10086<T> : MessageHandlerFilter<T>
2{
3 public override void Handle(T message, Action<T> next)
4 {
5 if (!(message is int))
6 {
7 next(message);
8 return;
9 }
10
11 var t = int.TryParse((message).ToString(), out int messageInt);
12 if (messageInt == 10086)
13 {
14 Debug.Log("Received 10086 Global");
15 return;
16 }
17
18 next(message);
19 }
20}
全局设置
1protected override void Configure(IContainerBuilder builder)
2{
3 // RegisterMessagePipe returns options.
4 var options = builder.RegisterMessagePipe(/* configure option */(t) =>
5 {
6 t.AddGlobalMessageHandlerFilter<MessageFilter10086<int>>();
7 });
针对某个单独的 Subscribe 来设置
1_subscriber.Subscribe((input)=> Debug.Log($"Received {input}"), new []{new MessageFilter10086<int>()}).AddTo(bag);
说实在话,我都已经用了消息管道了,我干嘛还要费劲手写 Event,不是非常理解。
所以我暂时认为这个功能没什么用
Request/response。经常写 Js 的应该非常熟悉的,中间件模式
目前我认知里觉得还是属于整花活系列,不过配合着 UniTask 能够整出一些有意思的功能
不过某些场景下:如网络,资源异步加载的情况下,是比较适合做这样的操作的
可以简单写一个范例
1public class TestRequestResponse1 : IAsyncRequestHandler<int, string>
2{
3 public async UniTask<string> InvokeAsync(int request, CancellationToken cancellationToken = default)
4 {
5 Debug.Log($"requestStart: {request}");
6 await UniTask.WaitForSeconds(1.0f);
7 Debug.Log($"requestEnd: {request}");
8 return $"ResponseResult:{request.ToString()}";
9 }
10}
注册到 Builder 上
1builder.RegisterAsyncRequestHandler<int, string, TestRequestResponse1>(options);
在具体类中使用
1[Inject] IAsyncRequestAllHandler<int, string> _requestAllHandler;
2
3public async UniTask StartRequest()
4{
5 var pb = _requestAllHandler.InvokeAllAsync(10086);
6 await UniTask.WhenAll(pb);
7 Debug.Log("RequestOver");
8}
甚至可以同时注入多个 Requsest
1public class TestRequestResponse1 : IAsyncRequestHandler<int, string>
2{
3 public async UniTask<string> InvokeAsync(int request, CancellationToken cancellationToken = default)
4 {
5 Debug.Log($"requestStart: {request}");
6 await UniTask.WaitForSeconds(1.0f);
7 Debug.Log($"requestEnd: {request}");
8 return $"ResponseResult:{request.ToString()}";
9 }
10}
11
12public class TestRequestResponse2 : IAsyncRequestHandler<int, string>
13{
14 public async UniTask<string> InvokeAsync(int request, CancellationToken cancellationToken = default)
15 {
16 Debug.Log($"request2Start: {request}");
17 await UniTask.WaitForSeconds(2.0f);
18 Debug.Log($"request2End: {request}");
19 return $"Response2Result:{request.ToString()}";
20 }
21}
22
23...
24protected override void Configure(IContainerBuilder builder)
25{
26 // RegisterMessagePipe returns options.
27 var options = builder.RegisterMessagePipe(/* configure option */(t) =>
28 {
29 });
30
31 // Setup GlobalMessagePipe to enable diagnostics window and global function
32 builder.RegisterBuildCallback(c =>
33 {
34 GlobalMessagePipe.SetProvider(c.AsServiceProvider());
35 });
36
37 builder.RegisterAsyncRequestHandler<int, string, TestRequestResponse1>(options);
38 builder.RegisterAsyncRequestHandler<int, string, TestRequestResponse2>(options);
39
40 builder.Register<MessageLayerMgr>(Lifetime.Singleton);
41}
42...
最后能够输出多个结果
默认情况下,多个 Request 是 parallel 执行的,你也可以通过修改 option,来让他们依次序执行
1var options = builder.RegisterMessagePipe(/* configure option */(t) =>
2{
3 t.DefaultAsyncPublishStrategy = AsyncPublishStrategy.Sequential;
4});
输出的结果就是依次执行的了
关于这个可以直接看文档的介绍,简单来说就是 subscribe 刚开始订阅的时候,可以马上返回最后一次 publish 的值
不过好像只支持 valuetype
这也是一个听起来很炫酷但是事实上我没想到有什么用的功能
没用,还是那句话,我觉得好像正常都应该是 SIngleton 的
我认为这里指的是他的 Request/response 的功能,也可能我理解错了,详情参考 sync/async
进程间和网络通信的目前我的客户端开发好像不太会用到,有兴趣的可以自己去体验一下