Project1

标题: 歪门邪道事件心得[1]:为什么我的事件标题手感不好 [打印本页]

作者: ⑨姐姐    时间: 2018-4-7 11:01
标题: 歪门邪道事件心得[1]:为什么我的事件标题手感不好
本帖最后由 ⑨姐姐 于 2018-9-12 16:36 编辑

一直想开这个坑,今天终于开了。这一系列会专注于一些比较边缘地带的知识,它们经常被作者们当做“玄学”,或者被不屑一顾。但是,每个现象都是有它的道理的,深入了解下去,就能对RM的运作方式更加熟悉,制作出更符合心意的游戏效果。
这系列面向的是有一定事件基础的制作者们,并没有脚本水平的要求(完全不会也没关系)。另一方面,也许写了很长时间脚本但对RM的默认内容关注不多的,也能从中得到一些有用的东西吧。

欢迎反馈关于内容深浅以及描述方式等等的各种问题,也欢迎提供各种大家在RM中的“玄学”以及一直当做习惯却难以理解的地方,在以后的几期中也可以把这些作为主题的。
-------------------------------------------------------------

一天,埃里克百无聊赖地在海洋上行走(不要问他是如何做到的)。


他觉得,是时候开始游戏了。“开始游戏”……就是说需要个游戏标题:



用图片当做三个按钮,然后用并行事件判断当前的选项和图片的透明度,像这样——





可是,埃里克发现了一个问题:按左右键的时候,不是每次都成功的,有时候按了但是毫无反应。
“为什么我的事件标题手感不好呢?明明所有地方都写对了。”

“你遇到问题总是愁眉苦脸,这可不是勇者该有的样子呢。”娜塔莉不知什么时候出现在这里。


“我猜,按键没反应,是不是RM内部的按键输入时好时坏?”

“不可能。”娜塔莉否定了埃里克的猜想,“如果RM的输入真的有问题,为什么其他菜单的窗口都那么流畅呢?”

“难道是很多人说的,事件的效率天生不如脚本,用事件就会卡顿?呜呜呜,我要去学脚本……”

“给我打起精神来你这白毛!你忘了事件的背后就是脚本在运行吗?这两者又有什么区别呢。”

“事件的背后就是脚本?”

“一看你就没有好好听大贤者的讲课。来,我们打开脚本编辑器,找到Game_Interpreter。46行。”

RUBY 代码复制
  1. #--------------------------------------------------------------------------
  2. # * Event Setup
  3. #--------------------------------------------------------------------------
  4. def setup(list, event_id = 0)
  5.   clear
  6.   @map_id = $game_map.map_id
  7.   @event_id = event_id
  8.   @list = list
  9.   create_fiber
  10. end

在埃里克疑惑的目光中,娜塔莉解释道:“你看,这个setup,就是把事件的内容放入脚本中去执行了。list是事件列表,event_id是事件编号。所以我们编辑的每一个事件,其实都会丢到脚本里去执行的。既然这样,为什么要说事件速度慢呢?根本就是无稽之谈。”

埃里克还是没有完全搞懂:“我还记得一些脚本,这种等于号的都只是把一个值放到另一个变量里的操作,比如a=2就是把a变成2。就算我生成了好多个变量,但它们也只是数据倒来倒去而已,事件放进去以后,怎么执行呢?”

“事件执行的奥秘,就在create_fiber里了。”

娜塔莉一遍说着,一遍顺着脚本往下追溯:

RUBY 代码复制
  1. #--------------------------------------------------------------------------
  2. # * Create Fiber
  3. #--------------------------------------------------------------------------
  4. def create_fiber
  5.   @fiber = Fiber.new { run } if @list
  6. end

RUBY 代码复制
  1. #--------------------------------------------------------------------------
  2. [font=&quot]# * Execute[/font]
  3. #--------------------------------------------------------------------------
  4. def run
  5.   wait_for_message
  6.   while @list[@index] do
  7.     execute_command
  8.     @index += 1
  9.   end
  10.   Fiber.yield
  11.   @fiber = nil
  12. end



“埃里克,你看这个run,不就是执行事件的过程吗。execute_command的意思是执行一条事件指令,@index += 1的意思是把事件执行的位置往下移一格。你可以想象,你用铅笔指着一条事件,执行完以后再执行下一条事件的感觉。”

埃里克反而更疑惑了:“从这里看来,我上面写的事件应该是一点问题也没有啊,怎么可能按键失效呢。除非事件没有正常执行,没能通过条件分歧判断到我的按键,才会出现失效的情况吧。”

娜塔莉沉思片刻,两眼突然放出了光芒,把埃里克吓了一跳:“我明白了,你的事件确实没问题,问题出在事件结束的时候。埃里克,我考你一个问题,从上面的run当中,你能看出事件结束的时候会发生什么吗?”

“事件结束的时候,@index就超过事件列表的长度了,所以@list[index]就拿不到事件的内容,这时候while……”

“答对了,while @list[index]的循环就会正常退出,事件也就结束了。接下来呢?”

“接下来……Fiber.yield,这是什么意思呢?”

娜塔莉把代码翻到了Game_Interpreter的236行:

RUBY 代码复制
  1. #--------------------------------------------------------------------------
  2. # * Wait
  3. #--------------------------------------------------------------------------
  4. def wait(duration)
  5.   duration.times { Fiber.yield }
  6. end

“这里duration.times就是重复duration那么多次的操作。你看,一个叫做wait的东西,重复了那么多次的Fiber.yield,还不能猜出Fiber.yield是做什么的?”

埃里克恍然大悟:“wait我知道,就是事件里的等待。用N次某个操作组成了等待N帧,那某个操作就是‘等待1帧’了吧。”

娜塔莉点点头:“在这里确实可以理解成等待1帧。准确地说,是暂时退出Fiber上下文,去执行其他内容,等到1帧以后再来继续执行这个Fiber。”[如果想要了解关于Fiber更多的知识,可以看这篇文章的介绍]

“你还是别讲Fiber啊上下文什么的了,我暂且理解成等待1帧没错吧,那为什么是这里会导致出问题呢?”

“并行事件只要在地图上,它就会循环重启执行,这一点你作为事件党,应该很了解吧。”[如果想要了解它的原理,可以参考Game_Event脚本,一直拉到最后就能看到的def update]

“那是当然,我埃里克再怎么说,也是游戏制作勇者啊。”

“可是你发现了吗,你用来判断按键的并行事件,其实不是和游戏帧率一样,每秒60次,而是每秒30次。”

“每秒30次……也就是实际上一次并行事件占用了2帧吗。是不是因为,当到达下一帧的时候原本并行事件该重启了,结果却因为上面多等待了一帧,结果没有重启?这等待的一帧,就是我的键盘‘失灵’的时机?”

“你终于明白了。”娜塔莉笑道,“实际上你每次按键,都是在赌随机数啊。赌你按键的时刻是不是恰好落在执行事件的一帧上。假如落到了等待的一帧,那就没反应啦。所以快改脚本吧。”

“不行,不行。我作为勇者,怎么能认输呢,我偏要用事件的方式,把这个问题解决了。”

“这回你倒是说对了,说不定有什么其他人的脚本,会利用到这条特性呢。那就看看你能用事件搞出什么花样吧。”

只过了片刻,埃里克就找到了解决问题的办法:




果然,这样处理以后,按键就完全流畅了。

那么今天的课后习题就是,为什么这样处理以后按键流畅了呢?上面提到的“多出的一帧”去哪儿了呢?

用到的工程: DeviantEventTutorial1.zip (1.33 MB, 下载次数: 291)

正确答案

作者: MCCF    时间: 2018-4-7 11:30
截图为什么是英文版的啊,这样和看脚本没啥区别吧
作者: 鸟猫    时间: 2018-4-7 11:38
唔哇啊,这样解释问题超有意思的啊(感觉在看启蒙读物!画面感超强!)

之前用的标题事件确实有遇到过这种“手感不好”的情况
于是当时高频率/反复地按着左右按键,
发现一个现象,如果你按得越快,则“无效”的概率也就越大
所以当时的一个猜测就是 :
“是不是由于上一次的 ‘←’ 或者 ‘→’ 的执行需要一定的时间,影响到下一个操作的读取,导致失效的?”
现在终于明白了具体原因,原来如此,原来如此。


【课后作业】
我猜想,应该是因为在并行事件里手动加入等待一帧之后,
相当于是把游戏的刷新率减半,也就是每秒30次,
这样就和并行事件的刷新率同步了,
等同于“剔除了”没有执行事件的帧数,
赌随机数的概率也就变成了100%,这样?
作者: MCCF    时间: 2018-4-7 11:54
MCCF 发表于 2018-4-7 11:30
截图为什么是英文版的啊,这样和看脚本没啥区别吧

正版也有中文版的啊
作者: SailCat    时间: 2018-4-7 13:09
XP一样有这个问题,只不过是20和40罢了。而且XP没有fiber,是靠return false来摸拟的。
作者: RaidenInfinity    时间: 2018-4-7 13:17
事件标题的操作手感不好,大部分是因为事件的按钮判定是用Input.press?(按住会重复触发)而不是Input.trigger?(按下到放开只算一次)。
目前现有的事件标题,只要改动按钮判定的方式就能姑且拯救一下手感。
作者: 百里_飞柳    时间: 2018-4-7 13:41
昨天才在另一个帖子里研究了一波默认的神奇interpreter类2333

这个事件修改后有两个地方与之前不一样了
1、新增了 等待一帧
2、新增了 循环

结合事件解释类里它的运行方法:一直循环直至完成全部指令,在真正取消自己的刷新前等待一帧(或者事件中途出现Fiber.yield,使得它的循环挂起,下一帧时再被resume,从上一帧执行到的位置继续这个循环)*具体见Fiber类
  1. #--------------------------------------------------------------------------
  2. # 运行
  3. #--------------------------------------------------------------------------
  4. def run
  5.   wait_for_message
  6.   while @list[@index] do
  7.     execute_command
  8.     @index += 1
  9.   end
  10.   Fiber.yield
  11.   @fiber = nil
  12. end
复制代码


因此,新增的这个 循环 指令,直接令事件永远不会执行完,也就是这个事件单凭自己是永远不会从run方法里的while @list[@index] do...end中结束的,也就更没有什么取消刷新前挂起一帧了。

但是,如果无聊自己试验过就知道,这种无限循环(无法结束的)会直接令游戏主逻辑卡死,也即画面不会被刷新、按键也不会被响应,
此时 等待一帧 的指令就有用了,我们人为在这个循环里添加一次挂起,当运行到这里时,这个事件这一帧要进行的操作就可以中止了,然后回到主逻辑里继续刷新其他的东西,这样主逻辑不受干扰继续刷新,而等到下一帧时,由于Fiber的特性,就让这个事件从上一帧中止的地方继续执行下去,我们又回到了事件里循环的开头,也即让这个事件随主逻辑1s60帧刷新
*主逻辑:游戏本身实际上是一个大循环,每一帧依次更新着画面、输入、声音等
作者: W.Q.C.    时间: 2018-4-7 21:24
好有趣的教学方式啊!!希望持续更新!!对纯事件党来说感觉很有用(XP应该原理相似?)
懂脚本果然能更深层次的挖掘事件的判定机制吗,事件标题手感这个事果然应该不少人遇过啊,
之前还折腾了好一阵子,自己最终是用“与事件接触”的方式来判定选项才让手感稍稍好了一点,
但是按太快还是会有失效的情况,不过至少还能看,也就这么一直用下来了,
(应该是人物走动触发事件的走动过程花的时间比起直接的按键判定花费的时间多得多)
现在看到⑨姐姐的这个帖子又学到了许多,以后就可以拿来试试看了。
手感这个东西确实很影响玩家体验,而且个人认为应该是许多纯事件RM游戏中比较常见的问题,
也希望这个系列能给一些只想用事件的新人一些启发吧
然后果然出现老鹰大大的身影了啊2333,一起吹爆!
(最后顺带借帖偷偷吹爆《审判者》,前几天终于有时间熬夜(平时没太长空闲时间来玩)玩了一周目,
虽然只是粗略的玩了一遍,都已经感受到游戏系统的新颖和超有感觉的故事氛围了,
打算等再空下时间来的时候再好好品味一番!)

作者: shitake    时间: 2018-4-8 15:43
本帖最后由 shitake 于 2018-4-8 15:47 编辑
RaidenInfinity 发表于 2018-4-7 13:17
事件标题的操作手感不好,大部分是因为事件的按钮判定是用Input.press?(按住会重复触发)而不是Input.trig ...


@SailCat

并不能简单的把使用 press 和手感差对应起来。倘若是人物移动,不适用press而是trigger 反而手感会变的更差。同样对于某些选单来说,不能长按就持续切换,反而只能按一下切换一下也是手感差到爆。

这里的核心问题是,输入事件的响应是跨帧的(即响应效果是持续多帧的)。如果使用 press 会在每一帧都触发事件。但之前的事件的响应并没有完成。这种情况下就会手感变差。

实际上在此处,我们需要的是对输入事件序列进行 filter 来过滤掉那些不满足条件的触发(上个事件的响应没有结束)。

将 press 提换成 trigger 更像是一个权宜之计。在事件响应较短的情况下(比如此处的选单),依靠按键事件本身的跨帧(在60 fps 的情况下,即使在快的按下弹起,都会跨越多个游戏帧)来做到延缓触发的效果,只能说是恰好撞对了结果。但是假如需求转变(比如人物移动,播完一个移动动画往往需要持续更多的帧,或者一个更复杂的 UI 切换动画)再用这个思路无疑就是往死胡同里钻。

由于 rm 在事件部分并没有提供上层的抽象。只有最底层的Input判断。所以这一filter流程就只能耦合在项目代码中。在 RMVA 里,对应的是如下代码:

  1.   def move_by_input
  2.     return if !movable? || $game_map.interpreter.running?
  3.     move_straight(Input.dir4) if Input.dir4 > 0
  4.   end
复制代码


对此的更好的处理,是 FRP[1]。注入 elm/ReactiveX 都是此类的很好的例子。在 FRP 中,event 以流(stream)的形式存在,我们可以对其进行 filter/map/merge 等操作。这些操作的返回值依然是事件流(EventStream)。当我们需要的时候,可以在流上订阅一个消费。

一个单击事件合并为双击事件的例子:
  1. click_event_stream = Event.get_event(:click)

  2. double_click_event_stream = click_event_stream.filter{ |e, last| e.time - last.time < 0.3  }

  3. double_click_event_stream.on{ puts 'double click' }
复制代码


除此之外,另一个思路是利用 Fiber。Fiber赋予我们随时挂起程序和返回到之前挂起的地方继续执行的能力。如果不想用 event stream。那么我们可以利用对 Fiber 的 yield 和 resume 来模拟类似的效果。(在这里 Fiber 的调用/挂起栈隐性的承担了 event stream 的工作)

这一思路的具体实现可以看看这个:RGUI::Event

上边的那个例子的实现:
  1. event_manager.on(:click)do |helper|
  2.   helper.filter{ helper.time_min(0.3) }
  3.   helper.trigger(:double_click)
  4. end
复制代码


1. Elm: Concurrent FRP for Functional GUIs
作者: Mayaru    时间: 2018-4-11 05:20
本帖最后由 Mayaru 于 2018-4-11 05:26 编辑

(哇!瞧我发现了什么!一个野生的九姐姐教程!青蛙味嘎嘣脆,蛋白质是触手的五倍x)
感谢教程~大概理解了(说起来我之前真的不知道rgss可以用while...那样的话for也可行吗)。但是index我不太确定是代表什么...Index初始值是什么?0吗?
作者: ⑨姐姐    时间: 2018-4-11 07:44
本帖最后由 ⑨姐姐 于 2018-4-11 07:54 编辑
Mayaru 发表于 2018-4-11 05:20
(哇!瞧我发现了什么!一个野生的九姐姐教程!青蛙味嘎嘣脆,蛋白质是触手的五倍x)
感谢教程~大概理解了 ...


for也是可以的,用法是 for [variable] in [range],比如
RUBY 代码复制
  1. for i in 1 .. 5
  2.   p i
  3. end

就能输出1、2、3、4、5。其中 1 .. 5 表示一个range。

上面的@index表示的是“当前执行事件的过程中正执行到的序号”。@list是事件列表,所以@list[@index]就是从@list中取出当前执行的那一条事件了。while @list[@index],只要当@index不超出@list的范围就能一直循环下去。它的初始值是@list的第一条,应该就是0了。
作者: 骷髅岛遗老    时间: 2018-4-11 18:38
RM确实有很多操作是直接设置体验dio差,但是中间加个等待一帧就手感贼6了,大概是因为等待一帧从某种角度避免了6楼说的毛病?
作者: 双叶荆棘    时间: 2018-6-1 21:24
【作业】多加了wait,还有其他事件
作者: 双叶荆棘    时间: 2018-6-7 20:01
双叶荆棘 发表于 2018-6-1 21:24
【作业】多加了wait,还有其他事件

其实这里面的原理……是加的脚本一定会控制时间什么的
作者: 双叶荆棘    时间: 2018-6-9 12:56
未执行完啊!我明白了——“因为事件没有结束,所以就跳过了多出的一帧”
作者: congwsbn    时间: 2020-12-9 00:05
今天才发现!!学到了!
作者: 精灵使者    时间: 2020-12-9 09:32
本帖最后由 精灵使者 于 2020-12-9 09:53 编辑

等待帧是个好东西,其实事件也可以等待帧(选择“等待”选项)
如果我记得没错的话,渐变画面需要等待相应的帧数,以及运行脚本后至少要等待1帧
如果是纯事件标题的话就可以用等待帧微调手感
一般的情况下我基本上是使用“事件陷阱”来保证选项成功触发
简单的说,和原版一样,使用变量来控制你的选项,然后按回车后根据变量触发打开的事件,应该是这样的。
事件菜单可以做到很多东西,例如LOGO和开头CG之类的。
顺便,还有一个重要的事。
第一个选项下的“移动图片”后面还要有个“1帧”对吧。
你那三个1帧需要3帧来完成这个动作
所以后面一定要至少等待3帧来保证图片正常的显示完成。
“脚本不会考虑执行图片处理的等待帧”
是这里出了问题
移动图片,更改色调等图片操作,都需要等待帧。
选择项中间也要必须插1帧,这样的话才能正确的反映输入情况,因为循环执行起来似乎整个游戏都停止工作了(?)
“有的时候跑得太快也不是啥好事”
所以写脚本的时候也会加入@wait_count或者是Graphics.sleep来控制脚本速度
作者: D_Lohy    时间: 2022-2-3 21:33
好家伙翻译到第一个次我就懂了问题的答案(小白 看不懂7楼
每次运行后需要等待1帧才能运行下一次
要修复这个bug需要改代码
爷不会代码
爷加个循环
循环一次不用等待1帧
所以就不会吞按键了XD
作者: RMVXA    时间: 2022-2-11 17:55
我感觉这么做就是为了优化游戏的运行吧。
这个引擎原本方向就是做简单的游戏吧。




欢迎光临 Project1 (https://rpg.blue/) Powered by Discuz! X3.1