FreeKill/doc/dev/gameevent.md

174 lines
11 KiB
Markdown
Raw Normal View History

# Fk的游戏事件
___
在Fk中“事件”指的大约是像`room:judge`, `room:damage`之类的操作。这些操作一般和某个游戏术语挂钩(“判定”、“伤害”),然后其中包含着一系列操作,比如伤害事件包含了与伤害事件有关的各种触发时机、以及扣减等实际的动作等。
之所以要把事件单独挑出来聊聊,是因为有以下几点需求:
* 事件要能够被半路中止。
* 对于被中止的事件,要能判断它能不能中止事件栈中的更低层事件。
* 对于被中止的事件,需要做“垃圾回收”(例如将处于处理区的相关卡牌移动到弃牌堆等等)
___
## 对于如何实现的构想
(施工完成后再来修改这一节)
首先是如何实现事件类。初步构想一下,应该有以下的属性/方法:
* 事件名(也可以是枚举值)
* 事件数据(一个表,内含所有要用到的数据)
* 事件id在一局游戏中唯一标记某个事件
* 事件的函数体,也就是具体要做的事情
* 事件被中止或者正常结束时,用来清理现场的函数
问题来了,既然事件本质上还是个函数体,那要怎么才能中止呢?
比较容易想到的是利用协程。协程差不多就是一种特殊的函数调用方式但是在协程体里面可以调用yield函数立刻返回到resume调用者然后还可能可以继续调用如果“中止事件”这一事件也被中止的话之类的奇怪情况总之可以纳入实现“事件”这种复杂概念的考虑。
事件必然会发生嵌套。所以对此更要慎重考虑。
现在只是设想!假设以`room:judge`入手:
```lua
function Room:judge(judgeStruct)
local judgeEvent = GameEvent:new(GameEvent.Judge, judgeStruct)
judgeEvent:exec()
end
```
总之可能是这样的吧。exec()就是实际执行事件,可能如下:
```lua
function GameEvent:exec()
local event_f = self.event_f
local co = coroutine.create(event_f)
while true do
local yield_result = coroutine.resume(co)
if yield_result == "__handleRequest" then
-- 正常yield掉如此层层yield最后到Room的主循环然后进requestLoop协程处理事务
-- 最后返回这里继续resume
coroutine.yield(yield_result)
else
-- 事件被中止,考虑做点什么
break
end
end
end
```
现在考虑嵌套的情况event1和event2嵌套也就是event1的体里面又创建了event2并exec此时的协程调用关系如下
RoomLogic -> event1 -> event2 | RequestLoop
此时event2中调用了某个耗时的函数比如room:delay或者各种request这时候就触发了yield。然后在上面的函数中就获取了yield返回值然后判断是正常yield后就进一步yield此时协程到了event1中。event1继续yield于是到了Room主协程主协程其实也调用了exec所以被yield切回到真正的主线程然后执行requestloop的协程。乍一看似乎没问题除了这个跳跃链有够长的。
这种开销看起来应该不大吧而且在AI Random Play这种非常需要性能的场所也根本不会发生这种类型的yield画饼总之先不考虑这块。
如果事件被中止按目前的实现来说就是在特定的时机return true了那么事件的本体也应该调用yield这就如同在目前--v0.0.1实现中那些函数常见的遇到true就直接返回了一样这样相当于从函数返回了。这种yield就会进入那个else分支进行这个事件类的有关清扫工作。
___
## 构想2 类似无懈可击的事件
考虑一类特殊的事件:“取消其他事件的事件”。它和普通事件一样,能被中止之类的,而它的作用在于取消掉其他事件。
接着上面的else分支继续考虑
```lua
else
local cancelEvent = GameEvent:new(GameEvent.CancelEvent, self)
local ret = cancelEvent:exec()
if ret then break end
```
似乎也没什么非常特殊的内容啊。
exec()的返回值哪里来?这好像真的是个问题呢。可以考虑返回布尔值表示事件是否中止了?或者更详细的,返回一个状态码,毕竟本质上身为协程自然能有协程该有的种种状态。这里只是初步考虑而已,就考虑前者好了。
___
## 落实 - 手杀皇甫嵩
手杀皇甫嵩是重构整个事件体系的罪魁祸首。其技能为若blah blah你可以终止本次判定然后blah blah。
而终止本次判定是目前的体系做不到的。
考虑如下技能片段:
```lua
on_effect = function(xxx)
local judge = {}
room:judge(judge)
if judge.card.number > 5 then xxx end
end
```
皇甫嵩能终止判定就算他在fk.Judge时机返回true算了。前文已经考虑过judge了他创建了新event并执行之。而今judge事件遭到打断room:judge可能可以返回一个返回值来告诉玩家已经被中断之类的。但是Luaer特别是像我这样的Luaer懒得考虑事件的合法性之类的而既然judge已经被终止那么judge.card就不应该被使用才行。
为此可以为judge表添加__index元方法当对key="card"进行取值时就直接yield掉除此之外的就rawget。
还有更复杂的情况呢。当皇甫嵩判乐的时候如果是黑桃那么他发动技能终止了判定然后像个无事人一样出牌呢。乐都还贴在他头上。考察一下Fk里面的乐是怎么写的原来是on_effect的末尾才移走啊那没事了。也就是说如果对judge.card的非法访问使得事件被中止了那么照这个逻辑乐是下不来的符合手刹了这下。
___
## 考虑 事件为何中止?
事件是协程,因此协程中止的方法就是事件中止的方法。有这两种:
* yield, 落实到Fk就是触发技的各种返回true
* error, 这不就是我经常发生的事情吗
前面也提到过发生yield的时候会有cancelEvent产生方便玩家反悔中止这次事件但因为error而中断事件是无法恢复的。试图resume一个报错的协程的话他会立刻因为error而自动yield。这个可以在exec函数里面多加考虑如果resume函数返回了true和特定值那就是正常情况。否则就是报错输出错误信息并返回。
那前文那个judge.card怎么办呢这种严格来说得算在error的范畴因为不是人为中止本次effect的。但是error的话势必要输出到屏幕而我个人聚德直接拿judge.card算是合法行为。这种情况或许可以定一个约定好的特殊错误信息在处理错误的时候如果是这个错误的话就不输出。
___
## 考虑 有哪些事件
在最开始的时候“依赖关系”这个现象的存在使得触发技多了个on_cost消耗但是现在on_cost已经成为界定skill是否发动了的标准。而在skill的effect环节依然存在着一环扣一环的关系比如前面举的room:judge例子。
假设Room.lua里面返回void的都算事件好了或者再细一点在函数体里面用了logic:trigger的void函数是事件算了这个也不好定义反正公道自在人心。但毫无疑问最为复杂的两个事件就是——使用牌和移动牌。
真是令人头大啊这俩可不是好惹的。不过看到它们可能从room.lua分家出去我其实还是有一丝欣慰
总之事件不止room.lua里面那些。就拿前面的考虑来说由于要中断on_effect所以on_effect肯定会算成一个事件可能叫SkillEffect事件吧。
再考虑万恶之源武将——老朱然,直接结束你的回合。(他只要回合内造成了伤害就能结束回合,但没说在谁的回合造成了伤害)所以进行回合也理应算是个事件。
___
## 考虑 老朱然
对于老朱然这种人而言,他想要杀掉的是回合事件,而能发动这个技能的时候,事件栈想必已经很深了,稍微模拟一下这个情景:老朱然杀界徐盛并打掉他一滴血,此时事件栈大概如下(还没正式设计各种事件,所以可能不妥):
* 伤害事件 - room:damage - 询问技能:是否发动胆守,点确定
* 技能生效事件 - activeskill:onEffect - 【杀】的effect
* 使用牌事件 - room:useCard - 出杀
* 进行阶段事件 - ? - 在出牌阶段
* 回合事件 - ? - 在回合
我们的限制条件无法获得room:damage的返回值或者说根本没想去获得其他同理。
coroutine.yield的功能也只有挂起协程并让相应的resume调用返回而已那么该怎么办呢由于以上种种限制的存在主要还是想把Luaer惯着我们不能对杀的onEffect下手其他函数都是核心函数改改也无妨咯。
还是结合情景考虑吧。胆守点了确定此时最直接的感受应该是return true。但是return true的意思是防止伤害都已经是“造成伤害后”了怎么防止哦return true也不会有人管你的所以这里要另辟蹊径。考虑直接yield此时会处于DamageEvent的exec()中也就是处于room:damage中他在处理中止信息。正常的中止的话会使用break跳出循环那么如果我访问调用栈直接让他一路yield到我们想要的那个事件如同yield到requestLoop那样呢
没错访问事件栈确实是个解决办法的可能方案。这时候用id指示事件的重要性就出来了可以传一个id表示事件不过话说回来传那个事件本身也没有任何关系就是了咯如果yield函数返回了一个GameEvent类的实例那么就在处理环节将其和self进行比较如果不同就继续yield直到退到相应的事件中。
这种跨越很多级事件的东西怎么取消呢?差不多得了,懒得考虑了,天天防止这防止那,随便逮到个东西就想把他防止掉/取消掉,三国杀的游戏逻辑就是被你们这群人毁掉的
总之这不考虑如何防止这种直接结束回合了毕竟这种不断yield的方式无法用事件进行描述。
___
## 考虑 内存泄漏的应对
首先声明Lua没有内存泄漏。但是如果有些东西用户不想要但是又不告诉lua的话lua就会觉得用户想要然后一直保存着它这在某种意义上也相当于内存泄漏了。拿实例来说如果事件被中止了那么在很多情况下确实就不需要了但Lua会认为协程是挂起的用户可能想要恢复于是一直保存着。
当然了对于这情况Lua提供了coroutine.close用来关掉一个协程。不过我们想要让这个事件彻底删掉该怎么办呢
方法很简单将它出栈不就行了。照这么说的话事件在exec开始的时候就入栈然后等待exec结束就出栈但对于老朱然这种人他把函数直接yield掉了因此有必要手动出栈。