Ver Fonte

删除无用文件

大爷 há 2 anos atrás
pai
commit
b3593e4844

+ 0 - 72
Book/1.1运行指南.md

@@ -1,72 +0,0 @@
-# 运行步骤  
-01. visual studio用户须知:
-   - 版本:
-      - Win10及以上用户,使用.net6, VS 版本,必须使用2022及以上。注意,VS 2019 不支持 .net6,不能使用。
-   - 依赖:
-     - 需要安装"使用Unity的游戏开发"扩展。
-     - 需要在"工具-选项-适用于Unity的工具-常规"里,把杂项里的禁止完整生成项目改为False,否则导致你Codes目录里的代码报错。
-
-02. 使用Rider2022.2.1(更新到最新版), 需要安装以下内容:
-   - Rider的Unity插件  
-   - 安装 .net6  
-
-03. master分支需要unity2021.3版(用到了C#10的语法)  
-
-04. 启动Unity, Edit->Preferences->External Tools External ScriptEditor选择VS或者Rider, Generate .csproj files for全部不要勾选    
-
-05. Unity菜单 File -> Open Project... -> Open 选中ET/Unity文件夹,点击选择文件夹按钮。  
-
-06.  点击Unity菜单 Assets -> Open C# Project 启动vs  
-
-07.  打开Unity菜单ET/BuildTool 点击BuildModelAndHotfix,这一步将编译客户端代码  
-
-08.  用Rider打开 ET/ET.sln 编译(**一定要全部工程编译,右键VS解决方案,全部编译**)  
-
-09. Unity中双击Scenes目录中的Init场景,点击Play即可运行  
-
-10. 注意这是ClientServer模式,Unity中运行了一个服务器。如果需要单独运行服务器,可以按照下面步骤:  
-
-11. 打开Unity菜单ET/ServerTool, 点击StartServer,启动服务器  
-
-12. 打开Unity菜单ET/BuildTool CodeMode选择Client, 点击BuildModelAndHotfix,这一步将编译客户端代码  
-
-13. Unity中双击Scenes目录中的Init场景,点击Play即可运行  
-
-# ENABLE_CODES模式
-1. 上面是把逻辑打成了一个dll,unity加载dll运行,这样Unity Editor将无法引用逻辑代码。如果Editor需要用到逻辑代码,可以开启ENABLE_CODES模式  
-2. Unity菜单ET/ChangeDefine/Add ENABLE_CODES即可添加ENABLE_CODES宏,这样逻辑代码将作为Editor模式放到Unity中,Editor代码就可以引用到逻辑代码  
-3. ENABLE_CODES模式不需要再BuildModelAndHotfix,直接点击Play就能运行  
-4. ENABLE_CODES只能在编辑器模式下使用,打包必须去掉ENABLE_CODES宏
-
-
-# 测试状态同步demo
-1. 想修改配置就进入 Excel 目录修改对应的表格,做运行步骤的第6步,然后重新运行 Server.App工程来启动服务端。
-
-2. Unity->tools菜单->打包工具,选择PC,勾选是否打包exe,点击开始打包,打出一个PC包在Release目录下。
-
-3. 运行Unity 登录 进入大厅 进入场景
-
-4. 运行PC包 登录 进入大厅
-
-5. 点击鼠标右键即可移动人物
-
-# 注意事项:
-
-一. 出错原因都是:  
-
-1. 中文目录。  
-2. VS 版本低
-3. Rider没有安装相关组件
-4. 没安装 .net6
-5. 没编译服务端所有工程
-6. Rider要更新到最新版本  
-7. Unity版本太低
-8. Win7 用户,没有特别设置
-9. *编译过程中如果出现依赖问题,也有可能是 Unity - External Tools - Generate .csproj files for:
-   当中勾选了 
-      1. Registry packages
-      2. Build-in packages
-10. *如果打包报错缺少"StreamingAsset",自己在 ET\Unity\Assets 下新建一个 StreamingAsset 文件夹即可。
-
-
-

+ 0 - 58
Book/1.2Why use .net core.md

@@ -1,58 +0,0 @@
-# Why use C# .net core for server-side?
-Game server side from the early single service to distributed, the development is more and more complex, the stability, development efficiency requirements are more and more high. The choice of development language has also gradually changed, from C to C++ to C++ + PYTHON or C++ + LUA, and now many companies are using erlang, go, java, c#. At present, it is a blossoming situation.
-
-But if you were to redo an online game server, without considering the compatibility with the company or what is already there, how would you choose? I thought carefully about this issue, there are probably several aspects of this need to consider: 1.
-
-###### 1. Stability of the language (fatal)
-The game server is characterized by high load and low latency. So generally the server-side process is with state, once hung means data loss, this is intolerable.
-
-###### 2. Runtime hot more (fatal)
-The game server logic is extremely complex and prone to bugs, but it can't be stopped often, so hot shifts to fix bugs are necessary. If a bug occurs, the developer can write code immediately and then fix it with a hot-change, which is not felt by online users at all.
-
-###### 3. Availability of concurrent process support (5 stars importance)
-With distributed server architecture, there is bound to be a lot of interaction between processes. Since it is difficult to split the game logic into multiple threads, it is generally single-threaded logic. If there is no concurrent support, a large number of callbacks are bound to be generated and code maintenance will become very difficult.
-
-###### 4. Compilation speed (5 stars in importance)
-In c++ development, 30% of the time is wasted on compilation. If compilation is fast or not needed, it will greatly improve development efficiency.
-
-###### 5. Cross-platform (4 stars)
-Generally the game server is set up on linux. But the usual development, the use of windows will be more convenient, if cross-platform, development and testing efficiency will be greatly improved, and do not need to get a separate development machine, the local computer can meet the usual development
-
-###### 6. Readability, refactorability (3 stars)
-The code can be reconstructed to greatly reduce the difficulty of writing code
-
-###### 7. Whether the library is complete, whether the ecology is perfect (3 stars)
-Library is complete, ecological good, you need to build their own wheel is less
-
-###### 8. Unified language with the client (3 stars)
-Client-server shared language, the advantages are very obvious, many codes can be reused, logic programmers no longer need to distinguish between the front and back end, both ends can write, a person can complete a function, greatly reducing the time cost of communication.
-
-###### 9. IDE support (3 stars)
-Code hints, refactoring and other support, excellent IDE can improve the development efficiency by several times.
-
-###### 10. Language performance (1 star)
-Currently server performance is not too much of a problem, but good performance is better than poor performance.  
-
-| Languages | C# | C/C++ | Java | Go | Lua | Python | Erlang |
-| -- | :--: | :--: | :--: | :--: | :--: | :--: | :--: |
-| Stability | Stable | Easy to hang | Stable | Stable | Stable | Stable | Stable | Stable | Stable
-| Runtime Hot Update | Support | Difficult to support | Support | Doesn't support | Support | Support | Support
-| Cross-Platform | Support | Difficult to Support | Support | Difficult to Support | Support | Support | Support
-| Coroutine | Have | Need to Implement | Bad Support | Support | Support | Support | Support
-| Compilation speed | fast | slow | fast | fast | no compilation | no compilation | fast
-| Readability | Good | Fair | Good | Fair | Poor | Poor | Poor|
-| Game Library and Ecology | Good | Good | Average | Average | Poor | Good | Average|
-| Client Unified Language | Unity | Unity, UE4 | Not available | Not available | Unity, UE4 | UE4 | Not available
-| IDE Support | Good | Good | Good | Average | Poor | Poor | Poor|
-| Language performance | Good | Very good | Good | Good | Poor | Very poor | Poor |
-
-As you can see from the table. 1:
-1. C/C++ has poor stability, slow compilation speed, and fatal flaws
-2. Go does not support hot changes, does not support generics, poor reconfigurability, can not share code with the client, a fatal flaw
-3. poor support for Java concurrency, unable to share code with the client
-4. Lua has few libraries, poor performance, poor code readability and reconfigurability, cross-platform dependence on C/C++, troublesome to handle, and poor ide support
-5. Python poor performance, poor code readability and reconfigurability, unable to share code with clients, poor ide support
-6. Erlang performance is poor, functional style is not easy to get started, ide support is poor
-7. C# .net core is very good in every convenience, but can't share code with UE4
-
-Currently Unity is the hottest game engine, C# server with Unity is a perfect match, basically can not find defects.

+ 0 - 58
Book/1.2为什么使用.net core.md

@@ -1,58 +0,0 @@
-# 为什么使用C# .net core做服务端?
-游戏服务端从早期的单服到分布式,开发越来越复杂,对稳定性,开发效率要求越来越高。开发语言的选择也逐步发生了变化,C 到 C++ 到 C++ + PYTHON 或者C++ + LUA 到现在 很多公司开始使用erlang,go,java,c#。目前是一个百花齐放的局面。
-
-但是如果是要你重新做一个网游server,不考虑对公司或者已有的东西兼容性,你会怎么选择?我仔细想了一下这个问题,大概有这个几个方面需要考虑:
-
-###### 1. 语言的稳定性(致命性)
-游戏服务器的特点是高负载低延时。所以一般服务端进程都是带状态的,一旦挂掉就意味着数据丢失,这点是无法容忍的。
-
-###### 2. 运行时热更(致命性)
-游戏服务器逻辑极其复杂,很容易出现bug,但是又不能经常停服,所以热更修复bug就显得十分必要。出现错误开发人员可以立即编写代码,然后热更修复,线上用户完全感觉不到。
-
-###### 3. 是否有协程支持(重要性5星)
-分布式服务器架构,进程与进程之间必然会有大量交互。由于游戏逻辑很难拆分成多线程,所以一般来说都是逻辑单线程。如果没有协程支持,必然产生大量回调,代码维护会变得非常困难。
-
-###### 4. 编译速度(重要性5星)
-使用c++开发,30%的时间都浪费在编译上。假如编译很快或者不需要编译,必定大大提高开发效率。
-
-###### 5. 跨平台(4星)
-一般游戏服务器都架设在linux上面。但是平常开发,使用windows会更加方便,如果跨平台,开发以及测试效率会大大提升,并且不需要单独搞一个开发机,本机电脑就可以满足平常开发
-
-###### 6. 可阅读性,可重构性(3星)
-代码可以重构能大大减轻写代码的难度
-
-###### 7. 库是否齐全,生态是否完善(3星)
-库齐全,生态好,自己需要造的轮子就少
-
-###### 8.跟客户端统一语言(3星)
-客户端服务端共用语言,优势十分明显,很多代码可以复用,逻辑程序员不再需要区分前后端,双端都可以写,一个人即可完成一个功能,大大减少了沟通的时间成本。
-
-###### 9. IDE的支持(3星)
-代码提示,重构等支持,优秀的IDE能提高几倍的开发效率。
-
-###### 10. 语言的性能(1星)
-目前服务器性能都不是太大问题,不过性能好总比性能差要强。
-
-| 语言 |  C# | C/C++ | Java | Go | Lua | Python | Erlang |
-| -- | :--: | :--: | :--: | :--: | :--: | :--: | :--: |
-| 稳定性 |  稳定 | 容易挂 | 稳定 | 稳定 | 稳定 | 稳定 | 稳定 |
-| 运行时热更 |  支持 | 较难支持 | 支持 | 不支持 | 支持 | 支持 | 支持 |
-| 跨平台 |  支持 | 较难支持 | 支持 | 支持 | 较难支持 | 支持 | 支持 |
-| 协程 |  有 | 需要自己实现 | 支持不好 | 支持 | 支持 | 支持 | 支持 |
-| 编译速度 |  快 | 慢 | 快 | 快 | 不需要编译 | 不需要编译 | 快|
-| 阅读性重构性 |  好 | 一般 | 好 | 一般 | 差 | 差 | 差|
-| 游戏库跟生态 |  好 | 好 | 一般 | 一般 | 差 | 好 | 一般|
-| 客户端统一语言 |  Unity | Unity、UE4 | 暂无 | 暂无 | Unity、UE4 | UE4 | 暂无|
-| IDE的支持 |  好 | 好 |好 | 普通| 差 | 差 | 差|
-| 语言的性能 | 好|极好| 好| 好| 差|很差|差|
-
-从表格可以看出:
-1. C/C++稳定性差,编译速度慢,存在致命缺陷
-2. Go不支持热更,由于不支持泛型,重构性较差,无法跟客户端共享代码,存在致命缺陷
-3. Java协程支持差,无法跟客户端共享代码
-4. Lua库少,性能差,代码可阅读性可重构性差,跨平台完全依赖C/C++,处理起来麻烦,ide支持差
-5. Python 性能很差,代码可阅读性可重构性差,无法跟客户端共享代码,ide支持差
-6. Erlang 性能差,函数式风格不好上手,ide支持差
-7. C# .net core各个方便都非常优秀,不过跟UE4无法共享代码
-
-当前Unity是最火的游戏引擎,C#服务端搭配Unity完全是天作之合,基本上找不到缺陷。

+ 0 - 119
Book/2.1CSharp Coroutine.md

@@ -1,119 +0,0 @@
-# What is a concurrent thread
-Speaking of concurrency, let's first understand what is asynchronous. Asynchronous simply means that I want to initiate a call, but the called party (may be other threads, or may be IO) will take some time to produce the result, and I don't want the call to block the entire thread of the caller, so I pass a callback function to the called party, and the called party will call back this callback function after it finishes running to notify the caller to continue The callback function will notify the caller to continue execution. As an example:  
-In the following code, the main thread keeps looping, sleep 1 millisecond per loop, add one to the count, and print once every 10,000 times.
-
-```csharp
-        private static void Main()
-        {
-            int loopCount = 0;
-            while (true)
-            {
-                int temp = watcherValue;
-                
-                Thread.Sleep(1);
-                
-                ++loopCount;
-                if (loopCount % 10000 == 0)
-                {
-                    Console.WriteLine($"loop count: {loopCount}");
-                }
-            }
-        }
-```
-At this point I need to add a function, at the beginning of the program, I want to print the value of loopCount after 5 seconds. Seeing that after 5 seconds we can think of the Sleep method, which will block the thread for a certain amount of time and then continue execution. We obviously can't Sleep in the main thread because it would break the logic of printing once every 10000 counts.
-```csharp
-    // example2_1
-    class Program
-    {
-        private static int loopCount = 0;
-
-        private static void Main()
-        {
-            OneThreadSynchronizationContext _ = OneThreadSynchronizationContext.Instance;
-            
-            WaitTimeAsync(5000, WaitTimeFinishCallback);
-            
-            while (true)
-            {
-                OneThreadSynchronizationContext.Instance.Update();
-                
-                Thread.Sleep(1);
-                
-                ++loopCount;
-                if (loopCount % 10000 == 0)
-                {
-                    Console.WriteLine($"loop count: {loopCount}");
-                }
-            }
-        }
-
-        private static void WaitTimeAsync(int waitTime, Action action)
-        {
-            Thread thread = new Thread(()=>WaitTime(waitTime, action));
-            thread.Start();
-        }
-        
-        private static void WaitTimeFinishCallback()
-        {
-            Console.WriteLine($"WaitTimeAsync finsih loopCount value is: {loopCount}");
-        }
-
-        /// <summary>
-        /// Waiting in another thread
-        /// </summary>
-        private static void WaitTime(int waitTime, Action action)
-        {
-            Thread.Sleep(waitTime);
-            
-            // Throw the action back to the main thread for execution
-            OneThreadSynchronizationContext.Instance.Post((o)=>action(), null);
-        }
-    }
-```
-We have designed a WaitTimeAsync method here. WaitTimeAsync is actually a typical asynchronous method that initiates a call from the main thread, passes in a WaitTimeFinishCallback method as an argument, opens a thread, and after the thread Sleeps for a certain amount of time, throws the passed callback back to the main thread for execution. OneThreadSynchronizationContext is a cross-thread queue, any thread can throw delegates into it, the Update method of OneThreadSynchronizationContext is called in the main thread and will take out these delegates and put them into the main thread for execution. Why does the callback method need to be thrown back to the main thread for execution? Because the loopCount is read in the callback method, and the loopCount is also read and written in the main thread, so either add a lock or always ensure that it is only read and written in the main thread. It is a bad practice to add locks, and having locks all over the code makes it difficult to read and maintain, and it is easy to create multi-threaded bugs. this is a common technique in multi-threaded development to package the logic into a delegate and throw it back to another thread.
-
-We may have a new requirement, after the execution of WaitTimeFinishCallback is finished, we want to wait for 3 seconds and print the loopCount again.
-```csharp
-        private static void WaitTimeAsync(int waitTime, Action action)
-        {
-            Thread thread = new Thread(()=>WaitTime(waitTime, action));
-            thread.Start();
-        }
-        private static void WaitTimeFinishCallback()
-        {
-            Console.WriteLine($"WaitTimeAsync finsih loopCount value is: {loopCount}");
-            WaitTimeAsync(3000, WaitTimeFinishCallback2);
-        }
-        
-        private static void WaitTimeFinishCallback2()
-        {
-            Console.WriteLine($"WaitTimeAsync finsih loopCount value is: {loopCount}");
-        }
-```
-We may also change the requirement at this point, we need to print the loopCount 5 seconds after the program starts, then 4 seconds after, then 3 seconds after, that is, insert another 3 seconds wait in the middle of the above logic.
-```csharp
-        private static void WaitTimeAsync(int waitTime, Action action)
-        {
-            Thread thread = new Thread(()=>WaitTime(waitTime, action));
-            thread.Start();
-        }
-        
-        private static void WaitTimeFinishCallback()
-        {
-            Console.WriteLine($"WaitTimeAsync finsih loopCount value is: {loopCount}");
-            WaitTimeAsync(4000, WaitTimeFinishCallback3);
-        }
-        
-        private static void WaitTimeFinishCallback3()
-        {
-            Console.WriteLine($"WaitTimeAsync finsih loopCount value is: {loopCount}");
-            WaitTimeAsync(3000, WaitTimeFinishCallback2);
-        }
-        
-        private static void WaitTimeFinishCallback2()
-        {
-            Console.WriteLine($"WaitTimeAsync finsih loopCount value is: {loopCount}");
-        }
-```
-This inserts a piece of code in the middle, which seems very cumbersome. Here you can answer what is a concurrent process, in fact this string of callbacks is a concurrent process.
-

+ 0 - 119
Book/2.1CSharp的协程.md

@@ -1,119 +0,0 @@
-# 什么是协程
-说到协程,我们先了解什么是异步,异步简单说来就是,我要发起一个调用,但是这个被调用方(可能是其它线程,也可能是IO)出结果需要一段时间,我不想让这个调用阻塞住调用方的整个线程,因此传给被调用方一个回调函数,被调用方运行完成后回调这个回调函数就能通知调用方继续往下执行。举个例子:  
-下面的代码,主线程一直循环,每循环一次sleep 1毫秒,计数加一,每10000次打印一次。
-
-```csharp
-        private static void Main()
-        {
-            int loopCount = 0;
-            while (true)
-            {
-                int temp = watcherValue;
-                
-                Thread.Sleep(1);
-                
-                ++loopCount;
-                if (loopCount % 10000 == 0)
-                {
-                    Console.WriteLine($"loop count: {loopCount}");
-                }
-            }
-        }
-```
-这时我需要加个功能,在程序一开始,我希望在5秒钟之后打印出loopCount的值。看到5秒后我们可以想到Sleep方法,它会阻塞线程一定时间然后继续执行。我们显然不能在主线程中Sleep,因为会破坏掉每10000次计数打印一次的逻辑。
-```csharp
-    // example2_1
-    class Program
-    {
-        private static int loopCount = 0;
-
-        private static void Main()
-        {
-            OneThreadSynchronizationContext _ = OneThreadSynchronizationContext.Instance;
-            
-            WaitTimeAsync(5000, WaitTimeFinishCallback);
-            
-            while (true)
-            {
-                OneThreadSynchronizationContext.Instance.Update();
-                
-                Thread.Sleep(1);
-                
-                ++loopCount;
-                if (loopCount % 10000 == 0)
-                {
-                    Console.WriteLine($"loop count: {loopCount}");
-                }
-            }
-        }
-
-        private static void WaitTimeAsync(int waitTime, Action action)
-        {
-            Thread thread = new Thread(()=>WaitTime(waitTime, action));
-            thread.Start();
-        }
-        
-        private static void WaitTimeFinishCallback()
-        {
-            Console.WriteLine($"WaitTimeAsync finsih loopCount的值是: {loopCount}");
-        }
-
-        /// <summary>
-        /// 在另外的线程等待
-        /// </summary>
-        private static void WaitTime(int waitTime, Action action)
-        {
-            Thread.Sleep(waitTime);
-            
-            // 将action扔回主线程执行
-            OneThreadSynchronizationContext.Instance.Post((o)=>action(), null);
-        }
-    }
-```
-我们在这里设计了一个WaitTimeAsync方法,WaitTimeAsync其实就是一个典型的异步方法,它从主线程发起调用,传入了一个WaitTimeFinishCallback回调方法做参数,开启了一个线程,线程Sleep一定时间后,将传过来的回调扔回到主线程执行。OneThreadSynchronizationContext是一个跨线程队列,任何线程可以往里面扔委托,OneThreadSynchronizationContext的Update方法在主线程中调用,会将这些委托取出来放到主线程执行。为什么回调方法需要扔回到主线程执行呢?因为回调方法中读取了loopCount,loopCount在主线程中也有读写,所以要么加锁,要么永远保证只在主线程中读写。加锁是个不好的做法,代码中到处是锁会导致阅读跟维护困难,很容易产生多线程bug。这种将逻辑打包成委托然后扔回另外一个线程是多线程开发中常用的技巧。
-
-我们可能又有个新需求,WaitTimeFinishCallback执行完成之后,再想等3秒,再打印一下loopCount。
-```csharp
-        private static void WaitTimeAsync(int waitTime, Action action)
-        {
-            Thread thread = new Thread(()=>WaitTime(waitTime, action));
-            thread.Start();
-        }
-        private static void WaitTimeFinishCallback()
-        {
-            Console.WriteLine($"WaitTimeAsync finsih loopCount的值是: {loopCount}");
-            WaitTimeAsync(3000, WaitTimeFinishCallback2);
-        }
-        
-        private static void WaitTimeFinishCallback2()
-        {
-            Console.WriteLine($"WaitTimeAsync finsih loopCount的值是: {loopCount}");
-        }
-```
-我们这时还可能改需求,需要在程序启动5秒后,接下来4秒,再接下来3秒,打印loopCount,也就是上面的逻辑中间再插入一个3秒等待。
-```csharp
-        private static void WaitTimeAsync(int waitTime, Action action)
-        {
-            Thread thread = new Thread(()=>WaitTime(waitTime, action));
-            thread.Start();
-        }
-        
-        private static void WaitTimeFinishCallback()
-        {
-            Console.WriteLine($"WaitTimeAsync finsih loopCount的值是: {loopCount}");
-            WaitTimeAsync(4000, WaitTimeFinishCallback3);
-        }
-        
-        private static void WaitTimeFinishCallback3()
-        {
-            Console.WriteLine($"WaitTimeAsync finsih loopCount的值是: {loopCount}");
-            WaitTimeAsync(3000, WaitTimeFinishCallback2);
-        }
-        
-        private static void WaitTimeFinishCallback2()
-        {
-            Console.WriteLine($"WaitTimeAsync finsih loopCount的值是: {loopCount}");
-        }
-```
-这样中间插入一段代码,显得非常麻烦。这里可以回答什么是协程了,其实这一串串回调就是协程。
-

+ 0 - 77
Book/2.2Better Coroutine.md

@@ -1,77 +0,0 @@
-# Better Coroutine
-The above article talks about how a string of callbacks is a concurrent process, and obviously writing code this way, adding logic and inserting logic is very error prone. We need to use asynchronous syntax to change the form of this asynchronous callback to a synchronous form, fortunately C# has been designed for us, see the code
-```csharp
-    // example2_2
-    class Program
-    {
-        private static int loopCount = 0;
-        
-        static void Main(string[] args)
-        {
-            OneThreadSynchronizationContext _ = OneThreadSynchronizationContext.Instance;
-
-            Console.WriteLine($"Main Thread: {Thread.CurrentThread.ManagedThreadId}");
-            
-            Crontine();
-            
-            while (true)
-            {
-                OneThreadSynchronizationContext.Instance.Update();
-                
-                Thread.Sleep(1);
-                
-                ++loopCount;
-                if (loopCount % 10000 == 0)
-                {
-                    Console.WriteLine($"loop count: {loopCount}");
-                }
-            }
-        }
-
-        private static async void Crontine()
-        {
-            await WaitTimeAsync(5000);
-            Console.WriteLine($"Current thread: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount's value is: {loopCount}");
-            await WaitTimeAsync(4000);
-            Console.WriteLine($"Current Thread: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih The value of loopCount is: {loopCount}");
-            await WaitTimeAsync(3000);
-            Console.WriteLine($"Current Thread: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih The value of loopCount is: {loopCount}");
-        }
-        
-        private static Task WaitTimeAsync(int waitTime)
-        {
-            TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
-            Thread thread = new Thread(()=>WaitTime(waitTime, tcs));
-            thread.Start();
-            return tcs.Task;
-        }
-        
-        /// <summary>
-        /// Waiting in another thread
-        /// </summary>
-        private static void WaitTime(int waitTime, TaskCompletionSource<bool> tcs)
-        {
-            Thread.Sleep(waitTime);
-            
-            // throw tcs back to the main thread for execution
-            OneThreadSynchronizationContext.Instance.Post(o=>tcs.SetResult(true), null);
-        }
-    }
-```
-In this code, in the WaitTimeAsync method, we use the TaskCompletionSource class instead of the previously passed Action parameter, and the WaitTimeAsync method returns a Task type result. waitTime we replace action() with tcs. SetResult(true), and the WaitTimeAsync method uses the await keyword in front of it, so that the sequence of callbacks can be changed to a synchronized form. This makes the code look very simple and much easier to develop.  
-
-Here is another trick, we found that WaitTime needs to throw tcs.SetResult back to the main thread for execution, Microsoft gives us a simple way to set up the synchronization context in the main thread by referring to example2_2_2
-```csharp
-// example2_2_2
-SynchronizationContext.SetSynchronizationContext(OneThreadSynchronizationContext.Instance);
-```
-SetResult(true) will be called directly in WaitTime, the callback will be automatically thrown to the synchronization context, and the synchronization context we can take out in the main thread to execute the callback, so automatically able to complete the operation back to the main thread
-```csharp
-        private static void WaitTime(int waitTime, TaskCompletionSource<bool> tcs)
-        {
-            Thread.Sleep(waitTime);
-
-            tcs.SetResult(true);
-        }
-```
-If you do not set the synchronization context, you will find that the printout of the current thread is not the main thread, which is also the use of many third-party libraries and . In fact, I think this design is not necessary, to the library developers to achieve better, especially in the game development, logic is all single-threaded, callback every time you go through the synchronization context is redundant, so the ET framework provides the implementation of ETTask does not use the synchronization context, the code is more concise and efficient, which will be discussed later.

+ 0 - 77
Book/2.2更好的协程.md

@@ -1,77 +0,0 @@
-# 更好的协程
-上文讲了一串回调就是协程,显然这样写代码,增加逻辑,插入逻辑非常容易出错。我们需要利用异步语法把这个异步回调的形式改成同步的形式,幸好C#已经帮我们设计好了,看代码
-```csharp
-    // example2_2
-    class Program
-    {
-        private static int loopCount = 0;
-        
-        static void Main(string[] args)
-        {
-            OneThreadSynchronizationContext _ = OneThreadSynchronizationContext.Instance;
-
-            Console.WriteLine($"主线程: {Thread.CurrentThread.ManagedThreadId}");
-            
-            Crontine();
-            
-            while (true)
-            {
-                OneThreadSynchronizationContext.Instance.Update();
-                
-                Thread.Sleep(1);
-                
-                ++loopCount;
-                if (loopCount % 10000 == 0)
-                {
-                    Console.WriteLine($"loop count: {loopCount}");
-                }
-            }
-        }
-
-        private static async void Crontine()
-        {
-            await WaitTimeAsync(5000);
-            Console.WriteLine($"当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}");
-            await WaitTimeAsync(4000);
-            Console.WriteLine($"当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}");
-            await WaitTimeAsync(3000);
-            Console.WriteLine($"当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}");
-        }
-        
-        private static Task WaitTimeAsync(int waitTime)
-        {
-            TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
-            Thread thread = new Thread(()=>WaitTime(waitTime, tcs));
-            thread.Start();
-            return tcs.Task;
-        }
-        
-        /// <summary>
-        /// 在另外的线程等待
-        /// </summary>
-        private static void WaitTime(int waitTime, TaskCompletionSource<bool> tcs)
-        {
-            Thread.Sleep(waitTime);
-            
-            // 将tcs扔回主线程执行
-            OneThreadSynchronizationContext.Instance.Post(o=>tcs.SetResult(true), null);
-        }
-    }
-```
-在这段代码里面,WaitTimeAsync方法中,我们利用了TaskCompletionSource类替代了之前传入的Action参数,WaitTimeAsync方法返回了一个Task类型的结果。WaitTime中我们把action()替换成了tcs.SetResult(true),WaitTimeAsync方法前使用await关键字,这样可以将一连串的回调改成同步的形式。这样一来代码显得十分简洁,开发起来也方便多了。  
-
-这里还有个技巧,我们发现WaitTime中需要将tcs.SetResult扔回到主线程执行,微软给我们提供了一种简单的方法,参考example2_2_2,在主线程设置好同步上下文,
-```csharp
-// example2_2_2
-SynchronizationContext.SetSynchronizationContext(OneThreadSynchronizationContext.Instance);
-```
-在WaitTime中直接调用tcs.SetResult(true)就行了,回调会自动扔到同步上下文中,而同步上下文我们可以在主线程中取出回调执行,这样自动能够完成回到主线程的操作
-```csharp
-        private static void WaitTime(int waitTime, TaskCompletionSource<bool> tcs)
-        {
-            Thread.Sleep(waitTime);
-
-            tcs.SetResult(true);
-        }
-```
-如果不设置同步上下文,你会发现打印出来当前线程就不是主线程了,这也是很多第三方库跟.net core内置库的用法,默认不回调到主线程,所以我们使用的时候需要设置下同步上下文。其实这个设计本人觉得没有必要,交由库的开发者去实现更好,尤其是在游戏开发中,逻辑全部是单线程的,回调每次都走一遍同步上下文就显得多余了,所以ET框架提供了不使用同步上下文的实现ETTask,代码更加简洁更加高效,这个后面会讲到。

+ 0 - 143
Book/2.3Single-threaded asynchronous.md

@@ -1,143 +0,0 @@
-# Single-threaded asynchronous
-The previous examples are multi-threaded implementations of asynchrony, but asynchrony is obviously not just multi-threaded. We used Sleep in the previous examples to achieve time waiting, each timer needs to use a thread, which will lead to frequent thread switching, this implementation is very inefficient, usually will not do so. General game logic will design a single-threaded timer, we do a simple implementation here, used to explain single-threaded asynchronous.
-```csharp
-    // example2_3
-    class Program
-    {
-        private static int loopCount = 0;
-
-        private static long time;
-        private static Action action;
-        
-        static void Main(string[] args)
-        {
-            Console.WriteLine($"Main thread: {Thread.CurrentThread.ManagedThreadId}");
-
-            Crontine();
-            
-            while (true)
-            {
-                Thread.Sleep(1);
-
-                CheckTimerOut();
-                
-                ++loopCount;
-                if (loopCount % 10000 == 0)
-                {
-                    Console.WriteLine($"loop count: {loopCount}");
-                }
-            }
-        }
-        
-        private static void Crontine()
-        {
-            WaitTimeAsync(5000, WaitTimeAsyncCallback1);
-        }
-
-        private static void WaitTimeAsyncCallback1()
-        {
-            Console.WriteLine($"Current thread: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount's value is: {loopCount}");
-            WaitTimeAsync(4000, WaitTimeAsyncCallback2);
-        }
-        
-        private static void WaitTimeAsyncCallback2()
-        {
-            Console.WriteLine($"Current thread: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount's value is: {loopCount}");
-            WaitTimeAsync(3000, WaitTimeAsyncCallback3);
-        }
-        
-        private static void WaitTimeAsyncCallback3()
-        {
-            Console.WriteLine($"Current thread: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount's value is: {loopCount}");
-        }
-
-        private static void CheckTimerOut()
-        {
-            if (time == 0)
-            {
-                return;
-            }
-            long nowTicks = DateTime.Now.Ticks / 10000;
-            if (time > nowTicks)
-            { return; }
-                return;
-            }
-
-            time = 0;
-            action.Invoke();
-        }
-        
-        private static void WaitTimeAsync(int waitTime, Action a)
-        {
-            Ticks / 10000 + waitTime;
-            action = a;
-        }
-    }
-```
-
-This example also implements a simple timing method, WaitTimeAsync will be called to record the callback method and time, the main thread will call CheckTimerOut every frame, CheckTimerOut inside to determine whether the timer is expired, expired then callback method is called. The whole logic is done in the main thread, also asynchronously. So asynchronous is not multi-threaded, single-threaded can also be asynchronous. The above example can be changed to await as well.
-```csharp
-    // example2_3_2
-    class Program
-    {
-        private static int loopCount = 0;
-
-        private static long time;
-        private static TaskCompletionSource<bool> tcs;
-        
-        static void Main(string[] args)
-        {
-            Console.WriteLine($"Main thread: {Thread.CurrentThread.ManagedThreadId}");
-
-            Crontine();
-            
-            while (true)
-            {
-                Thread.Sleep(1);
-
-                CheckTimerOut();
-                
-                ++loopCount;
-                if (loopCount % 10000 == 0)
-                {
-                    Console.WriteLine($"loop count: {loopCount}");
-                }
-            }
-        }
-        
-        private static async void Crontine()
-        {
-            await WaitTimeAsync(5000);
-            Console.WriteLine($"Current thread: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount's value is: {loopCount}");
-            await WaitTimeAsync(4000);
-            Console.WriteLine($"Current Thread: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih The value of loopCount is: {loopCount}");
-            await WaitTimeAsync(3000);
-            Console.WriteLine($"Current Thread: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih The value of loopCount is: {loopCount}");
-        }
-
-        private static void CheckTimerOut()
-        {
-            if (time == 0)
-            {
-                return;
-            }
-            long nowTicks = DateTime.Now.Ticks / 10000;
-            if (time > nowTicks)
-            { return; }
-                return;
-            }
-
-            time = 0;
-            tcs.SetResult(true);
-        }
-        
-        private static Task WaitTimeAsync(int waitTime)
-        {
-            TaskCompletionSource<bool> t = new TaskCompletionSource<bool>();
-            Time = DateTime.Now.Ticks / 10000 + waitTime;
-            tcs = t;
-            return t.Task;
-        }
-    }
-```
-The above example all calls are done in the main thread and use await, so await does not open multithreading, await specific use of multithreading depends entirely on the specific implementation

+ 0 - 143
Book/2.3单线程异步.md

@@ -1,143 +0,0 @@
-# 单线程异步
-前面几个例子都是多线程实现的异步,但是异步显然不仅仅是多线程的。我们在之前的例子中使用了Sleep来实现时间的等待,每一个计时器都需要使用一个线程,会导致线程切换频繁,这个实现效率很低,平常是不会这样做的。一般游戏逻辑中会设计一个单线程的计时器,我们这里做一个简单的实现,用来讲解单线程异步。
-```csharp
-    // example2_3
-    class Program
-    {
-        private static int loopCount = 0;
-
-        private static long time;
-        private static Action action;
-        
-        static void Main(string[] args)
-        {
-            Console.WriteLine($"主线程: {Thread.CurrentThread.ManagedThreadId}");
-
-            Crontine();
-            
-            while (true)
-            {
-                Thread.Sleep(1);
-
-                CheckTimerOut();
-                
-                ++loopCount;
-                if (loopCount % 10000 == 0)
-                {
-                    Console.WriteLine($"loop count: {loopCount}");
-                }
-            }
-        }
-        
-        private static void Crontine()
-        {
-            WaitTimeAsync(5000, WaitTimeAsyncCallback1);
-        }
-
-        private static void WaitTimeAsyncCallback1()
-        {
-            Console.WriteLine($"当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}");
-            WaitTimeAsync(4000, WaitTimeAsyncCallback2);
-        }
-        
-        private static void WaitTimeAsyncCallback2()
-        {
-            Console.WriteLine($"当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}");
-            WaitTimeAsync(3000, WaitTimeAsyncCallback3);
-        }
-        
-        private static void WaitTimeAsyncCallback3()
-        {
-            Console.WriteLine($"当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}");
-        }
-
-        private static void CheckTimerOut()
-        {
-            if (time == 0)
-            {
-                return;
-            }
-            long nowTicks = DateTime.Now.Ticks / 10000;
-            if (time > nowTicks)
-            {
-                return;
-            }
-
-            time = 0;
-            action.Invoke();
-        }
-        
-        private static void WaitTimeAsync(int waitTime, Action a)
-        {
-            time = DateTime.Now.Ticks / 10000 + waitTime;
-            action = a;
-        }
-    }
-```
-
-这个例子同样实现了一个简单的计时方法,WaitTimeAsync调用时会将回调方法跟时间记录下来,主线程每帧都会调用CheckTimerOut,CheckTimerOut里面判断计时器是否过期,过期了则调用回调方法。整个逻辑都在主线程中完成,同样是异步方法。所以异步并非多线程,单线程同样可以异步。上面的例子同样可以改成await的形式。
-```csharp
-    // example2_3_2
-    class Program
-    {
-        private static int loopCount = 0;
-
-        private static long time;
-        private static TaskCompletionSource<bool> tcs;
-        
-        static void Main(string[] args)
-        {
-            Console.WriteLine($"主线程: {Thread.CurrentThread.ManagedThreadId}");
-
-            Crontine();
-            
-            while (true)
-            {
-                Thread.Sleep(1);
-
-                CheckTimerOut();
-                
-                ++loopCount;
-                if (loopCount % 10000 == 0)
-                {
-                    Console.WriteLine($"loop count: {loopCount}");
-                }
-            }
-        }
-        
-        private static async void Crontine()
-        {
-            await WaitTimeAsync(5000);
-            Console.WriteLine($"当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}");
-            await WaitTimeAsync(4000);
-            Console.WriteLine($"当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}");
-            await WaitTimeAsync(3000);
-            Console.WriteLine($"当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}");
-        }
-
-        private static void CheckTimerOut()
-        {
-            if (time == 0)
-            {
-                return;
-            }
-            long nowTicks = DateTime.Now.Ticks / 10000;
-            if (time > nowTicks)
-            {
-                return;
-            }
-
-            time = 0;
-            tcs.SetResult(true);
-        }
-        
-        private static Task WaitTimeAsync(int waitTime)
-        {
-            TaskCompletionSource<bool> t = new TaskCompletionSource<bool>();
-            time = DateTime.Now.Ticks / 10000 + waitTime;
-            tcs = t;
-            return t.Task;
-        }
-    }
-```
-上面这个例子所有调用全部在主线程中完成,并且使用了await,因此await并不会开启多线程,await具体用没用多线程完全取决于具体的实现

+ 0 - 226
Book/3.2The powerful MongoBson library.md

@@ -1,226 +0,0 @@
-# The powerful MongoBson library
-Back-end development, statistics about these scenarios need to use serialization: 1.
-1. object clone by serialization deserialization
-2. server-side database storage data, binary
-3. distributed server-side, multi-process messages, binary
-4. back-end logs, text format
-5. various server-side configuration files, text format
-
-There are very, very many C# serialization libraries, protobuf, json and so on. But these serialization libraries should not be readable and small for all scenarios. protobuf does not support complex object structures (cannot use inheritance) and is suitable for messages, but not for database storage and log formats. json is suitable for log formats, but is too big for network messages and data storage. We certainly want a library that can satisfy all the above scenarios for the following reasons.
-1. you think about one day your configuration file needs to be saved in the database, you do not need to do the format conversion, the back-end directly to the configuration message sent by the front-end to save to the database, which can not reduce very many errors?
-2. one day some server-side configuration files do not use the file format, need to be placed in the database, again, only a few lines of code to complete the migration.
-3. one day the back-end server crash, you need to scan the logs for data recovery, the logs for deserialization into C# objects, one by one for processing, and then into objects saved to the database is complete.
-4. objects saved in the database, you can directly see the text content, you can do a variety of SQL-like operations
-5. Imagine a scenario where a configuration text object, deserialized into memory, sent via network messages, and stored in the database. The whole process is done in one go.
-
-Simply put, it reduces various data conversions, reduces code, improves development efficiency, and improves maintainability. MongoDB library can serialize both text and BSON binary format, and MongoDB itself is a very much used database in the game. Its support features are as follows.
-1. support complex inheritance structure
-2. support for ignoring certain fields serialization
-3. support field default values
-4. structure with extra fields can still be deserialized, which is very useful for multi-version protocols
-5. support for ISupportInitialize interface, this is a godsend when deserialization
-6. support for text json and binary bson serialization
-7. MongoDB database support
-
-A brief introduction to the mongo bson library
-### 1. Support serialization deserialization into json or bson
-```csharp
-    public sealed class Player
-    {
-        public long Id;
-
-        public string Account { get; private set; }
-
-        public long UnitId { get; set; }
-    }
-
-    Player player1 = new Player() { Id = 1 };
-    string json = player1.ToJson();
-    Console.WriteLine($"player1 to json: {json}");
-    Console.WriteLine($"player to bson: {player.ToBson().ToHex()}");
-    // output:
-    // player to json: { "_id" : NumberLong(1), "C" : [], "Account" : null, "UnitId" : NumberLong(0) }
-    // player to bson: B000000125F69640001000000000000000A4163636F756E740012556E69744964000000000000000000000000
-
-
-```
-Note that mongo's json is a bit different from the standard json, if you want to use the standard json, you can pass in a JsonWriterSettings object and restrict the use of JsonOutputMode.Strict mode
-```csharp
-    // use standard json
-    Player player2 = new Player() { Id = 1 };
-    Console.WriteLine($"player to json: {player2.ToJson(new JsonWriterSettings() {OutputMode = JsonOutputMode.Strict})}");
-    // player to json: { "_id" : 1, "C" : [], "Account" : null, "UnitId" : 0 }
-```
-
-Deserialize json:
-```csharp
-            // deserialize json
-        Player player11 = BsonSerializer.Deserialize<Player>(json);
-        Console.WriteLine($"player11 to json: {player11.ToJson()}");
-```
-Deserialize bson:
-```csharp
-    // deserialize bson
-    using (MemoryStream memoryStream = new MemoryStream(bson))
-    {
-        Player player12 = (Player) BsonSerializer.Deserialize(memoryStream, typeof (Player));
-        Console.WriteLine($"player12 to json: {player12.ToJson()}");
-    }
-```
-
-### 2. Some fields can be ignored
-[BsonIgnore] This tag is used to disable field serialization.
-```csharp
-	public sealed class Player
-	{
-        public long Id;
-
-		[BsonIgnore]
-		public string Account { get; private set; }
-		
-		public long UnitId { get; set; }
-    }
-
-    Player player = new Player() { Id = 2, UnitId = 3, Account = "panda"};
-	Console.WriteLine($"player to json: {player.ToJson()}");
-    // player to json: { "_id" : 2, "UnitId" : 3 }
-```
-### 3. Support for default values and taking aliases
-The [BsonElement] field with this tag will serialize even private fields (only public fields are serialized by default), and the tag can take a string parameter to assign an alias to the field serialization.
-```csharp
-	public sealed class Player
-	{
-        public long Id;
-
-		public string Account { get; private set; }
-
-		[BsonElement("UId")]
-		public long UnitId { get; set; }
-    }
-    Player player = new Player() { Id = 2, UnitId = 3, Account = "panda"};
-	Console.WriteLine($"player to json: {player.ToJson()}");
-    // player to json: { "_id" : 2, "Account" : "panda", "UId" : 3 }
-```
-### 4. Upgrade version support
-[BsonIgnoreExtraElements] This tag is used on top of class, used to ignore extra fields when deserializing, general version compatibility needs to be considered, low version of the protocol needs to be able to deserialize
-otherwise the new version adds fields, the old version structure deserialization will be wrong
-```csharp
-	[BsonIgnoreExtraElements]
-	public sealed class Player
-	public
-        public long Id;
-
-		public string Account { get; private set; }
-
-		[BsonElement("UId")
-		public long UnitId { get; set; }
-    }
-```
-### 5. Support for complex inheritance structures
-The power of the mongo bson library is that it fully supports serialization deserialization inheritance structures. Note that inheritance deserialization requires registration of all parent classes, and there are two ways to do this.
-a. You can declare the inherited subclasses on top of the parent class using the [BsonKnownTypes] tag, so that mongo will automatically register them, e.g.:
-```csharp
-    [BsonKnownTypes(typeof(Entity))]
-    public class Component
-    {
-    }
-    [BsonKnownTypes(typeof(Player))]
-    public class Entity: Component
-    {
-    }
-    public sealed class Player: Entity
-    {
-        public long Id;
-        
-        public string Account { get; set; }
-		
-        public long UnitId { get; set; }
-    }
-```
-This is flawed because the framework doesn't know what subclasses a class will have, and this is invasive to the framework code, and we want to uncouple this
-. You can scan the assembly for the types of all subclass parents and register them with the mongo driver
-```csharp
-			Type[] types = typeof(Game).Assembly.GetTypes();
-			foreach (Type type in types)
-			{
-				if (!type.IsSubclassOf(typeof(Component))))
-				{
-					continue;
-				}
-
-				BsonClassMap.LookupClassMap(type);
-			}
-
-			BsonSerializer.RegisterSerializer(new EnumSerializer<NumericType>(BsonType.String));
-```
-This completely automates the registration and the user does not need to relate whether the class is registered or not.
-
-### 6. ISupportInitialize interface
-mongo bson deserialization supports an ISupportInitialize interface, ISupportInitialize has two methods
-```csharp
-    public interface ISupportInitialize
-    {
-        void BeginInit();
-        void EndInit();
-    }
-```
-BeginInit is called before deserialization and EndInit is called after deserialization. This interface is very useful now to perform some operations after deserialization. For example
-```csharp
-	[BsonIgnoreExtraElements]
-	public class InnerConfig: AConfigComponent
-	{
-		[BsonIgnore]
-		public IPEndPoint IPEndPoint { get; private set; }
-		
-		public string Address { get; set; }
-
-		public override void EndInit()
-		{
-			this.IPEndPoint = NetworkHelper.ToIPEndPoint(this.Address);
-		}
-	}
-````
-InnerConfig is the configuration of the process inner network address in ET. Since IPEndPoint is not very configurable, we can configure it as a string and then convert the string to IPEndPoint in EndInit when deserializing.
-I also added this call to the protobuf deserialization method, refer to ProtobufHelper.cs, ET's protobuf because to support ilruntime, so remove the map support, if we want a map how to do? Here I gave the generated code are done, the proto messages are changed to a partial class, so that we can extend the class themselves, for example.
-```csharp
-message UnitInfo
-{
-	int64 UnitId = 1;
-
-	float X = 2;
-	float Y = 3;
-	float Z = 4;
-}
-
-// protobuf
-message G2C_EnterMap // IResponse
-{
-	int32 RpcId = 90;
-	int32 Error = 91;
-	string Message = 92;
-	// own unit id
-	int64 UnitId = 1;
-	// all the units
-	repeated UnitInfo Units = 2;
-}
-```
-This network message has a repeated UnitInfo field, which is actually an array in protobuf, which is not very convenient to use, I want to convert it to a Dictionary<Int64, UnitInfo> field, we can do something like this
-```csharp
-    public partial class G2C_EnterMap: ISupportInitialize
-    {
-        public Dictionary<Int64, UnitInfo> unitsDict = new Dictionary<long, UnitInfo>();
-        
-        public void BeginInit()
-        {
-        }
-
-        public void EndInit()
-        {
-            foreach (var unit in this.Units)
-            {
-                this.unitsDict.Add(unit.UnitId, unit);
-            }
-        }
-    }
-```
-The message is extended by such a piece of code, and after deserialization, it is automatically converted into a Dictionary.

+ 0 - 226
Book/3.2强大的MongoBson库.md

@@ -1,226 +0,0 @@
-# 强大的MongoBson库
-后端开发,统计了一下大概有这些场景需要用到序列化:
-1. 对象通过序列化反序列化clone
-2. 服务端数据库存储数据,二进制
-3. 分布式服务端,多进程间的消息,二进制
-4. 后端日志,文本格式
-5. 服务端的各种配置文件,文本格式
-
-C#序列化库有非常非常多了,protobuf,json等等。但是这些序列化库都无法应当所有场景,既要可读又要小。protobuf不支持复杂的对象结构(无法使用继承),做消息合适,做数据库存储和日志格式并不好用。json做日志格式合适,但是做网络消息和数据存储就太大。我们当然希望一个库能满足上面所有场景,理由如下:
-1. 你想想某天你的配置文件需要放到数据库中保存,你不需要进行格式转换,后端直接把前端发过来的配置消息保存到数据库中,这是不是能减少非常多错误呢?
-2. 某天有些服务端的配置文件不用文件格式了,需要放在数据库中,同样,只需要几行代码就可以完成迁移。
-3. 某天后端服务器crash,你需要扫描日志进行数据恢复,把日志进行反序列化成C#对象,一条条进行处理,再转成对象保存到数据库就完成了。
-4. 对象保存在数据库,直接就可以看到文本内容,可以做各种类sql的操作
-5. 想像一个场景,一个配置文本对象,反序列化到内存,通过网络消息发送,存储到数据库中。整个过程一气呵成。
-
-简单来说就是减少各种数据转换,减少代码,提高开发效率,提高可维护性。当然,Mongo Bson就能够满足。MongoDB库既可以序列化成文本也可以序列化成BSON的二进制格式,并且MongoDB本身就是一个游戏中使用非常多的数据库。Mongo Bson非常完善,是我见过功能最全使用最强大的序列化库,有些功能十分贴心。其支持功能如下:
-1. 支持复杂的继承结构
-2. 支持忽略某些字段序列化
-3. 支持字段默认值
-4. 结构多出多余的字段照样可以反序列化,这对多版本协议非常有用
-5. 支持ISupportInitialize接口使用,这个在反序列化的时候简直就是神器
-6. 支持文本json和二进制bson序列化
-7. MongoDB数据库支持
-
-简单的介绍下mongo bson库
-### 1.支持序列化反序列化成json或者bson
-```csharp
-    public sealed class Player
-    {
-        public long Id;
-
-        public string Account { get; private set; }
-
-        public long UnitId { get; set; }
-    }
-
-    Player player1 = new Player() { Id = 1 };
-    string json = player1.ToJson();
-    Console.WriteLine($"player1 to json: {json}");
-    Console.WriteLine($"player to bson: {player.ToBson().ToHex()}");
-    // output:
-    // player to json: { "_id" : NumberLong(1), "C" : [], "Account" : null, "UnitId" : NumberLong(0) }
-    // player to bson: B000000125F69640001000000000000000A4163636F756E740012556E6974496400000000000000000000
-
-
-```
-注意mongo的json跟标准的json有点区别,如果想用标准的json,可以传入一个JsonWriterSettings对象,限制使用JsonOutputMode.Strict模式
-```csharp
-    // 使用标准json
-    Player player2 = new Player() { Id = 1 };
-    Console.WriteLine($"player to json: {player2.ToJson(new JsonWriterSettings() {OutputMode = JsonOutputMode.Strict})}");
-    // player to json: { "_id" : 1, "C" : [], "Account" : null, "UnitId" : 0 }
-```
-
-反序列化json:
-```csharp
-            // 反序列化json
-        Player player11 = BsonSerializer.Deserialize<Player>(json);
-        Console.WriteLine($"player11 to json: {player11.ToJson()}");
-```
-反序列化bson:
-```csharp
-    // 反序列化bson
-    using (MemoryStream memoryStream = new MemoryStream(bson))
-    {
-        Player player12 = (Player) BsonSerializer.Deserialize(memoryStream, typeof (Player));
-        Console.WriteLine($"player12 to json: {player12.ToJson()}");
-    }
-```
-
-### 2.可以忽略某些字段
-[BsonIgnore]该标签用来禁止字段序列化。
-```csharp
-	public sealed class Player
-	{
-        public long Id;
-
-		[BsonIgnore]
-		public string Account { get; private set; }
-		
-		public long UnitId { get; set; }
-    }
-
-    Player player = new Player() { Id = 2, UnitId = 3, Account = "panda"};
-	Console.WriteLine($"player to json: {player.ToJson()}");
-    // player to json: { "_id" : 2, "UnitId" : 3 }
-```
-### 3.支持默认值以及取别名
-[BsonElement] 字段加上该标签,即使是private字段也会序列化(默认只序列化public字段),该标签还可以带一个string参数,给字段序列化指定别名。
-```csharp
-	public sealed class Player
-	{
-        public long Id;
-
-		public string Account { get; private set; }
-
-		[BsonElement("UId")]
-		public long UnitId { get; set; }
-    }
-    Player player = new Player() { Id = 2, UnitId = 3, Account = "panda"};
-	Console.WriteLine($"player to json: {player.ToJson()}");
-    // player to json: { "_id" : 2, "Account" : "panda", "UId" : 3 }
-```
-### 4.升级版本支持
-[BsonIgnoreExtraElements] 该标签用在class上面,反序列化时用来忽略多余的字段,一般版本兼容需要考虑,低版本的协议需要能够反
-序列化高版本的内容,否则新版本加了字段,旧版本结构反序列化会出错
-```csharp
-	[BsonIgnoreExtraElements]
-	public sealed class Player
-	{
-        public long Id;
-
-		public string Account { get; private set; }
-
-		[BsonElement("UId")]
-		public long UnitId { get; set; }
-    }
-```
-### 5.支持复杂的继承结构
-mongo bson库强大的地方在于完全支持序列化反序列化继承结构。需要注意的是,继承反序列化需要注册所有的父类,有两种方法:
-a. 你可以在父类上面使用[BsonKnownTypes]标签声明继承的子类,这样mongo会自动注册,例如:
-```csharp
-    [BsonKnownTypes(typeof(Entity))]
-    public class Component
-    {
-    }
-    [BsonKnownTypes(typeof(Player))]
-    public class Entity: Component
-    {
-    }
-    public sealed class Player: Entity
-    {
-        public long Id;
-        
-        public string Account { get; set; }
-		
-        public long UnitId { get; set; }
-    }
-```
-这样有缺陷,因为框架并不知道一个类会有哪些子类,这样做对框架代码有侵入性,我们希望能解除这个耦合
-。可以扫描程序集中所有子类父类的类型,将他们注册到mongo驱动中
-```csharp
-			Type[] types = typeof(Game).Assembly.GetTypes();
-			foreach (Type type in types)
-			{
-				if (!type.IsSubclassOf(typeof(Component)))
-				{
-					continue;
-				}
-
-				BsonClassMap.LookupClassMap(type);
-			}
-
-			BsonSerializer.RegisterSerializer(new EnumSerializer<NumericType>(BsonType.String));
-```
-这样完全的自动化注册,使用者也不需要关心类是否注册。
-
-### 6.ISupportInitialize接口
-mongo bson反序列化时支持一个ISupportInitialize接口,ISupportInitialize有两个方法
-```csharp
-    public interface ISupportInitialize
-    {
-        void BeginInit();
-        void EndInit();
-    }
-```
-BeginInit在反序列化前调用,EndInit在反序列化后调用。这个接口非常有用了,可以在反序列化后执行一些操作。例如
-```csharp
-	[BsonIgnoreExtraElements]
-	public class InnerConfig: AConfigComponent
-	{
-		[BsonIgnore]
-		public IPEndPoint IPEndPoint { get; private set; }
-		
-		public string Address { get; set; }
-
-		public override void EndInit()
-		{
-			this.IPEndPoint = NetworkHelper.ToIPEndPoint(this.Address);
-		}
-	}
-```
-InnerConfig是ET中进程内网地址的配置,由于IPEndPoint不太好配置,我们可以配置成string形式,然后反序列化的时候在EndInit中把string转换成IPEndPoint。
-同样我给protobuf反序列化方法也加上了这个调用,参考ProtobufHelper.cs,ET的protobuf因为要支持ilruntime,所以去掉了map的支持,假如我们想要一个map怎么办呢?这里我给生成的代码都做了手脚,把proto消息都改成了partial class,这样我们可以自己扩展这个class,比如:
-```csharp
-message UnitInfo
-{
-	int64 UnitId  = 1;
-
-	float X = 2;
-	float Y = 3;
-	float Z = 4;
-}
-
-// protobuf
-message G2C_EnterMap // IResponse
-{
-	int32 RpcId = 90;
-	int32 Error = 91;
-	string Message = 92;
-	// 自己的unit id
-	int64 UnitId = 1;
-	// 所有的unit
-	repeated UnitInfo Units = 2;
-}
-```
-这个网络消息有个repeated UnitInfo字段,在protobuf中其实是个数组,使用起来不是很方便,我希望转成一个Dictionary<Int64, UnitInfo>的字段,我们可以做这样的操作:
-```csharp
-    public partial class G2C_EnterMap: ISupportInitialize
-    {
-        public Dictionary<Int64, UnitInfo> unitsDict = new Dictionary<long, UnitInfo>();
-        
-        public void BeginInit()
-        {
-        }
-
-        public void EndInit()
-        {
-            foreach (var unit in this.Units)
-            {
-                this.unitsDict.Add(unit.UnitId, unit);
-            }
-        }
-    }
-```
-通过这样一段代码把消息进行扩展一下,反序列化出来之后,自动转成了一个Dictionary。

+ 0 - 79
Book/3.3Everything is Entity.md

@@ -1,79 +0,0 @@
-# Everything is Entity
-The ECS design is very popular right now, mainly because of the success of Watchtower, which has led to the explosion of this technology. The most important design of ECS is the complete separation of logic and data. That is, EC is pure data, System is actually logic, by data-driven logic. What does data-driven logic mean? It is very simple to detect data changes through Update and subscribe to data changes through the event mechanism, which is called data-driven. Other features such as cache hits are not too important in writing logic, modern games are scripted, even the performance of the script can tolerate how will care about cache hits that performance improvement?  
-
-For example, in order to reuse, the data must be split into very small particles, which will lead to a very large number of components. But the game is developed cooperatively by multiple people, each person is basically only familiar with their own modules, which may end up causing a large number of redundant components. Another problem is that the common ECS is flat, with only one layer of Entity and Component. It is like a company, the biggest is the boss, the boss with hundreds of people under him, the boss can not know all the people, to complete a task, the boss can not pick out the people they need. A reasonable approach is to have several managers under the boss, several supervisors under each manager, and several workers under each supervisor, so as to form a tree-like management structure that is easy to manage. This is similar to the ET approach, where Entity can manage Component, Component can manage Entity, and even Component can mount Component. e.g. a person is composed of head, body, hands, feet, and the head is composed of eyes, ears, nose, mouth.
-```csharp
-    Head head = human.AddComponent<Head>();
-    head.AddComponent<Eye>();
-    head.AddComponent<Mouse>();
-    AddComponent<Nose>();
-    AddComponent<Ear>();
-    human.AddComponent<Body>();
-    AddComponent<Hand>();
-    human.AddComponent<Leg>();
-```
-In ET, all data is Entity, including Entity, and Entity can be used either as a component or as a child of other Entity. Generic data is placed on Entity as a member, and less generic data can be hung on Entity as a component. For example, the design of items, all items have fields configured id, quantity, level, these fields are not necessary to make components, put on Entity will be more convenient to use.
-```csharp
-    class Item: Entity
-    {
-        // The configuration Id of the props
-        public int ConfigId { get; set; }
-        // The number of props
-        public int Count { get; set; }
-        // The level of the props
-        public int Level { get; set; }
-    }
-```
-This design data of ET is a kind of tree structure, very hierarchical and can understand the whole game architecture very easily. Scene, and the data of different modules are mounted on top of Game. Scene, and each module can mount a lot of data under itself. You don't have to think too much about how to design the classes, where to put the data, whether mounting here will lead to redundancy, etc. for each new feature. For example, if my player needs to do a prop system, I can design an ItemsComponent to be mounted on the Player, and I need to develop a SpellComponent to be mounted on the Player for skills. If the whole service needs to do an activity, get an activity component to hang on top of the Game. This design will be very easy to assign tasks and very modular.  
-
-# Some details of the component
-### 1. Component creation
-ComponentFactory provides three methods to create components Create, CreateWithParent, CreateWithId. Create is the simplest way to create components, it does several things  
-a. Construct a component based on the component type  
-b. Add the component to the event system and throw an AwakeSystem  
-c. Enables object pooling or not  
-CreateWithParent provides a Parent object on top of Create, which is set to the Component.Parent field. createWithId is used to create ComponentWithId or its subclasses, and you can set an Id on top of Create itself, Component can choose whether to use a pool of objects when it is created. All three types of factory methods have a fromPool parameter, the default is true.
-### 2. Release of components
-Component inherits an IDisposable interface. It should be noted that Component has unmanaged resources and must be called to delete a Component. This interface does the following  
-a. Throw Destroy System  
-b. If the component was created using an object pool, then it is put back into the object pool here  
-c. Remove the component from the global EventSystem and set the InstanceId to 0  
-If the component is mounted on Entity, then when Entity calls Dispose, it will automatically call the Dispose method of all components on it.  
-
-### 3. Role of InstanceId
-Any Component comes with an InstanceId field, which is reset when the component is constructed, or when the component is removed from the object pool, and which identifies the identity of the component. Why is such a field needed? There are several reasons  
-1. the existence of the object pool, the component may not be released, but returned to the object pool. In an asynchronous call, it is likely that the component has already been released and then reused, so that we need a way to be able to distinguish whether the previous component object has been released, such as the following code.
-```csharp
-		public static async ETVoid UpdateAsync(this ActorLocationSender self)
-		{
-			try
-			{
-				long instanceId = self.InstanceId;
-				while (true)
-				{
-					if (self.InstanceId ! = instanceId)
-					{
-						return;
-					}
-					ActorTask actorTask = await self.GetAsync();
-					
-					if (self.InstanceId ! = instanceId)
-					{
-						return;
-					}
-					if (actorTask.ActorRequest == null)
-					{
-						return;
-					}
-
-					await self.RunTask(actorTask);
-				}
-			}
-			catch (Exception e)
-			{
-				Log.Error(e);
-			}
-		}
-```
-While (true) is an asynchronous method, after await self.GetAsync() it is likely that the ActorLocationSender object has been released, and it is even possible that the object has been utilized again by other logic from the object pool. We can determine whether the object has been released by the change of InstanceId. 2.  
-2. InstanceId is globally unique and has location information, so you can find the location of the object by InstanceId and send the message to the object. This design will be utilized in the Actor message. Here for the time being will not talk about it.

+ 0 - 79
Book/3.3一切皆实体.md

@@ -1,79 +0,0 @@
-# 一切皆实体
-目前十分流行ECS设计,主要是守望先锋的成功,引爆了这种技术。守望先锋采用了状态帧这种网络技术,客户端会进行预测,预测不准需要进行回滚,由于组件式的设计,回滚可以只回滚某些组件即可。ECS最重要的设计是逻辑跟数据的完全分离。即EC是纯数据,System实际上就是逻辑,由数据驱动逻辑。数据驱动逻辑是什么意思呢?很简单通过Update检测数据变化,通过事件机制来订阅数据变化,这就是所谓的数据驱动了。其它的特点例如缓存命中,在编写逻辑上来说并不太重要,现代游戏都用脚本,连脚本的性能都能容忍怎么会在乎缓存命中那点性能提升?ET在设计的时候吸收了这些想法,但是并不完全照搬,目前的设计是我经过长期的思考跟重构得来的,还是有些自己特色。  
-
-传统的ECS写逻辑作者看来存在不少缺陷,比如为了复用,数据必然要拆成非常小的颗粒,会导致组件非常非常多。但是游戏是多人合作开发的,每个人基本上只熟悉自己的模块,最后可能造成组件大量冗余。还有个问题,常见的ECS是扁平式的,Entity跟Component只有一层。组件一多,开发功能可能不知道该使用哪些Component。好比一家公司,最大的是老板,老板手下带几百个人,老板不可能认识所有的人,完成一项任务,老板没法挑出自己需要的人。合理的做法是老板手下应该有几个经理,每个经理手下应该有几个主管,每个主管管理几个工人,这样形成树状的管理结构才会容易管理。这类似ET的做法,Entity可以管理Component,Component管理Entity,甚至Component还可以挂载Component。例如:人由头,身体,手,脚组成,而头又由眼睛,耳朵,鼻子,嘴巴组成。
-```csharp
-    Head head = human.AddComponent<Head>();
-    head.AddComponent<Eye>();
-    head.AddComponent<Mouse>();
-    head.AddComponent<Nose>();
-    head.AddComponent<Ear>();
-    human.AddComponent<Body>();
-    human.AddComponent<Hand>();
-    human.AddComponent<Leg>();
-```
-ET中,所有数据都是Entity,包括Entity,Entity既可以当成组件使用,也可以当做其它Entity的孩子。通用的数据放在Entity身上作为成员,不太通用的数据可以作为组件挂在Entity身上。比如物品的设计,所有物品都有配置id,数量,等级的字段,这些字段没有必要做成组件,放在Entity身上使用会更加方便。
-```csharp
-    class Item: Entity
-    {
-        // 道具的配置Id
-        public int ConfigId { get; set; }
-        // 道具的数量
-        public int Count { get; set; }
-        // 道具的等级
-        public int Level { get; set; }
-    }
-```
-ET的这种设计数据是一种树状的结构,非常有层次,能够非常轻松的理解整个游戏的架构。顶层Game.Scene,不同模块的数据都挂载在Game.Scene上面,每个模块自身下面又可以挂载很多数据。每开发一个新功能不用思考太多,类该怎么设计,数据放在什么地方,挂载这里会不会导致冗余等等。比如我玩家需要做一个道具系统,设计一个ItemsComponent挂在Player身上即可,需要技能开发一个SpellComponent挂在Player身上。全服需要做一个活动,搞个活动组件挂在Game.Scene上面。这种设计任务分派会很简单,十分的模块化。  
-
-# 组件的一些细节
-### 1.组件的创建
-组件的创建不要自己去new,应该统一使用ComponentFactory创建。ComponentFactory提供了三组方法用来创建组件Create,CreateWithParent,CreateWithId。Create是最简单的创建方式,它做了几个处理  
-a. 根据组件类型构造一个组件  
-b. 将组件加入事件系统,并且抛出一个AwakeSystem  
-c. 是否启用对象池  
-CreateWithParent在Create的基础上提供了一个Parent对象,设置到Component.Parent字段上。CreateWithId是用来创建ComponentWithId或者其子类的,在Create的基础上可以自己设置一个Id, Component在创建的时候可以选择是否使用对象池。三类工厂方法都带有一个fromPool的参数,默认是true。
-### 2.组件的释放
-Component都继承了一个IDisposable接口,需要注意,Component有非托管资源,删除一个Component必须调用该接口。该接口做了如下的操作  
-a. 抛出Destroy System  
-b. 如果组件是使用对象池创建的,那么在这里会放回对象池  
-c. 从全局事件系统(EventSystem)中删除该组件,并且将InstanceId设为0  
-如果组件挂载Entity身上,那么Entity调用Dispose的时候会自动调用身上所有Component的Dispose方法。  
-
-### 3.InstanceId的作用
-任何Component都带有一个InstanceId字段,这个字段会在组件构造,或者组件从对象池取出的时候重新设置,这个InstanceId标识这个组件的身份。为什么需要这么一个字段呢?有以下几个原因  
-1. 对象池的存在,组件未必会释放,而是回到对象池中。在异步调用中,很可能这个组件已经被释放了,然后又被重新利用了起来,这样我们需要一种方式能区分之前的组件对象是否已经被释放,例如下面这段代码:
-```csharp
-		public static async ETVoid UpdateAsync(this ActorLocationSender self)
-		{
-			try
-			{
-				long instanceId = self.InstanceId;
-				while (true)
-				{
-					if (self.InstanceId != instanceId)
-					{
-						return;
-					}
-					ActorTask actorTask = await self.GetAsync();
-					
-					if (self.InstanceId != instanceId)
-					{
-						return;
-					}
-					if (actorTask.ActorRequest == null)
-					{
-						return;
-					}
-
-					await self.RunTask(actorTask);
-				}
-			}
-			catch (Exception e)
-			{
-				Log.Error(e);
-			}
-		}
-```
-while (true)中是段异步方法,await self.GetAsync()之后很可能ActorLocationSender对象已经被释放了,甚至有可能这个对象又被其它逻辑从对象池中再次利用了起来。我们这时候可以通过InstanceId的变化来判断这个对象是否已经被释放掉。  
-2. InstanceId是全局唯一的,并且带有位置信息,可以通过InstanceId来找到对象的位置,将消息发给对象。这个设计将会Actor消息中利用到。这里暂时就不讲了。

+ 0 - 134
Book/3.4EventSystem.md

@@ -1,134 +0,0 @@
-# EventSystem  
-One of the most important features of ECS is the separation of data and logic, and the second is data-driven logic. What is data-driven logic? Not very well understood, let's take an example
-A moba game, the heroes have blood bars, which are displayed on the character's head, and also on the top left avatar UI. This time the server sends a blood deduction message. How do we handle this message? In the first way, we modify the hero's blood value in the message handling function, modify the blood bar display on the avatar, and modify the blood bar on the avatar UI at the same time. This approach obviously causes coupling between modules. In the second method, the blood value is only changed in the blood deduction message processing function, and the change of blood value throws an hpchange event, and both the avatar module and the UI module subscribe to the blood value change event and handle their own logic in the subscribed method, so that each module is responsible for its own logic without coupling.
-ET provides a variety of events, all of which can be subscribed multiple times:  
-1. AwakeSystem, which is thrown after the component factory creates a component, and is thrown only once, with parameters
-```csharp
-    Player player = ComponentFactory.Create<Player>();
-
-    // Subscribe to Player's Awake event
-    public class PlayerAwakeSystem: AwakeSystem<Player>
-    {
-        public override void Awake(Player self)
-        {
-        }
-    }
-````
-2. StartSystem, component UpdateSystem call before throwing
-```csharp
-    // Subscribe to Player's Start event
-    public class PlayerStartSystem: StartSystem<Player>
-    {
-        public override void Start(Player self)
-        {
-        }
-    }
-````
-3. UpdateSystem, component thrown every frame
-```csharp
-    // Subscribe to Player's Update event
-    public class PlayerUpdateSystem: UpdateSystem<Player>
-    {
-        public override void Update(Player self)
-        {
-        }
-    }
-````
-4. DestroySystem, thrown when the component is deleted
-```csharp
-    // Subscribe to Player's Destroy event
-    public class PlayerDestroySystem: DestroySystem<Player>
-    {
-        public override void Destroy(Player self)
-        {
-        }
-    }
-
-    Player player = ComponentFactory.Create<Player>();
-    // The Destroy event will be fired here
-    player.Dispose();
-```
-5. ChangeSystem, thrown when the content of the component changes, needs to be triggered manually by the developer
-```csharp
-    // Subscribe to Player's Destroy event
-    public class PlayerChangeSystem: ChangeSystem<Player>
-    {
-        public override void Change(Player self)
-        {
-        }
-    }
-
-    Player player = ComponentFactory.Create<Player>();
-    // need to trigger ChangeSystem manually
-    Game.EventSystem.Change(player);
-```
-6. DeserializeSystem, thrown after component deserialization
-```csharp
-    // Subscribe to Player's Deserialize event
-    public class PlayerDeserializeSystem: DeserializeSystem<Player>
-    {
-        public override void Deserialize(Player self)
-        {
-        }
-    }
-
-    // Here player2 will trigger the Deserialize event
-    Player player2 = MongoHelper.FromBson<Player>(player.ToBson());
-```
-7. LoadSystem, EventSystem thrown when loading dll, used for server-side hot update, reload dll to do some processing, such as re-register handler
-```csharp
-    // Subscribe to Player's Load event
-    public class PlayerLoadSystem: LoadSystem<Player>
-    {
-        public override void Load(Player self)
-        {
-        }
-    }
-```
-8. normal Event, thrown by the developer himself, can take up to three parameters. Also the client hot change layer can subscribe to the mono layer Event events
-```csharp
-    int oldhp = 10;
-    int newhp = 5;
-    // Throw hp change event
-    Game.EventSystem.Run("HpChange", oldhp, newhp);
-
-    // UI subscribe to the hp change event
-    [Event("HpChange")]
-    public class HpChange_ShowUI: AEvent<int, int>
-    {
-        public override void Run(int a, int b)
-        {
-            throw new NotImplementedException();
-        }
-    }
-
-    // The model header blood bar module also subscribes to the hp change event
-    [Event("HpChange")
-    public class HpChange_ModelHeadChange: AEvent<int, int>
-    {
-        public override void Run(int a, int b)
-        {
-            throw new NotImplementedException();
-        }
-    }
-```
-
-9. There are many other events, such as message events. Message events are declared using MessageHandler and can take parameters to specify which server to subscribe to.
-```csharp
-	[MessageHandler(AppType.Gate)]
-	public class C2G_LoginGateHandler : AMRpcHandler<C2G_LoginGate, G2C_LoginGate>
-	{
-		protected override void Run(Session session, C2G_LoginGate message, Action<G2C_LoginGate> reply)
-		{
-			G2C_LoginGate response = new G2C_LoginGate();
-			reply(reply);
-		}
-	}
-```
-More specific message events will be explained in detail when we talk about messages  
-10. numeric events, numeric module and then explain  
-...... , more events to be developed by yourself.
-
-The logic of the ET framework is driven by the various events above.
-
-

+ 0 - 134
Book/3.4事件机制EventSystem.md

@@ -1,134 +0,0 @@
-# 事件机制EventSystem  
-ECS最重要的特性一是数据跟逻辑分离,二是数据驱动逻辑。什么是数据驱动逻辑呢?不太好理解,我们举个例子
-一个moba游戏,英雄都有血条,血条会在人物头上显示,也会在左上方头像UI上显示。这时候服务端发来一个扣血消息。我们怎么处理这个消息?第一种方法,在消息处理函数中修改英雄的血数值,修改头像上血条显示,同时修改头像UI的血条。这种方式很明显造成了模块间的耦合。第二种方法,扣血消息处理函数中只是改变血值,血值的改变抛出一个hpchange的事件,人物头像模块跟UI模块都订阅血值改变事件,在订阅的方法中分别处理自己的逻辑,这样各个模块负责自己的逻辑,没有耦合。
-ET提供了多种事件,事件都是可以多次订阅的:  
-1. AwakeSystem,组件工厂创建组件后抛出,只抛出一次,可以带参数
-```csharp
-    Player player = ComponentFactory.Create<Player>();
-
-    // 订阅Player的Awake事件
-    public class PlayerAwakeSystem: AwakeSystem<Player>
-    {
-        public override void Awake(Player self)
-        {
-        }
-    }
-```
-2. StartSystem,组件UpdateSystem调用前抛出
-```csharp
-    // 订阅Player的Start事件
-    public class PlayerStartSystem: StartSystem<Player>
-    {
-        public override void Start(Player self)
-        {
-        }
-    }
-```
-3. UpdateSystem,组件每帧抛出
-```csharp
-    // 订阅Player的Update事件
-    public class PlayerUpdateSystem: UpdateSystem<Player>
-    {
-        public override void Update(Player self)
-        {
-        }
-    }
-```
-4. DestroySystem,组件删除时抛出
-```csharp
-    // 订阅Player的Destroy事件
-    public class PlayerDestroySystem: DestroySystem<Player>
-    {
-        public override void Destroy(Player self)
-        {
-        }
-    }
-
-    Player player = ComponentFactory.Create<Player>();
-    // 这里会触发Destroy事件
-    player.Dispose();
-```
-5. ChangeSystem,组件内容改变时抛出,需要开发者手动触发
-```csharp
-    // 订阅Player的Destroy事件
-    public class PlayerChangeSystem: ChangeSystem<Player>
-    {
-        public override void Change(Player self)
-        {
-        }
-    }
-
-    Player player = ComponentFactory.Create<Player>();
-    // 需要手动触发ChangeSystem
-    Game.EventSystem.Change(player);
-```
-6. DeserializeSystem,组件反序列化之后抛出
-```csharp
-    // 订阅Player的Deserialize事件
-    public class PlayerDeserializeSystem: DeserializeSystem<Player>
-    {
-        public override void Deserialize(Player self)
-        {
-        }
-    }
-
-    // 这里player2会触发Deserialize事件
-    Player player2 = MongoHelper.FromBson<Player>(player.ToBson());
-```
-7. LoadSystem,EventSystem加载dll时抛出,用于服务端热更新,重新加载dll做一些处理,比如重新注册handler
-```csharp
-    // 订阅Player的Load事件
-    public class PlayerLoadSystem: LoadSystem<Player>
-    {
-        public override void Load(Player self)
-        {
-        }
-    }
-```
-8. 普通的Event,由开发者自己抛出,可以最多带三个参数。另外客户端热更层也可以订阅mono层的Event事件
-```csharp
-    int oldhp = 10;
-    int newhp = 5;
-    // 抛出hp改变事件
-    Game.EventSystem.Run("HpChange", oldhp, newhp);
-
-    // UI订阅hp改变事件
-    [Event("HpChange")]
-    public class HpChange_ShowUI: AEvent<int, int>
-    {
-        public override void Run(int a, int b)
-        {
-            throw new NotImplementedException();
-        }
-    }
-
-    // 模型头顶血条模块也订阅hp改变事件
-    [Event("HpChange")]
-    public class HpChange_ModelHeadChange: AEvent<int, int>
-    {
-        public override void Run(int a, int b)
-        {
-            throw new NotImplementedException();
-        }
-    }
-```
-
-9. 除此之外还有很多事件,例如消息事件。消息事件使用MessageHandler来声明,可以带参数指定哪种服务器需要订阅。
-```csharp
-	[MessageHandler(AppType.Gate)]
-	public class C2G_LoginGateHandler : AMRpcHandler<C2G_LoginGate, G2C_LoginGate>
-	{
-		protected override void Run(Session session, C2G_LoginGate message, Action<G2C_LoginGate> reply)
-		{
-			G2C_LoginGate response = new G2C_LoginGate();
-			reply(response);
-		}
-	}
-```
-更具体的消息事件等到讲消息的时候再细细讲解了  
-10. 数值事件,数值模块再讲解  
-......, 更多的事件由自己去开发。
-
-ET框架的逻辑就是由以上各种事件来驱动的。
-
-

+ 0 - 98
Book/4.1Component-based design.md

@@ -1,98 +0,0 @@
-# Component-based design
-
-In terms of code reuse and organization of data, object-oriented may be the first response. Object-oriented three features inheritance, encapsulation, polymorphism, to a certain extent can solve a lot of code reuse, data reuse problems. But object-oriented is not a panacea, it also has great flaws: # # 1.  
-
-## 1. data structure coupling is very strong
-Once a field is added or removed from a parent class, it may have to affect all subclasses and affect all subclass-related logic. This seems very inflexible, in a complex set of inheritance system, to change the fields in the parent class will become more and more troublesome, let's say ABC is a subclass of D, one day found the need to add a data that AB has, but C does not, then this data is certainly not good to put into the parent class, only AB abstracted out of a parent class E, E inherited from D, AB common fields added to E, once the inheritance structure Once the inheritance structure has changed, the interface may also need to change, let's say there is an interface incoming parameter type is E, when AB no longer needs the common field, then the inheritance relationship needs to be adjusted so that AB inherits D again, then the interface incoming parameter type needs to be changed to D, where the logic code is likely to be adjusted. What's worse is that the game logic changes very complicated and very often, maybe today a field is added, tomorrow it is deleted, if every time to adjust the inheritance structure, it is a nightmare. Inheritance structure feels very powerless in the face of frequent data structure adjustment.    
-## 2. Difficult to hot-plug
-Inheritance structure can't add or delete fields at runtime, for example, Player usually walks, and then rides after using mount. The problem is that the information about the mount would need to be hanging on top of the Player object all the time. This makes it inflexible. Why do I need to have the horse data in memory when I'm not riding? The interface has the same problem, a class implements an interface, then the interface will always stick to the class, you want to get rid of her can not, or horse riding for example, the player player can ride, then may inherit a riding interface, the problem is, when I the Player from the mount, the player player still has the riding interface, there is no way to There is no way to dynamically delete this interface! The example may not be quite right, but the reasoning should be clear.  
-
-The use of object-oriented may lead to disastrous consequences, game development in the new and old, there are good skills, there are poor skills. People like to be lazy, when you find the trouble to adjust the inheritance relationship, it is possible to add a field in AB to save trouble directly into the parent class D. The result is that C somehow has an extra field that is useless. The key can not be found, and finally lead to the parent class D is getting bigger and bigger, in the end, it may simply not ABC, directly let all the objects into D, convenient well! Yes, many games are doing this, the development of the end simply do not care about the inheritance relationship, because you want to manage can not manage.  
-
-## 3. method and data coupling
-Traditional object-oriented are class with methods, and especially advocate virtual function polymorphism. Methods and data put together brings a lot of coupling problems. In order to solve these coupling, we came up with a large number of design patterns, such as dependency interfaces, dependency transposition. To be honest, this is pants down, in order to understand the coupling, make the class into an interface, and then inherit from the interface, is this not called dependency? These practices lead to code that is full of interfaces and extremely difficult to read. There is no standard for writing up code, and the code written by experts and rookies is completely different. Most coders are logic boys, who have time to think about how to design this class every day ah? As the logic becomes more and more complex class inside the method will become increasingly large, the terrible thing is that this is the method of the class, extremely difficult to refactor, many projects can see the class inside the existence of tens of thousands of lines of code of virtual functions. Gosh!
-
-
-Object-oriented in the face of complex game logic is very powerless, so many game developers have gone backwards, using process-oriented development game, process-oriented, simple and brutal, do not consider complex inheritance, do not consider abstraction, do not consider polymorphism, is the development of the session of freestyle, roll up your sleeves on the jerk, but at the same time, the reusability of the code logic, data reusability is also greatly reduced. Process-oriented is also not a good game development model.  
-
-
-Component pattern is a good solution to object-oriented and process-oriented defects, in the game client is very widely used, Unity3d, Unreal 4, and so on are using the component pattern. The characteristics of the component pattern.
-1. Highly modular, a component is a piece of data plus a paragraph of logic  
-2. Components can be hot-pluggable, you need to add, do not need to remove  
-3. Very few dependencies between types, any type of adding or removing components will not affect other types.  
-
-
-But at present only a very small number of server-side design using components, Watchtower server-side should be the use of component design, Watchtower developers called ECS architecture, in fact, is a variant of the component model, E is Entity, C is Component, S is System, in fact, is the component Component logic and data stripped, the logic part called System, the topic is far away, or back to the ET framework to put.  
-
-ET framework uses the design of components. Everything is Entity and Component, any class inherited from Entity can be mounted components, such as the player class.
-
-```C#
-public sealed class Player : Entity
-{
-    public string Account { get; private set; }
-    public long UnitId { get; set; }
-	
-    public void Awake(string account)
-    {
-        this.Account = account;
-    }
-	
-    public override void Dispose()
-    {
-        if (this.Id == 0)
-        {
-            return;
-        }
-        base.Dispose();
-    }
-}
-```
-Mount a MoveComponent to the player object so that the player can move, a Backpack component to the player so that the player can manage items, a Skill component to the player so that the player can cast skills, and a Buff component to manage buffs.  
-
-```C#
-player.AddComponent<MoveComponent>();
-player.AddComponent<ItemsComponent>();
-player.AddComponent<SpellComponent>();
-AddComponent<BuffComponent>(); player.AddComponent<BuffComponent>();
-```
-
-Components are highly reusable, for example, an NPC, he can also move, just put MoveComponent on the NPC, some NPCs can also cast skills, then put SpellComponent on it, NPCs do not need a backpack, then there is no need to hang ItemsComponent  
-
-ET framework modules are all made into the form of components, a process is also made of different components stitched together. Let's say Loginserver needs to connect externally and also needs to connect with the server internally, then login server hooks up  
-
-```C#
-// intranet network component NetInnerComponent, which handles the connection to the intranet  
-Game.Scene.AddComponent<NetInnerComponent, string, int>(innerConfig.Host, innerConfig.Port);
-// NetOuterComponent, an extranet component, handles the connection to the client
-Game.Scene.AddComponent<NetOuterComponent, string, int>(outerConfig.Host, outerConfig.Port);
-```
-
-For example, battle server does not need to connect to the external network (external network messages are forwarded by gateserver), so it is natural to just mount an internal network component.
-Similar to Unity3d components, ET framework also provides component events, such as Awake, Start, Update, etc.. To add these events to a Component or Entity, you must write a helper class. For example, if the NetInnerComponent component needs Awake and Update methods, then add a class like this.  
-
-```C#
-[ObjectEvent]
-public class NetInnerComponentEvent : ObjectEvent<NetInnerComponent>, IAwake, IUpdate
-{
-    public void Awake()
-    {
-        this.Get().Awake();
-    }
-
-    public void Update()
-    {
-        this.Get().Update();
-    }
-}
-````
-
-In this way, NetInnerComponent calls its Awake method after AddComponent and calls the Update method every frame.
-ET does not use reflection like Unity to implement this kind of functionality, because reflection performance is poor, and the advantage of this implementation is that the class can be placed in the hot update dll, so that the component's Awake Start, Update method and other methods can be placed in the hot update layer. Entity and Component will be made into a class without methods, methods are put into the hot update layer to facilitate the hot update to fix logic bugs.  
-
-The biggest advantage of component-based development is that whether rookie or expert, the development of a function can quickly know how to organize data how to organize the logic. Object-oriented can be completely abandoned. Object-oriented development using the most headache is that I should inherit which class it? Before doing the most horrible is Unreal three, Unreal three inheritance structure is very multi-layer, completely do not know where they need to start inheritance. In the end, it could lead to a very small function that inherits a very large class, which is common in Unreal 3 development. So Unreal 4 switched to the component pattern. The module isolation of the component pattern is very good, the technical rookie a component written very poorly, will not affect the other modules, the big deal is to rewrite the component on the good. 
-
-ET's component design innovation, method and data separation, completely decoupled, no brainstorming how to uncouple, write static methods at will, there is no coupling, even the code written by rookies is easy to refactor.
-
-It is because ET uses the detachable component model, ET can load all server components to the same process, then this one process can be used as a set of distributed servers. From then on it is possible to debug distributed servers with vs. Because of this, the usual development only use a process, when the release of the release into multiple processes on the line. Honestly, not to brag, this is a great invention, this invention solves a big problem in the development of distributed game servers, greatly improving the efficiency of development.    
-
-

+ 0 - 98
Book/4.1组件式设计.md

@@ -1,98 +0,0 @@
-# 组件式设计
-
-在代码复用和组织数据方面,面向对象可能是大家第一反应。面向对象三大特性继承,封装,多态,在一定程度上能解决不少代码复用,数据复用的问题。不过面向对象不是万能的,它也有极大的缺陷:  
-
-## 1. 数据结构耦合性极强
-一旦父类中增加或删除某个字段,可能要影响到所有子类,影响到所有子类相关的逻辑。这显得非常不灵活,在一套复杂的继承体系中,往父类中改变字段会变得越来越麻烦,比方说ABC是D的子类,某天发现需要增加一个AB都有的数据,但是C没有,那么这个数据肯定不好放到父类中,只能将AB抽象出来一个父类E,E继承于D,AB共有的字段加到E中,一旦继承结构发生了变化,可能接口也要改变,比方说之前有个接口传入参数类型是E,当AB不再需要共用的那个字段,那么需要调整继承关系,让AB重新继承D,那么这个接口的传入参数类型需要改成D,其中的逻辑代码很可能也要发生调整。更可怕的是游戏逻辑变化非常复杂,非常频繁,可能今天加了个字段,明天又删掉了,假如每次都要去调整继承结构,这简直就是噩梦。继承结构面对频繁的数据结构调整感觉很无力。    
-## 2. 难以热插拔
-继承结构无法运行时增加删除字段,比如玩家Player平常是走路,使用坐骑后就骑马。问题是坐骑的相关信息就需要一直挂在Player对象上面。这就显得很不灵活,我不骑马的时候内存中为啥要有马的数据?接口也有同样的问题,一个类实现了一个接口,那么这个接口就永远粘在了这个类身上,你想甩掉她都不行,还是以骑马为例,玩家Player可以进行骑行,那么可能继承一个骑行的接口,问题是,当我这个Player从坐骑上下来时,玩家Player身上还是有骑行的接口,根本没法动态删掉这个接口!可能例子举得不是很对,但是道理表述的应该很清楚了。  
-
-使用面向对象可能导致灾难性后果,游戏开发中有新人有老人,有技术好的,有技术差的。人都是喜欢偷懒的,当你发现调整继承关系麻烦的时候,有可能AB中增加一个字段为了省事直接就放到父类D中去了。导致C莫名奇妙的多了一个无用的字段。关键还没法发现,最后导致父类D越来越大,到最后有可能干脆就不用ABC了,直接让所有对象都变成D,方便嘛!是的,很多游戏就是这么干的,开发到最后根本就不管继承关系了,因为想管也管不了了。  
-
-## 3. 方法与数据耦合
-传统面向对象都是class中带有方法,并且特别提倡虚函数多态。方法跟数据放在一起带来了特别多耦合的问题。为了解决这些耦合,大家想出了大量的设计模式,比如依赖接口,依赖转置。说实话,这就是脱裤子放屁,为了解耦合,把类做成接口,然后继承接口,难道这就不叫依赖了?这些做法导致,代码中到处是接口,代码阅读极其困难。写起代码来也没有个标准,高手跟菜鸡写出来的代码完全是两回事。大部分码农都是逻辑仔,谁有时间天天想这个类要怎么设计啊?随着逻辑越来越复杂类里面的方法将越来越庞大,可怕的是,这是这个类的方法,极其难以重构,很多项目中能看到类里面存在上万行代码的虚函数。天哪!
-
-
-面向对象在面对复杂的游戏逻辑时很无力,所以很多游戏开发者又倒退了回去,使用面向过程进行开发游戏,面向过程,简单粗暴,不考虑复杂的继承,不考虑抽象,不考虑多态,是开发届的freestyle,挽起袖子就开撸,但同时,代码逻辑的复用性,数据的复用性也大大降低。面向过程也不是一种好的游戏开发模式。  
-
-
-组件模式很好的解决了面向对象以及面向过程的种种缺陷,在游戏客户端中使用非常广泛,Unity3d,虚幻4,等等都使用了组件模式。组件模式的特点:
-1.高度模块化,一个组件就是一份数据加一段逻辑  
-2.组件可热插拔,需要就加上,不需要就删除  
-3.类型之间依赖极少,任何类型增加或删除组件不会影响到其它类型。  
-
-
-但是目前只有极少有服务端使用了组件的设计,守望先锋服务端应该是使用了组件的设计,守望先锋的开发人员称之为ECS架构,其实就是组件模式的一个变种,E就是Entity,C就是Component,S是System,其实就是将组件Component的逻辑与数据剥离,逻辑部分叫System,话题扯远了,还是回到ET框架来把。  
-
-ET框架使用了组件的设计。一切都是Entity和Component,任何类继承于Entity都可以挂载组件,例如玩家类:
-
-```C#
-public sealed class Player : Entity
-{
-    public string Account { get; private set; }
-    public long UnitId { get; set; }
-	
-    public void Awake(string account)
-    {
-        this.Account = account;
-    }
-	
-    public override void Dispose()
-    {
-        if (this.Id == 0)
-        {
-            return;
-        }
-        base.Dispose();
-    }
-}
-```
-给玩家对象挂载个移动组件MoveComponent,这样玩家就可以移动了,给玩家挂上一个背包组件,玩家就可以管理物品了,给玩家挂上技能组件,那么玩家就可以施放技能了,加上Buff组件就可以管理buff了。  
-
-```C#
-player.AddComponent<MoveComponent>();
-player.AddComponent<ItemsComponent>();
-player.AddComponent<SpellComponent>();
-player.AddComponent<BuffComponent>();
-```
-
-组件是高度可以复用的,比如一个NPC,他也可以移动,给NPC也挂上MoveComponent就行了,有的NPC也可以施放技能,那么给它挂上SpellComponent,NPC不需要背包,那么就不用挂ItemsComponent了  
-
-ET框架模块全部做成了组件的形式,一个进程也是由不同的组件拼接而成。比方说Loginserver需要对外连接也需要与服务器内部进行连接,那么login server挂上  
-
-```C#
-// 内网网络组件NetInnerComponent,处理对内网连接  
-Game.Scene.AddComponent<NetInnerComponent, string, int>(innerConfig.Host, innerConfig.Port);
-// 外网网络组件NetOuterComponent,处理与客户端连接
-Game.Scene.AddComponent<NetOuterComponent, string, int>(outerConfig.Host, outerConfig.Port);
-```
-
-比如battle server就不需要对外网连接(外网消息由gateserver转发),那么很自然的只需要挂载一个内网组件即可。
-类似Unity3d的组件,ET框架也提供了组件事件,例如Awake,Start,Update等。要给一个Component或者Entity加上这些事件,必须写一个辅助类。比如NetInnerComponent组件需要Awake跟Update方法,那么添加一个这样的类即可:  
-
-```C#
-[ObjectEvent]
-public class NetInnerComponentEvent : ObjectEvent<NetInnerComponent>, IAwake, IUpdate
-{
-    public void Awake()
-    {
-        this.Get().Awake();
-    }
-
-    public void Update()
-    {
-        this.Get().Update();
-    }
-}
-```
-
-这样,NetInnerComponent在AddComponent之后会调用其Awake方法,并且每帧调用Update方法。
-ET没有像Unity使用反射去实现这种功能,因为反射性能比较差,而且这样实现的好处是这个类可以放到热更dll中,这样组件的Awake Start,Update方法以及其它方法都可以放到热更层中。将Entity和Component做成没有方法的类,方法都放到热更层,方便热更修复逻辑bug。  
-
-组件式开发最大的好处就是不管菜鸟还是高手,开发一个功能都能很快的知道怎么组织数据怎么组织逻辑。可以完全放弃面向对象。使用面向对象开发最头疼的就是我该继承哪个类呢?之前做过最恐怖的就是虚幻三,虚幻三的继承结构非常多层,完全不知道自己需要从哪里开始继承。最后可能导致一个非常小的功能,继承了一个及其巨大的类,这在虚幻三开发中屡见不鲜。所以虚幻4改用了组件模式。组件模式的模块隔离性非常好,技术菜鸟某个组件写得非常差,也不会影响到其它模块,大不了重写这个组件就好了。 
-
-ET的组件设计有所创新,方法跟数据分离,完全解除耦合,不用绞尽脑汁去想怎么解除耦合,随意写静态方法即可,根本不存在耦合,即使是菜鸟写的代码也很容易重构。
-
-正是因为ET使用了可拆卸的组件模式,ET可以将所有服务器组件都装到同一个进程上,那么这一个进程就可以当作一组分布式服务器使用。从此用vs调试分布式服务器成为了可能。正因为这样,平常开发只使用一个进程,发布的时候发布成多个进程就行了。说实在的,不是吹牛,这是一个伟大的发明,这一发明解决了分布式游戏服务器开发中的大大大难题,极大的提高了开发效率。    
-
-

+ 0 - 95
Book/5.4Actor Model.md

@@ -1,95 +0,0 @@
-# Actor model
-### Actor introduction
-Before discussing the Actor model, we should discuss the architecture of ET. There are two architectures for game servers in order to use multi-core, single-threaded multi-process and single-process multi-threaded architecture. ET uses single-threaded multi-process architecture, while the traditional Actor model is generally single-process multi-threaded architecture, which is a major difference. The advantages and disadvantages are as follows.  
-1. the logic needs to be single-threaded this is the same, erlang process logic is single-threaded, skynet lua virtual machine is also single-threaded. et in a process is actually equivalent to an erlang process, a skynet lua virtual machine.  
-2. the use of single-threaded multi-process does not need to write their own set of profiler tools, you can use a lot of ready-made profiler tools, such as view memory, cpu occupation directly with top command, this point erlang and skynet need to get another set of tools.  
-3. multi-process single-threaded architecture also has a benefit, a single physical machine and multiple physical machines is no difference, single process multi-threaded also need to consider the processing of multiple physical machines.  
-4. multi-process single-threaded architecture is a bit of a drawback is that messages need to be serialized and deserialized across processes, taking up a bit of resources. In addition, sending network messages will have a few milliseconds delay. Generally these effects can be ignored.  
-
-The original Actor model is used for single-process multi-threaded architecture, there is a reason for this, because multi-threaded architecture developers can easily access shared variables at will, let's say a variable a, thread 1 can access, thread 2 can also access, so that both threads need to add locks when accessing the variable a, shared variables more locks everywhere, will become unmaintainable, the framework must not appear The framework must not have a situation where there are threads sharing variables everywhere. In order to ensure that the multi-threaded architecture does not go wrong, it is necessary to provide a development model that ensures easy and safe multi-threaded development. erlang's concurrency mechanism is the actor model. erlang virtual machine uses multiple threads to take advantage of multiple cores. erlang has designed a mechanism that designs its own processes on top of the virtual machine. At its simplest, each erlang process manages its own variables, and the logic of each erlang process runs on a single thread. The logic between the erlang process and the process is completely isolated, so that there are no two threads accessing the same variable and there is no multithreaded competition. The next question arises, since each erlang process has its own data and the logic is completely isolated, how should the two erlang processes communicate with each other? This is where the Actor model comes in. erlang has designed a messaging mechanism: one process can send messages to other processes, and erlang processes communicate with each other through messages. Isn't this the same message queue used by operating systems for inter-process communication? Yes, in fact, it is similar. erlang inside the process id to get the process can send messages to this process.   
-
-If the message is only sent to the process, it is still a bit inconvenient. For example, if you take an erlang process as a moba team process, and there are 10 players in the battle process, if you use erlang's actor message, the message can only be sent to the battle process, but often the message needs to be sent to a player, then erlang needs to distribute the message to the specific player again according to the player id in the message, so it actually goes around one more time. This is actually an extra detour.
-
-### ET's Actor
-According to the characteristics of its own architecture, ET does not completely copy the Actor model of erlang, but provides the Entity object-level Actor model. In ET, an Actor is an Entity object, and a MailboxComponent component attached to an Entity is an Actor. You only need to know the Entity's InstanceId to send messages to the Entity. In fact, erlang's Actor model is a special case of ET, such as giving the ET server Game.Scene as an Actor, so that it can become a process-level Actor. It only needs to know the InstanceId (ET) or the Pid (erlang) of the process to send it to the other party.
-
-| Language | ET | Erlang | Skynet |
-| | ET | Erlang | Skynet | -- | :--: | :--: | :--: |
-| Architecture | Single-Threaded Multi-Process | Single-Process Multi-Threaded | Single-Process Multi-Threaded |
-| Actor | Entity | erlang process | lua virtual machine |
-| ActorId | Entity.InstanceId | erlang processId | service address |
-
-### Use of ET's Actor
-For a normal Actor, we can refer to the Gate Session. map has a Unit, and the Unit holds the gate session corresponding to the player. thus, if a message in map needs to be sent to the client, it only needs to send the message to the gate session, and the gate session forwards it to the client when it receives the message. The map process sending messages to the gate session is a typical actor model. It doesn't need to know the location of the gate session, it just needs to know its InstanceId. messageHelper.cs gets an ActorMessageSender from GateSessionActorId and sends it.
-```csharp
-// Get an ActorSenderComponent from Game.Scene, then get an ActorMessageSender by InstanceId
-ActorSenderComponent actorSenderComponent = Game.Scene.GetComponent<ActorSenderComponent>();
-ActorMessageSender actorMessageSender = actorSenderComponent.Get(unitGateComponent.GateSessionActorId);
-// send
-actorMessageSender.Send(message);
-
-// rpc
-var response = actorMessageSender.Call(message);
-```
-
-The question is how do you know the InstanceId of the gate session in map? This is where you need to find a way to pass it on, for example in ET, when the player is logging in to gate, the gate session hooks up a mailbox MailBoxComponent, C2G_LoginGateHandler.cs
-```csharp
-session.AddComponent<MailBoxComponent, string>(MailboxType.GateSession);
-```
-The InstanceId of this gate session is brought into the map when the player logs into the map process, in C2G_EnterMapHandler.cs
-```csharp
-M2G_CreateUnit createUnit = (M2G_CreateUnit)await mapSession.Call(new G2M_CreateUnit() { PlayerId = player.Id, GateSessionId = session. InstanceId });
-```
-
-### Handling of Actor messages
-First, the message arrives at the MailboxComponent, which has a type, and different types of mailboxes can do different processing. Currently, there are two types of mailboxes, GateSession and MessageDispatcher; GateSession mailboxes will immediately forward messages to the client when they are received, and MessageDispatcher types will again distribute the Actor messages to the specific Handler for processing. The default MailboxComponent type is MessageDispatcher. customizing a mailbox type is also very simple, inherit the IMailboxHandler interface and add the MailboxHandler tag. So why do we need to add such a feature? This feature does not exist in other actor models, and messages are generally received and distributed. The reason is that GateSession is not designed for distribution, so I added the mailbox type here. messageDispatcher has two ways of handling messages, one is to handle the messages sent by the other party, and the other is rpc messages  
-```csharp
-    // To handle Send messages, you need to inherit the AMActorHandler abstract class. The first generic parameter of the abstract class is the type of the Actor, and the second parameter is the type of the message
-	[ActorMessageHandler(AppType.Map)]
-	public class Actor_TestHandler : AMActorHandler<Unit, Actor_Test>
-	{
-		protected override ETTask Run(Unit unit, Actor_Test message)
-		{
-			Log.Debug(message.Info);
-		}
-	}
-
-    // To handle Rpc messages, you need to inherit the AMActorRpcHandler abstract class, the first generic parameter of the abstract class is the type of the Actor, the second parameter is the type of the message, and the third parameter is the type of the returned message
-    [ActorMessageHandler(AppType.Map)]
-	public class Actor_TransferHandler : AMActorRpcHandler<Unit, Actor_TransferRequest, Actor_TransferResponse>
-	{
-		protected override async ETTask Run(Unit unit, Actor_TransferRequest message, Action<Actor_TransferResponse> reply)
-		{
-			Actor_TransferResponse response = new Actor_TransferResponse();
-
-			try
-			{
-				reply(response);
-			}
-			catch (Exception e)
-			{
-				ReplyError(response, e, reply);
-			}
-		}
-	}
-```
-
-We should note that Actor messages have the potential to deadlock, such as A call to B, B call to C, and C call to A. Because MailboxComponent is essentially a message queue, it opens a concurrent process that will process one message at a time, returning ETTask to indicate that the message processing class will block MailboxComponent queue of other messages. So if there is a deadlock, we don't want a message processing to block the rest of the MailboxComponent messages, we can just open a new thread in the message processing class to handle it. For example:
-```csharp
-	[ActorMessageHandler(AppType.Map)]
-	public class Actor_TestHandler : AMActorHandler<Unit, Actor_Test>
-	{
-		protected override ETTask Run(Unit unit, Actor_Test message)
-		{
-			RunAsync(unit, message).Coroutine();
-		}
-
-        public ETVoid RunAsync(Unit unit, Actor_Test message)
-        {
-            Log.Debug(message.Info);
-        }
-	}
-```
-For related information, you can Google the Actor deadlock problem.
-
-

+ 0 - 95
Book/5.4Actor模型.md

@@ -1,95 +0,0 @@
-# Actor模型
-### Actor介绍
-在讨论Actor模型之前先要讨论下ET的架构,游戏服务器为了利用多核一般有两种架构,单线程多进程跟单进程多线程架构。两种架构本质上其实区别不大,因为游戏逻辑开发都需要用单线程,即使是单进程多线程架构,也要用一定的方法保证单线程开发逻辑。ET采用的是单线程多进程的架构,而传统Actor模型一般是单进程多线程的架构,这点是比较大的区别,不能说谁更好,只能说各有优势。优劣如下:  
-1. 逻辑需要单线程这点都是一样的,erlang进程逻辑是单线程的,skynet lua虚拟机也是单线程的。ET中一个进程其实相当于一个erlang进程,一个skynet lua虚拟机。  
-2. 采用单线程多进程不需要自己再写一套profiler工具,可以利用很多现成的profiler工具,例如查看内存,cpu占用直接用top命令,这点erlang跟skynet都需要自己另外搞一套工具。  
-3. 多进程单线程架构还有个好处,单台物理机跟多台物理机是没有区别的,单进程多线程还需要考虑多台物理机的处理。  
-4. 多进程单线程架构一点缺陷是消息跨进程需要进行序列化反序列化,占用一点资源。另外发送网络消息会有几毫秒延时。一般这些影响可以忽略。  
-
-最开始Actor模型是给单进程多线程架构使用的,这是有原因的,因为多线程架构开发者很容易随意的访问共享变量,比方说一个变量a, 线程1能访问,线程2也能访问,这样两个线程在访问变量a的时候都需要加锁,共享变量多了之后锁到处都是,会变得无法维护,框架肯定不能出现到处是线程共享变量的情况。为了保证多线程架构不出问题,必须提供一种开发模型保证多线程开发简单又安全。erlang语言的并发机制就是actor模型。erlang虚拟机使用多线程来利用多核。erlang设计了一种机制,它在虚拟机之上设计了自己的进程。最简单的,每个erlang进程都管理自己的变量,每个erlang进程的逻辑都跑在一个线程上,erlang进程跟进程之间逻辑完全隔离,这样就不存在两个线程访问同一变量的情况了也就不存在多线程竞争的问题。接下来问题又出现了,既然每个erlang进程都有自己的数据,逻辑完全是隔离的,两个erlang进程之间应该怎么进行通信呢?这时Actor模型就登场了。erlang设计了一种消息机制:一个进程可以向其它进程发送消息,erlang进程之间通过消息来进行通信,看到这会不会感觉很熟悉?这不就是操作系统进程间通信用的消息队列吗?没错,其实是类似的。erlang里面拿到进程的id就能给这个进程发送消息。   
-
-如果消息只发给进程其实还是有点不方便。比如拿一个erlang进程做moba战队进程,战斗进程中有10个玩家,如果使用erlang的actor消息,消息只能发送给战斗进程,但是很多时候消息是需要发送给一个玩家的,这时erlang需要根据消息中的玩家Id,把消息再次分发给具体的玩家,这样其实多绕了一圈。
-
-### ET的Actor
-ET根据自己架构得特点,没有完全照搬erlang的Actor模型,而是提供了Entity对象级别的Actor模型。这点跟erlang甚至传统的Actor机制不一样。ET中,Actor是Entity对象,Entity挂上一个MailboxComponent组件就是一个Actor了。只需要知道Entity的InstanceId就可以发消息给这个Entity了。其实erlang的Actor模型不过是ET中的一种特例,比如给ET服务端Game.Scene当做一个Actor,这样就可以变成进程级别的Actor。Actor本质就是一种消息机制,这种消息机制不用关心位置,只需要知道对方的InstanceId(ET)或者进程的Pid(erlang)就能发给对方。
-
-| 语言 | ET | Erlang | Skynet |
-| -- | :--: | :--: | :--: |
-| 架构 |  单线程多进程 | 单进程多线程 | 单进程多线程 |
-| Actor |  Entity | erlang进程 | lua虚拟机 |
-| ActorId |  Entity.InstanceId | erlang进程Id | 服务地址 |
-
-### ET的Actor的使用
-普通的Actor,我们可以参照Gate Session。map中一个Unit,Unit身上保存了这个玩家对应的gate session。这样,map中的消息如果需要发给客户端,只需要把消息发送给gate session,gate session在收到消息的时候转发给客户端即可。map进程发送消息给gate session就是典型的actor模型。它不需要知道gate session的位置,只需要知道它的InstanceId即可。MessageHelper.cs中,通过GateSessionActorId获取一个ActorMessageSender,然后发送。
-```csharp
-// 从Game.Scene上获取ActorSenderComponent,然后通过InstanceId获取ActorMessageSender
-ActorSenderComponent actorSenderComponent = Game.Scene.GetComponent<ActorSenderComponent>();
-ActorMessageSender actorMessageSender = actorSenderComponent.Get(unitGateComponent.GateSessionActorId);
-// send
-actorMessageSender.Send(message);
-
-// rpc
-var response = actorMessageSender.Call(message);
-```
-
-问题是map中怎么才能知道gate session的InstanceId呢?这就是你需要想方设法传过去了,比如ET中,玩家在登录gate的时候,gate session挂上一个信箱MailBoxComponent,C2G_LoginGateHandler.cs中
-```csharp
-session.AddComponent<MailBoxComponent, string>(MailboxType.GateSession);
-```
-玩家登录map进程的时候会把这个gate session的InstanceId带进map中去,C2G_EnterMapHandler.cs中
-```csharp
-M2G_CreateUnit createUnit = (M2G_CreateUnit)await mapSession.Call(new G2M_CreateUnit() { PlayerId = player.Id, GateSessionId = session.InstanceId });
-```
-
-### Actor消息的处理
-首先,消息到达MailboxComponent,MailboxComponent是有类型的,不同的类型邮箱可以做不同的处理。目前有两种邮箱类型GateSession跟MessageDispatcher。GateSession邮箱在收到消息的时候会立即转发给客户端,MessageDispatcher类型会再次对Actor消息进行分发到具体的Handler处理,默认的MailboxComponent类型是MessageDispatcher。自定义一个邮箱类型也很简单,继承IMailboxHandler接口,加上MailboxHandler标签即可。那么为什么需要加这么个功能呢,在其它的actor模型中是不存在这个特点的,一般是收到消息就进行分发处理了。原因是GateSession的设计,并不需要进行分发处理,因此我在这里加上了邮箱类型这种设计。MessageDispatcher的处理方式有两种一种是处理对方Send过来的消息,一种是rpc消息  
-```csharp
-    // 处理Send的消息, 需要继承AMActorHandler抽象类,抽象类第一个泛型参数是Actor的类型,第二个参数是消息的类型
-	[ActorMessageHandler(AppType.Map)]
-	public class Actor_TestHandler : AMActorHandler<Unit, Actor_Test>
-	{
-		protected override ETTask Run(Unit unit, Actor_Test message)
-		{
-			Log.Debug(message.Info);
-		}
-	}
-
-    // 处理Rpc消息, 需要继承AMActorRpcHandler抽象类,抽象类第一个泛型参数是Actor的类型,第二个参数是消息的类型,第三个参数是返回消息的类型
-    [ActorMessageHandler(AppType.Map)]
-	public class Actor_TransferHandler : AMActorRpcHandler<Unit, Actor_TransferRequest, Actor_TransferResponse>
-	{
-		protected override async ETTask Run(Unit unit, Actor_TransferRequest message, Action<Actor_TransferResponse> reply)
-		{
-			Actor_TransferResponse response = new Actor_TransferResponse();
-
-			try
-			{
-				reply(response);
-			}
-			catch (Exception e)
-			{
-				ReplyError(response, e, reply);
-			}
-		}
-	}
-```
-
-我们需要注意一下,Actor消息有死锁的可能,比如A call消息给B,B call给C,C call给A。因为MailboxComponent本质上是一个消息队列,它开启了一个协程会一个一个消息处理,返回ETTask表示这个消息处理类会阻塞MailboxComponent队列的其它消息。所以如果出现死锁,我们就不希望某个消息处理阻塞掉MailboxComponent其它消息的处理,我们可以在消息处理类里面新开一个协程来处理就行了。例如:
-```csharp
-	[ActorMessageHandler(AppType.Map)]
-	public class Actor_TestHandler : AMActorHandler<Unit, Actor_Test>
-	{
-		protected override ETTask Run(Unit unit, Actor_Test message)
-		{
-			RunAsync(unit, message).Coroutine();
-		}
-
-        public ETVoid RunAsync(Unit unit, Actor_Test message)
-        {
-            Log.Debug(message.Info);
-        }
-	}
-```
-相关资料可以谷歌一下Actor死锁的问题。
-
-

+ 0 - 77
Book/5.5Actor Location-EN.md

@@ -1,77 +0,0 @@
-# Actor Location
-### Actor Location
-Actor model only needs to know each other's InstanceId to send messages, which is very convenient, but sometimes we may not know each other's InstanceId, or an Actor's InstanceId will change. ET provides a mechanism to send messages to such objects, called Actor Location The mechanism is called Actor Location. The principle is relatively simple.
-1. because the InstanceId is changing, the object's Entity.Id is unchanged, so we can first think of using Entity.Id to send actor messages
-2. provide a location process (Location Server), the Actor object can store its Entity.Id and InstanceId as kv to the location process. Before sending the Actor message, go to the location process to look up the InstanceId of the Actor object before sending the actor message. 3.
-3. When an Actor object is created in a process or migrated to a new process, it needs to register its Id and InstanceId to the Location Server. 4.
-4. because the Actor object can be migrated, the message may be sent to the Actor has been migrated to other processes, so sending the Actor Location message needs to provide a reliable mechanism
-5. ActorLocationSender provides two methods, Send and Call, Send a message also requires the recipient to return a message, and only when the return message is received will the next message be sent. 6.
-6. If the Actor object is migrated away, it will return the error that the Actor does not exist, the sender will wait for 1 second after receiving this error, then go back to get the InstanceId of the Actor and resend it, currently it will try 5 times, after 5 times, it will throw an exception and report the error
-7. ActorLocationSender will not query the Location Server every time it sends a message, because object migration is relatively rare after all, only the first time to query, then cache the InstanceId, and re-query after the failure to send.
-8. actor object in the migration process, it is possible that other processes send over messages, when an error will occur, so the location server provides a Lock mechanism. Before the object is transmitted, the information in the process is deleted, and then a lock is added to the location server, and once the lock is on, other requests for the key will be queued.
-9. before transmission because the other party deleted the actor of the process, so other processes will fail to send, then they will retry. When retrying, they will re-request the location server, which will be found to be locked, so they will keep waiting.
-10. When the transmission is completed, the lock on the location server is unlocked, and the new address is updated, and then other location requests are responded to. Other requests sent to this actor continue.
-
-Note that the Actor model is purely a server-side message communication mechanism, which has nothing to do with the client. We can use the server-side actor model for forwarding, so some client-side messages also inherit the actor interface. What happens if we don't use the actor interface on the client side? For example, the message Frame_ClickMap
-```protobuf
-message Frame_ClickMap // IActorLocationMessage
-{
-	int64 ActorId = 93;
-	int64 Id = 94;
-	
-	float X = 1;
-	float Y = 2;
-	float Z = 3;
-}
-```
-We may not need the field ActorId, the message is sent to Gate, gate sees that it is a Frame_ClickMap message, it needs to be forwarded to the Unit on the Map, forwarding is still good, gate can get the location of the unit corresponding to the map from the session, and then forward it, the problem comes, Frame_ ClickMap message to the map, how does the map know which object the message needs to be given to? There are several designs at this point.
-1. bring the Id of the unit in the underlying protocol of forwarding, which requires more complex underlying protocol support.
-2. use a message to Frame_ClickMap message wrapping, wrapping the message with the Id of the Unit, wrapping with the message means greater consumption, increasing GC.
-Personally, I feel that these two are very poor, not good, and even if distributed to the unit object processing, how to solve the problem of message re-entry it? unit object still needs to hang a message processing queue, and then receive the message thrown into the queue. Isn't this a duplication of the actor model? The current ET message sent to unit in the client did a design, the message into an actor message, gate received found to be an actor message, sent directly to the corresponding actor, the solution can be said to be beautiful. In fact, the client is still using session.send and call to send messages, send the message does not know that the message is an actor message, only to the gate, the gate judgment, refer to OuterMessageDispatcher.cs
-
-### Actor Location message processing
-ActorLocation messages are sent
-```csharp
-// Get the ActorLocationSenderComponent from Game.Scene, then get the ActorLocationSender via Entity.Id
-ActorLocationSender actorLocationSender = Game.Scene.GetComponent<ActorLocationSenderComponent>().Get(unitId);
-// Send the message through the ActorLocationSender
-actorLocationSender.Send(actorLocationMessage);
-// send the Rpc message
-IResponse response = await actorLocationSender.Call(actorLocationRequest);
-```
-
-ActorLocation message processing is almost the same as Actor messages, the difference is that the two abstract classes inherited are different, note that the abstract class of actorlocation has an additional Location
-```csharp
-	// The first generic parameter of the abstract class is the type of the Actor, and the second parameter is the type of the message.
-	[ActorMessageHandler(AppType.Map)]
-	public class Frame_ClickMapHandler : AMActorLocationHandler<Unit, Frame_ClickMap>
-	{
-		protected override ETTask Run(Unit unit, Frame_ClickMap message)
-		{
-			Vector3 target = new Vector3(message.X, message.Y, message.Z);
-			unit.GetComponent<UnitPathComponent>().MoveTo(target).Coroutine();
-			
-		}
-	}
-
-	// To handle Rpc messages, you need to inherit the AMActorRpcHandler abstract class. The first generic parameter of the abstract class is the type of the Actor, the second parameter is the type of the message, and the third parameter is the type of the returned message
-	[ActorMessageHandler(AppType.Map)]
-	public class C2M_TestActorRequestHandler : AMActorLocationRpcHandler<Unit, C2M_TestActorRequest, M2C_TestActorResponse>
-	{
-		protected override async ETTask Run(Unit unit, C2M_TestActorRequest message, Action<M2C_TestActorResponse> reply)
-		{
-			reply(new M2C_TestActorResponse(){Info = "actor rpc response"});
-			await ETTask.CompletedTask;
-		}
-	}
-```
-
-### ET's actor and actor location analogy
-There are many cities (processes) in China, and many people (entity objects) living in the cities, each with an ID number (Entity.Id). A person needs to apply for a residence permit in each city and is assigned a unique residence permit number (InstanceId). The format of the residence permit number is 2 bytes city number + 4 bytes time + 2 bytes increment. The ID number never changes, but the residence permit number changes every time you go to a city.
-Now there is a China Post (actor). Suppose Xiaoming wants to send a letter to his girlfriend Xiaohong
-1. Xiaohong must mount a mailbox (MailboxComponent) himself in order to receive the letter, and Xiaohong will process the message when he receives it. Note that processing is done here one by one. It is possible that Hong will receive letters from many people at the same time. But she must read one letter at a time. Let's say Xiaoming and Xiaobao both send letters to Xiaohong, and Xiaohong receives Xiaoming's letter first, and then Xiaobao's letter. Xiaohong reads Xiaoming's letter first, and Xiaoming's letter asks Xiaohong to make a phone call to her grandmother (to produce a concordance) and then write back to herself, noting that Xiaohong also cannot read the next letter during this period, and must finish the phone call before she can read Xiaobao's letter. Of course Xiao Hong himself can choose to start reading Xiao Bao's letter without finishing the process, by opening a new concatenation for Xiao Ming's letter.
-2. Suppose Xiaoming knows Xiaohong's residence permit number, then the post (actor) can find the city (process) where Xiaohong lives according to the two docks of the residence permit number, and then find Xiaohong and deliver the message to Xiaohong's mailbox (MailboxComponent) according to Xiaohong's residence permit number. This is the simplest native actor model
-3. ET also supports a set of actor location mechanism. Suppose Xiao Ming doesn't know Xiao Hong's residence permit number, but he knows Xiao Hong's ID number, what should he do? The postal service has developed a set of advanced post (actor location) to think of a way, if a person often moves, it still wants to receive letters, then he must report his residence permit and ID card to the central government (location server) when he goes to a new city, so that the advanced post can send mail through the ID card number. The method is to go to the central government to get Hong's residence permit number, and then use the actor mechanism to send. 4.
-4. Suppose Xiao Hong was in Guangzhou city before, and Xiao Ming used Xiao Hong's ID card to send a letter to Xiao Hong. Advanced post obtains Xiao Hong's residence permit number and sends a letter to Xiao Hong. During this process of sending the letter, Xiaohong moved, from Guangzhou to Shenzhen, at which time Xiaohong reported her new residence permit on the central government. When the letter from the senior post arrives in Guangzhou, it is found that Xiao Hong is not in Guangzhou. Then the senior postal service will go to the central government again to get Xiao Hong's residence permit and resend it, which may succeed or fail again, this process will be repeated several times, if it is unsuccessful, it will tell Xiao Ming that the letter has failed to be sent.
-5. senior postal mail is more expensive, and people do not move a lot, usually Xiao Ming will remember Xiao Hong's residence permit after sending letters with senior postal mail, and next time send letters directly with the residence permit, and then send letters with senior postal mail if it fails.
-There are two kinds of return receipts, one without content, just means Xiao Hong received the letter, and one with Xiao Hong's return letter. Xiaoming can choose which return receipt form to use when he sends the letter. Xiao Ming cannot send two letters to Xiao Hong at the same time, he must wait for Xiao Hong's acknowledgement to arrive before Xiao Ming can continue sending the letter.

+ 0 - 77
Book/5.5Actor Location-ZH.md

@@ -1,77 +0,0 @@
-# Actor Location
-### Actor Location
-Actor模型只需要知道对方的InstanceId就能发送消息,十分方便,但是有时候我们可能无法知道对方的InstanceId,或者是一个Actor的InstanceId会发生变化。这种场景很常见,比如:很多游戏是分线的,一个玩家可能从1线换到2线,还有的游戏是分场景的,一个场景一个进程,玩家从场景1进入到场景2。因为做了进程迁移,玩家对象的InstanceId也就变化了。ET提供了给这类对象发送消息的机制,叫做Actor Location机制。其原理比较简单:
-1. 因为InstanceId是变化的,对象的Entity.Id是不变的,所以我们首先可以想到使用Entity.Id来发送actor消息
-2. 提供一个位置进程(Location Server),Actor对象可以将自己的Entity.Id跟InstanceId作为kv存到位置进程中。发送Actor消息前先去位置进程查询到Actor对象的InstanceId再发送actor消息。
-3. Actor对象在一个进程创建时或者迁移到一个新的进程时,都需要把自己的Id跟InstanceId注册到Location Server上去
-4. 因为Actor对象是可以迁移的,消息发过去有可能Actor已经迁移到其它进程上去了,所以发送Actor Location消息需要提供一种可靠机制
-5. ActorLocationSender提供两种方法,Send跟Call,Send一个消息也需要接受者返回一个消息,只有收到返回消息才会发送下一个消息。
-6. Actor对象如果迁移走了,这时会返回Actor不存在的错误,发送者收到这个错误会等待1秒,然后重新去获取Actor的InstanceId,然后重新发送,目前会尝试5次,5次过后,抛出异常,报告错误
-7. ActorLocationSender发送消息不会每次都去查询Location Server,因为对象迁移毕竟比较少见,只有第一次去查询,之后缓存InstanceId,以后发送失败再重新查询。
-8. Actor对象在迁移过程中,有可能其它进程发送过来消息,这时会发生错误,所以location server提供了一种Lock的机制。对象在传送前,删掉在本进程的信息,然后在location server上加上锁,一旦锁上后,其它的对该key的请求会进行队列。
-9. 传送前因为对方删除了本进程的actor,所以其它进程会发送失败,这时候他们会进行重试。重试的时候会重新请求location server,这时候会发现被锁了,于是一直等待
-10. 传送完成后,要unlock location server上的锁,并且更新新的地址,然后响应其它的location请求。其它发给这个actor的请求继续进行下去。
-
-注意,Actor模型是纯粹的服务端消息通信机制,跟客户端是没什么关系的,很多用ET的新人看到ET客户端消息也有Actor接口,以为这是客户端跟服务端通信的机制,其实不是的。ET客户端使用这个Actor完全是因为Gate需要对客户端消息进行转发,我们可以正好利用服务端actor模型来进行转发,所以客户端有些消息也是继承了actor的接口。假如我们客户端不使用actor接口会怎么样呢?比如,Frame_ClickMap这个消息
-```protobuf
-message Frame_ClickMap // IActorLocationMessage
-{
-	int64 ActorId = 93;
-	int64 Id = 94;
-	
-	float X = 1;
-	float Y = 2;
-	float Z = 3;
-}
-```
-我们可能就不需要ActorId这个字段,消息发送到Gate,gate看到是Frame_ClickMap消息,它需要转发给Map上的Unit,转发还好办,gate可以从session中获取对应的map的unit的位置,然后转发,问题来了,Frame_ClickMap消息到了map,map怎么知道消息需要给哪个对象呢?这时候有几种设计:
-1. 在转发的底层协议中带上unit的Id,需要比较复杂的底层协议支持。
-2. 用一个消息对Frame_ClickMap消息包装一下,包装的消息带上Unit的Id,用消息包装意味着更大的消耗,增加GC。
-个人感觉这两种都很差,不好用,而且就算分发给unit对象处理了,怎么解决消息重入的问题呢?unit对象仍然需要挂上一个消息处理队列,然后收到消息扔到队列里面。这不跟actor模型重复了吗?目前ET在客户端发给unit的消息做了个设计,消息做成actor消息,gate收到发现是actor消息,直接发到对应的actor上,解决的可以说很漂亮。其实客户端仍然是使用session.send跟call发送消息,发送的时候也不知道消息是actor消息,只有到了gate,gate才进行了判断,参考OuterMessageDispatcher.cs
-
-### Actor Location消息的处理
-ActorLocation消息发送
-```csharp
-// 从Game.Scene上获取ActorLocationSenderComponent,然后通过Entity.Id获取ActorLocationSender
-ActorLocationSender actorLocationSender = Game.Scene.GetComponent<ActorLocationSenderComponent>().Get(unitId);
-// 通过ActorLocationSender来发送消息
-actorLocationSender.Send(actorLocationMessage);
-// 发送Rpc消息
-IResponse response = await actorLocationSender.Call(actorLocationRequest);
-```
-
-ActorLocation消息的处理跟Actor消息几乎一样,不同的是继承的两个抽象类不同,注意actorlocation的抽象类多了个Location
-```csharp
-	// 处理send过来的消息, 需要继承AMActorLocationHandler抽象类,抽象类第一个泛型参数是Actor的类型,第二个参数是消息的类型
-	[ActorMessageHandler(AppType.Map)]
-	public class Frame_ClickMapHandler : AMActorLocationHandler<Unit, Frame_ClickMap>
-	{
-		protected override ETTask Run(Unit unit, Frame_ClickMap message)
-		{
-			Vector3 target = new Vector3(message.X, message.Y, message.Z);
-			unit.GetComponent<UnitPathComponent>().MoveTo(target).Coroutine();
-			
-		}
-	}
-
-	// 处理Rpc消息, 需要继承AMActorRpcHandler抽象类,抽象类第一个泛型参数是Actor的类型,第二个参数是消息的类型,第三个参数是返回消息的类型
-	[ActorMessageHandler(AppType.Map)]
-	public class C2M_TestActorRequestHandler : AMActorLocationRpcHandler<Unit, C2M_TestActorRequest, M2C_TestActorResponse>
-	{
-		protected override async ETTask Run(Unit unit, C2M_TestActorRequest message, Action<M2C_TestActorResponse> reply)
-		{
-			reply(new M2C_TestActorResponse(){Info = "actor rpc response"});
-			await ETTask.CompletedTask;
-		}
-	}
-```
-
-### ET的actor跟actor location的比喻
-中国有很多城市(进程),城市中有很多人(entity对象)居住,每个人都有身份证号码(Entity.Id)。一个人每到一个市都需要办理居住证,分配到唯一的居住证号码(InstanceId),居住证号码的格式是2个字节市编号+4个字节时间+2个字节递增。身份证号码是永远不会变化的,但是居住证号码每到一个城市都变化的。
-现在有个中国邮政(actor)。假设小明要发信给女朋友小红
-1. 小红为了收信,自己必须挂载一个邮箱(MailboxComponent),小红收到消息就会处理。注意这里处理是一个个进行处理的。有可能小红会同时收到很多人的信。但是她必须一封一封的信看,比方说小明跟小宝都发了信给小红,小红先收到小明的信,再收到了小宝的信。小红先读小明的信,小明信中让小红给外婆打个电话(产生协程)再给自己回信,注意这期间小红也不能读下一封信,必须打完电话后才能读小宝的信。当然小红自己可以选择不处理完成就开始读小宝的信,做法是小红开一个新的协程来处理小明的信。
-2. 假设小明知道小红的居住证号码,那么邮政(actor)可以根据居住证号码头两位找到小红居住的城市(进程),然后再根据小红的居住证编号,找到小红,把消息投递到小红的邮箱(MailboxComponent)中。这种是最简单的原生的actor模型
-3. ET还支持了一套actor location机制。假设小明不知道小红的居住证号码,但是他知道小红的身份证号码,怎么办呢?邮政开发了一套高级邮政(actor location)想了一个办法,如果一个人经常搬家,它还想收到信,那他到一个新的城市都必须把自己的居住证跟身份证上报到中央政府(location server),这样高级邮政能够通过身份证号码来发送邮件。方法就是去中央政府拿到小红的居住证号码,再利用actor机制发送。
-4. 假设小红之前在广州市,小明用小红的身份证给小红发信件了。 高级邮政获取了小红的居住证号码,给小红发信。发信的这个过程中,小红搬家了,从广州搬到了深圳,这时小红在中央政府上报了自己新的居住证。 高级邮政的信送到到广州的时候发现,小红不在广州。那么高级邮政会再次去中央政府获取小红的居住证,重新发送,有可能成功有可能再次失败,这个过程会重复几次,如果一直不成功则告诉小明,信件发送失败了。
-5. 高级邮政发信比较贵,而且人搬家的次数并不多,一般小明用高级邮政发信后会记住小红的居住证,下次再发的时候直接用居住证发信,发送失败了再使用高级邮政发信。
-6. 高级邮政的信都是有回执的,有两种回执,一种回执没有内容,只表示小红收到了信,一种回执带了小红的回信。小明在发信的时候可以选择使用哪种回执形式。小明给小红不能同时发送两封信,必须等小红的回执到了,小明才能继续发信。

+ 0 - 210
Book/5.6Numerical component design.md

@@ -1,210 +0,0 @@
-Similar to world of warcraft, moba such skills are extremely complex, flexibility requires a very high skill system, must need a set of its flexible numerical structure to match. Numerical structure is well designed, the realization of the skill system will be very simple, otherwise it is a disaster. For example, in World of Warcraft, a character has many numerical attributes, such as movement speed, strength, anger, energy, concentration value, magic value, blood, maximum blood, physical attack, physical defense, spell attack, spell defense, etc. There are dozens of attributes. Attributes and attributes affect each other, buffs will add absolute value to attributes, increase the percentage, or some kind of buff will come back to you after counting all the increased value and doubling it.
-
-## Common practice:
-The general is to write a value class.
-```c#
-class Numeric
-{
-    public int Hp;
-    public int MaxHp;
-    public int Speed;
-    // Energy
-    public int Energy;
-    public int MaxEnergy;
-    // Magic
-    public int Mp;
-    public int MaxMp;
-    .....
-}
-```
-On second thought, I'm a thief using energy why should I have a value of Mp? I am a mage using magic why should there be a field for energy? I'm not sure what to do with this, just pretend you didn't see it? I can not, I come to an inheritance?
-```C#
-// Mage values
-calss MageNumeric: Numeric
-{
-    // magic
-    public int Mp;
-    public int MaxMp;
-}
-
-// Thief value
-calss RougeNumeric: Numeric
-{
-    // Energy
-    public int Energy;
-    public int MaxEnergy;
-}
-````
-10 races, each race 7, 8 kinds of heroes, just these values class inheritance relationship, you have to be confused it. Object-oriented is difficult to adapt to the needs of this flexible and complex.
-
-And look at the Numeric class, each value can not just design a field, for example, I have a buff will increase 10 points Speed, and a kind of buff to increase 50% of the speed, then I must add at least three secondary attribute fields
-```c#
-class Numeric
-{
-    // speed final value
-    public int Speed;
-    // Speed initial value
-    public int SpeedInit;
-    // Speed increase value
-    public int SpeedAdd;
-    // Speed increase percentage value
-    public int SpeedPct;
-}
-```
-After SpeedAdd and SpeedPct are changed, a calculation is performed to calculate the final speed value. buff only needs to go to modify SpeedAdd and SpeedPct on the line.
-```c#
-Speed = (SpeedInit + SpeedAdd) * (100 + SpeedPct) / 100
-```
-Each property may have several indirect effects on the value, you can think about how large this class is, a rough estimate of more than 100 fields. The trouble is that the formula is basically the same, but just can not be unified into a function, such as MaxHp, also has a buff effect
-```c#
-class Numeric
-{
-    public int Speed;
-    public int SpeedInit;
-    public int SpeedAdd;
-    public int SpeedPct;
-    
-    public int MaxHp;
-    public int MaxHpInit;
-    public int MaxHpAdd;
-    public int MaxHpPct;
-}
-```
-Also have to write a formula for calculating Hp
-```c#
-MaxHp = (MaxHpInit + MaxHpAdd) * (100 + MaxHpPct) / 100
-```
-Dozens of properties, you have to write dozens of times, and each secondary property changes to correctly call the corresponding formula calculation. Very troublesome!
-This design also has a big problem, buff configuration table to fill the corresponding attribute field is not very good to fill, for example, sprint buff (increase speed 50%), how to configure the buff table to make the program simple to find and operate the SpeedPct field? Not a good idea.
-
-## ET framework uses the Key Value form to save the value of the property
-```c#
-Using System.Collections.Generic;
-
-Generic; namespace Model
-Generic; namespace Model {
-    public enum NumericType
-    {
-		Max = 10000,
-
-		Speed = 1000,
-		SpeedBase = Speed * 10 + 1,
-	    SpeedAdd = Speed * 10 + 2,
-	    SpeedPct = Speed * 10 + 3,
-	    SpeedFinalAdd = Speed * 10 + 4,
-	    SpeedFinalPct = Speed * 10 + 5,
-
-	    Hp = 1001,
-	    HpBase = Hp * 10 + 1,
-
-	    MaxHp = 1002,
-	    MaxHpBase = MaxHp * 10 + 1,
-	    MaxHpAdd = MaxHp * 10 + 2,
-	    MaxHpPct = MaxHp * 10 + 3,
-	    MaxHpFinalAdd = MaxHp * 10 + 4,
-		MaxHpFinalPct = MaxHp * 10 + 5,
-	}
-
-	public class NumericComponent: Component
-	{
-		public readonly Dictionary<int, int> NumericDic = new Dictionary<int, int>();
-
-		public void Awake()
-		{
-			// initialize base value here
-		}
-
-		public float GetAsFloat(NumericType numericType)
-		{
-			return (float)GetByKey((int)numericType) / 10000;
-		}
-
-		public int GetAsInt(NumericType numericType)
-		{
-			return GetByKey((int)numericType);
-		}
-
-		public void Set(NumericType nt, float value)
-		{
-			this[nt] = (int) (value * 10000);
-		}
-
-		public void Set(NumericType nt, int value)
-		{
-			this[nt] = value;
-		}
-
-		public int this[NumericType numericType]
-		{
-			get
-			{
-				return this.GetByKey((int) numericType);
-			}
-			set
-			{
-				int v = this.GetByKey((int) numericType);
-				if (v == value)
-				{
-					return;
-				}
-
-				NumericDic[(int)numericType] = value;
-
-				Update(numericType);
-			}
-		}
-
-		private int GetByKey(int key)
-		{
-			int value = 0;
-			This.NumericDic.TryGetValue(key, out value);
-			return value;
-		}
-
-		public void Update(NumericType numericType)
-		{
-			if (numericType > NumericType.Max)
-			{
-				return;
-			}
-			int final = (int) numericType / 10;
-			int bas = final * 10 + 1; 
-			int add = final * 10 + 2;
-			int pct = final * 10 + 3;
-			int finalAdd = final * 10 + 4;
-			int finalPct = final * 10 + 5;
-
-			// A value may be affected by a variety of circumstances, such as speed, adding a buff may increase the speed of the absolute value of 100, but also some buffs increase the speed of 10%, so a value can be controlled by 5 values of the final result
-			// final = (((base + add) * (100 + pct) / 100) + finalAdd) * (100 + finalPct) / 100;
-			this.NumericDic[final] = ((this.GetByKey(base) + this.GetByKey(add)) * (100 + this.GetByKey(pct)) / 100 + this.GetByKey(finalAdd)) * (100 + this. GetByKey(finalPct)) / 100;
-			Game.EventSystem.Run(EventIdType.NumbericChange, this.Entity.Id, numericType, final);
-		}
-	}
-}
-```
-1. values are saved with key value, key is the type of value, defined by NumericType, value are integers, float type can also be converted to integers, for example, multiply by 1000; key value to save properties will become very flexible, for example, mage no energy properties, then initialize the mage object does not add energy key value It's fine. Thieves do not have mana, no spell damage, etc., the initialization will not need to add these.  
-
-2. world of warcraft, a value by 5 values to influence, you can unify the use of a formula.
-```
-final = (((base + add) * (100 + pct) / 100) + finalAdd) * (100 + finalPct) / 100;
-```
-For example, the speed value speed, there is an initial value speedbase, there is a buff1 to increase the absolute speed by 10 points, then buff1 will add 10 to speedadd when it is created, buff1 minus 10 to speedadd when it is deleted, buff2 increases the speed by 20%, then buff2 adds to speedpct when it is created The 5 values are changed and the corresponding properties can be recalculated by using the Update function in a unified way. buff configuration is quite simple. If the corresponding NumericType is filled in the buff configuration, the program can easily manipulate the corresponding value.
-
-3. Changes in properties can be uniformly thrown to other modules to subscribe to the event, writing a property change monitor becomes very simple. For example, the achievement module needs to develop an achievement life value over 1000, will get the achievement of longevity master. Then the person developing the achievement module will subscribe to the HP changes as follows.
-```
-	/// Monitor hp value changes
-	[NumericWatcher(NumericType.Hp)
-	public class NumericWatcher_Hp : INumericWatcher
-	{
-		public void Run(long id, int value)
-		{
-		    if (value > 1000)
-		    {
-		        // get achievement longevity master achievement
-		    }
-		}
-	}
-```
-Similarly, recording an exception log for a gold change greater than 10,000 at a time, etc. can be done this way.
-
-With this numerical component, a moba skill system can be said to be half complete.

+ 0 - 214
Book/5.6数值组件设计.md

@@ -1,214 +0,0 @@
-类似魔兽世界,moba这种技能极其复杂,灵活性要求极高的技能系统,必须需要一套及其灵活的数值结构来搭配。数值结构设计好了,实现技能系统就会非常简单,否则就是一场灾难。比如魔兽世界,一个人物的数值属性非常之多,移动速度,力量,怒气,能量,集中值,魔法值,血量,最大血量,物理攻击,物理防御,法术攻击,法术防御,等等多达几十种之多。属性跟属性之间又相互影响,buff又会给属性增加绝对值,增加百分比,或者某种buff又会在算完所有的增加值之后再来给你翻个倍。
-
-## 普通的做法:
-一般就是写个数值类:
-```c#
-class Numeric
-{
-    public int Hp;
-    public int MaxHp;
-    public int Speed;
-    // 能量
-    public int Energy;
-    public int MaxEnergy;
-    // 魔法
-    public int Mp;
-    public int MaxMp;
-    .....
-}
-```
-仔细一想,我一个盗贼使用的是能量,为什么要有一个Mp的值?我一个法师使用的是魔法为什么要有能量的字段?纠结这个搞毛,当作没看见不就行了吗?实在不行,我来个继承?
-```C#
-// 法师数值
-calss MageNumeric: Numeric
-{
-    // 魔法
-    public int Mp;
-    public int MaxMp;
-}
-
-// 盗贼数值
-calss RougeNumeric: Numeric
-{
-    // 能量
-    public int Energy;
-    public int MaxEnergy;
-}
-```
-10个种族,每个种族7,8种英雄,光这些数值类继承关系,你就得懵逼了吧。面向对象是难以适应这种灵活的复杂的需求的。
-
-再来看看Numeric类,每种数值可不能只设计一个字段,比如说,我有个buff会增加10点Speed,还有种buff增加50%的speed,那我至少还得加三个二级属性字段
-```c#
-class Numeric
-{
-    // 速度最终值
-    public int Speed;
-    // 速度初始值
-    public int SpeedInit;
-    // 速度增加值
-    public int SpeedAdd;
-    // 速度增加百分比值
-    public int SpeedPct;
-}
-```
-SpeedAdd跟SpeedPct改变后,进行一次计算,就可以算出最终的速度值。buff只需要去修改SpeedAdd跟SpeedPct就行了。
-```c#
-Speed = (SpeedInit + SpeedAdd) * (100 + SpeedPct) / 100
-```
-每种属性都可能有好几种间接影响值,可以想想这个类是多么庞大,初略估计得有100多个字段。麻烦的是计算公式基本一样,但是就是无法统一成一个函数,例如MaxHp,也有buff影响
-```c#
-class Numeric
-{
-    public int Speed;
-    public int SpeedInit;
-    public int SpeedAdd;
-    public int SpeedPct;
-    
-    public int MaxHp;
-    public int MaxHpInit;
-    public int MaxHpAdd;
-    public int MaxHpPct;
-}
-```
-也得写个Hp的计算公式
-```c#
-MaxHp=(MaxHpInit + MaxHpAdd) * (100  + MaxHpPct) / 100
-```
-几十种属性,就要写几十遍,并且每个二级属性改变都要正确调用对应的公式计算. 非常麻烦!
-这样设计还有个很大的问题,buff配置表填对应的属性字段不是很好填,例如疾跑buff(增加速度50%),在buff表中怎么配置才能让程序简单的找到并操作SpeedPct字段呢?不好搞。
-
-## ET框架采用了Key Value形式保存数值属性
-```c#
-using System.Collections.Generic;
-
-namespace Model
-{
-    public enum NumericType
-    {
-		Max = 10000,
-
-		Speed = 1000,
-		SpeedBase = Speed * 10 + 1,
-	    SpeedAdd = Speed * 10 + 2,
-	    SpeedPct = Speed * 10 + 3,
-	    SpeedFinalAdd = Speed * 10 + 4,
-	    SpeedFinalPct = Speed * 10 + 5,
-
-	    Hp = 1001,
-	    HpBase = Hp * 10 + 1,
-
-	    MaxHp = 1002,
-	    MaxHpBase = MaxHp * 10 + 1,
-	    MaxHpAdd = MaxHp * 10 + 2,
-	    MaxHpPct = MaxHp * 10 + 3,
-	    MaxHpFinalAdd = MaxHp * 10 + 4,
-		MaxHpFinalPct = MaxHp * 10 + 5,
-	}
-
-	public class NumericComponent: Component
-	{
-		public readonly Dictionary<int, int> NumericDic = new Dictionary<int, int>();
-
-		public void Awake()
-		{
-			// 这里初始化base值
-		}
-
-		public float GetAsFloat(NumericType numericType)
-		{
-			return (float)GetByKey((int)numericType) / 10000;
-		}
-
-		public int GetAsInt(NumericType numericType)
-		{
-			return GetByKey((int)numericType);
-		}
-
-		public void Set(NumericType nt, float value)
-		{
-			this[nt] = (int) (value * 10000);
-		}
-
-		public void Set(NumericType nt, int value)
-		{
-			this[nt] = value;
-		}
-
-		public int this[NumericType numericType]
-		{
-			get
-			{
-				return this.GetByKey((int) numericType);
-			}
-			set
-			{
-				int v = this.GetByKey((int) numericType);
-				if (v == value)
-				{
-					return;
-				}
-
-				NumericDic[(int)numericType] = value;
-
-				Update(numericType);
-			}
-		}
-
-		private int GetByKey(int key)
-		{
-			int value = 0;
-			this.NumericDic.TryGetValue(key, out value);
-			return value;
-		}
-
-		public void Update(NumericType numericType)
-		{
-			if (numericType > NumericType.Max)
-			{
-				return;
-			}
-			int final = (int) numericType / 10;
-			int bas = final * 10 + 1; 
-			int add = final * 10 + 2;
-			int pct = final * 10 + 3;
-			int finalAdd = final * 10 + 4;
-			int finalPct = final * 10 + 5;
-
-			// 一个数值可能会多种情况影响,比如速度,加个buff可能增加速度绝对值100,也有些buff增加10%速度,所以一个值可以由5个值进行控制其最终结果
-			// final = (((base + add) * (100 + pct) / 100) + finalAdd) * (100 + finalPct) / 100;
-			this.NumericDic[final] = ((this.GetByKey(bas) + this.GetByKey(add)) * (100 + this.GetByKey(pct)) / 100 + this.GetByKey(finalAdd)) * (100 + this.GetByKey(finalPct)) / 100;
-			Game.EventSystem.Run(EventIdType.NumbericChange, this.Entity.Id, numericType, final);
-		}
-	}
-}
-```
-1.数值都用key value来保存,key是数值的类型,由NumericType来定义,value都是整数,float型也可以转成整数,例如乘以1000;key value保存属性会变得非常灵活,例如法师没有能量属性,那么初始化法师对象不加能量的key value就好了。盗贼没有法力值,没有法术伤害等等,初始化就不用加这些。  
-
-2.魔兽世界中,一个数值由5个值来影响,可以统一使用一条公式:
-```
-final = (((base + add) * (100 + pct) / 100) + finalAdd) * (100 + finalPct) / 100;
-```
-比如说速度值speed,有个初始值speedbase,有个buff1增加10点绝对速度,那么buff1创建的时候会给speedadd加10,buff1删除的时候给speedadd减10,buff2增加20%的速度,那么buff2创建的时候给speedpct加20,buff2删除的时候给speedpct减20.甚至可能有buff3,会在最终值上再加100%,那么buff3将影响speedfinalpct。这5个值发生改变,统一使用Update函数就可以重新计算对应的属性了。buff配置中对应数值字段相当简单,buff配置中填上相应的NumericType,程序很轻松就能操作对应的数值。
-
-3.属性的改变可以统一抛出事件给其它模块订阅,写一个属性变化监视器变得非常简单。例如成就模块需要开发一个成就生命值超过1000,会获得长寿大师的成就。那么开发成就模块的人将订阅HP的变化:
-```
-	/// 监视hp数值变化
-	[NumericWatcher(NumericType.Hp)]
-	public class NumericWatcher_Hp : INumericWatcher
-	{
-		public void Run(long id, int value)
-		{
-		    if (value > 1000)
-		    {
-		        //获得成就长寿大师成就
-		    }
-		}
-	}
-```
-同理,记录一次金币变化大于10000的异常日志等等都可以这样做。
-
-有了这个数值组件,一个moba技能系统可以说已经完成了一半。
-
-**代码地址:https://github.com/egametang/Egametang**
-
-

+ 0 - 140
Book/6.1AI Framwork.md

@@ -1,140 +0,0 @@
-# AI framework
-## 1. Several AI designs
-AI in the game a lot, but why do people always feel ai writing up very difficult, I later thought about it, the main reason is the use of improper methods. Before people write ai mainly have several options.
-### a. State machine  
-I do not know who came up with this approach, really powerless to complain. Originally any data on the object is the state, this method and to define some state into a new kind of node, the object on the state change will cause the conversion between nodes, the implementation of the corresponding method, such as OnEnter OnExit and so on. Here is an example of a monster, monsters can be divided into a variety of states, patrol, attack, chase, return. The state changes of the monster are:
-
-Patrol->Chase Patrol state found a distant enemy to chase state  
-Patrol->Attack Patrol find enemy can be attacked to attack state  
-Attack->Chase The attack state finds an enemy in the distance and goes after it.  
-Attack->Return Attack state find the enemy is too far to return state  
-chase->return chase state found too far from the enemy to return to the state  
-
-There are so many state transitions that it's hard to find out if I've missed them here. Once there are more nodes, any two nodes may need to be connected, and it will become a super complex mesh structure, the complexity is the square of N, and it is very difficult to maintain. In order to solve the problem of complex mesh structure and then upgraded to a hierarchical state machine and so on. Of course, various patching methods still do not solve the essential problem. It is not your problem to use bad state machines, it is the problem of state machines.
-
-### b. Behavior tree
-The ai of the behavior tree is responsive ai, the tree from top to bottom (or from left to right execution, here from top to bottom for example) is actually the action node ranked a priority, the action above the first to determine whether to meet the conditions, meet the implementation. We won't go into details here. The complexity of the behavior tree is N, greatly simplified than the state machine, but there are still many defects, ai too complex when the tree will become very large, and difficult to reconfigure. For example, in our own project, we want to make a robot ai similar to a human, automatically do tasks, fight monsters, play the system in the game, chat with people, and even attack others. Imagine how complex this tree will become! Another drawback of the behavior tree is that some action nodes are a persistent process, that is to say, a concurrent process, behavior tree management up concurrent process is not very good, such as the above example, need to move to the target side, this move is made into a concurrent process it, or every frame move it? This is a difficult problem, how to do it is not comfortable.
-
-## 2. my approach
-What is ai? Very simple, ai is constantly based on the current state, perform the appropriate behavior. Remember these two sentences, it is important, this is the essence of ai! These two sentences are divided into two parts, one is the state judgment, the second is the execution of behavior. State judgment is well understood, what is the behavior? Take the above example of the monster state machine, the behavior of the monster is Patrol, attack the enemy, return to the patrol point. For example.
-
-Patrol (when the monster is within the patrol range, there is no enemy around, choose the next patrol point, move)  
-attack the enemy (when the monster found within the guard range of the enemy, if the attack distance enough to attack, not enough to move over to attack)  
-Return (when the monster found more than a certain distance from the birth point, plus the invincibility buff, move to the birth point, to the birth point, remove the invincibility buff)  
-
-Unlike the state machine, these three state changes do not care about what the last state is, only about whether the current conditions are met, meet the implementation of the behavior. Behavior may be able to perform instantly, but also may be a continuous process, such as patrol, choose the next patrol point to move over, go to a point and then choose a point, and so on and so forth. For example, attacking the enemy, may need to move to the target to attack.
-
-How to design this ai framework? Here it is very simple, abstract ai nodes, each node contains a conditional judgment, with the implementation of behavior. The behavior method should be a concurrent process
-```csharp
-public class AINode
-{
-	public virtual bool Check(Unit unit) // test whether the condition is met
-	{		
-	}
-
-	public virtual ETTask Run(Unit unit)
-	{		
-	}
-}
-```
-Thinking further, if the monster is on patrol and finds an enemy, then the monster should interrupt the current patrol and go on to perform the act of attacking the enemy instead. So our behavior should need to support being interrupted, which means that the behavior concurrent should support cancellation, and this is especially important to note that any concurrent in the behavior Run method should support the cancellation operation!
-```csharp
-public class AINode
-{
-	public virtual bool Check(Unit unit)
-	{		
-	}
-
-	public virtual ETVoid Run(Unit unit, ETCancelToken cancelToken)
-	{
-	}
-}
-````
-
-Implement three ai nodes XunLuoNode(patrol) GongjiNode(attack) FanHuiNode(return)
-
-```csharp
-public class XunLuoNode: AINode
-{
-	public virtual bool Check(Unit unit)
-	{
-		if (not in patrol range)
-		{
-			return false;
-		}
-		if (there are enemies around)
-		{
-			return false;
-		}
-		return true;
-	}
-
-	public virtual ETVoid Run(Unit unit, ETCancelToken cancelToken)
-	{
-		while (true)
-		{
-			Vector3 nextPoint = FindNextPoint();
-			bool ret = await MoveToAsync(nextPoint, cancelToken); // move to the target point, return false means the process is canceled
-			if (!ret)
-			{
-				return;
-			}
-			// stay for two seconds, note that any concurrent process must be able to be cancelled here
-			Wait(2000, cancelToken). bool ret = await TimeComponent;
-			if (!ret)
-			{
-				return;
-			}
-		}
-	}
-}
-```
-The same can be achieved for the other two nodes. It's not enough to design the nodes, you also need to string the nodes together so that the ai can rotate
-```csharp
-AINode[] aiNodes = {xunLuoNode, gongjiNode, fanHuiNode};
-AINode current;
-ETCancelToken cancelToken;
-while(true)
-{
-	// Every second you need to re-determine if the new behavior is satisfied, this time can be set by yourself
-	await TimeComponent.Instance.Wait(1000);
-
-	AINode next;
-	foreach(var node in aiNodes)
-	{
-		if (node.Check())
-		{
-			next = node;
-			break;
-		}
-	}
-
-	if (next == null)
-	{
-		continue;
-	}
-
-	// If the next node is the same as the current one, then it is not executed
-	if (next == current)
-	{
-		continue;
-	}
-
-	// Stop the current concurrent process
-	cancelToken.Cancel();
-
-	// Execute the next concurrent process
-	cancelToken = new ETCancelToken();
-	next.Run(unit, cancelToken).Coroutine();
-}
-```
-This code is very simple, meaning that it iterates through the nodes every second until it finds a node that satisfies the conditions and then executes it, waiting for the next second to determine, before executing the next node, interrupting the currently executing concurrent process.
-A few misconceptions about the use:
-1. behavior if there is a concurrent process must be able to cancel, and pass in cancelToken, otherwise something will go wrong, because once the monster meets the execution of the next node, you need to cancel the current concurrent process.
-2. different from the behavior tree and state machine, the role of the node is only a piece of logic, the node does not need to share. Shared is the concurrent methods, such as MoveToAsync, monster patrol node can be used, the monster attack enemy node in pursuit of the enemy can also be used.
-3. nodes can do very large, such as automatically do the task node, move to the npc, pick up the task, according to the task of subtasks to do subtasks, such as moving to the monster point to fight monsters, move to the collection of things to collect, etc., after doing all the subtasks, move to the task npc to turn in the task. All of this is written in a while loop, using a concurrent string.
-
-Thinking about a big question, how do you design a piezo bot? What does a piezo bot need to do? Automatically do tasks, automatically play various systems, automatically attack enemies, will counterattack, will find people to chat, etc.. Just make an ai node for each of the above mentioned. Brothers, AI simple or not?
-
-
-

+ 0 - 140
Book/6.1AI框架.md

@@ -1,140 +0,0 @@
-# AI框架
-## 1. 几种AI的设计
-AI在游戏中很多,但是为什么大家总是感觉ai编写起来十分困难,我后来思考了一番,主要原因是使用的方法不当。之前大家编写ai主要有几种方案:
-### a. 状态机  
-我是不知道谁想出来这个做法的,真是无力吐槽。本来对象身上任何数据都是状态,这种方法又要把一些状态定义成一种新的节点,对象身上状态变化会引起节点之间的转换,执行对应的方法,比如OnEnter OnExit等等。这里以怪物来举例,怪物可以分为多种状态,巡逻,攻击,追逐,返回。怪物的状态变化有:
-
-巡逻->追逐  巡逻状态发现远处有敌人变追逐状态  
-巡逻->攻击  巡逻发现可以攻击敌人变攻击状态  
-攻击->追逐  攻击状态发现敌人有段距离于是去追逐  
-攻击->返回  攻击状态发现距离敌人过远变返回状态  
-追逐->返回  追逐状态发现距离敌人过远变返回状态  
-
-太多状态转换了,这里有没有漏掉我已经难以发现了。一旦节点更多,任何两个节点都可能需要连接,将成为超级复杂的网状结构,复杂度是N的平方级,维护起来十分困难。为了解决网状结构变复杂的问题于是又升级为分层状态机等等。当然各种打补丁的方法还是没能解决本质的问题。用不好状态机不是你们的问题,是状态机的问题。
-
-### b. 行为树
-可能大家都觉得状态机解决复杂ai实在太困难了,于是有人想出了行为树来做ai。行为树的ai是响应式ai,这棵树从上往下(或者从左往右执行,这里以从上往下举例)实际上是把action节点排了个优先级,上面的action最先判断是否满足条件,满足则执行。这里就不详细讲了。行为树的复杂度是N,比状态机大大简化了,但是仍然存在不少缺陷,ai太复杂的时候,树会变得非常大,而且难以重构。比如我们自己项目,要做一个跟人差不多的机器人ai,自动做任务,打怪,玩游戏中的系统,跟人聊天,甚至攻击别人。想象一下,这颗树将变得多复杂!行为树的另外一个缺陷是某些action节点是个持久的过程,也就是说是个协程,行为树管理起协程起来不太好处理,比如上面的例子,需要移动到目标身边,这个移动究竟是做成协程呢,还是每帧move呢?这是个难题,怎么做都不舒服。
-
-## 2. 我的做法
-ai是什么呢?很简单啊,ai就是不停的根据当前的状态,执行相应的行为。记住这两句话,很重要,这就是ai的本质!这两句话分成两部分,一是状态判断,二是执行行为。状态判断好理解,行为是啥?以上面状态机的怪物举例子,怪物的行为就是 巡逻,攻击敌人,返回巡逻点。比如:
-
-巡逻  (当怪物在巡逻范围内,周围没有敌人,选择下一个巡逻点,移动)  
-攻击敌人  (当怪物发现警戒范围内有敌人,如果攻击距离够就攻击,不够就移动过去攻击)  
-返回  (当怪物发现离出生点超过一定距离,加上无敌buff,往出生点移动,到了出生点,删除无敌buff)  
-
-跟状态机不一样的是,这3个状态的变化完全不关心上一个状态是啥,只关心当前的条件是否满足,满足就执行行为。行为可能能瞬间执行,也可能是一段持续的过程,比如巡逻,选下一个巡逻点移动过去,走到了再选一个点,不停的循环。比如攻击敌人,可能需要移动到目标去攻击。
-
-怎么设计这个ai框架呢?到这里就十分简单了,抽象出ai节点,每个节点包含条件判断,跟执行行为。行为方法应该是一个协程
-```csharp
-public class AINode
-{
-	public virtual bool Check(Unit unit) // 检测条件是否满足
-	{		
-	}
-
-	public virtual ETTask Run(Unit unit)
-	{		
-	}
-}
-```
-进一步思考,假如怪物在巡逻过程中,发现敌人,那么怪物应该要打断当前的巡逻,转而去执行攻击敌人的行为。因此我们行为应该需要支持被打断,也就是说行为协程应该支持取消,这点特别需要注意,行为Run方法中任何协程都要支持取消操作!
-```csharp
-public class AINode
-{
-	public virtual bool Check(Unit unit)
-	{		
-	}
-
-	public virtual ETVoid Run(Unit unit, ETCancelToken cancelToken)
-	{
-	}
-}
-```
-
-实现三个ai节点 XunLuoNode(巡逻)  GongjiNode(攻击)  FanHuiNode(返回)
-
-```csharp
-public class XunLuoNode: AINode
-{
-	public virtual bool Check(Unit unit)
-	{
-		if (不在巡逻范围)
-		{
-			return false;
-		}
-		if (周围有敌人)
-		{
-			return false;
-		}
-		return true;
-	}
-
-	public virtual ETVoid Run(Unit unit, ETCancelToken cancelToken)
-	{
-		while (true)
-		{
-			Vector3 nextPoint = FindNextPoint();
-			bool ret = await MoveToAsync(nextPoint, cancelToken); // 移动到目标点, 返回false表示协程取消
-			if (!ret)
-			{
-				return;
-			}
-			// 停留两秒, 注意这里要能取消,任何协程都要能取消
-			bool ret = await TimeComponent.Instance.Wait(2000, cancelToken);
-			if (!ret)
-			{
-				return;
-			}
-		}
-	}
-}
-```
-同理可以实现另外两个节点。光设计出节点还不行,还需要把各个节点串起来,这样ai才能转动
-```csharp
-AINode[] aiNodes = {xunLuoNode, gongjiNode, fanHuiNode};
-AINode current;
-ETCancelToken cancelToken;
-while(true)
-{
-	// 每秒中需要重新判断是否满足新的行为了,这个时间可以自己定
-	await TimeComponent.Instance.Wait(1000);
-
-	AINode next;
-	foreach(var node in aiNodes)
-	{
-		if (node.Check())
-		{
-			next = node;
-			break;
-		}
-	}
-
-	if (next == null)
-	{
-		continue;
-	}
-
-	// 如果下一个节点跟当前执行的节点一样,那么就不执行
-	if (next == current)
-	{
-		continue;
-	}
-
-	// 停止当前协程
-	cancelToken.Cancel();
-
-	// 执行下一个协程
-	cancelToken = new ETCancelToken();
-	next.Run(unit, cancelToken).Coroutine();
-}
-```
-这段代码十分简单,意思就是每秒钟遍历节点,直到找到一个满足条件的节点就执行,等下一秒再判断,执行下一个节点之前,先打断当前执行的协程。
-几个使用误区:
-1. 行为中如果有协程必须能够取消,并且传入cancelToken,否则会出大事,因为怪物一旦满足执行下个节点,需要取消当前协程。
-2. 跟行为树与状态机不同,节点的作用只是一块逻辑,节点并不需要共享。共享的是协程方法,比如MoveToAsync,怪物巡逻节点可以使用,怪物攻击敌人节点中追击敌人也可以使用。
-3. 节点可以做的非常庞大,比如自动做任务节点,移动到npc,接任务,根据任务的子任务做子任务,比如移动到怪点打怪,移动到采集物去采集等等,做完所有子任务,移动到交任务npc交任务。所有的一切都是写在一个while循环中,利用协程串起来。
-
-思考一个大问题,怎么设计一个压测机器人呢?压测机器人需要做到什么?自动做任务,自动玩各种系统,自动攻击敌人,会反击,会找人聊天等等。把上面说的每一条做成一个ai节点即可。兄弟们,AI简不简单?
-
-
-

+ 0 - 135
Book/6.2AI框架-行为机.md

@@ -1,135 +0,0 @@
-# AI框架介绍-行为机(Behavior Machine)
-## 什么是行为机
-顾名思义,类比状态机每个节点是一个状态,行为机每个节点是描述一种行为。行为机每个节点之间是互斥的,并且节点相互之间完全不用关心是怎么切换的。这里就不讲状态机跟行为树是怎么做ai的了,这里只讲用行为机怎么做一个ai。举个例子 mmo中的小怪策划案,大致会这么写:   
-
-小怪在出生点周围巡逻。发现周围有玩家则选择一个玩家做目标,追击该目标玩家,追到目标玩家则攻击目标玩家,发现距离出生点太远则返回,返回到出生点则继续巡逻
-
-### 1.定义ai的各种行为
-我们首先定义好怪物有哪些行为。很简单,我们直接根据策划案中的字面意思,怪物大致有这么几种行为:  
-a.巡逻  
-b.选择一个玩家追击并且攻击  
-e.返回出生点。  
-
-注意很多状态机会把移动作为一种状态,这在行为机中是不对的,因为巡逻,追击,返回都会有移动,移动只是玩家行为节点中的一个部分,移动跟巡逻,追击,返回并不是互斥的。  
-
-节点不要拆的太细,因为每个行为是个协程,我们可以在行为节点中写十分复杂的逻辑,比如有些同学可能会把 选择一个玩家追击并且攻击 这一个节点拆成 a 选择目标 b 追击目标 c 攻击目标. 
-
-甚至还有人会把巡逻拆的更细,拆成a.寻找一个点 b.移动 c.等待一定时间。这都是状态机跟行为树的思维,因为状态机跟行为树可能希望移动节点可以共用。这都是增加了麻烦,不合理,不要用状态机跟行为树的思维去想行为机。行为机中,节点只是描述一种行为,并不需要共用,共用的永远是各种函数。 
-
-### 2.填充满足行为的条件
-我们把每个行为确定好对应的条件,一旦条件满足则会进入该行为,取消上一个行为(协程)  
-a. 巡逻的条件:身上没有玩家目标,周围没有玩家,距离出生点 < 10米  
-b. 选择一个玩家追击并且攻击: 周围有玩家,距离出生点 < 10米  
-c. 返回出生点: 距离出生点 > 20米  
-其实条件一旦列出,那么节点中的Check方法自然也就实现了  
-
-### 3.实现行为
-a. 巡逻的伪代码:
-```csharp
-while (true)
-{
-    pos = 出生点周围找一个点
-    bool ret = await MoveToAsync(pos,cancelToken);
-    if (!ret) // false表示协程取消, 则需要return,停止整个协程
-    {
-        return;
-    }
-    // 移动到了,随机等待2-4秒
-    randomTime = RandomHelper.Random(2000, 4000);
-    bool ret = await TimeComponent.Instance.Wait(randomTime, cancelToken)
-    if (!ret) // false表示协程取消, 则需要return,停止整个协程
-    {
-        return;
-    }
-}
-```
-这样,如果b c条件不满足的话,怪物就永远在巡逻节点协程中,不停的找一个点移动,等待,移动,等待
-
-b. 选择目标追击并且攻击目标节点的伪代码:
-```csharp
-while (true)
-{
-    target = SelectTarget()
-    while (true)
-    {
-        while (true)
-        {
-            // 追击目标
-            pos = 计算离目标0.2米的一个点
-            // 这里不能以目标作为移动目标,因为怪物要距离玩家稍远一点
-            await MoveToAsync(pos, cancelToken);
-            if (!ret) // false表示协程取消, 则需要return,停止整个协程
-            {
-                return;
-            }
-            // 距离玩家 < 0.5米,表示追到了玩家,就不需要追了
-            if (距离玩家<0.5米)
-            {
-                break;
-            }            
-        }
-        // 追击到了,攻击玩家
-        while (true)
-        {
-            // 
-            spellId = SelectSpell();
-            if (spellId == 0) // 可能技能在cd,等待500ms再试
-            {
-                bool ret = await TimeComponent.Instance.Wait(500, cancelToken)
-                if (!ret) // false表示协程取消, 则需要return,停止整个协程
-                {
-                    return;
-                }
-                continue;
-            }
-            await CastSpell(target);
-            // 攻击完成后停止一段时间
-            bool ret = await TimeComponent.Instance.Wait(1000, cancelToken)
-            if (!ret) // false表示协程取消, 则需要return,停止整个协程
-            {
-                return;
-            }
-
-            // 距离玩家 > 0.5米, 距离玩家远了,break攻击循环,继续追击
-            if (距离玩家<0.5米)
-            {
-                break;
-            }    
-        }
-    }
-    
-
-    // 这里加个time,防止上面两个while循环没有进入,结果就会导致一直执行 target = 选择一个目标 这句话,会导致死循环
-    bool ret = await TimeComponent.Instance.Wait(100, cancelToken)
-    if (!ret) // false表示协程取消, 则需要return,停止整个协程
-    {
-        return;
-    }
-}
-```
-
-c. 返回出生点伪代码:
-```csharp
-while (true)
-{
-    // 整个返回过程是无敌的
-    using (Buff buff = AddBuff(无敌))
-    {
-        pos = 找到离出生点10米的点
-        bool ret = await MoveToAsync(pos,cancelToken);
-        if (!ret) // false表示协程取消, 则需要return,停止整个协程
-        {
-            return;
-        }
-        // 移动到了, buff会删除,或者切换成其它状态,协程退出也会删除无敌buff
-    }
-}
-```
-
-其实巡逻跟返回两个节点也可以合并成一个节点,这个大家自己去尝试尝试。可以看出,行为机编写是非常简单,代码是非常易读的。就这么一个怪物的逻辑,用行为树来编写,节点就很多了,而且并不好阅读跟重构。整个逻辑可能还有些瑕疵,不过意思应该很明白了。
-
-# 总结
-1. 行为机节点并不需要共用,行为机的节点只是表示一段逻辑,可以做的非常非常非常庞大,比如做机器人的时候,一个做任务,只有一个节点。里面代码调用了无数的协程方法。
-2. 行为机共用的是函数,不是节点,不要想着这个节点应该抽出来共用,这个想法是错误的。
-3. 行为机永远只关注当前行为,永远不需要关心上一个行为,当满足行为条件就直接打断上个协程,执行当前节点的协程即可。所以只需要定义好玩家有哪些行为,ai自然而然的就写出来了。
-4. 节点不要拆的太细,没有必要

+ 0 - 3
Book/7.1代码规范.md

@@ -1,3 +0,0 @@
-# 代码规范
-[分析器说明](https://www.yuque.com/u28961999/yms0nt/)  
-

BIN
Book/donate.png


+ 0 - 21
LICENSE

@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2018 tanghai
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.

+ 0 - 197
README.md

@@ -1,197 +0,0 @@
-# English: please use your browser to translate to english  
-
-# __讨论QQ群 : 474643097__  
-
-# [ET论坛](https://et-framework.cn)  
-
-# [ET商店](https://github.com/egametang/ET/tree/master/Store)  
-
-# [ET6.0视频教程上线](https://edu.uwa4d.com/course-intro/1/375)   
-
-# [运行指南](https://github.com/egametang/ET/blob/master/Book/1.1%E8%BF%90%E8%A1%8C%E6%8C%87%E5%8D%97.md)  
-
-# [分析器说明](https://www.yuque.com/u28961999/yms0nt/)
-
-# Benchmark
-100W Ping Pong 平均耗时4秒左右,平均每秒收发20W的消息。这个网络性能远远超过主线程的需求,大家可以自己测试一下,测试方法:
-Unity Menu->ServerTools select Benchmark, Start Watcher。然后在Logs目录,打开Debug日志等一会所有连接完成就能看到下面的日志了。  
-2022-12-02 22:19:48.9837 (C2G_BenchmarkHandler.cs:13) benchmark count: 1000001  
-2022-12-02 22:19:53.4621 (C2G_BenchmarkHandler.cs:13) benchmark count: 2000001  
-2022-12-02 22:19:57.0416 (C2G_BenchmarkHandler.cs:13) benchmark count: 3000001  
-2022-12-02 22:20:00.6186 (C2G_BenchmarkHandler.cs:13) benchmark count: 4000001  
-2022-12-02 22:20:04.1384 (C2G_BenchmarkHandler.cs:13) benchmark count: 5000001  
-2022-12-02 22:20:08.2236 (C2G_BenchmarkHandler.cs:13) benchmark count: 6000001  
-2022-12-02 22:20:12.2842 (C2G_BenchmarkHandler.cs:13) benchmark count: 7000001  
-2022-12-02 22:20:15.8544 (C2G_BenchmarkHandler.cs:13) benchmark count: 8000001  
-2022-12-02 22:20:19.4085 (C2G_BenchmarkHandler.cs:13) benchmark count: 9000001  
-2022-12-02 22:20:24.2969 (C2G_BenchmarkHandler.cs:13) benchmark count: 10000001  
-2022-12-02 22:20:41.1448 (C2G_BenchmarkHandler.cs:13) benchmark count: 11000001  
-2022-12-02 22:20:44.7174 (C2G_BenchmarkHandler.cs:13) benchmark count: 12000001  
-2022-12-02 22:20:48.3188 (C2G_BenchmarkHandler.cs:13) benchmark count: 13000001  
-2022-12-02 22:20:51.7793 (C2G_BenchmarkHandler.cs:13) benchmark count: 14000001  
-2022-12-02 22:20:55.3379 (C2G_BenchmarkHandler.cs:13) benchmark count: 15000001  
-2022-12-02 22:20:58.8810 (C2G_BenchmarkHandler.cs:13) benchmark count: 16000001  
-2022-12-02 22:21:02.5156 (C2G_BenchmarkHandler.cs:13) benchmark count: 17000001  
-2022-12-02 22:21:06.0132 (C2G_BenchmarkHandler.cs:13) benchmark count: 18000001  
-2022-12-02 22:21:09.5320 (C2G_BenchmarkHandler.cs:13) benchmark count: 19000001  
-
-
-# ET7 发布! 18岁亦菲
-1. 调整结构,机器人工程与服务器合并,更易使用,一个进程同时可以做server,也能创建机器人,真正的ALL IN ONE! -- 已实现  
-2. 客户端跟服务端合并,服务端代码全部放在了客户端,客户端中可以带一个服务端,开发超级方便,服务端发布的时候可以选择发布成Dotnet也可以发布成UnityServer,终极All IN ONE  -- 已实现  
-3. Entity可视化,客户端跟服务端所有的Entity都实现了可视化,开启ENABLE_CODES宏,运行游戏,查看Hierarchy面板,展开Init/Global/Scene(Process)即可看到 -- 已实现  
-4. 因为所有代码都在Unity中,所以开发ET插件变得非常容易,直接使用Unity导入导出即可  -- 已实现  
-5. 增加软路由,可以防各种网络攻击而不影响正常玩家,网游必备!-- 已实现  
-6. 各种事件跟网络消息订阅带上DomainSceneType,更精确,更不容易出错 -- 已实现  
-7. sj兄弟添加了各种分析器,分析器保证了写出的代码必须符合ET规范,否则编译不通过!(这点ET6也增加上了) -- 已实现  
-8. ET7已经去除客户端热更新,请大家自己选择接入,接入huatuo或者ILRuntime都非常简单, 注意!(不要混淆客户端热更新跟服务端热更新,服务端热更新,ET一直都有)  
-9. 网络改成独立线程,序列化反序列化都在网络线程处理,主线程压力大大减轻。并且重新整理了网络层代码,更优美了  
-10. 集成Unity.Mathematic数学库,逻辑层客户端跟服务端都使用这一套数学库,这样服务端跟客户端完全统一了  
-11. ENABLE_CODES模式下拆分成4个程序集,解决分析器失效的问题  
-12. Game管理的Singleton增加ISingletonUpdate跟ISingletonLateUpdate接口,实现相应的接口即可执行对应的Update跟LateUpdate方法,Game类解除了跟EventSystem等单间类的耦合关系  
-13. Actor消息判断如果是发向自己的进程则不用通过网络,直接处理即可,大大提升性能  
-
-
-# ET6 发布!ET6相比ET5有巨大变化,可以说是凤姐变亦菲,6.0拥有如下惊人的特点
-1. 客户端逻辑全热更新(基于ILRuntime),没有不能更的部分  
-2. 客户端服务端均可热重载,开发不用重启客户端服务端即可修改逻辑代码,开发极其方便  
-3. 机器人框架,ET6的客户端的逻辑跟表现分离,机器人程序直接共享利用客户端的逻辑层代码做压测,只需要极少代码即可做出机器人,方便压测服务端  
-4. 测试用例框架,利用客户端的逻辑层代码写单元测试,每个单元测试都是完整的游戏环境,无需各种恶心的mock  
-5. AI框架,比行为树更加方便,写AI比写UI还简单  
-6. 新的服务端架构,极其优美  
-7. 内外网kcp网络,性能强劲,搭配软路由模块,可以防各种网络攻击  
-
-# ET开发的商业mmo项目千古风流成功上线,64核128G内存的单服单物理机1.5W在线(实际线上策划为了生态限制为单服6000人同时在线,6000人的话cpu消耗约为30%)。为了堆栈行号正常,线上跑得是Debug版,如果使用Release版开启优化,性能还能翻一倍,达到单物理机3W在线!上线5个月来十分稳定。千古风流使用了ET框架从零开发,用时两年,这个开发速度可以说无人出其右。千古风流的成功上线证明了ET具备开发任何大型游戏的能力,开发速度,开发效率都令人叹为观止!千古风流使用到的客户端服务器技术:  
-1. 动态副本跟分线,按需分配,用完回收  
-2. 分线合线,分线人数较少会把多条线合并。合线功能基本上其它mmo游戏很少见到  
-3. 客户端服务端场景无缝切换,也就是无缝大世界技术  
-4. 跨服副本,跨服战场  
-5. 前后端一体化,利用客户端代码开发服务器压测机器人,4台24核机器轻松模拟1W人做任务  
-6. 千古风流各种ai设计,使用ET的全新开发的ai框架,使ai开发简单到跟写ui一样简单  
-7. 测试用例框架,大部分重要系统,千古风流都写了测试用例,跟市面上的测试用例不同,每个千古风流的测试用例都是一个完整的游戏环境,针对协议级别,不需要搞各种接口去mock。写起来非常快速。  
-8. 九宫格的aoi实现,动态调整看见的玩家,降低服务器负载  
-9. 防攻击,千古风流开发了软路由功能,即使攻击也只能攻击到软路由,一旦被攻击,玩家客户端发现几秒钟无响应,即可动态切换到其它软路由,用户几乎无感知。整个过程客户端网络连接不断开,数据不丢失。  
-10. 还有很多很多,这里就不啰嗦了  
-
-
-# ET的介绍:
-ET是一个开源的游戏客户端(基于unity3d)服务端双端框架,服务端是使用C# .net core开发的分布式游戏服务端,其特点是开发效率高,性能强,双端共享逻辑代码,客户端服务端热更机制完善,同时支持可靠udp tcp websocket协议,支持服务端3D recast寻路等等  
-
-# ET的功能:
-### 1.可用VS单步调试的分布式服务端,N变1  
-一般来说,分布式服务端要启动很多进程,一旦进程多了,单步调试就变得非常困难,导致服务端开发基本上靠打log来查找问题。平常开发游戏逻辑也得开启一大堆进程,不仅启动慢,而且查找问题及其不方便,要在一堆堆日志里面查问题,这感觉非常糟糕,这么多年也没人解决这个问题。ET框架使用了类似守望先锋的组件设计,所有服务端内容都拆成了一个个组件,启动时根据服务器类型挂载自己所需要的组件。这有点类似电脑,电脑都模块化的拆成了内存,CPU,主板等等零件,搭配不同的零件就能组装成一台不同的电脑,例如家用台式机需要内存,CPU,主板,显卡,显示器,硬盘。而公司用的服务器却不需要显示器和显卡,网吧的电脑可能不需要硬盘等。正因为这样的设计,ET框架可以将所有的服务器组件都挂在一个服务器进程上,那么这个服务器进程就有了所有服务器的功能,一个进程就可以作为整组分布式服务器使用。这也类似电脑,台式机有所有的电脑组件,那它也完全可以当作公司服务器使用,也可以当作网吧电脑。  
-### 2.随意可拆分功能的分布式服务端,1变N  
-分布式服务端要开发多种类型的服务器进程,比如Login server,gate server,battle server,chat server friend server等等一大堆各种server,传统开发方式需要预先知道当前的功能要放在哪个服务器上,当功能越来越多的时候,比如聊天功能之前在一个中心服务器上,之后需要拆出来单独做成一个服务器,这时会牵扯到大量迁移代码的工作,烦不胜烦。ET框架在平常开发的时候根本不太需要关心当前开发的这个功能会放在什么server上,只用一个进程进行开发,功能开发成组件的形式。发布的时候使用一份多进程的配置即可发布成多进程的形式,是不是很方便呢?随便你怎么拆分服务器。只需要修改极少的代码就可以进行拆分。不同的server挂上不同的组件就行了嘛!  
-### 3.跨平台的分布式服务端  
-ET框架使用C#做服务端,现在C#是完全可以跨平台的,在linux上安装.netcore,即可,不需要修改任何代码,就能跑起来。性能方面,现在.netcore的性能非常强,比lua,python,js什么快的多了。做游戏服务端完全不在话下。平常我们开发的时候用VS在windows上开发调试,发布的时候发布到linux上即可。ET框架还提供了一键同步工具,打开unity->tools->rsync同步,即可同步代码到linux上  
-```bash
-./Run.sh Config/StartConfig/192.168.12.188.txt 
-```
-即可编译启动服务器。  
-### 4.提供协程支持  
-C#天生支持异步变同步语法 async和await,比lua,python的协程强大的多,新版python以及javascript语言甚至照搬了C#的协程语法。分布式服务端大量服务器之间的远程调用,没有异步语法的支持,开发将非常麻烦。所以java没有异步语法,做单服还行,不适合做大型分布式游戏服务端。例如:  
-
-```c#
-// 发送C2R_Ping并且等待响应消息R2C_Ping
-R2C_Ping pong = await session.Call(new C2R_Ping()) as R2C_Ping;
-Log.Debug("收到R2C_Ping");
-
-// 向mongodb查询一个id为1的Player,并且等待返回
-Player player = await Game.Scene.GetComponent<DBProxyComponent>().Query<Player>(1);
-Log.Debug($"打印player name: {player.Name}")
-```
-可以看出,有了async await,所有的服务器间的异步操作将变得非常连贯,不用再拆成多段逻辑。大大简化了分布式服务器开发  
-### 5.提供类似erlang的actor消息机制  
-erlang语言一大优势就是位置透明的消息机制,用户完全不用关心对象在哪个进程,拿到id就可以对对象发送消息。ET框架也提供了actor消息机制,实体对象只需要挂上MailBoxComponent组件,这个实体对象就成了一个Actor,任何服务器只需要知道这个实体对象的id就可以向其发送消息,完全不用关心这个实体对象在哪个server,在哪台物理机器上。其实现原理也很简单,ET框架提供了一个位置服务器,所有挂载MailBoxComponent的实体对象都会将自己的id跟位置注册到这个位置服务器,其它服务器向这个实体对象发送消息的时候如果不知道这个实体对象的位置,会先去位置服务器查询,查询到位置再进行发送。
-### 6.提供服务器不停服动态更新逻辑功能  
-热更是游戏服务器不可缺少的功能,ET框架使用的组件设计,可以做成守望先锋的设计,组件只有成员,无方法,将所有方法做成扩展方法放到热更dll中,运行时重新加载dll即可热更所有逻辑。
-### 7.客户端使用C#热更新,热更新一键切换  
-可以使用csharp.lua或者ILRuntime稍加改造即可做客户端热更。再也不用使用狗屎lua了,客户端可以实现所有逻辑热更新,包括协议,config,ui等等。  
-### 8.客户端热重载  
-开发不用重启客户端即可修改客户端逻辑代码,开发极其方便  
-
-### 9.客户端服务端用同一种语言,并且共享代码  
-下载ET框架,打开服务端工程,可以看到服务端引用了客户端很多代码,通过引用客户端代码的方式实现了双端共享代码。例如客户端服务端之间的网络消息两边完全共用一个文件即可,添加一个消息只需要修改一遍。  
-### 10.KCP ENET TCP Websocket协议无缝切换  
-ET框架不但支持TCP,而且支持可靠的UDP协议(ENET跟KCP),ENet是英雄联盟所使用的网络库,其特点是快速,并且网络丢包的情况下性能也非常好,这个我们做过测试TCP在丢包5%的情况下,moba游戏就卡的不行了,但是使用ENet,丢包20%仍然不会感到卡。非常强大。框架还支持使用KCP协议,KCP也是可靠UDP协议,据说比ENET性能更好,使用kcp请注意,需要自己加心跳机制,否则20秒没收到包,服务端将断开连接。协议可以无缝切换。  
-### 11. 3D Recast寻路功能  
-可以Unity导出场景数据,给服务端做recast寻路。做MMO非常方便,demo演示了服务端3d寻路功能  
-### 12. 服务端支持repl,也可以动态执行一段新代码  
-这样就可以打印出进程中任何数据,大大简化了服务端查找问题的难度,开启repl方法,直接在console中输入repl回车即可进入repl模式  
-### 13.提供客户端机器人框架支持  
-几行代码即可创建机器人登录游戏。机器人压测轻而易举,机器人跟正常的玩家完全一样,上线前用机器人做好压测,大大降低上线崩溃几率   
-### 14.AI框架  
-ET的AI框架让AI编写比UI还简单     
-### 15.测试用例框架  
-跟市面上的测试用例不同,ET的测试用例都是一个完整的游戏环境,针对协议级别,不需要搞各种接口去mock。写起来非常快速    
-
-### 16.还有很多很多功能,我就不详细介绍了  
-a.及其方便检查CPU占用和内存泄漏检查,vs自带分析工具,不用再为性能和内存泄漏检查而烦恼  
-b.使用NLog库,打log及其方便,平常开发时,可以将所有服务器log打到一个文件中,再也不用一个个文件搜索log了  
-c.统一使用Mongodb的bson做序列化,消息和配置文件全部都是bson或者json,并且以后使用mongodb做数据库,再也不用做格式转换了。  
-d.提供一个同步工具  
-
-ET框架是一个强大灵活的分布式服务端架构,完全可以满足绝大部分大型游戏需求。使用这套框架,客户端开发者就可以自己完成双端开发,节省大量人力物力,节省大量沟通时间。  
-
-  
-相关网站:  
-[ET论坛](https://et-framework.cn)  
-
-群友分享:  
-[行为树与fgui分支(Duke Chiang开发维护)](https://github.com/DukeChiang/ET.git)   
-[ET学习笔记系列(烟雨迷离半世殇写)](https://www.lfzxb.top/)   
-[图形渲染与ET学习笔记(咲夜詩写)](https://acgmart.com/)   
-[框架服务端运行流程](http://www.cnblogs.com/fancybit/p/et1.html)  
-[ET启动配置](http://www.cnblogs.com/fancybit/p/et2.html)  
-[框架demo介绍](http://www.jianshu.com/p/f2ea0d26c7c1)  
-[linux部署](http://gad.qq.com/article/detail/35973)  
-[linux部署,mongo安装,资源服搭建](http://www.tinkingli.com/?p=25)  
-[ET框架心跳包组件开发](http://www.tinkingli.com/?p=111)  
-[ET框架Actor使用与心得](http://www.tinkingli.com/?p=117)  
-[基于ET框架和UGUI的简单UI框架实现(渐渐写)](http://www.tinkingli.com/?p=124)  
-[ET框架笔记 (笑览世界写)](http://www.tinkingli.com/?p=76)  
-[ET框架如何用MAC开发](http://www.tinkingli.com/?p=147)  
-[ET的动态添加事件和触发组件](http://www.tinkingli.com/?p=145)  
-
-商业项目:  
-1. [千古风流](https://www.qiangu.com/)  
-2. [魔法点点2](https://www.taptap.com/app/227804)  
-3. [养不大](https://www.taptap.com/app/71064)  
-4. 天天躲猫猫2(ios2019春节下载排行19)  
-5. [牛虎棋牌](https://gitee.com/ECPS_admin/PlanB)  
-6. [五星麻将](https://github.com/wufanjoin/fivestar)  
-
-群友demo:  
-1. [斗地主(客户端服务端)](https://github.com/Viagi/LandlordsCore)  
-2. [背包系统](https://gitee.com/ECPS_admin/planc)  
-3. [移动端渲染技术demo](https://github.com/Acgmart/Sekia_TechDemo)  
-
-
-
-视频教程:  
-[字母哥ET6.0教程](https://edu.uwa4d.com/course-intro/1/375)   
-[肉饼老师主讲](http://www.taikr.com/my/course/972)  
-[官剑铭主讲](https://edu.manew.com/course/796)  
-[ET新手教程-初见主讲](https://pan.baidu.com/s/1a5-j2R5QctZpC9n3sMC9QQ) 密码: ru1j  
-[ET新手教程新版-初见主讲](https://www.bilibili.com/video/av33280463/?redirectFrom=h5)  
-[ET在Mac上运行指南-L主讲](https://pan.baidu.com/s/1VUQbdd1Yio7ULFXwAv7X7A) 密码: l3e3  
-[ET框架系列教程-烟雨主讲-6.0版本](https://space.bilibili.com/33595745/favlist?fid=759596845&ftype=create)  
-
-.net core 游戏资源分享  
-[各种dotnet core项目收集](https://github.com/thangchung/awesome-dotnet-core)  
-
-__讨论QQ群 : 474643097__
-
-
-# 支付宝捐赠  
-![使用支付宝对该项目进行捐赠](https://github.com/egametang/ET/blob/master/Book/donate.png)
-
-# 友情链接  
-[Box2DSharp](https://github.com/Zonciu/Box2DSharp)  box2d的C#移植版,性能很强  
-[xasset](https://github.com/xasset/xasset) 致力于为 Unity 项目提供了一套 精简稳健 的资源管理环境  
-[QFramework](https://github.com/liangxiegame/QFramework) Your first K.I.S.S Unity3d Framework  
-[ET UI框架](https://github.com/zzjfengqing/ET-EUI) 字母哥实现的UI框架,ET风格,各种事件分发  
-[ETCsharpToXLua](https://github.com/zzjfengqing/ETCsharpToXLua) 字母哥使用csharp.lua实现的ET客户端热更新  
-[et-6-with-ilruntime](https://www.lfzxb.top/et-6-with-ilruntime) 烟雨使用ILRuntime实现的ET客户端热更新  
-[Luban](https://github.com/focus-creative-games/luban) 适用于大中型项目的游戏配置解决方案  
-