AboutPostsGithub

使用自定义可视组件的长按跳过功能

在游戏结束的时候,往往存在一个很长的制作人员名单,虽然有些人会主动看完,但是大部分情况下玩家都更需要一个跳过的选项。在游玩黑神话悟空的时候,我发现许多CG都可以长按跳过,于是准备抄一下这个交互。

首先,将滚动制作人员名单的暂停交互逻辑以Screen的形式封装——在一个确定的滚动时长之后自动结束,大概像是这样调用:

"虽然我看不见自己的脸,但我猜,那个时候我的脸应该一瞬间变得通红。我用手想遮住我的嘴角,可还是无法控制的笑起来。"
    "然后,我奔向她——"
    call screen holdTimer()
    jump afterTheRoc

使用holdTimer来阻止主控流程的前进,直到手动返回交互值。

screen holdTimer(StaffImage=None,RollingTime=25,ringColor="#659282"):
  modal True
  add StaffImage
  add HodingMask(Progress(percent=0,type="circle",ringColor=ringColor),time=3000):
    xysize (1920, 1080)
  timer RollingTime action [With(Dissolve(4)), Return()]
  on "show" action [SetVariable("renpy.config.skipping", None),SetVariable("_skipping", False)]
  on "hide" action SetVariable("_skipping", True)

其中,使用timer关键字和入参 StaffImage 配合,构造自动播放的部分。值得一说的是,为了阻止玩家使用快进,在展示和隐藏的事件时手动设置_skipping,它用于阻止快进。而 "renpy.config.skipping"则是中断玩家的快进状态(如果不中断的话,在播放完毕名单后会继续快进)

接下来就是关键的长按跳过的逻辑了,这里我使用了 Creator-Defined Displayables ,即创作者定义的可视组件来完成这部分的需求。

import pygame
  class HodingMask(renpy.Displayable):
    def __init__(self, child,time,refreshRate=0.025, **kwargs):

      # 向renpy.Displayable构造器传入额外的特性(property)。
      super(HodingMask, self).__init__(**kwargs)
      # 保存需要长按的时间
      self.targetTime = time
      # 初始化已经按住的时间
      self.holdTime = 0
      # 子组件
      self.child = renpy.displayable(child)
      # 鼠标状态
      self.isHold = False
      # 百分比
      self.percent = 0
      # 上一次绘制的时间点
      self.lastRenderTime = 0
      # 默认的刷新速度
      self.refreshRate = refreshRate
      # 提示
      self.tip = Text(f"长按{int(time/1000)}秒跳过滚动字幕")
      # 上一次按下的时间
      self.lastMouseDownTime = 0
      # 上一次抬起的时间
      self.lastMouseUpTime = 0
      
        
    def render(self, width, height, st, at):
      self.lastRenderTime = st
      alpha = 0
      dissolve_time = 0.5
      if self.isHold and self.lastMouseDownTime + dissolve_time < st:
        alpha = 1
      elif self.isHold and self.lastMouseDownTime + dissolve_time >= st:
        alpha = (st - self.lastMouseDownTime) / dissolve_time
      elif not self.isHold and self.lastMouseUpTime + dissolve_time > st:
        alpha = 1 - (st - self.lastMouseUpTime) / dissolve_time
      alpha = max(0, min(1, alpha))
      child_render = renpy.render(Transform(child=self.child,alpha=alpha), width, height, st, at)
      render = renpy.Render(width, height
      render.blit(child_render, (40, 22))
      renpy.timeout(self.refreshRate)
      # 返回渲染器。
      return render

    def event(self, ev, x, y, st):
      if ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1:
        self.isHold = True
        self.lastMouseDownTime = st
      elif ev.type == pygame.MOUSEBUTTONUP and ev.button == 1:
        self.isHold = False
        self.lastMouseUpTime = st
      if ev.type == pygame.USEREVENT:
        self.update(st)
      
      return self.child.event(ev, x, y, st)

    def visit(self):
      return [ self.child,self.tip ]

    def update(self,st):
      if self.percent == 0 and not self.isHold:
        return renpy.redraw(self.child, 1)

      if self.percent == 100 or self.holdTime >= self.targetTime:
        self.percent = 100
        renpy.end_interaction(True)
        self.percent = 0
        self.holdTime = 0
        self.lastRenderTime = 0
        self.isHold = False
        return None
      
      during = int((st - self.lastRenderTime) * 1000)
      if self.isHold:
        self.holdTime += during
      elif self.holdTime > 0:
        self.holdTime -= during
      
      self.percent = max(0, min(100,int(self.holdTime / self.targetTime * 100)))
      if self.child and self.child.setPercent:
        self.child.setPercent(self.percent)
      renpy.redraw(self.child, 0)

忽略 setPercent 部分,那是另外一个cddProgress,用于展示进度。

cdd需要实现 eventrender两个关键方法,前者用于接受事件(包括renpy自定义事件和基于pygame的用户事件),后者则是用于绘制本可视组件及其子组件。

具体业务逻辑不详细说明了,关键在于在 renpy.redraw()renpy.timeout()两个API,redraw会调用 cdd的render方法,而 timeout 则会手动触发一次自定义事件,可以直接认为是调用 event方法,这样就构成了一个事件循环绘制的流程,我们无需在意Renpy如何实现绘制线程和事件传递。