FreeKill/docs/diy/10-eventstack.rst
2023-04-25 14:26:38 +08:00

125 lines
6.7 KiB
ReStructuredText
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

.. SPDX-License-Identifier: GFDL-1.3-or-later
解析:事件栈
============
fk和神杀的一大区别就是拥有事件栈。下面对事件栈稍作解说并说明如何利用它让DIY更便捷。
何谓事件
--------
相信各位对 *触发时机* 这个概念不会陌生。拿伤害举例吧,就涉及 *造成伤害时**受到伤害后* 等等时机。那么 *伤害* 本身呢?换句话说, ``room:damage`` 里面发生了啥?
在神杀中伤害就是直接执行一系列代码。Fk在这方面则是有区别的——它新建了一个伤害 *事件* ,然后执行这个事件(也就是正常的伤害流程)。
为什么要做一个事件机制呢?答案是为了更好的处理插结状况,以及实现老朱然(划掉
事件有以下几个特点:
- 能携带额外信息: 这一点就让代码在插结的情况下也能正确运行
- 能被随时中断而不影响游戏运行: 以胆为守!
- 即使被中断了也能清理现场: 后面会详细说说关于这个的注意事项
当然了本文不会讨论事件机制的具体实现方法而只会说明它在DIY能派上哪些用场。
初步观察事件栈
--------------
Fk提供了一个方便的函数 ``GameLogic:dumpEventStack()`` 它能输出当前的事件栈。你可以直接在代码中调用它或者在用dbg大法调试代码时现场调用它。
总之来举个例子,我去洛神里面加一行给大伙看看效果好了:
.. code:: lua
on_use = function(self, event, target, player, data)
local room = player.room
room.logic:dumpEventStack() -- 打印此时事件栈
room:obtainCard(player.id, data.card, false)
end,
玩一下游戏,人机对我们使用了【杀】,我们掉血发动奸雄,此时可以看到这样的输出:(仅用作例子,实际上随着游戏的更新结果不一定相同)
::
===== Start of event stack dump =====
Stack level #7: GameEvent.SkillEffect
Stack level #6: GameEvent.Damage
Stack level #5: GameEvent.SkillEffect
Stack level #4: GameEvent.UseCard
Stack level #3: GameEvent.Phase
Stack level #2: GameEvent.Turn
Stack level #1: GameEvent.Round
===== End of event stack dump =====
来看看吧。栈顶层的就是当前的事件——SkillEffect即技能生效事件。此时生效的技能是奸雄。然后往上一层就是伤害事件了再往上是【杀】的生效事件再往上是使用卡牌【杀】的事件再往上是执行阶段事件这里执行出牌阶段再往上是执行回合最底层的是执行轮次。
是不是很清晰的列出了所有插结情况呢顺便一提dumpEventStack还接收一个布尔型参数。如果你传一个true进去的话能看到更加详细的输出不过可能详细过头就是了。这种简略版输出是默认情况刚好也便于说明。
如何调查事件栈
--------------
Fk提供了一系列用来调查事件栈的函数。
首先是获取当前的游戏事件—— ``GameLogic:getCurrentEvent()`` 。这个函数会把事件栈栈顶的事件返回。
然后的东西就是根据事件来找比他更早的事件了,或者说找自己的父事件。
我们可以用 ``event.parent`` 获取这个游戏事件event的父事件。也就是刚好比他早一点的事件在栈中处于它下层紧挨着的事件。比如前面的栈中Damage事件的父事件是SkillEffect事件。
由于父事件也是事件所以它也有parent属性也就是说可以写出诸如 ``event.parent.parent...`` 的代码。当然了这样写不好看也可能出bug所以有一个方便的函数—— ``GameEvent:findParent(eventType)`` 。该函数像遍历链表一般的找到比这个事件更早的、类型符合参数指定的事件。比如 ``event:findParent(GameEvent.Turn)`` 就返回距离此事件最近的回合事件。
如何给事件附加数据
------------------
事件虽是类的实例,但本质上是一张表。所以直接用 ``event.xxx = xxx`` 就能给他附加数据了。
获得数据也是一样的简单,用 ``event.xxx`` 就行了。
这样做的最大好处就是解决了插结问题。下面举个插结的例子以便大家能更加了解。
没有!
如何终止事件
------------
``GameEvent:shutdown()`` 。这个函数能终止一切结算并结束这个事件。如果该事件不是栈顶的事件的话,那么会先逐一终止比他更晚发生的所有事件,然后再终止这个事件。
GameLogic里面也提供了方便的函数。 ``GameLogic:breakEvent()`` 可以终止当前事件。 ``GameLogic:breakTurn()`` 是个专为老朱然封装的函数,能终止一切结算并结束本回合。
还有个可能也是非常常用的只要发生了任何错误那么当前事件也会被终止。比如你不小心操作了本不该为nil的对象或者直接手动调用error函数或者发生assert failed等等报错后。这种情况下事件终止更像是一种错误处理方式而不是你真的想终止这个事件了。
.. note::
在fk比较早期的时候还没有事件机制一说。因此只要发生任何错误都会整局游戏立刻停止因为Lua报错导致自己停止运行了。这是不是很糟糕呢不小心写了个bug结果整个游戏都卡住不动了。而有了事件机制后bug只会影响当前事件而不至于威胁到整个游戏的进行。这也为拓展开发带来了更大的容错率啊。
关于事件的灾后清理
------------------
在事件意外终止后,有一些事情也是要做的,并不是真正的直接终止结算走人了。比如在使用牌事件终止后,被使用的牌会从处理区进入弃牌堆,以及诸如此类的。
大家在制作DIY的时候应该会经常用到标记保存技能的相关数据。为了让事件即使被中断也能清理好这些标记就要去熟悉这些事件终止时是如何执行清理的。
.. hint::
这里提一下refresh only。所谓refresh only的触发方式就是只会执行触发技的can_refresh和on_refresh而不去管can_trigger之类的。结合refresh的定位这是不是用来清理各种标记的绝好时机呢
下面的各种触发如无特殊说明都是refresh only的。
首先是阶段被终止后:
- 清除所有玩家本阶段的卡牌/技能使用记录。
- 清除所有玩家所有以-phase结尾的标记。
- 触发EventPhaseEnd时机。
然后是回合被终止后:
- 清除所有玩家本回合卡牌/技能使用记录以及-turn结尾的标记
- 触发“结束阶段开始时”
- 触发“结束阶段结束时”
- 触发EventPhaseChanging从结束阶段到NotActive
- 触发“NotActive开始时”
- 触发“回合结束时”
以上就是常见的要注意的点建议在编写用于清理标记的on_refresh时也考虑一下。