当前位置:首页 > 综合 > 正文

符斗祭的背后(二):游戏逻辑的整体架构

0. 扯淡

Meh. 上次开篇就说“开了个新坑,不知道能不能坚持下去”,结果发现已经有半年了…… 坑还是要填的,嗯。

最近实在是状态不佳,看着一堆需求一点想要搞的欲望都没有。于是就来填坑了。

不知道东方符斗祭(THB)的,可以看以下系列文章上一篇的开头介绍,这里不重复了。

上一篇:符斗祭的背后(一):自动更新机制的演进 - 的碎碎念 - 知乎专栏

1. 这游戏怎么立项的?

大学的时候参加了一个“工作室”,说是工作室其实就是每天瞎折腾,顺便帮学校打打工,赚个学校给分配的办公室(有据点的感觉不错呢)。

跟工作室的小伙伴磨合的时候,被带进了 DotA 坑。然而玩的时候不仅手残,也没有大菊观,每次都是“卧槽你的大呢?”或者“卧槽你的大怎么这么早就交了?”……啊,还有“每回你说你要死了,都特别准”…… 经历了诸如“第六人”等等让人挠头的评价之后,我决定不按他们的游戏规则玩了!我去试着对 DotA 地图做2次开发,添加自己想要的英雄。到现在还记得当时拆的地图的版本(6.67c)。

中间的过程不表,还算是挺有意思的,跟近10万行的混淆过的 Jass 代码作斗争。这个开发过程中,最刷新我世界观的,是对 War3 的逻辑层的结算的认识:所有的机器都运行相同的逻辑,而且所有的游戏对象、随机数都要同步,真正需要通信的只有用户的输入!我不知道其他人怎么样,但是当时的我对下面这种代码都会很不舒服:

var_a = var_b = 0
while True:
    var_a += 123
    var_b += 123
    ...

我一定会写成这样:

var_a = var_b = 0
while True:
    var_a += 123
    var_b = var_a
    ...

尽管代码没意义但是应该能理解我的意思,我总会觉得第一种跑着跑着就会跑偏(尽管对整数来说并不会)。

直到符斗祭上线好久之后,我才直到这个逆天的全节点同步的结构有个高大上的名字叫 (戳这里:魔兽争霸3技术分析资源汇总)。

世界观刷新以后,就特别想找个机会跟之前的做法做个决裂。

然后呢,我又通过室友的介绍入了英雄杀的坑(对……腾讯的狗熊杀……当然跟同学面杀的时候用的三国杀)。这里倒是没什么神奇的经历。

玩的久了,自然会有一些自己的关于人物设计的想法,作为东方狗自然就开始往东方人物上面套了。

某天,我的 Dell D630 突然罢工了(还记得显卡门么……?)。报修。3天的时间无所事事,于是开始践行之前的脑洞,做了些设计,之后便一发不可收拾了。

晚上睡觉看着天花板,思考怎么对三国杀的游戏规则建模,也要满足能联网玩这个要求,于是自然想到了 War3 的结构,发现真的可以直接套上去。正好对于编程方面,当时正好处在不知道干啥的阶段。在学 但是非常担心坚持不下去,所以在积极的找可以做的项目(CRUD就免了……),于是当即立 flag,我TM要把这个写出来!毕竟我这么牛X,怎么能没有个能拿出手的项目呢(逃

PS: 学一门新语言能找到合适的项目真的非常重要。 之前学过 ,感觉非常好…… 但是野鸡大学自己玩的大学生哪有什么分布式的项目让你折腾啊,于是就弃坑了。现在还想学 Rust 和 Lua,然而找不到什么可以搞的东西(或者说,感兴趣的)。

1. 总体的架构

THB 的结算的核心很简单,就是两个概念: 和 (大概有的人看完了这两个名字就知道我想说啥了)。

总之, 就是游戏中的某个动作。结算的时候 会形成一个栈。比如,出牌阶段,一名角色使用弹幕(杀)对另一名角色造成了伤害时,在这个链条上,整个的结算栈是这样:

5 Damage(a, b, 1)               # a 对 b 造成了一点伤害
4 Attack(a, b)                  # a 对 b 弹幕效果
3 LaunchCard(a, b, AttackCard)  # a 对 b 使用一张弹幕(杀)
2 ActionStage(a)                # a 的出牌阶段
1 PlayerTurn(a)                 # a 的行动回合
0 Game                          # 游戏逻辑开始(整个游戏是一个巨大的 Action)

用来截获并处理事件。

一个典型的 长这样(天狗盾的,跟三国杀中仁王盾等价):

class MomijiShieldHandler(EventHandler):
    # EventHandler 的优先级,这个需要在蓬莱玉枝的前面执行
    # 因为蓬莱玉枝会改变弹幕的性质,会导致拦截失效
    execute_before = ('HouraiJewelHandler', )
    def handle(self, evt_type, act):
        # 如果是弹幕效果的“事件发生前”时点
        if evt_type == 'action_before' and isinstance(act, Attack):
            tgt = act.target
            # 如果弹幕目标没装备天狗盾,就不做处理了
            if not tgt.has_skill(MomijiShieldSkill):
                return act
            # 如果弹幕颜色不是黑色,就不做处理了
            if not act.associated_card.color == Card.BLACK:
                return act
            g = Game.getgame()
            # 拦截这个弹幕
            g.process_action(MomijiShield(act))
        # 原样返回当前的 Action
        # (被拦截的话,当前的 Action 会被取消)
        return act

可以被当作事件的东西有很多,比如每一个 在触发的时候都会产生 、apply、after 这几个事件,分别代表事件发生前、事件发生时,事件发生后。听起来貌似 和 apply 差不多,实际上有微妙的区别:按照约定,事件发生前, 是可以插入结算另外一个 或者任意修改当前的 的,更改属性(在目标脸黑的情况下你造成的伤害+1),直接取消掉(对方不想造成伤害,并且向你扔了一只狗),甚至替换成一个其他的 (楼观剑的实现有用到过);事件发生时的时候,整个 就是板上钉钉了,不允许任何修改,只允许在这里插入结算。

然后就是上文说到的,所有的客户端和服务端同步运行这些逻辑。

2. 跟 War3 的正版 的区别

1) THB 并不是字面意义上的 ,或者说 “”。THB 中并没有时间片,所有体现了Lock(或者说同步)的地方,都是在需要通信的地方,比如用户输入和隐藏信息的揭示,这里会对通信做编号,可以当作 中的对时间片的编号。

2)THB 中用户输入实际上是业务逻辑,是类似于“选一张牌”或者“选一个角色”这样的输入,而不是“鼠标在 (x,y) 点了一下”这样业务无关的信息。所以用户输入是在游戏逻辑中显式请求的,但是 War3 的用户输入的处理是引擎完成的,Jass 脚本中无法对 UI 输入做什么。

3)War3 中所有的 peer 都知悉所有的游戏状态,THB 中不是,只有服务器知悉完整的游戏状态,各个客户端只知道跟自己有关的信息:隐藏卡牌在逻辑代码中真的是隐藏卡牌,并不是 UI 做的处理,跟 War3 的战争迷雾不一样,也就不存在“全图”的可能性。

4) THB 中没有做检测 的机制,在 的第一时间有可能不会出问题,但是之后就会炸掉,就像 C 程序的 bug 一样,非常难搞。(本来想甩锅给“哎呀我们的逻辑代码又不是跑在 VM 上这个没法搞啊”,但是想了下貌似搞起来没什么问题……)

5) THB 中的随机数不是同步的,而是在服务器端生成,然后告诉相关的客户端的。因为存在洗牌,不可能让随机数同步的。

3. 结算和通信

做了个动画,希望能帮助理解

技能叫 ,主要设定是“目标需要选择一张牌交给我,否则我对目标造成一点伤害”。

逻辑游戏棋怎么玩_类似数马一类的逻辑游戏_游戏逻辑

动画么没有涉及 的内容,不过我觉得不用解释了。

4. UI 层

UI 层的展现和处理用户输入也是通过 来处理的。客户端的 UI 会在 列表中的最前面插入一个用来截获各种事件的 ,转发给 UI 层,然后 UI 层根据这些信息来处理用户输入的时候会发一个 事件,然后在需要用户输入的时候 会通知 UI,并且挂起逻辑层的结算,UI 等玩家输入后将结果填回,再将挂起的逻辑层结算恢复。

5. 坑

要说坑的话……

1) 首先各种 ! 所有用到 dict、set 之类的无序集合的时候都要万分小心,千万不能遍历!一遍历就 ,因为在每个机器上元素在集合内的位置可能不同,遍历时每个机器上的顺序就会不同,服务器是 Linux,客户端是 ,会显得更明显。自己测的时候却不容易测出来。而且没有什么办法可以禁止类似的遍历。

2) 在手机版上因为是 Unity 引擎,没法像 PC 版把 UI 信息的传递做成同步的(涉及 event loop 的切换,开销巨大,后面的文章会讲),于是就是异步的了,这样会有非常恼人的问题:比如方片周让玩家 a 猜,无论怎么样,这张牌会展示给所有人,结算,然后交给玩家 a 后…… 洗牌!如果 ui 在这个过程中都没有机会做什么的话,看到的就会是“隐藏卡牌”…… 有办法,但是懒得修了…… _(:3」∠)_

6. 其他的有趣的事实

1) 洗牌算法一开始写的又臭又长(貌似超过100行了),还满是 bug,各种 。后来想通了,缩减到两行了(省略了支持代码):

seed = sync_primitive(g.random.getrandbits(63), a)  # 服务器与客户端 a 同步一个随机数
random.Random(seed).shuffle(cardlist)               # 用这个随机数做种子,用标准库的 shuffle 洗牌

2) 之前学算法的时候学到拓扑排序,总觉得没啥用。然后在这里面居然用上了,用来给 们排序。前文中能看到 之间是有先后顺序的。

欢迎问问题!

------

题图:火焰猫燐,东方地灵殿中出场人物。:和茶

你可能想看:

有话要说...

取消
扫码支持 支付码