查看: 3923|回复: 4

[RMVA发布] 半成品QTE系统 with 教程

[复制链接]

…あたしは天

梦石
0
星屑
2308
在线时间
4033 小时
注册时间
2010-10-4
回帖
10548

开拓者贵宾

发表于 2014-11-5 17:00:41 | 显示全部楼层 |阅读模式

加入我们,或者,欢迎回来。

您需要 登录 才可以下载或查看,没有账号?注册会员

×
本帖最后由 taroxd 于 2014-11-8 16:34 编辑

今天开运动会提前一点放学,于是花了半个小时拿着学校图书馆的破电脑随手瞎写了点东西玩玩。用法不告诉你由于时间仓促,没有做美化和注释等工作

我就说,技能公式是个好东西,kira☆~

核心代码是 Taroxd::QTE “模块",和战斗场景里对 update_basic 的修改。Sprite 什么的爱怎么玩怎么玩,爱怎么改怎么改。

仅用于脚本技术交流之用,大概没人愿意真的用到游戏里= =

也许有空的话,我会将这段代码写出的过程当做一个教程来发布。

@KEY  @VIPArcher

[pre lang="ruby" line="1"]
# 技能公式范例:
# Taroxd::QTE.set { b.add_state 1 }; a.atk * 4 - b.def * 2

module Taroxd end

class << Taroxd::QTE = Object.new

  WAITING = 60  # 执行 set 到触发时间的帧数
  
  SCREEN_WAITING = WAITING + 60 # 执行 set 时画面等待的帧数

  TOLERENCE = 1 # 准许误差的帧数

  # key 为 nil 时表示没有 QTE 设置
  # when_triggered: QTE 触发时执行的脚本
  def set(key = :C, &when_triggered)
    @frame = WAITING
    @key = key
    @when_triggered = when_triggered
    if SceneManager.scene_is? Scene_Battle
      SceneManager.scene.abs_wait(SCREEN_WAITING)
    end
  end

  def update
    return unless @key
    @frame -= 1
    return terminate if @frame < -TOLERENCE
    trigger if hit? && @frame.abs <= TOLERENCE
  end

  def pos
    @key && 1 - @frame.fdiv(WAITING)
  end

  private

  def hit?
    Input.trigger?(@key)
  end

  def trigger
    @when_triggered.call
    terminate
  end

  def terminate
    @key = nil
  end
end

class Sprite_QTE < Sprite

  QTE = Taroxd::QTE

  def initialize(_ = nil)
    super
    self.bitmap = Bitmap.new(50, 8)
    self.x = 100
    self.y = 100
    self.z = 200
  end

  def update
    refresh if QTE.pos != @pos
  end

  def dispose
    bitmap.dispose
    super
  end

  private

  def refresh
    @pos = QTE.pos
    bitmap.clear
    return unless @pos
    pos = @pos * bitmap.width
    bitmap.fill_rect(0, 0, bitmap.width, bitmap.height, Color.new(0,0,0))
    bitmap.fill_rect(0, 0, pos, bitmap.height, Color.new(255,255,255))
  end

end

class Scene_Battle

  alias ub_20141105 update_basic
  def update_basic
    ub_20141105
    Taroxd::QTE.update
  end
end

class Spriteset_Battle

  alias ct_20141105 create_timer
  def create_timer
    ct_20141105
    @qte_sprite = Sprite_QTE.new
  end

  alias ut_20141105 update_timer
  def update_timer
    ut_20141105
    @qte_sprite.update
  end

  alias dt_20141105 dispose_timer
  def dispose_timer
    dt_20141105
    @qte_sprite.dispose
  end

end[/pre]      

--------------------------教程部分-----------------------

就拿这个简单的 QTE 系统为例。要写脚本,请先回答如下几个问题。

下面的每一个问题的回答都不是标准答案。
你可以有不同的想法,而不同的回答对应着不同的实现。

1. 预期的效果是什么?
  当带有 QTE 的技能发动时,场上会显示一个东西,此时战斗场景暂停。
  然后在指定时机按下 QTE 对应的按键,就可以触发某个效果。

2. 处理逻辑是什么?
  QTE 触发 -> 场景暂停 -> 进行 QTE 的处理 -> 场景继续
  进行 QTE 的处理时,画面会随着 QTE 状态的变化而更新

3. 需要什么数据?
  1) 怎么样算作触发 QTE?
  2) 触发 QTE 的效果是什么?
  3) 怎样表示当前 QTE 的状态?

  在这里,我们可以这么回答:
    1) 在某一段时间之间按下某个按键,就算做 QTE 触发成功
    2) 利用 Ruby 中 block 的特性,我们可以将效果用 block 包装起来。触发时执行这个效果即可。
    3) 可以利用时间的推移来表示

4. 开始写代码

1)数据
  一般来说,你需要一个类来存储数据。
  类似于 Game_Actor 这类,就是游戏数据的体现。
  在这里,由于 QTE 不会同时触发多个,我们用一个模块就足以完成数据的管理了。

[pre lang="ruby" line="1"]
# 技能公式范例:
# Taroxd::QTE.set { b.add_state 1 }; a.atk * 4 - b.def * 2

module Taroxd end

class << Taroxd::QTE = Object.new

  def set(&when_triggered)
  end

  private

end

[/pre]

前两行注释是预定的使用方法,也是对第三步中第二个问题的部分回答。
我们后面是要根据这个来写脚本的。

module Taroxd end
初始化一个 Taroxd 模块作为命名的空间,因为 QTE 这个常量名还是很有可能引起冲突的。

class << Taroxd::QTE = Object.new
这行代码可能有些难懂。你可以理解为 module Taroxd::QTE ,然后后面定义模块方法的时候就不需要写 self 了。这是一个偷懒的地方。

private 上方是写要给脚本的其他部分调用的方法,而 private 的下方写内部使用的方法。
两种方法分开写之后,可以使脚本更加清晰。

下面开始解决第三步中的问题。

1) 在某一段时间之间按下某个按键,就算做 QTE 触发成功
于是我们需要表示出“一段时间”,还有“按下某个按键”。

“一段时间”可以从调用 set 开始计算,过了 x 帧后,在 y 帧的误差范围内的一段时间。
x,y 可以是变量。这里为了方便,暂且设置为两个常量。

虽然我们可以用 Graphics.frame_count 来计算帧数,但是这里使用了每帧 update 的方法。

[pre lang="ruby" line="1"]
# 技能公式范例:
# Taroxd::QTE.set { b.add_state 1 }; a.atk * 4 - b.def * 2

module Taroxd end

class << Taroxd::QTE = Object.new

  WAITING = 60  # 执行 set 到触发时间的帧数

  def set(&when_triggered)
    @frame = WAITING
  end

  def update
    @frame -= 1
  end

  private

  def hit?
    Input.trigger?(:C)
  end

end
[/pre]

首先,这里是把 @frame 初始化成最大值,然后每帧递减的方式实现的。
当然,初始化为 0,然后每帧递增也是可以的。

然后,把 hit? 单独拿出来写成一个方法是为了便于将来修改。
毕竟将来并不一定会用 :C 做按键。甚至可以导入全键盘脚本等。
这一步很轻松地就完成了,接下来是下一步。

2) 利用 Ruby 中 block 的特性,我们可以将效果用 block 包装起来。触发时执行这个效果即可。
这个同样可以轻松完成。

[pre lang="ruby" line="1"]

  def set(&when_triggered)
    @frame = WAITING
    @when_triggered = when_triggered
  end

  private

  def trigger
    @when_triggered.call
  end

[/pre]

那么实际触发怎么做呢?
因为我们有(预定)每帧执行的 update。因此我们可以直接在 update 中进行触发效果的判断与处理。
注意到 @frame 的意义就是按键的时间与当前时间的差值,我们可以非常简单地判定当前是不是在时间范围内。

[pre lang="ruby" line="1"]

  TOLERENCE = 1 # 准许误差的帧数

  def update
    @frame -= 1
    trigger if hit? && @frame.abs <= TOLERENCE
  end

[/pre]

3) 可以利用时间的推移来表示 QTE 的状态

由于 @frame 的存在,这个要求十分简单。
[pre lang="ruby" line="1"]
  def pos
    1 - @frame.fdiv(WAITING)
  end
[/pre]

pos 是 position 的简称,表示位置。
注意,由于 @frame 和 WAITING 都是整数,所以这里不能用除号,要用浮点数的除法 fdiv。



仔细想想,数据的部分到这里就结束了吗?
实际上还是有一点问题的。

比如说,在时间范围内,如果玩家手速够快,连续按了两次按键,那么 QTE 的效果不是就触发了两次了吗?
此外,当前的脚本如何在显示时判断是否需要显示 QTE 界面呢?(也许你可以说 pos > 1,不过这样总是有点让人不爽)

这里的解决办法是,在效果触发或者过期后,将 QTE 表示为不可用的状态。
我们可以用一个实例变量表示 QTE 当前是否启动。

反正要用一个实例变量,顺便把可变按键的效果做了吧。
@key 如果为 nil,表示当前 QTE 处于不可用的状态,否则表示需要按下的按键。

[pre lang="ruby" line="1"]
# 技能公式范例:
# Taroxd::QTE.set { b.add_state 1 }; a.atk * 4 - b.def * 2

module Taroxd end

class << Taroxd::QTE = Object.new

  WAITING = 60  # 执行 set 到触发时间的帧数

  SCREEN_WAITING = WAITING + 60 # 执行 set 时画面等待的帧数

  TOLERENCE = 1 # 准许误差的帧数

  # key 为 nil 时表示没有 QTE 设置
  # when_triggered: QTE 触发时执行的脚本
  def set(key = :C, &when_triggered)
    @frame = WAITING
    @key = key
    @when_triggered = when_triggered
  end

  def update
    return unless @key
    @frame -= 1
    return terminate if @frame < -TOLERENCE
    trigger if hit? && @frame.abs <= TOLERENCE
  end

  def pos
    @key && 1 - @frame.fdiv(WAITING)
  end

  private

  def hit?
    Input.trigger?(@key)
  end

  def trigger
    @when_triggered.call
    terminate
  end

  def terminate
    @key = nil
  end
end
[/pre]

第二步,制作便于测试的显示部分。

[pre lang="ruby" line="1"]
class Sprite_QTE < Sprite

  QTE = Taroxd::QTE

  def initialize(_ = nil)
    super
    self.bitmap = Bitmap.new(50, 8)
    self.x = 100
    self.y = 100
    self.z = 200
  end

  def update
    refresh if QTE.pos != @pos
  end

  def dispose
    bitmap.dispose
    super
  end

  private

  def refresh
    @pos = QTE.pos
    bitmap.clear
    return unless @pos
    pos = @pos * bitmap.width
    bitmap.fill_rect(0, 0, bitmap.width, bitmap.height, Color.new(0,0,0))
    bitmap.fill_rect(0, 0, pos, bitmap.height, Color.new(255,255,255))
  end

end

class Spriteset_Battle

  alias ct_20141105 create_timer
  def create_timer
    ct_20141105
    @qte_sprite = Sprite_QTE.new
  end

  alias ut_20141105 update_timer
  def update_timer
    ut_20141105
    @qte_sprite.update
  end

  alias dt_20141105 dispose_timer
  def dispose_timer
    dt_20141105
    @qte_sprite.dispose
  end

end

[/pre]

目前这段代码的主要作用还是测试 QTE 的数据是否和预期相同。

最后是场景的逻辑,这里为了速度再次偷一下懒,把画面的暂停写在了 Taroxd::QTE.set 里面。
这样的话,我们并不知道场景会在哪里暂停,也不知道这样的暂停会带来什么后果。
因此,这样的代码是不规范的,仅作测试之用,需要在将来调整。


[pre lang="ruby" line="1"]
class << Taroxd::QTE = Object.new

  SCREEN_WAITING = WAITING + 60

  def set(key = :C, &when_triggered)
    @frame = WAITING
    @key = key
    @when_triggered = when_triggered
    if SceneManager.scene_is? Scene_Battle
      SceneManager.scene.abs_wait(SCREEN_WAITING)
    end
  end

end

class Scene_Battle

  alias ub_20141105 update_basic
  def update_basic
    ub_20141105
    Taroxd::QTE.update
  end
end
[/pre]

最后完整的代码(半成品)如下:

[pre lang="ruby" line="1"]
# 技能公式范例:
# Taroxd::QTE.set { b.add_state 1 }; a.atk * 4 - b.def * 2

module Taroxd end

class << Taroxd::QTE = Object.new

  WAITING = 60  # 执行 set 到触发时间的帧数

  SCREEN_WAITING = WAITING + 60 # 执行 set 时画面等待的帧数

  TOLERENCE = 1 # 准许误差的帧数

  # key 为 nil 时表示没有 QTE 设置
  # when_triggered: QTE 触发时执行的脚本
  def set(key = :C, &when_triggered)
    @frame = WAITING
    @key = key
    @when_triggered = when_triggered
    if SceneManager.scene_is? Scene_Battle
      SceneManager.scene.abs_wait(SCREEN_WAITING)
    end
  end

  def update
    return unless @key
    @frame -= 1
    return terminate if @frame < -TOLERENCE
    trigger if hit? && @frame.abs <= TOLERENCE
  end

  def pos
    @key && 1 - @frame.fdiv(WAITING)
  end

  private

  def hit?
    Input.trigger?(@key)
  end

  def trigger
    @when_triggered.call
    terminate
  end

  def terminate
    @key = nil
  end
end

class Sprite_QTE < Sprite

  QTE = Taroxd::QTE

  def initialize(_ = nil)
    super
    self.bitmap = Bitmap.new(50, 8)
    self.x = 100
    self.y = 100
    self.z = 200
  end

  def update
    refresh if QTE.pos != @pos
  end

  def dispose
    bitmap.dispose
    super
  end

  private

  def refresh
    @pos = QTE.pos
    bitmap.clear
    return unless @pos
    pos = @pos * bitmap.width
    bitmap.fill_rect(0, 0, bitmap.width, bitmap.height, Color.new(0,0,0))
    bitmap.fill_rect(0, 0, pos, bitmap.height, Color.new(255,255,255))
  end

end

class Scene_Battle

  alias ub_20141105 update_basic
  def update_basic
    ub_20141105
    Taroxd::QTE.update
  end
end

class Spriteset_Battle

  alias ct_20141105 create_timer
  def create_timer
    ct_20141105
    @qte_sprite = Sprite_QTE.new
  end

  alias ut_20141105 update_timer
  def update_timer
    ut_20141105
    @qte_sprite.update
  end

  alias dt_20141105 dispose_timer
  def dispose_timer
    dt_20141105
    @qte_sprite.dispose
  end

end
[/pre]

将常量 TOLERENCE 的值改大一些,然后测试吧!
测试的结果没有出什么问题,说明这段脚本的数据部分已经完成了。
正巧,QTE 脚本的场景逻辑部分相当简单。所以,这个脚本已经完成了一大半了。

剩下的一小半就是作业啦~

1. 完善显示
  1) 在屏幕上显示应该按的按键(你应该将 @key 暴露给外部)
  2) 美化。
  3) 根据技能目标的不同,调整显示的位置。
  4) 在 QTE 触发成功时,播放动画效果和音效。

2. 把数据部分 Taroxd::QTE.set 中对场景的设置放到 Scene_Battle 中的合适位置。

3. 当前代码中,敌人使用技能也会触发 QTE。请禁止这个功能,或者敌人使用时有一定概率随机触发 QTE。

4. 当前使用该脚本需要在技能公式中设置,要求一定的脚本知识,并且可能会写不下。
   此外,计算伤害是在 QTE 触发之前计算的。当技能使用者为多个目标时,也会有不想要的结果。
   请更换一种设置方式,通过读取备注来设置 QTE 的效果,使设置更加人性化。

5. 思路不要受到这段脚本的束缚。请重新回答一开始提出的问题,重新写出你自己的代码,使之符合你个性化的需求。

6. 为你的代码补全注释。  

评分

参与人数 2星屑 +200 收起 理由
VIPArcher + 100 认可答案
RyanBern + 100 塞糖

查看全部评分

梦石
0
星屑
9557
在线时间
5074 小时
注册时间
2013-6-21
回帖
3459

开拓者贵宾剧作品鉴家

发表于 2014-11-5 17:51:16 | 显示全部楼层
VA简约版的也有了?脚本长度越来越短了啊……我得找时间精简一下我的脚本。
那么VX版的谁来做一个?感觉就是QTE们在开会啊

点评

当时学校里时间有限,这段代码是以最快速度写出来的……细节都没有考虑  发表于 2014-11-8 20:14
看了教程才知道原来咱俩实现的QTE不是同一种,顺便说一下refresh那里可以用src_rect的方法来改变矩形宽度,不用每一帧都fill_rect了  发表于 2014-11-8 19:36
只是半成品。Sprite 明显是在瞎写  发表于 2014-11-6 12:33
回复

使用道具 举报

无限の剣制

梦石
0
星屑
10445
在线时间
5019 小时
注册时间
2013-2-28
回帖
4942

开拓者贵宾

发表于 2014-11-5 18:11:53 | 显示全部楼层
用法不是在注释里有吗?
等教程
回复

使用道具 举报

梦石
0
星屑
9292
在线时间
2504 小时
注册时间
2011-5-20
回帖
14521

开拓者

发表于 2014-11-6 22:07:59 | 显示全部楼层
感觉兼容性应该会很高···另外VA里面一秒是多少帧?貌似不是20···
[img]http://service.t.sina.com.cn/widget/qmd/5339802982/c02e16bd/7.png
回复

使用道具 举报

梦石
0
星屑
9292
在线时间
2504 小时
注册时间
2011-5-20
回帖
14521

开拓者

发表于 2014-11-8 15:54:16 | 显示全部楼层
技能公式写满了···写不下QAQ

点评

请完成作业4  发表于 2014-11-8 16:32
[img]http://service.t.sina.com.cn/widget/qmd/5339802982/c02e16bd/7.png
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.

在本版发帖返回顶部