LOADING

Ascending...

UAC电子化日志:后端重构与变更

2026/9/8 coding

原有架构的问题

原版架构(UacCore v2)存在诸多问题。虽然已经实现了基本的功能,且UAC的高度动态化和运行时加载架构也有了个雏形,但是还是存在诸多问题,包括但不限于:

  • 功能方面:
    • 完全不能实际使用
    • 实际的结算逻辑问题很多
    • 完全没有实现一个服务端该有的功能,连鉴权都没做
  • 代码方面:
    • 难以置信的石山
    • 各种各样不规范的问题
    • 各种巨型类和巨型方法
    • 因为边想边写带来的大量架构问题
    • 甚至混着同时用两个功能一样的json库(Text.Json和Newtonsoft.Json)

因此一个月前我开始着手开发新版服务端,基本架构还是与原版类似,采用事件驱动架构、运行时编译的动态加载,同时大幅更改了原先的一些架构决策。现在已经阶段性完成,因此写一篇日志简单记录一下。

新版架构的变更

再见了CellEdge

CellEdge这个类相当之神奇。按照直觉来看,一个格子地图是由Wall(墙)和Cell(地块)组成,角色没走一步就需要跨越一次Wall,但是我在这里非常别扭的引入了CellEdge来指代墙。这是因为在UAC中:

  • 角色的移动(Step)是使用Start格的坐标+End格的坐标表示的,其中Start和End相邻
  • 角色的移动需要与Wall检测,以避免不合法的移动,例如穿墙
    在这样的情况下,我当时脑子一抽:既然需要比较角色Step和Wall,那我干脆把他们用一个类表示就好了!
    于是就有了CellEdge这一坨。
    这个想法本身倒没有什么问题,代码当然可以正常运行。问题是,这似乎让代码变得令人困惑。一个纯概念的CellEdge居然同时指代Wall和Step,这会让任何不了解前提的人困惑,也让一些方法的命名变得极其别扭,例如AllCellEdgeOnLine之类。
    新版第一时间就将它拆成了两个类:Step和Wall,两者具备完全相同的数据结构,但前者还包含了方向,后者不包含。这顺带还引入了实现基于移动方向的机制的可能性。

自动化Handler注册与SkillDefination

在原架构中,角色文件需要在Initialize()这个函数中手动将自己的所有监听器注册进Bus里面,且多个技能的Handler全部塞在角色类的主文件里面,无法方便的查看这个Handler来自哪个技能、与其他Handler是什么关系。对于被动技能,更是别扭的将是否触发的逻辑与实际执行的逻辑塞在一个函数内。
为此,我引入了EventHandlerAttribute。Attribute是C#提供的编译期反射工具,允许为C#对象添加运行期可读的特性数据。
现在,在初始化一场游戏前,程序会扫描所有(不单是角色主体,还包括地形规则)带有EventHandlerAttribute标记的函数,并依据特性内标记的Event类型自动注册到EventBus上。
另外,对于Handler的触发逻辑与实际执行逻辑混在一块的问题,现在要求角色文件的主体不包含细分逻辑,只持有Context的引用而不允许改变。触发器需要去调用位于SkillDefinition内的实际技能逻辑。

Server与Core分离

原架构中main函数即一局游戏的主要逻辑。现在,我将整个项目分成两部分:Server&Core。

  • Server:ASP.Net Core/Console项目,负责鉴权、负载平衡以及房间分配和启用,未来也许还会包含BP阶段
  • Core:纯C#类库(lib),包含游戏的主体逻辑,且提供多个接口供Server使用
    每当启动一局游戏,Server就会依据预设和全局设定新建一个Core实例,并向其注入需要的地图和角色实例。反射加载角色也是在Server端进行。

…