<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
  <title>Relog</title>
  <link>https://www.this.red</link>
  <description></description>
  <language>zh-CN</language>
  <lastBuildDate>Sat, 29 Aug 2026 20:43:17 GMT</lastBuildDate>
  <atom:link href="https://www.this.red/rss.xml" rel="self" type="application/rss+xml" />
  <item>
    <title>让服务器框架适应 AI</title>
    <link>https://www.this.red/let-server-framework-adapt-to-ai/</link>
    <guid isPermaLink="true">https://www.this.red/let-server-framework-adapt-to-ai/</guid>
    <pubDate>Sat, 29 Aug 2026 00:00:00 GMT</pubDate>
    <description>AI 盛行已是现状，游戏服务器框架必须主动适应：从 CSV 配表、压测、自主 debug 的 skill，到有状态可热更的 MCP server，最终走向一人一功能的全栈开发。</description>
    <content:encoded><![CDATA[<p>我自己的服务器框架考虑到了ai盛行的现状，也想更加适应ai。</p>
<p>为此我新增了一些实用的skill：</p>
<p>配表/导表：之前做了配xlsx的skill，让ai自主去写，去导出。不过现在我认为csv才是最适合ai的，直接去ai改csv里的文本就行了，让ai去调用a代码去改b文件，然后再导出c格式，简直是脱裤子放屁。甚至我觉得平时开发，直接csv裸奔，服务器客户端都用csv，打包才打包成其他的格式，这样才最适合ai开发。</p>
<p>压力测试：ai非常擅长做这种活儿。搞个机器人，ai自己去输入参数去调用，自己去读日志，最后整理出文档。</p>
<p>自主debug：rider，vscode等ide都带有mcp server，ai可以极其方便调用。我平时使用rider，rider里能做到的我比较惊喜的，可以让ai自主去打断点调试，非常神奇。</p>
<p>我想让ai自主写代码，自主配表，自主启停，自主修bug。并且为了达到快速修bug的目的，ai应该能够自主去模拟客户端发协议，自主断点调试每一行代码。</p>
<p>IDE开放了mcp server和非常多的skill，为了让ai更顺畅在我的框架上开发，我认为仅仅是skill是不够的</p>
<p>为此我又搞了个服务器框架的mcp server，它自带了一些工具，比如常见的登录、登出、断开、发消息等等。</p>
<p>因为要模拟客户端，所以这个mcp server是有状态的，相对于游戏服务器就是个client，并且ai可以自主去编写其中的逻辑，自主热更。</p>
<p>这样就达到了模拟真人测试功能。顺便可以给功能的每一行代码都断点调试，甚至每个字段的值都能看得见，所见即所得。</p>
<p>当然这种便捷开发一定是全栈开发，在同一个会话里同时开发客户端服务器，一个功能分给一个人，省去了扯皮，效率极高。</p>
]]></content:encoded>
  </item>
  <item>
    <title>回顾与展望2026</title>
    <link>https://www.this.red/20260217-NewYear/</link>
    <guid isPermaLink="true">https://www.this.red/20260217-NewYear/</guid>
    <pubDate>Tue, 17 Feb 2026 04:30:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>春节第一天，回顾一下我去年的事。</p>
<p>2025年也是承上启下的一年，有不舍的结束，也有新的开始。</p>
<h2 id="section">流水账</h2>
<p>3月提了辆su7max，一个人请假去提车。好久没摸过车了，本来想慢慢开回去，错过了一个掉头，结果导航重新规划，又导航到高速了。</p>
<p>4月离职，我其实很看好完美的，即使现在也是，北京的游戏公司，完美再怎么不好也是瘦死的骆驼比马大。等今年异环上线，绝对是一个新的高度。怀念免费停车和免费充电。</p>
<p>在离职后，同事有的去旅游，有的回家玩，我则是漫长的没日没夜的沉睡。</p>
<p>5月之后才开始找工作，找来找去，大大小小的公司都面过了，面麻了。</p>
<p>后来感觉面试通过与否可能也和眼缘有关吧。我来了这边后发现，网易完美等的一些人来面我现在的岗位也没有能通过的。</p>
<p>7月末去广州了，顺丰还能运车，长见识了，北京运到广州，价格也还合适。快递员来取车，说他们还能运飞机: ）。到了广州之后就换了粤A牌照。</p>
<p>之后就是来阿里灵犀开始搬砖</p>
<p>11月项目组团建，去了景德镇，体验了一下捏陶瓷。</p>
<h2 id="gamejam">GameJam</h2>
<p>年前参加了GlobalGameJam，第一次参加线下GGJ。为了准备这个GGJ，提前准备的框架感觉比我前几个月的代码量都多。</p>
<p>程序应该平时多积累，否则跟不上时代了。见识到了mcp强大的地方，队友用mcp连预制体都拼好了，只需要向ai提问，游戏demo就有了，非常适合GameJam使用。但是也有不便的地方，如果要修改bug，得review生成的代码。</p>
<p>总想着能和《拣爱》的剧情一样能找到个女朋友。但是这次是失败了，效率太低，净顾着介绍游戏了，忘了要联系方式。</p>
<h2 id="section-1">开源</h2>
<p>写Lua真不如我写C#效率十分之一。</p>
<p>之前技术水平不够，也很少有时间思考，是时候积累一下自己了，先写了个<a href="https://github.com/XBlueC/DeltaTrack">DeltaTrack</a>试试水。</p>
<h2 id="section-2">今年搁家看的动漫：</h2>
<ul>
<li>未麻的部屋
神作，值得一看</li>
<li>鬼灭之刃 一到三</li>
<li>咒术回战</li>
</ul>
<h2 id="section-3">今年在电影院看的电影：</h2>
<p>根据时间顺序由近及远排列。</p>
<ul>
<li>藏地情书</li>
<li>阿凡达3</li>
<li>极限审判</li>
<li>匿杀</li>
<li>他年她日</li>
<li>疯狂动物城2</li>
<li>鬼灭之刃：无限城篇 第一章 猗窝座再袭
头一回体验4D电影，确实有点意思。</li>
<li>即兴谋杀</li>
<li>天鹰战士：最后的冲击</li>
<li>震耳欲聋</li>
<li>同甘共苦</li>
<li>捕风追影</li>
<li>浪浪山小妖怪</li>
<li>罗小黑战记2</li>
<li>长安的荔枝</li>
<li>你行！你上！</li>
<li>戏台</li>
<li>新·驯龙高手</li>
<li>F1：狂飙飞车</li>
<li>名侦探柯南：独眼的残像</li>
<li>不说话的爱</li>
<li>黎明的一切</li>
</ul>
<p>没想到看了这么多电影</p>
<h2 id="section-4">展望新的一年</h2>
<p>新的一年里要提升自己的核心竞争力，游戏人要想活下去，不能只靠单个技术了，得适应环境。</p>
<p>新的一年的目标</p>
<ul>
<li>写出自己想要的游戏服务器框架</li>
<li>找个女朋友</li>
<li>尝试一下独立游戏开发</li>
</ul>
]]></content:encoded>
  </item>
  <item>
    <title>游戏服务器框架 Actor模型</title>
    <link>https://www.this.red/20251209-game-server-actor/</link>
    <guid isPermaLink="true">https://www.this.red/20251209-game-server-actor/</guid>
    <pubDate>Tue, 09 Dec 2025 20:21:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>Actor模型是什么？</p>
<blockquote>
<p>在计算机科学中，演员模型（英语：Actor model）是一种并发运算上的模型。“演员”是一种程序上的抽象概念，被视为并发运算的基本单元：当一个演员接收到一则消息，它可以做出一些决策、建立更多的演员、发送更多的消息、决定要如何回答接下来的消息。演员可以修改它们自己的私有状态，但是只能通过消息间接的相互影响（避免了基于锁的同步）。<a href="https://zh.wikipedia.org/wiki/%E6%BC%94%E5%91%98%E6%A8%A1%E5%9E%8B">1</a></p>
</blockquote>
<p>按我的理解，每个Actor是独立的计算单元，可并行运行。Actor之间可以发消息互相通信。彼此之间没有共享状态，避免了锁竞争。在CPU频率涨幅变慢，但核数越来越多下，非常适合构建大型分布式系统。</p>
<p>在实现分布式Actor模型时，我遇到了以下问题。</p>
<h2 id="actorid">ActorId如何设计？</h2>
<p>ActorId的设计涉及到如何寻址。在分布式环境下，ActorId要通过解析，最终能路由到一个进程中的唯一Actor。</p>
<p>例如：</p>
<pre><code class="language-csharp">var player = cluster.GetActor&lt;PlayerActor&gt;(&quot;player-123&quot;);
player.Tell(new Login());
</code></pre>
<p>ActorId就是标识符，不需要考虑Actor的物理位置。</p>
<p>一些框架把进程号/线程号、时间戳合在一起，生成一个ActorId。通过进程号即可从配表里拿到IP和端口，即可给对应进程发消息，进程再去分发消息到对应的Actor。</p>
<p>游戏服务器中，如果玩家是Actor。同一个场景的玩家分发消息时ActorId一定能路由到同一个进程。</p>
<p>未完待续</p>
<h2 id="actor">Actor的调度</h2>
<p>Actor模型需要保证顺序处理邮箱中的消息，调度的粒度不应该是消息，而是一整个Actor。</p>
<p>假如一个Actor的消息堆积过多，还会造成其他Actor的消息不能及时处理，造成Actor饥饿。</p>
<p>有一种任务窃取的调度器，将Actor绑定到线程。Actor的执行任务使用双端队列来存储，工作线程从头获取Actor，窃取线程从尾部获取Actor。</p>
<p>这个方法需要处理竞争问题，当窃取线程越多，队列尾部竞争越大，反而会对CPU造成资源浪费。常见解决方法当没有窃取到任务，则等待更长的时间。</p>
<p>窃取线程也是工作线程转变的，工作线程处理完绑定自身的Actor的任务，去窃取其他工作线程的任务。如果在等待过程中，自身的Actor有消息需要处理，需要立刻打断这个等待。</p>
<p>但是实际上，假如一个玩家是一个Actor，根本不可能从玩家的消息队列的队尾去先执行后接收的消息，这样消息会无序。所以游戏服务器中并不能在Actor粒度是玩家的情况下实现任务窃取。</p>
<h2 id="section">其他</h2>
<p>Orleans提出了虚拟Actor。 首先定义粗粒度的行为接口，之后便可以以本地调用的方式调取远程Actor的方法，非常方便。</p>
]]></content:encoded>
  </item>
  <item>
    <title>最近对于ECS的认知</title>
    <link>https://www.this.red/20251130-ecs/</link>
    <guid isPermaLink="true">https://www.this.red/20251130-ecs/</guid>
    <pubDate>Sun, 30 Nov 2025 20:30:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>最近又在折腾ECS框架。目的是做一个效率优先，性能不差的框架在服务器上使用。</p>
<p>在写之前一直有几个问题困扰着我。</p>
<p>Entity应该设计成struct还是class？</p>
<p>Component应该设计成struct还是class？</p>
<p>Arch type和稀疏集怎么选？</p>
<p>如何让开发效率高？</p>
<h1 id="entitystructclass">Entity应该设计成struct还是class？</h1>
<p>一直有人诟病ET框架的Entity是class，效率低下，承载不了大量实体。</p>
<p>我怀疑他们没仔细看文档，文档里已经写明效率优先，牺牲缓存命中率。而且他们家游戏里哪里有数十万个实体？还有弱联网的单机游戏，根本就没有高效遍历组件的逻辑，更没必要提缓存命中率了。</p>
<p>并且，Entity是class还是struct和承载大量实体，关系不大。首先数据都在Component上，Entity只是个索引罢了。system逻辑里遍历大量实体的数据，都是按查询的Compnents结果去修改的。</p>
<h1 id="componentstructclass">Component应该设计成struct还是class？</h1>
<p>如果是为了缓存命中率，那么Component应该设计成struct。并且为了缓存命中率，component里不应该有引用类型的字段。</p>
<h1 id="arch-type">Arch type和稀疏集怎么选？</h1>
<p>一些ECS框架实现用的稀疏集，一些用Arch type。</p>
<p>大多数情况下，Arch type的效率高于稀疏集的，除非增删组件很频繁。</p>
<p>比如实现MovementSystem，需要查询具有Position和Velocity的实体，遍历去修改Position。</p>
<p>如果使用稀疏集存储component的话，需要在for循环里先查询Position再查询Velocity，Position和Velocity不是连续存储，所以缓存命中率一定是比Arch type低的。</p>
<p>Arch type是直接把具有一组组件的实体存储在一起，内存连续，这样遍历时缓存命中率最优秀。</p>
<p>但是如果新建一个MovementComponent，Position和Velocity放到组件里，此时内存就连续了。</p>
<h1 id="section">如何让开发效率高？</h1>
<p>优先要考虑的就是游戏类型，要不要用ECS，到底有没有大量实体，没有大量实体，就先考虑缓存命中率，很不切实际。</p>
<p>EC模式能让数据和逻辑解耦，逻辑更清晰，效率已经有了很大提升。</p>
<p>写游戏最重要的是首先要让游戏能跑起来，优化都是后面的事。别等原型没做出来，项目先没了。</p>
]]></content:encoded>
  </item>
  <item>
    <title>游戏服务器框架 数据落库脏标记模块</title>
    <link>https://www.this.red/20251008-trackable-field/</link>
    <guid isPermaLink="true">https://www.this.red/20251008-trackable-field/</guid>
    <pubDate>Wed, 08 Oct 2025 23:30:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>在修改完玩家的数据后，需要将修改的数据进行保存。 有几种方式</p>
<ul>
<li>玩家离线后保存全量数据</li>
<li>定时保存脏数据</li>
</ul>
<p>一般的项目都是定时保存脏数据的，脏数据何时标记为脏，以及哪些字段为脏需要开发者处理。</p>
<h2 id="section">脏数据的标记方式</h2>
<p>在平时的开发中碰到了这几种脏数据的标记方式</p>
<h3 id="set">set访问器</h3>
<p>在C#中可以将脏标记处理逻辑放到set访问器中实现精准定位哪些字段被改变。</p>
<pre><code class="language-csharp">public class Player
{
    private int _health;

    public int Health
    {
        get =&gt; _health;
        set
        {
            if (_health != value)
            {
                ... 脏标记处理
                _health = value;
            }
        }
    }
}
</code></pre>
<p>当然set访问器这种方式是最精准的知晓数据是否有改动，但是手写get/set很烦人</p>
<p>基础类型int、string等可以通过set访问器去判断是否有修改，但是如果字段是Dictionary、List，此时调用了Add/Remove方法照样能避开set访问器，更不用提如果字段是用户自定义的引用类型。</p>
<h3 id="etgetcomponent">ET框架中GetComponent触发事件</h3>
<p>在ET框架中，Entity调用GetComponent方法时，会触发一个事件，在事件的处理逻辑中可将Get的组件放到待处理列表，表示这个组件被修改，需要被保存。</p>
<p>这个做法并不好，有很多时候GetComponent并不会修改里面的数据，不仅会导致序列化浪费CPU，如果没有进行处理还会执行很多不必要的逻辑。</p>
<h3 id="lua">Lua</h3>
<p>通过 __newindex和 __index元方法拦截所有读写操作。</p>
<h2 id="section-1">设计目标</h2>
<p>用C#实现一个脏标记系统，用于游戏服务器中数据落地模块。</p>
<ul>
<li>支持跟踪基础类型如int，string</li>
<li>支持跟踪集合如Dictionary，List，HashSet</li>
<li>用户不用手写多余代码，只需要定义model，在model类上加特性即可使用脏标记系统。可在字段上加特性，跟踪指定字段。</li>
<li>支持跟踪用户自定义类型字段</li>
<li>支持序列化，反序列化</li>
</ul>
<p>支持以下用法的脏标记检测</p>
<pre><code class="language-csharp">[DirtyTrackable]
public partial class Player
{
    [DirtyTrack] private int _health;
    [DirtyTrack] private string _name;
    [DirtyTrack] private List&lt;Item&gt; _inventory = new List&lt;Item&gt;();
    [DirtyTrack] private Item _item;
}
[DirtyTrackable]
public partial class Item
{
    [DirtyTrack] private string _itemId;
    [DirtyTrack] private int _quantity;  
}

var player = new Player();

player.Health -= 10; // 基础类型修改检测
player.Inventory.Add(new Item() { ItemId = &quot;sword&quot;, Quantity = 1 }); // 集合本身修改检测
player.Item.ItemId = &quot;IsDirty&quot;; // 自定义嵌套类型检测
player.Inventory[0].Quantity--; // 集合中的可追踪类型检测
player.Inventory = new List&lt;Item&gt;(); // 支持直接赋值新集合
</code></pre>
<p>体验了一下，效果非常不错。仅仅需要加特性即可体验到便利。</p>
<p>GitHub地址：<a href="https://github.com/XBlueC/DeltaTrack">DeltaTrack</a></p>
]]></content:encoded>
  </item>
  <item>
    <title>为何在游戏服务端用C#的很少？</title>
    <link>https://www.this.red/20250521-java-csharp/</link>
    <guid isPermaLink="true">https://www.this.red/20250521-java-csharp/</guid>
    <pubDate>Wed, 21 May 2025 22:00:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>前公司的游戏服务端的技术栈是Java，C++/Lua，C#。偶然和其他项目Java主程进行交流，这是发生的一段对话。</p>
<blockquote>
<p>“你们为什么用C#写服务器，是不是服务器用的winserver？”
”不是，服务器是Ubuntu。“
“那你们用的是Mono那套吗？”
“不是的，.Net很早就能跨平台了，.Net5就统一了。”</p>
</blockquote>
<p>和很多人解释过C#可跨平台，发现他们对于C#的印象还停留在很多年前，按理说两个这么像的语言，双方开发者应该都关注一下的。</p>
<p>同时用过Java与C#，一直很纳闷，为什么C#写起来这么爽，在国内却不温不火。 最近在看游戏服务端的工作，发现用C#的太少了，基本都是C++，Java，Golang。</p>
<p>接下来我从以下几个方面进行对比一下Java与C#两个语言。</p>
<h2 id="section">跨平台</h2>
<p>对于Java，发布就是为了跨平台的。</p>
<p>C#在2004年Mono首次证明 .NET 可跨平台运行。之后2016发布.NET Core 1.0，跨平台还是太晚了，Java都把份额抢走了。</p>
<p>目前国内C#主要是在游戏，上位机，桌面开发等领域。</p>
<h2 id="javac-async-await">Java虚拟线程与C# async await</h2>
<p>在虚拟线程之前还能用“Java不适合写异步逻辑，只能写多段逻辑和死亡回调”来对比Java。比如以下这个C#逻辑换做Java来写得拆分成多段，用C#写就如行云流水。</p>
<pre><code class="language-csharp">// 发送C2R_Ping并且等待响应消息R2C_Ping
R2C_Ping pong = await session.Call(new C2R_Ping()) as R2C_Ping;
Log.Debug(&quot;收到R2C_Ping&quot;);

// 向mongodb查询一个id为1的Player，并且等待返回
Player player = await Game.Scene.GetComponent&lt;DBProxyComponent&gt;().Query&lt;Player&gt;(1);
Log.Debug($&quot;打印player name: {player.Name}&quot;)
</code></pre>
<p>Java19中提供了预览版的虚拟线程，在Java21中正式发布。没有引入await async关键字，隐式异步，以同步写法实现异步逻辑，适用于IO密集型任务，由JVM调度。</p>
<p>由于在虚拟线程中阻塞不会阻塞平台线程，所以之前用异步回调的写法在虚拟线程中可改成同步，一定程度上缓解了死亡回调的问题，也让代码逻辑变得连贯，不用再拆成多段逻辑。</p>
<p>C# 中的 async/await通过编译器将异步代码转换为状态机，实现非阻塞操作。其中不好的一点就是，只要代码里一处有异步，会连带调用的方法都改成异步，一路await到底，有种“异步的传染性”。</p>
<h2 id="section-1">调试对比</h2>
<p>我在20年的时候用Idea写Java，当时Idea还不能在调试的时候进行热重载，需要安装JRebel插件。前几天装了Idea新版本，发现能直接支持热重载了，我又重新体验了一下写Java。</p>
<p>在调试时修改代码并热重载发现，Main类里修改方法不会生效，在改了其他的类，旧的对象里的方法不会生效，创建新的对象会生效。(盲猜就是重新编译加载字节码替换)</p>
<p>Idea里调试Java只能向前执行，比Rider少了一些按钮，调试C#的时候能反向调试。</p>
<p>我在写C#的时候，发现C#的热重载简直是神奇。我甚至能先点开调试让程序运行，再去对应的方法里断点，然后写代码，<strong>边写代码边热重载边调试</strong>。这在写很复杂的逻辑时非常爽，比如写一个英雄的技能逻辑，我能边调试边写代码，然后看最后结果，直到符合要求。</p>
<p><a href="https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debuggercmds/time-travel-debugging-overview">时间旅行调试 - 概述 - Windows drivers | Microsoft Learn</a></p>
<h2 id="section-2">热更新</h2>
<p>在游戏服务器中，Java可通过动态加载新类来替换旧类实现热更新。C#可通过动态加载程序集进行热更新。</p>
<h2 id="section-3">泛型</h2>
<p>都说Java里的泛型是个残废，因为类型擦除导致运行时拿不到类型信息。
例如java类型擦除  List&lt;String&gt;会变成List，假如有两个方法名相同，传入泛型参数不同的List，编译时会报错。</p>
<pre><code class="language-csharp">// C#（编译时类型安全）
public T CreateInstance&lt;T&gt;() where T : new() 
{
    return new T();
}
</code></pre>
<pre><code class="language-java">// Java（类型擦除导致需要强制转换）
public &lt;T&gt; T createInstance(Class&lt;T&gt; type) throws Exception {
    return type.newInstance();
}
</code></pre>
<h2 id="section-4">值类型</h2>
<p>Java里除了基础类型外没有值类型，C#可定义struct明确栈分配，除非装箱。对于性能的影响就是Java自动装箱开销大，C#零开销。</p>
<p>值类型在游戏中还是很重要的，比如向量之间的计算。</p>
<h2 id="section-5">其他</h2>
<p>还有很多没提到的点，比如指针操作等</p>
<p>在游戏开发的领域，如果客户端是Unity的话，最优解是这样的</p>
<p>客户端：Unity+HybirdCLR热更 -&gt; 全C#脚本与热更新</p>
<p>服务器：C#游戏服务器 -&gt; 全C#脚本与热更新，与客户端可共用协议代码与相关逻辑代码，例如战斗逻辑或是写客户端机器人进行压测。<strong>代码逻辑直接复用，这是其他的语言都做不到的</strong>。</p>
<p>数据库：Mongodb -&gt; 相比于关系型数据库不用写Sql语句，关注点主要放在游戏业务实现，大大提升开发效率</p>
<hr />
<p>但是国内意识到C#很适合游戏双端开发的公司很少，估计是熟悉Java的程序开发还没到退休年纪，都做到了管理层，一套框架用十年，自然就不会考虑新技术栈了。</p>
]]></content:encoded>
  </item>
  <item>
    <title>回顾这段工作</title>
    <link>https://www.this.red/20250410-my-work/</link>
    <guid isPermaLink="true">https://www.this.red/20250410-my-work/</guid>
    <pubDate>Thu, 10 Apr 2025 02:30:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>这段旅程到此结束。回顾一下我在这个项目里做了哪些事吧。</p>
<p>我刚来这个项目的时候，项目还没有在外网部署。在部署的时候发现了很多问题，首先登录都有死锁（其实目前还是有一个隐藏的死锁没处理）。其次服务器的承载量没有压测过，谁心里都没底上线后会是什么样。</p>
<p>我们这个项目是2d的推关游戏，客户端unity，服务器是c#的。玩家在通关关卡后，会由服务器去验算一遍战斗。我用dottrace去分析这个战斗验算代码，发现简单的战斗一局10s，验算耗时20ms左右，复杂的战斗一局可能120s，验算100ms左右。</p>
<p>当时我们的战斗验算是放在逻辑服的，逻辑服是由两个线程组成，一个网络线程，一个逻辑线程。我在同时压测登录与战斗时发现，在登录不到500人的时候，机器人就频繁掉线了。并且请求的响应时间超过了30s。</p>
<p>A玩家的战斗验算能卡住B玩家的登录，这是极其不合理的。战斗验算是个 CPU 密集型任务，并不适合在逻辑服线程处理。之后我将战斗验算放到了单独的进程去处理，玩家的战斗数据由逻辑服向战斗服发送请求，战斗服处理完毕后再返回给逻辑服。</p>
<p>后来压测过程中发现，在200人左右同时验证的时候，战斗验证请求的响应时间就有超过10s的。因为战斗验证只部署了一个进程，所以这个解决方法还是没有解决问题。</p>
<p>无非是两个方案，一是多部署几个战斗进程。二是将战斗进程改成多线程。目的是利用CPU多核。</p>
<p>多部署战斗进程，耗费的内存会比较高，但是实现简单。</p>
<p>后来选了改成多线程的方案。</p>
<p>在看战斗验算的代码的时候，发现代码量比较大，而且从最开始的设计上，就没有考虑会有多个战斗同时验算，对于战斗中用到的资源没有考虑线程安全问题，例如线程安全的对象池。这个对象池是个static的单例，很多代码都是直接从World中获取对象池。</p>
<p>有人会有疑问，那把对象池做成线程安全的不就行了？</p>
<p>我们这个对象池是有定制化的，里面会写日志，记录单局游戏对象创建的个数，写日志会从World取当前帧数，还有其他非线程安全的操作。</p>
<pre><code class="language-csharp">public class World
{
    public static readonly GameObjectPool GameObjectPool = new();
    public static readonly XXX XXX = new();
}
</code></pre>
<p>当时想到的最好的做法是仿照ECS做个树形的结构，子节点能获取到父节点和根节点，World彼此之间互不影响。</p>
<p>在实现的时候发现，代码改动太大了，而且我们这个是客户端与服务器通用的代码，相当于重写。在折腾了许久后，竟然实现了一个改动极少的方案。</p>
<p>大致思路就是 用当前线程id去对应资源。</p>
<ul>
<li>在启动服务器时，创建一个线程池，每个线程都创建单个线程用到的资源，每个线程去遍历阻塞队列。</li>
<li>当有战斗验算请求时，新建BattleValidateItem加入阻塞队列，异步等待结果。</li>
<li>验算结束后，设置tcs，将验算结果返回。</li>
</ul>
<pre><code class="language-csharp">public class BattleValidateItem
{
	public BattleValidateReq Request; // 战斗验算请求
	public TaskCompletionSource&lt;BattleInfoValidateInfoItem&gt; TaskCompletionSource; // tcs
}

public class World
{
	public static GameObjectPool GameObjectPool =&gt; ConfigDict[Environment.CurrentManagedThreadId];
    public static readonly ConcurrentDictionary&lt;int, GameObjectPool&gt; ConfigDict = new();
}

// 此方法在线程池中运行。
private static void ValidationLoop()
{
    ... 线程初始化
    var gameObjectPool = new GameObjectPool();
    var xxx = new XXX();
    foreach (var item in BlockingCollection) // 当阻塞队列中没有抢到数据时，代码是阻塞的。
	{
	    int threadId = Environment.CurrentManagedThreadId;
        World.ConfigDict[threadId] = gameObjectPool;
        World.XXX[threadId] = xxx; //... 其他用到的资源
        var result = BattleRunHelper.Run(item.Request); // 验算战斗
        item.TaskCompletionSource.SetResult(result); // tcs设置验算结果，await后的代码继续执行。
        World.ConfigDict.Remove(threadId);
        ...
	}
}
</code></pre>
<p>战斗验算是CPU 密集型任务，验证过程中不会有线程切换，由此在代码改动极少的情况下实现了多线程战斗验算。当然，如果在战斗验算里加入写文件等异步操作，导致线程id改变，代码就不适用了。</p>
<p>... 未完待续</p>
]]></content:encoded>
  </item>
  <item>
    <title>ET框架相关</title>
    <link>https://www.this.red/20250303-ET/</link>
    <guid isPermaLink="true">https://www.this.red/20250303-ET/</guid>
    <pubDate>Mon, 03 Mar 2025 02:22:22 GMT</pubDate>
    <content:encoded><![CDATA[<p>最近想要用C#写一个自己的游戏服务器框架。由于工作中使用的是ET框架7.2，导致写的时候脑子都是ET的模样。</p>
<h2 id="section">多线程相关</h2>
<p>ET框架在8.1版本使用了多线程。作者抽象出了<code>Fiber</code>的概念。每个Fiber之间不互相操作内存，通过发消息通信。通过分析器限制全局静态变量写法，避免多线程操作内存的问题。</p>
<p>Fiber的重要组成部分：</p>
<ul>
<li>ThreadSynchronizationContext，用于同步上下文，执行异步方法的回调。</li>
<li>SceneRoot : 用来管理所有的Entity</li>
<li>EntitySystem : 用来管理所有的System</li>
<li>Mailboxes : 用来管理所有的Actor的Mailbox</li>
</ul>
<p>作者实现了3种调度器。</p>
<ul>
<li>MainThreadScheduler：主线程调度器，加入的Fiber会在主线程的Update中设置线程同步上下文后执行Update。</li>
<li>ThreadPoolScheduler：线程池调度器，加入的Fiber会在线程池中执行。</li>
<li>ThreadScheduler：线程调度器，加入的Fiber会在指定的线程中执行。</li>
</ul>
<h2 id="section-1">热更新相关</h2>
<h3 id="section-2">热更限制</h3>
<ul>
<li><p>热更代码
目前除Hotfix程序集以外的程序集均无法热更。不能新加协议（但是可以新加现有协议的Handler），不能新加组件。需要在热更后执行一些操作的组件需要实现LoadSystem，正常逻辑绝大多数不需要实现LoadSystem。</p>
</li>
<li><p>热更配置
目前暂无热更配置后的事件。需要注意的是有些数据已经存在了数据库里，并且此时玩家也不在线，热更配置影响不到。
如果热更活动配置，可加热更配置事件实现，将活动组件内的活动重新计时。</p>
</li>
</ul>
<h3 id="section-3">热更是怎么实现的</h3>
<p><code>CodeLoader</code>类里<code>AssemblyLoadContext</code>对象控制了Hotfix程序集的加载和卸载。热更时，在加载新的Hotfix程序集后，将App,Core,Model,Hotfix以及他们引用的程序集的所有Type都加载到EventSystem中，执行实现LoadSystem的类的Load方法。如<code>MessageDispatcherComponentLoadSystem.Load</code>，会将标记<code>MessageHandlerAttribute</code>的Handler类重新创建一份对象用于处理玩家请求。
服务器端的Hotfix只有逻辑，没有数据。数据被放在Model中，所以服务器端可以在运行时重新加载修复dll。需要注意旧的Hotfix程序集里的对象或方法被持有的情况。</p>
]]></content:encoded>
  </item>
  <item>
    <title>实现一个ECS+Actor游戏服务器框架</title>
    <link>https://www.this.red/AServer/</link>
    <guid isPermaLink="true">https://www.this.red/AServer/</guid>
    <pubDate>Sat, 01 Mar 2025 02:50:00 GMT</pubDate>
    <content:encoded><![CDATA[<h2 id="section">设计目标</h2>
<p>构建一个多进程多线程的分布式游戏服务器框架。</p>
<ul>
<li>使用Actor模型。</li>
<li>采用ECS模式。</li>
<li>支持热更新。</li>
<li>使用Mongodb作为数据库。</li>
<li>支持服务发现，可动态不停服平行扩展。</li>
</ul>
<p>为了快速搭建ECS框架，先不考虑底层内存优化。</p>
<hr>
<h2 id="actoridentityid">ActorId与EntityId</h2>
<p>Actor有个ActorId，ECS有个EntityId。ActorId是Actor的唯一标识，包含Actor的位置信息。</p>
<p>在写的时候发现Entity在各个框架都有不同的实现。Entity其中的Version是版本号，为了防止悬空引用，每次复用时会递增。</p>
<p>玩家还会有PlayerId，这里我的想法是用PlayerId进行客户端与服务器的交互，对应关系是PlayerId =&gt; ActorId =&gt; EntityId。</p>
<h2 id="ecsentitycomponentstructclass">ECS中Entity与Component用struct还是class？</h2>
<p>先说我的想法</p>
<p>Entity选用struct。
当组件是纯数据（无方法或简单方法）且需高性能时（如 Position, Velocity）用 struct。
当组件需复杂行为、继承或动态性时，并且需要存库时用 class（如 AIBehavior, Inventory）。</p>
<p>关于用struct还是class，各个框架有不同的实现</p>
<p><strong>Entity 的实现类型</strong></p>
<table>
<thead>
<tr>
<th>框架</th>
<th>Entity 类型</th>
<th>设计理由</th>
</tr>
</thead>
<tbody>
<tr>
<td>Entitas</td>
<td>class</td>
<td>简化 API 设计，支持面向对象模式（如事件回调），适合快速原型开发。</td>
</tr>
<tr>
<td>Arch</td>
<td>struct</td>
<td>数据导向设计，内存连续存储，减少缓存未命中，适合高性能场景。</td>
</tr>
</tbody>
</table>
<p><strong>Component 的实现类型</strong></p>
<table>
<thead>
<tr>
<th>框架</th>
<th>Component 类型</th>
<th>设计理由</th>
</tr>
</thead>
<tbody>
<tr>
<td>Entitas</td>
<td>class</td>
<td>面向对象特性，支持继承和事件回调。</td>
</tr>
<tr>
<td>Arch</td>
<td>struct</td>
<td>数据驱动设计，内存连续存储，减少缓存未命中，适合高性能场景。</td>
</tr>
</tbody>
</table>
<h2 id="section-1">关于多线程</h2>
<p>在写创建实体的代码时，突然想到一般的ECS框架是否会多线程调用？如果多线程调用，创建，销毁都要保证线程安全，这样太累了。</p>
<p>后来经过思考，创建与销毁不需要多线程调用，update可以多线程调用。在update时创建实体销毁实体等操作可存到无锁队列，在update结束后统一由主线程执行。Arch的CommandBuffer印证了我的想法。</p>
<p>帧同步游戏不能用多线程，由于线程间顺序不好控制，容易造成不同步。</p>
]]></content:encoded>
  </item>
  <item>
    <title>最近的一些服务器优化</title>
    <link>https://www.this.red/20241202-cache/</link>
    <guid isPermaLink="true">https://www.this.red/20241202-cache/</guid>
    <pubDate>Mon, 02 Dec 2024 01:02:22 GMT</pubDate>
    <content:encoded><![CDATA[<p>最近在配合压测游戏服务器，遇到一些问题，简单记录一下。</p>
<p>我们在16核32G的Ubuntu实例上部署的游戏服务器进程，用的数据库是4核8G的Mongodb。压测的结果是注册速度没有达到指标，我们设定的指标是100TPS。</p>
<p>在压测注册的时候，发现缓存服的进程CPU利用率异常高。在与同事沟通后，多开了几个缓存进程。再次测试，发现多开的缓存进程的CPU利用率也都变得很高。</p>
<p>C#版的Mongodb驱动默认连接池大小是100，我们游戏设置的并发数是31。我尝试把Mongodb的连接池调大，并发数调大。再次用机器人进行测试，情况不仅没有改善，并且还抛出了队列已满的异常。</p>
<p>查看服务器面板的监控的时候，发现Mongodb没有CPU这个监控，让运维添加上监控后再次测试。发现Mongodb的CPU在80人每秒注册的时候到了100%，说明瓶颈在与数据库的交互中。</p>
<p>Mongodb是NoSQL，谈到NoSQL总会想到Redis，我本以为Mongodb性能会很高和Redis一样，所以在平时代码编写上都把它当做内存数据库用的。</p>
<p>再次检查代码，发现了很多不必要的直接读写数据库的操作。并且玩家的账户信息没有对uid建立索引，在注册步骤的查询时会导致全表扫描。:(</p>
<p><strong>优化</strong></p>
<p>所有玩家的功能逻辑都不直接操作数据库，玩家的数据在整合后发送到缓存服，缓存服定时将玩家的脏数据同步到数据库，当然如果是充值这些特殊功能，可以视情况而定。Mongodb有批量操作的功能，各个玩家的同一个表可以进行批处理，减少对数据库的访问。</p>
<p>在启动服务器的时候将必须数据，比如账户信息等，从数据库中加载到内存中。活跃玩家的数据则可以直接加载到内存中。</p>
<p>在关服的时候，需要注意把缓存进程的数据同步到数据库。并且需要加入兜底机制。</p>
]]></content:encoded>
  </item>
  <item>
    <title>花束般的恋爱</title>
    <link>https://www.this.red/20241109-We-Made-a-Beautiful-Bouquet/</link>
    <guid isPermaLink="true">https://www.this.red/20241109-We-Made-a-Beautiful-Bouquet/</guid>
    <pubDate>Sat, 09 Nov 2024 00:28:22 GMT</pubDate>
    <content:encoded><![CDATA[<p>之前一直刷到《花束般的恋爱》的片段，我发现女主角很眼熟，后来查了一下，女主角是<a href="https://zh.wikipedia.org/wiki/%E6%9C%89%E6%9D%91%E6%9E%B6%E7%B4%94">有村架纯</a>，之前出演过浪客剑心里的雪代巴。雪代巴嘛，那看起来眼熟就不奇怪了。</p>
<p>一直想看这个电影但是由于一些原因一直搁置了。终于，晚上看完了这部电影。</p>
<p>从之前看过的片段里，完全感觉不到男女主有会分开的可能。</p>
<p>他们两个人真的很相似，有共同的喜好，有共同的观点。所以能走到一起也就不奇怪了。前面这些情节我是在边看边咿咿啊啊中度过。</p>
<p>在娟第一次决定找工作的时候，我不知为什么，有一种很强烈的异样的并且又熟悉的感觉，像是尘封已久的潘多拉魔盒又被打开。她为了面试，不熟练地从平底鞋换成高跟鞋，被迫变得成熟。我仿佛此刻就是男主的视角，看到她一个人去面试，为她担心。担心面试不通过被刁难，怕她被拒绝之后一个人情绪低落。</p>
<p><img src="https://cdn.this.red/img/20241109/home1.jpg" alt="" /></p>
<p><img src="https://cdn.this.red/img/20241109/home2.jpg" alt="" /></p>
<p>随着进入社会，麦为了给娟更好的生活，经常加班，陪娟的时间就越来越少了，曾经一起分享的音乐，电影等爱好，在忙碌的工作中仿佛变得不再重要。他们之间的共同话题开始慢慢变少。娟依然保持着对爱好的热情，想一直维持在当初恋爱时的状态，下班后一起看电影，打游戏。但是此时的麦无暇顾及娟，麦的变化让娟感觉失落和孤独。</p>
<p>麦努力工作是为了给娟更好的生活。他认为物质基础能稳固两人的关系，经济基础决定上层建筑。但是娟更看重情感上的陪伴，娟没有充分理解麦在工作中面临的压力，以及他为了两人的未来做出的努力。她觉得自己在这段关系中的情感需求得不到满足，两人之间的情感连接也因此变得薄弱。</p>
<p>对于电影，我个人认为有一处情节不太好。娟的老板邀请她去“吃拉面”，她没有拒绝。我觉得没必要在电影里表现这一情节，虽然很真实，但是这种真实的剧情把娟这个角色表现地太复杂了，这对麦是极度不公平的。这个情节仿佛在说，即使伴侣付出了很多，但是也抵不过偶然在旁边嘘寒问暖的人。这样的剧情安排似乎完全没有考虑观众的感受，它是在宣扬这种行为的合理性？还是在向我们灌输世界上本就没有忠贞爱情的观念？</p>
<p>现实往往比电影情节更复杂。以前总是听到“门当户对”这个词，不以为然，天真地以为相爱能抵万难。后来随着年龄的增长，慢慢对这些事改变了看法。现实就像顾里说的：“没有物质的爱情就是一盘散沙”。</p>
<p>所以说明娟的想法是错的，麦是对的？并不是，很多事情并没有对错之分。他们分开的原因更多的是没有在冲突发生后好好交流和换位思考，所以两个人之间的裂缝随着时间的推移越来越大。可能麦与娟当时恰好处在人生的不同阶段吧。</p>
<hr />
<p>电影看到一半突然想吃烤肠，上次用空气炸锅忘了刷锅，为了去油就只能先加热五分钟。锅加热后又想晾一会再刷。不知不觉电影结束了，随着电影的结束，突然发现想吃烤肠的心情也没了。</p>
]]></content:encoded>
  </item>
  <item>
    <title>网站快照</title>
    <link>https://www.this.red/20240814-website-snapshot/</link>
    <guid isPermaLink="true">https://www.this.red/20240814-website-snapshot/</guid>
    <pubDate>Wed, 14 Aug 2024 02:28:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>看到博客里的内容很少，我突然想起自己是从2019年开始写博客的。但当时因为某些原因，我删掉了所有写过的内容。后来由于经常重装系统，我习惯性地直接删除磁盘，所以那些数据都找不回来了。</p>
<p>平时在百度或谷歌搜索时，如果遇到网页打不开、访问慢、404或其他错误码的情况，为了快速找到需要的信息，我会直接查看该网站的快照，看看里面到底有什么内容。但最近一两年，我发现一些主流搜索引擎删除了快照功能，导致我的搜索体验不如从前。</p>
<p>后来因为一些原因发现了竟然还有专门存储快照的网站</p>
<ul>
<li><a href="http://web.archive.org/">web.archive.org</a></li>
</ul>
<blockquote>
<p><a href="http://web.archive.org/">web.archive.org</a>，通常被称为**“Wayback Machine”（时光机）<strong>，是一个互联网数字档案库。它由位于旧金山的非营利组织</strong>互联网档案馆（Internet Archive）**在2001年推出，旨在保存和存档全球网站的历史版本。通过这个网站，用户可以查看过去不同时间点的网站快照，了解网站在历史上的变化情况。</p>
</blockquote>
<p>我试了试，能搜到我之前的网站19年10月的快照。（后来发现，这个域名被别人拿来做菠菜了）</p>
<p><img src="https://cdn.this.red/img/20240814021201.png" alt="" /></p>
<p>如图所示，样式乱了，有很多图和样式都没有加载出来，也就是没有收录部分css和图片。</p>
<br>
<p>翻来翻去，翻到了我之前做的路由交换协议作业。当时我会把写完的作业放到博客上然后分享给别人，还在各个搜索引擎里搜关键词，看网站的搜索排名。</p>
<p><img src="https://cdn.this.red/img/20240814020854.png" alt="" /></p>
<p>现在一看这些，一丁点印象都没了。</p>
<p>可惜了我最初的中文域名网站没有收录进去，本来想看看当时写的东西copy过来的。</p>
]]></content:encoded>
  </item>
  <item>
    <title>ET框架包体结构</title>
    <link>https://www.this.red/20240710-et-net/</link>
    <guid isPermaLink="true">https://www.this.red/20240710-et-net/</guid>
    <pubDate>Wed, 10 Jul 2024 01:46:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>之前都是潦草地过ET框架代码，因为最近需要与压测平台对接，对方平台为golang语言，需要了解包体结构，才能连接服务器，所以不得不将网络收发整理一遍。特此记录一下。</p>
<p>服务器框架：<a href="https://github.com/egametang/ET/tree/release7.2">GitHub - egametang/ET at release7.2</a></p>
<p>游戏前后端使用KCP协议进行通信。前后端使用protobuf进行消息序列化。不包含软路由收发，由压测机进程直连网关。</p>
<h2 id="section">建立连接</h2>
<p>KCP没有实现类似TCP的三次握手机制，需要自行KCP之上实现自己的握手协议。ET框架中其中部分协议类型包体如下：</p>
<p><img src="https://cdn.this.red/img/20240710-et-proto.png" alt="常用包体类型" /></p>
<center>常用包体类型</center>
<p><img src="https://cdn.this.red/img/20240710-et-msg.png" alt="MSG包体结构" /></p>
<center>MSG包体结构</center>
<pre><code class="language-c#">// 其中协议头的byte值
SYN=1
ACK=2
FIN=3
MSG=4
</code></pre>
<blockquote>
<p>注：其中包体的LocalConn与RemoteConn都是发送方对接收方而言。例如客户端给服务器发送MSG包，LocalConn为客户端本地id，相对于服务器而言，客户端的LocalConn就是RemoteConn。</p>
</blockquote>
<ol>
<li><p>首先客户端向服务器发送SYN包，SYN=1，LocalConn是个随机uint值，此时未建立连接RemoteConn值为 0</p>
</li>
<li><p>服务器收到客户端发来的SYN包，返回ACK包。包体内容为，ACK=2，RemoteConnId为服务器创建的随机uint值，LocalConn为客户端的LocalConn。</p>
</li>
<li><p>客户端收到服务器的ACK包，发送MSG消息，服务器收到MSG消息，说明连接完成。MSG消息的包体内容为，MSG=4，LocalConn为客户端LocalConn，bytes部分为KCP包体。
当建立连接后，客户端与游戏服务端发游戏协议的包体类型为MSG类型。</p>
</li>
</ol>
<h2 id="kcp">KCP包结构</h2>
<p>KCP包结构如下：</p>
<pre><code class="language-C#">0               4   5   6       8 (BYTE)
+---------------+---+---+-------+
|     conv      |cmd|frg|  wnd  |
+---------------+---+---+-------+   8
|     ts        |     sn        |
+---------------+---------------+  16
|     una       |     len       |
+---------------+---------------+  24
|                               |
|        DATA (optional)        |
|                               |
+-------------------------------+
</code></pre>
<p><strong>其中conv为对方的RemoteConn</strong>，DATA为游戏包体内容。ET框架的KCP经过了魔改，conv不一定要一样。</p>
<p><img src="https://cdn.this.red/img/20240710-et-kcp.png" alt="" /></p>
<h2 id="section-1">游戏包结构</h2>
<p>此框架KCP使用消息模式，每个KCP包里DATA部分只有一个数据包，结构如下：</p>
<p><img src="https://cdn.this.red/img/20240710-et-proto-message.png" alt="" /></p>
<center>普通包体</center>
<p>OpCode为协议编号，Message为Protobuf协议序列化后的字节数组。
需要注意的是，当消息大小超过最大包大小10000时，消息将会分片发送。步骤如下：</p>
<ol>
<li>首先发送分片包，包头的四个字节值为0，PackageSize=包体总内容大小</li>
</ol>
<p><img src="https://cdn.this.red/img/20240710-et-proto-message-seg.png" alt="" /></p>
<center>分片发送的第一个包</center>
<ol start="2">
<li>之后发送分片消息，由对方进行组装，直到组装完成后再解析</li>
</ol>
<h3 id="section-2">如何识别是分片包</h3>
<p>​     当包大小等于8，并且包头四个字节转换为int的值为0时，说明消息分片。
​     PacketSize为这个分片包的总内容大小。</p>
<h2 id="kcp-1">KCP参数</h2>
<p>客户端连接游戏服务器的KCP参数如下</p>
<pre><code class="language-c#">nodelay=1
interval=10
resend=2
nc=1
sndwnd=256
rcvwnd=256
mtu=470
minrto=30 
</code></pre>
<h2 id="section-3">其他</h2>
<p>客户端与服务器通信使用小端序</p>
]]></content:encoded>
  </item>
  <item>
    <title>游戏中的聊天</title>
    <link>https://www.this.red/20240430-im/</link>
    <guid isPermaLink="true">https://www.this.red/20240430-im/</guid>
    <pubDate>Tue, 30 Apr 2024 02:53:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>一般游戏内一般能与好友聊天，世界聊天。但是如果加上陌生人聊天呢？陌生人的头像和昵称如何显示？何时更新？</p>
<h2 id="section">聊天会话</h2>
<p>聊天会话就像是给好友，陌生人，频道聊天包了一层。游戏会话作用有记录群名与群id或对方名字与id，最后聊天时间，是否有消息未读等信息，以便于界面显示与红点逻辑。</p>
<p>游戏会话可分为单聊会话与频道会话。</p>
<ul>
<li><p>单聊会话
顾名思义，玩家与好友或者陌生人一对一聊天。</p>
</li>
<li><p>频道会话</p>
<ul>
<li>系统频道
推送系统消息，玩家不可在其中发消息</li>
<li>群组频道
给在群组中的人推送消息</li>
<li>全服频道
全服广播消息</li>
</ul>
</li>
</ul>
<h2 id="section-1">游戏中的消息类型</h2>
<p>一般游戏中的聊天，玩家能发送的消息有以下几种</p>
<ul>
<li><p>文字消息与表情消息
文字消息与表情消息在unity中可以用TMP的图文混排实现</p>
</li>
<li><p>图片表情消息
图片表情消息记录一下图片的Id即可实现，不能让用户自定义上传图片，还需要监管。</p>
</li>
<li><p>语音消息
语音消息需要另接入SDK实现。消息里包含一条语音消息的url，其他玩家点击消息则从SDK服务器下载到本地播放。</p>
</li>
<li><p>自定义消息
自定义消息一般是游戏内功能。比如分享游戏英雄或者装备。</p>
</li>
</ul>
<h2 id="section-2">聊天中的数据包</h2>
<p>a给b发送一条私聊消息，那么这条消息要包含</p>
<ul>
<li><p>消息Id：用于保证消息唯一</p>
</li>
<li><p>FromId：发送方Id</p>
</li>
<li><p>ToId：接收方Id</p>
</li>
<li><p>消息类型：确认是属于文字消息，语音消息，自定义消息等类型</p>
</li>
<li><p>消息内容：消息的内容</p>
</li>
<li><p>发送时间：时间戳，用于排序</p>
</li>
<li><p>发送方其他信息，包括名字，头像等，用于更新本地信息。</p>
</li>
</ul>
<p>频道消息则加上了频道id</p>
<h2 id="section-3">昵称和头像更新问题</h2>
<p>假设a与b是好友，a更新头像与昵称，b可以立即知道。因为a与b互相为好友关系，遍历一下通知很方便。</p>
<p>如果a与b不是好友，a只是与b聊天一次的陌生人，那么a更新头像与昵称，b没必要通知到。</p>
<h3 id="section-4">何时更新陌生人昵称和头像呢</h3>
<p>当a又与b聊天时，可从聊天的消息中获取a最新的名字和头像，更新本地信息。</p>
<p>当b查看a的资料详情时，也要更新对方的资料。</p>
<h3 id="section-5">历史消息中的昵称头像如何更新</h3>
<p>在我处理的时候，本地的历史消息中不存放对方昵称与头像，对方的个人信息与历史消息分开存储，显示的时候直接取本地最新信息。</p>
<h2 id="section-6">参考</h2>
<p><a href="https://cloud.tencent.com/document/product/269/54139">即时通信 IM 昵称头像相关问题-常见问题-文档中心-腾讯云 (tencent.com)</a></p>
]]></content:encoded>
  </item>
  <item>
    <title>记录一次回合制游戏简单框架-GameJam</title>
    <link>https://www.this.red/20240428-GameJam/</link>
    <guid isPermaLink="true">https://www.this.red/20240428-GameJam/</guid>
    <pubDate>Sun, 28 Apr 2024 00:30:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>公司有个GameJam活动，要做个demo。这个GameJam最开始只是凭借想法和演示视频就能评比。后面根据评比结果来做demo。因为球体荒野乱斗的难度比较大，选了做回合制的游戏。</p>
<p>GameJam不适合做联机游戏。当时非要装大尾巴狼，做联网的demo。最后demo功能包括匹配，进房间，各个回合对战，结算，还算比较完善。</p>
<p>这里记录一下回合制游戏的框架是什么样的。</p>
<h2 id="section">回合制游戏的流程</h2>
<p>如果是个PVP回合制游戏，一局游戏的过程基本上是这样的。</p>
<ul>
<li><p>点击匹配。服务器维护一个list等其他玩家也点击匹配。</p>
</li>
<li><p>匹配成功后服务器开房间。向房间内的玩家广播匹配成功以及房间内的成员信息。</p>
</li>
<li><p>客户端切换到战斗场景。当切换完毕，通知服务器可以开始游戏了。</p>
</li>
<li><p>服务器向房间内玩家广播双方的站位，英雄以及装扮等信息。</p>
</li>
<li><p>服务器接收到客户端可以开始游戏的请求后，启动回合的状态机，之后会由状态机来驱动游戏运行。</p>
</li>
<li><p>当回合内符合结算条件后，比如将对方都击败，或是达到最大回合数，开始结算发奖励。之后将房间关闭。</p>
</li>
</ul>
<h2 id="section-1">回合是如何驱动的</h2>
<p>回合制游戏的回合在服务器上是个状态机。每个状态实现了进入，执行，退出，更新四个方法。</p>
<pre><code class="language-c#">public interface ITBSState
{
    void Enter(TBSRoom room);
    void Execute(TBSRoom room);
    void Exit(TBSRoom room);
    void Update(TBSRoom room, float deltaTime);
}
</code></pre>
<p>这里我把一局游戏分成了四种状态</p>
<ul>
<li><p>回合开始</p>
<p>给所有客户端推送回合开始，包括回合数，回合等待时间。</p>
<p>判断是否超过最大回合数，如果超过最大回合数，广播战斗结束并且关闭战斗房间。</p>
</li>
<li><p>回合准备</p>
<p>在update中检查是否接收到每个玩家的操作，如果没有接收全部玩家操作就等待，如果等待超过规定时间后，给玩家默认操作，并进入战斗计算状态。</p>
</li>
<li><p>回合战斗计算</p>
<p>计算战斗结果，把结果发送到每个客户端，进入回合结束状态</p>
</li>
<li><p>回合结束</p>
<p>等待每个客户端播放动画完成，当每个客户端都告诉服务器播放完动画，则进入回合开始状态。</p>
</li>
</ul>
<p>目前游戏中就是这四个状态之间顺序切换，各个状态之间的切换都加了最大等待时间，避免某个状态抛异常导致状态切换停转。</p>
<h2 id="section-2">战斗计算大概是什么样</h2>
<p>回合制的战斗计算中很重要的部分就是战斗技能。buff也可以看做持续n个回合的技能。</p>
<p>比如一个技能是战斗回合前给自己加血，等自己行动时给最低血量友方加血，自身回合结束后加血，被打时候加血，打别人时候加血。。。</p>
<p>这其中就包含</p>
<ul>
<li>回合开始前</li>
<li>自身回合开始</li>
<li>造成伤害前</li>
<li>。。。</li>
</ul>
<p>计算伤害的时候按顺序把这些阶段都算一遍就OK了。</p>
<p>由此可见每个技能在计算伤害的时候都可以抽象出一个IAbility接口。</p>
<pre><code class="language-c#">public interface IAbility
{
    void OnTurnBefore(){} // 战斗回合开始前调用的方法，用于执行回合开始前的准备工作。
    void OnTurnStart(){} // 战斗回合开始时调用的方法，用于执行回合开始时的逻辑。
    void OnTurnEnd(){} // 战斗回合结束时调用的方法，用于执行回合结束时的逻辑。
    void OnBeforeDealDamage(){} // 造成伤害前调用的方法，用于执行造成伤害前的准备工作。
    void DealDamage(){} // 造成伤害时调用的方法，具体实现应包含伤害计算和应用逻辑。
    void OnAfterDealDamage(){} // 造成伤害后调用的方法，用于执行造成伤害后的逻辑，如治疗、状态应用等。
    void OnBeforeTakeDamage(){} // 承受伤害前调用的方法，用于执行承受伤害前的准备工作。
    void TakeDamage(){} // 承受伤害时调用的方法，具体实现应包含伤害计算和应用逻辑。
    void OnAfterTakeDamage(){} // 承受伤害后调用的方法，用于执行承受伤害后的逻辑，如治疗、状态应用等。

    ...

}
</code></pre>
<p>每个战斗单位都可以携带多个技能，在计算前先把各个单位按速度排序，每个战斗单位必须有个速度的属性，才能合理确定先执行谁的技能。</p>
<p>其中每次执行技能都会产生一个伤害数据，包括a打b扣了c血量加了buff等信息。当产生数据后，需要缓存起来广播到各个客户端。</p>
]]></content:encoded>
  </item>
  <item>
    <title>TMP实现超链接</title>
    <link>https://www.this.red/TMPHyperlink/</link>
    <guid isPermaLink="true">https://www.this.red/TMPHyperlink/</guid>
    <pubDate>Tue, 05 Dec 2023 00:00:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>最近有个需求是在UI上展示一个超链接，玩家点击能跳界面。之前也玩过一些游戏，点击玩家的物品的文字链接，能查看对应玩家的物品。在搜索了一番后得出以下答案。</p>
<p>使用<a href="https://docs.unity3d.com/Packages/com.unity.textmeshpro@3.0/api/TMPro.TMP_TextUtilities.html#TMPro_TMP_TextUtilities_FindIntersectingLink_TMPro_TMP_Text_UnityEngine_Vector3_UnityEngine_Camera_">TMP_TextUtilities.FindIntersectingLink</a>可获取给定位置链接的索引。</p>
<pre><code class="language-c#">[RequireComponent(typeof(TMP_Text))]
public class TMP_HyperLink : MonoBehaviour, IPointerClickHandler
{
    public void OnPointerClick(PointerEventData eventData)
    {
        var textMeshPro = GetComponent&lt;TMP_Text&gt;();
        int linkIndex =
            TMP_TextUtilities.FindIntersectingLink(textMeshPro, eventData.position, eventData.pressEventCamera);
        if (linkIndex != -1)
        {
            var linkInfo = textMeshPro.textInfo.linkInfo[linkIndex];
            OpenLink(linkInfo.GetLinkID());
        }
    }


    private static void OpenLink(string linkStr)
    {
        // todo 解析linkStr为想要的参数信息
    }
}
</code></pre>
<p>可解析<code>linkStr</code>为想要的参数，比如解析成链接类型与数值，判断链接类型为打开玩家界面，数值就是玩家的id。本想在这里用个Json字符串保存信息，之后发现这个方法并不支持解析多个 <code>&quot;</code>，即使加了转义字符也不行。</p>
]]></content:encoded>
  </item>
  <item>
    <title>todo</title>
    <link>https://www.this.red/todo/</link>
    <guid isPermaLink="true">https://www.this.red/todo/</guid>
    <pubDate>Tue, 03 Oct 2023 23:30:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>我有一个很早之前就有的想法，在玩饥荒的时候发现地图不能一直走然后走到原点，然后才发现饥荒是个平面的地图。如果饥荒是个球体地图多好。后来思考，感觉球体越大，给人的感觉越像平面，所以目前的游戏大多是在平面上。后来戴森球计划出来之后才发现，球体地图确实可行，观感不错。后来沉迷了一段时间荒野乱斗，于是有个想法，将荒野乱斗的地图如果改成球体，会不会火？</p>
<p>使用服务器物理引擎的多人对战游戏。暂时没想好玩法，不过地图是球体的，有挺大难度（该补补数学了🤣）。</p>
<p>最开始想帧同步，但是发现不可能是帧同步，由于球体地图的重力是时刻垂直于球体，然后球体并不是规则的，重力还是一直指向球体中心的，我感觉使用float不可避免，所以感觉同步很困难。感觉一定得是服务器端物理。这样既可以做开房间游戏，也能做成mmo。</p>
<p>突然发现这个C#物理库完全满足了我的要求，并且性能不错。
<a href="https://github.com/bepu/bepuphysics2">bepu/bepuphysics2: Pure C# 3D real time physics simulation library, now with a higher version number. (github.com)</a></p>
<p>淦！</p>
]]></content:encoded>
  </item>
  <item>
    <title>游戏排行榜</title>
    <link>https://www.this.red/rank/</link>
    <guid isPermaLink="true">https://www.this.red/rank/</guid>
    <pubDate>Mon, 11 Sep 2023 03:50:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>最近在做游戏排行榜服务器，记录一下想法。</p>
<p>定时排名：</p>
<ol>
<li>只要前100，未进前100不记录排名。经典的TopK算法。
关于TopK，看知乎有个O(n)的方法：先遍历找到第n大的元素，之后根据这个元素划分。这样只需遍历两次，所以时间复杂度是O(n)。</li>
<li>全服都要知道自己排名。也就是在排名时用一次语言内置的Sort方法，并且记录一下自己的排名。经测试在普通配置电脑上使用C#中给一百万名玩家排名一次仅需400ms左右，内置的Sort排序性能真的不错。</li>
</ol>
<p>实时排名：</p>
<p>假如有一百万名玩家，给全服所有玩家排名一次要400ms，100次就是40s，所以肯定不能每次玩家战力刷新时，排行榜执行一次List.Sort了。</p>
<ol>
<li>新建一个RankList，包括一个双向链表和一个字典。双向链表的节点里包含前后节点，当前排名，当前的值。字典的key是玩家id，字典的value是对应节点。
初始化时调用一次List.Sort使List有序，之后倒序遍历，构建双链表。
每次插入会遍历到对应的节点，插入之后遍历刷新后面节点的排名。
找玩家名次直接用字典去找对应节点即可找到名次。</li>
<li>在1里提的RankList在每次玩家排名改变时都会让其后面节点排名加一，效率很低。可以把所有玩家分成区间，区间包括区间人数，最大值与最小值。当新加入一个玩家在区间里，区间人数就加一。区间内玩家排名改变也只是改变了区间内玩家的排名。排行榜一般都是两头人少，中间人多，这个方法的难点在于区间的划分，需要合理划分每段区间内的人数。</li>
</ol>
<p>实际上游戏并不需要实时排名，直接Sort才是最优解。</p>
]]></content:encoded>
  </item>
  <item>
    <title>ET框架的软路由</title>
    <link>https://www.this.red/ETStudy1/</link>
    <guid isPermaLink="true">https://www.this.red/ETStudy1/</guid>
    <pubDate>Sat, 19 Aug 2023 22:43:00 GMT</pubDate>
    <content:encoded><![CDATA[<p>分布式拒绝服务攻击（DDoS）是一种恶意网络攻击，旨在通过同时向目标服务器发送大量的流量或请求，超出其处理能力，从而导致服务不可用或降级。为了防止DDoS，ET框架提供了软路由。</p>
<p><strong>ET框架里的软路由就是个UDP请求转发服务器</strong>，就是将能直接访问的服务器与客户端之间又加了一层。普通的游戏，网关服务器绑定的地址是公网地址可以直接访问的，这样可能会被DDos打死，加了软路由后，暴露在外的只是一堆软路由。网关和其他服务器都是内网地址，外面不能直接访问。当客户端发一个到网关的请求时，先发到这个路由，再由路由向内网转发。</p>
<h3 id="section">软路由是怎样工作的？</h3>
<ol>
<li>软路由一定是准备多个的，需要多个IP与机器。当同时有很多个软路由时，攻击的成本就很高了。当一个软路由被流量打死了，还能换另一个软路由去连接。并且实际上软路由还能随时添加与关闭！在获取软路由的地址时，客户端会访问CDN获取软路由的地址。所以在修改软路由配置时需要更新一下CDN。</li>
<li>客户端在访问CDN得到软路由地址与realm内网地址和其他服务器内网地址后，选择一个软路由发送请求，请求里包括客户端想要访问的内网地址，软路由收到请求后将udp包转发给想要访问的内网地址。</li>
<li>软路由是UDP服务器，客户端与服务器之间使用kcp进行通信，处理“连接”的逻辑在应用层，能自行控制消息的重发。在切换软路由时也能保证消息不丢失。</li>
<li>客户端连接会每隔2秒ping一次，在ping消息10秒没有收到回复时，重新请求获取一个新的软路由去连接。</li>
</ol>
<h3 id="section-1">加了软路由是否就安全了？</h3>
<p>加了软路由后是否就高枕无忧了呢？并不是，还是要有其他安全措施的。</p>
<p>软路由确实能保证被攻击后，gate，realm不会被流量打死，但是软路由未被打死之前的流量确实都到了内网了。</p>
<p>所以理论上来说会存在这么一种攻击方案：</p>
<p>首先解包游戏客户端。每个软路由都给一定的流量去发伪造的登录sdk的验证请求，发给登录验证服，登录验证服需要通过http请求访问登录sdk服务器去验证。当有大量流量都去访问sdk服务器，也是会对游戏服务器产生压力。</p>
]]></content:encoded>
  </item>
  <item>
    <title>如果我做一个博客系统</title>
    <link>https://www.this.red/BlogSystem/</link>
    <guid isPermaLink="true">https://www.this.red/BlogSystem/</guid>
    <pubDate>Sat, 03 Jun 2023 13:52:22 GMT</pubDate>
    <content:encoded><![CDATA[<h2 id="section">前言</h2>
<p>自从写博客以来，一直都想做一个自己的博客系统。之前尝试过解析md文件，并使用C#的blazor框架制作博客系统，但是blazor首次加载网页很慢，完全比不上静态网页，加载个网页竟然需要先下载那么多库。制作了个demo后就搁置了。</p>
<h2 id="section-1">一些博客系统</h2>
<p>之前用过WordPress，Hexo等，他们的模板有很多，很容易就能做出一个漂亮的网站。</p>
<h3 id="wordpress">WordPress</h3>
<p>最流行的博客系统，使用PHP编写。</p>
<p>优点：最流行的博客系统，具有非常多的主题和插件。</p>
<p>痛点：</p>
<ul>
<li>PHP这语言，写WordPress模板的时候恶心死我了。</li>
</ul>
<h3 id="hexo">Hexo</h3>
<p>是一种静态网页生成器，将md文件解析，和模板结合，每个md都生成一个网页。它使用EJS（Embedded JavaScript）模板引擎来实现模板功能。EJS是一种简单的JavaScript模板语言，允许你在HTML中嵌入JavaScript代码，文件后缀名是ejs。</p>
<p>优点：不需维护数据库或服务器</p>
<p>痛点：静态网页生成器很难实现动态网页的一些功能，比如检测在线人数，评论等，都要引入第三方js。</p>
<h3 id="hugo">Hugo</h3>
<p>也是静态网页生成器，用Go语言编写，感觉比Hexo好一点，在使用Go的开发者里应该很流行。</p>
<p>优点：不需维护数据库或服务器</p>
<p>痛点：和Hexo一样。</p>
<h3 id="xlog">xLog</h3>
<p>将博客数据存储在区块链上，以 NFT 的形式存在，只能由掌管私钥的你自己控制。需要安装一个以太坊钱包才能进行操作。</p>
<p>优点：不需维护数据库或服务器，足够现代化，是一个非常优秀的博客系统。</p>
<p>痛点：</p>
<ul>
<li>链上操作都是透明和不可撤销的，这导致用户无法真正删除一篇文章，通过区块链历史可以看到文章的修改记录。</li>
<li>国内访问不快。</li>
<li>作者因为在国内租房时被自如恶心了，之后去英国留学了。感觉作者对国内的态度值得怀疑🤣。</li>
</ul>
<h2 id="section-2">静态博客系统是怎么做的呢</h2>
<p>静态博客系统就是个静态网页生成器。以Hexo为例，它会把所需目录下的文件都记录到db.json中，看文件内容：</p>
<ol>
<li>记录了所需的文件，计算了各个文件的hash值，可能用于检测文件是否被改变。</li>
<li>md文件的标题，时间等，内容已经被转换成了html</li>
</ol>
<p>其余所做的工作就是把md文件的内容填到Html模板中。</p>
<p>想要的功能：</p>
<p>我需要个C#的博客生成器，方便我修改并且添加功能。并且在制作博客模板时候能给出智能提示。</p>
<h2 id="section-3">命令行</h2>
<ul>
<li>生成网页</li>
<li>http服务器</li>
</ul>
<h2 id="section-4">实时预览</h2>
<p>我需要所见即所得。需要实时监测到文件变化，在文件改变时重新生成网页。并且如果正在显示该网页，需要通知浏览器刷新网页。</p>
<p>监测的文件有：主题模板，js，css，主题配置，md文件</p>
<p>在主题模板改变时，实时编译，并且刷新</p>
<h2 id="section-5">普通功能</h2>
<ul>
<li><p>md转HTML</p>
</li>
<li><p>主页，文章页，归档页，标签页，单独页</p>
</li>
<li><p>目录</p>
</li>
</ul>
<h2 id="markdown">测试markdown</h2>
<p>待我完成后进行<a href="/test">Markdown测试</a></p>
<p>在探索的时候发现了这个库<a href="https://github.com/adoconnection/RazorEngineCore">RazorEngineCore</a></p>
<p>这篇文章对我很有帮助</p>
<p><a href="https://www.codeproject.com/Articles/5260233/Building-String-Razor-Template-Engine-with-Bare-Ha">Building String Razor Template Engine with Bare Hands (.NET Core) - CodeProject</a></p>
]]></content:encoded>
  </item>
</channel>
</rss>
