#encoding:utf-8
#==============================================================================
# ■ 定时器v3.0 By_QQEat ————2022.01.16
#
# 说明:定时执行脚本, tween 动画等。
#
# [创建、释放等基础方法]
#
# @timer = Timer.new # 创建
# @timer.update(tag=nil) # 更新(传标签参数后, 则只更新指定标签的任务)
# @timer.dispose # 释放
# @timer.disposed? # 是否已释放?
# @timer.clear # 清空计时任务
# @timer.timer # 查看所有任务
#
# [添加任务]
#
# @timer.cancel(*tags) # 删除定时任务(支持正则表达式)
# @timer.after(delay, action, tag=nil)
# @timer.every(delay, action, count=nil, after=nil, tag=nil)
# @timer.during(delay, action, after=nil, tag=nil)
# @timer.script{|wait| ...; wait.call(n); ... }
# @timer.tween(delay, subject, target, method, after=nil, tag=nil, *args, &update)
# after/every/tween # [delay]参数支持整数/范围类型
#
# @timer.progress(tag) # 查看定时器的进度(0~1)
# @timer.has?(tag, include=false) # 是否有指定任务, 如果存在则返回任务, 不存在返回 nil
# # tag 支持正则表达式、字符串
# # include 表示是否以包含匹配(只在 tag 为字符串时有效)。
# Timer.tween_(t, a, b, m) # 类方法, 用来查看单步值
#
# [范例]
#
# # 添加一60帧后执行一次的定时
# @timer.after(60, proc { p 'after: 60帧后输出' })
#
# # 添加一个每50帧循环执行的定时
# @timer.every(50, proc {|n| p "every: 每50帧输出一次, 当前第#{n}次" })
#
# # 添加一个每50帧循环执行的定时, 循环次数为 4 次, 由于第一个时间参数为负数, 表示第一次循环不需要等待
# @timer.every(-50, proc { p 'every: 每50帧输出一次, 4次后结束' }, 4, proc{ p 'every: 执行完成' })
#
# # 添加持续执行 2.5 秒时间的任务(持续循环执行,运行时间超出指定秒数后,则不再执行第二次, 但第一次不会中断)
# @timer.during(2.5, proc { p '我会持续执行2.5秒' }, proc{ p 'during执行完成' })
#
# # 将附加块传入函数的内部创建Fiber,使用wait.call(n)不影响主线程进行等待,下面的代码相当于一个简化版的after集合。
# @timer.script{|wait|
# puts 1
# wait.call 60 # 等待60帧
# puts 2
# wait.call 60 # 等待60帧
# puts 3
# }
#
# # 上面的代码等同于下面的
# puts 1
# @timer.after(60, proc{
# puts 2
# @timer.after(60, proc{
# puts 3
# })
# })
#
#
# [Tween补间方法]
# [说明]
# tween 为补间(动画)方法, 用于对变量增减补间所用(绘制、显示等)
# 定时器流程为调用 subject 对象中的【实例变量/Hash#Key/方法[v/v=]】来调整直到
# 与 target 里存在的【实例变量/Hash#Key】值一致, 属性值只支持整数/小数。
# [渐变方式]
# 'linear'
# 'bezier'
# 'in-out-' + method
# 'out-in-' + method
# 'in-' + method
# 'out-' + method
# [渐变方法]
# 'quad/cubic/quart/quint/sine/expo/circ/back/bounce/elastic/bezier'
# [Bezier]
# 使用 bezier 补间方法需要在 tween 的 tag 参数后传入一个数组([x1, y1, x2, y2])
# 例:@timer.tween(10, s, t, 'bezier', nil, nil, [0,0,1,1])
# 或者:@timer.tween(10, s, t, CubicBezier.new(0,0,1,1))
# Bezier曲线图在线查看:https://cubic-bezier.com/
#
# # tween范例:修改哈希
# subject = {:x => 50, :y => 10, :other => { :float => 0.0 } }
# target = {:x => 300, :y => 100, :other => { :float => 10.0 } }
# @timer.tween(120, subject, target, 'in-out-cubic', proc { p 'tween: 执行完成' })
#
# # tween范例:修改实例变量
# class Test; attr_accessor :value; def initialize(v); @value = v; end; end
# subject = Test.new(50)
# @timer.tween(120, subject, {value: 100}, 'in-out-cubic', proc { p 'tween: 执行完成' })
#
# # tween范例:没实例变量则修改方法(需要两个方法同时存在, 如:x/x=)
# sprite = Sprite.new; sprite.bitmap = Bitmap.new(50,50)
# @timer.tween(120, sprite, {x: 100, y: 50, src_rect: {width: 25}}, 'linear', proc { p 'tween: 执行完成' })
#
# # tween范例:其他对象(此操作同理也是利用修改方法, x/x= y/y=)
# s = Struct.new(:x, :y)[0, 0]
# @timer.tween(120, s, {x: 100, y: 100}, 'linear', proc { p 'tween: 执行完成' })
#
# # tween范例:Target 也可以是非 Hash 的带有实例变量的对象(会检测target中所有实例变量来处理)
# class Test; attr_accessor :value; def initialize(v); @value = v; end; end
# t1 = Test.new(30); t2 = Test.new(100)
# @timer.tween(120, t1, t2, 'linear', proc { p 'tween: 执行完成' })
#
# # tween范例:每帧更新回调
# @timer.tween(120, Sprite.new, {x: 10, src_rect: {width: 50}}, 'linear'){|t, i, r, k, v|
# p "#{t}, #{i}, #{r}, #{k}, #{v}"
#
# # t 当前定时对象, t[:time] 即为当前帧
# # i (0当前帧更新前/1对象赋值前/2对方赋值后/3当前帧更新后)
# # r 操作的对象
# # k 操作的属性
# # v (要增加的值/增加后的值)
#
# }
#
# # tween范例:错误忽略
# $spr = Sprite.new
# @timer.tween(6000, $spr, {x: 10, _try_: true}, 'linear')
# # target里声明_try_为真, 则表示在计时器执行中忽略错误,
# # 这时在计时器执行中如果$spr执行了dispose方法,
# # 定时器则会忽略错误不会报错导致游戏停止运行。
#
# # tween范例(单步取值)
# Timer.tween_(0.3, 100, 200, 'linear') # 返回 100->200 使用 linear 方法 30% 进度的值
#
# [target参数特殊属性]
# _cb_(Proc):作用与 tween 的附加区块功能类似。
# 参数为(对象, 当前帧), 会在当前属性调用完毕后调用一次 _cb_ 。
#
# _try_(Boolean):设置为true后, 定时器的任务触发错误时不会停止。
#
# _before_(Hash):设置属性的初始值。
#
#==============================================================================
class Timer
#--------------------------------------------------------------------------
# ● config
#--------------------------------------------------------------------------
TWEEN_METHOD_ASYNCHRONOUS = false # 异步, 默认关(开启后多个tween操作同一对象的方法时, 所取得的属性是否独立/共用(默认))
#--------------------------------------------------------------------------
# ● attr
#--------------------------------------------------------------------------
attr_reader :timer
#--------------------------------------------------------------------------
# ● initialize
#--------------------------------------------------------------------------
def initialize
@timer = {}
@_attr = Hash.new{|h,k| h[k] = Hash.new{|h2,k2| h2[k2] = {} } } # attr->tag->obj->key
end
#--------------------------------------------------------------------------
# ● dispose / disposed?
#--------------------------------------------------------------------------
def dispose; return if disposed?; self.clear; @timer = @_attr = nil; end
def disposed?; self.timer.nil?; end
#--------------------------------------------------------------------------
# ● clear
#--------------------------------------------------------------------------
def clear
@_attr.clear if @_attr
self.timer.clear
end
#--------------------------------------------------------------------------
# ● progress
#--------------------------------------------------------------------------
def progress(tag)
return nil unless t = has?(tag)
return t[:time] / t[:delay].to_f
end
#--------------------------------------------------------------------------
# ● has?
#--------------------------------------------------------------------------
def has?(tag, include=false)
if tag.is_a?(String) then
if include then
self.timer.find{|k, v| k.include?(tag) }
else
self.timer[tag]
end
elsif tag.is_a?(Regexp) then
self.timer.find{|k, v| k =~ tag }
end
end
#--------------------------------------------------------------------------
# ● update
#--------------------------------------------------------------------------
def update(tags=nil)
tags = tags ? [tags].flatten : self.timer.keys
for tag in tags do
timer = has?(tag)
next unless timer
timer[:time] += 1
case timer[:type]
when :after
if timer[:time] >= timer[:delay] then
timer[:action].call
self.cancel(tag)
end
when :every
if timer[:time] >= timer[:delay] then
timer[:action].call timer[:counter], timer[:delay]
timer[:time] -= timer[:delay]
timer[:delay] = _getResolvedDelay(timer[:any_delay], true)
timer[:counter] += 1
if timer[:count] > 0 then
if timer[:counter] >= timer[:count] then
timer[:after].call if timer[:after] != nil
self.cancel(tag)
end
end
end
when :during
if Graphics.frame_count.to_f / Graphics.frame_rate - (timer[:time] -= 1) <= timer[:delay] then
timer[:action].call
else
timer[:after].call if timer[:after] != nil
self.cancel(tag)
end
when :tween
ps = timer[:time]/timer[:delay].to_f
s = _tween(timer[:method], ps > 1 ? 1 : ps, *timer[:args])
ds = s - timer[:last_s]
timer[:last_s] = s
timer[:update].call(timer, 0) if timer[:update]
for _,info in timer[:payload] do
ref,key,delta,initial,type,param = info
cb,try,before = param
add = delta*ds
timer[:update].call(timer, 1, ref, key, add) if timer[:update]
begin
_v = timer[:time] == 1 ? before[key] : nil
case type
when -1 # hash
_v = ref[key] unless _v
_v += add
# Accuracy
if timer[:time] >= timer[:delay] then
_v = _v.round(initial.class == Float ? initial.to_s.split('.')[1].size : 0)
end
ref[key] = _v
when 0 # variable
key = "@#{key}" unless key.to_s.include?('@')
_v = ref.instance_variable_get(key) unless _v
_v += add
# Accuracy
if timer[:time] >= timer[:delay] then
_v = _v.round(initial.class == Float ? initial.to_s.split('.')[1].size : 0)
end
ref.instance_variable_set(key, _v)
when 1 # method
ref_id = ref.__id__
data = @_attr[tag]
@_attr.values.find{|v| data[ref_id] = v[ref_id] } unless TWEEN_METHOD_ASYNCHRONOUS
data[ref_id][key] = _v if _v
_v = data[ref_id][key] ||= ref.__send__(key)
_v += add
# Accuracy
if timer[:time] >= timer[:delay] then
_v = _v.round(initial.class == Float ? initial.to_s.split('.')[1].size : 0)
end
ref.__send__("#{key}=", data[ref_id][key] = _v) # Fine-tuning
end
rescue Exception => e
raise e unless try
end
cb.call(ref, timer[:time]-1) if cb
timer[:update].call(timer, 2, ref, key, _v) if timer[:update]
end
timer[:update].call(timer, 3) if timer[:update]
if timer[:time] >= timer[:delay] then
timer[:after].call if timer[:after] != nil
self.cancel(tag)
end
end
end
end
#--------------------------------------------------------------------------
# ● cancel
#--------------------------------------------------------------------------
def cancel(*tags)
tags.each do |t|
if t.is_a?(Regexp) then
cancel(*self.timer.keys.select{|k| k =~ t })
else
@_attr.delete(t)
self.timer.delete(t)
end
end
end
#--------------------------------------------------------------------------
# ● after
#--------------------------------------------------------------------------
def after(delay, action, tag=nil)
tag ||= UUID()
self.cancel(tag)
self.timer[tag] = {
:tag=>tag,
:type=>:after,
:time=>0,
:delay=>_getResolvedDelay(delay),
:action=>action,
}
return tag
end
#--------------------------------------------------------------------------
# ● every
#--------------------------------------------------------------------------
def every(delay, action, count=nil, after=nil, tag=nil)
if count.is_a?(String) then
tag, count = count, 0
elsif count.is_a?(Fixnum) and after.is_a?(String) then
tag, after = after, nil
end
tag ||= UUID()
self.cancel(tag)
t = _getResolvedDelay(delay)
t = t.abs if first = t < 0 # The first cycle does not need to wait
self.timer[tag] = {
:tag=>tag,
:type=>:every,
:time=>first ? t : 0,
:any_delay=>delay,
:delay=>t,
:action=>action,
:counter=>0,
:count=>count || 0,
:after=>after,
}
return tag
end
#--------------------------------------------------------------------------
# ● during
#--------------------------------------------------------------------------
def during(delay, action, after=nil, tag=nil)
if after.is_a?(String) then
tag, after = after, nil
end
tag ||= UUID()
self.cancel(tag)
self.timer[tag] = {
:tag=>tag,
:type=>:during,
:time=>Graphics.frame_count.to_f / Graphics.frame_rate,
:delay=>delay,
:action=>action,
:after=>after,
}
return tag
end
#--------------------------------------------------------------------------
# ● script
#--------------------------------------------------------------------------
def script
return unless block_given?
f = Fiber.new{|wait| yield(wait) }
f.resume(proc{|t| self.after(t, proc{ f.resume }); Fiber.yield })
end
#--------------------------------------------------------------------------
# ● tween
#--------------------------------------------------------------------------
def tween(delay, subject, target, method, after=nil, tag=nil, *args, &update)
if after.is_a?(String) then
after, tag = nil, after
end
tag ||= UUID()
self.cancel(tag)
self.timer[tag] = {
:tag=>tag,
:type=>:tween,
:time=>0,
:delay=>_getResolvedDelay(delay),
:subject=>subject,
:target=>target,
:method=>method,
:update=>update,
:after=>after,
:args=>args,
:last_s=>0,
:payload=>tweenCollectPayload(subject, target, {}),
}
return tag
end
#--------------------------------------------------------------------------
# ● _getResolvedDelay
#--------------------------------------------------------------------------
def _getResolvedDelay(delay, abs=false)
case delay
when Numeric
return abs ? delay.abs : delay
when Range
a, b = delay.first, delay.last
return a+rand(b-a+(delay.exclude_end? ? 0 : 1))
end
end
#--------------------------------------------------------------------------
# ● tweenCollectPayload
#--------------------------------------------------------------------------
def tweenCollectPayload(subject, target, out)
unless target.is_a?(Hash) # object convert to hash.
hash = {}
target.instance_variables.each do |var|
hash[var.to_sym] = target.instance_variable_get(var)
end
target = hash
end
keys = target.select{|k, v| v.is_a?(Numeric) }.keys
before = target[:_before_] || {}
for k, v in target do
next if [:_cb_, :_try_, :_before_].include?(k) # callback ignore
# with instance attribute
ref = (subject.is_a?(Hash) ? subject[k] : subject.instance_variable_get('@' << k.to_s))
# with method(method/method=)
ref_ = nil
if ref.nil? then
ref = ref_ = begin
subject.__send__(k)
rescue Exception => e
puts e.message
raise RGSSError.new("#{self.to_s.insert(-2, '#tween')}: error from method `#{k}' in subject(#{subject}).\nThere are method(#{k}) in target, maybe not in subject ?")
end
end
if v.is_a?(Numeric) then
raise TypeError.new("#{self.to_s.insert(-2, '#tween')}: wrong class type `#{k}' in subject of (#{ref.class} for Numeric)") unless ref.is_a?(Numeric)
delta = v - (before[k] || ref)
out[out.size+1] = [
subject, k, delta, v,
(ref_.nil? ? (subject.is_a?(Hash) ? -1 : 0) : 1),
[
k == keys[-1] ? target[:_cb_] : nil,
target[:_try_],
before,
],
]
else # Hash or Object , Continue recursion
tweenCollectPayload(ref, v, out)
end
end
return out
end
#--------------------------------------------------------------------------
# ● tween step
#--------------------------------------------------------------------------
def self.tween_(t, a, b, m)
@@timer ||= self.new
t >= 1 ? b : @@timer.instance_eval{ _tween(m, t) } * (b-a) + a
end
#--------------------------------------------------------------------------
# ● _tween
#--------------------------------------------------------------------------
def _tween(method, *args)
t = {
:out => proc{|f| proc {|s,*a| 1 - f.call(1-s, *a) } },
:chain => proc{|f1,f2| proc {|s,*a| (s < 0.5 ? f1.call(2*s, *a) : 1 + f2.call(2*s-1, *a))*0.5 } },
:linear => proc{|s| s },
:quad => proc{|s| s ** 2 },
:cubic => proc{|s| s ** 3 },
:quart => proc{|s| s ** 4 },
:quint => proc{|s| s ** 5 },
:sine => proc{|s| 1-Math.cos(s*Math::PI/2) },
:expo => proc{|s| 2 ** (10*(s-1)) },
:circ => proc{|s| 1-Math.sqrt(1-s*s) },
:back => proc{|s, bounciness| bounciness ||= 1.70158; s*s*((bounciness+1)*s - bounciness) },
:bounce => proc{|s| a, b = 7.5625, 1/2.75; [a*s**2, a*(s-1.5*b)**2 + 0.75, a*(s-2.25*b)**2 + 0.9375, a*(s-2.625*b)**2 + 0.984375].min },
:elastic => proc{|s, amp, period| amp, period = (amp ? [1, amp].max : 1), (period ? period : 0.3); (-amp*Math.sin(2*Math::PI/period*(s-1) - Math.asin(1/amp)))*2**(10*(s-1)) },
:bezier => proc{|s, arr| CubicBezier.new(*arr).solve(s) },
}
if method.is_a?(CubicBezier)
return method.solve(args[0])
elsif method == 'linear' then
return t[:linear].call(*args)
elsif method == 'bezier' then
return t[:bezier].call(*args)
elsif method[0...7] == 'in-out-' then
met = t[method[7..-1].to_sym]
f1 = met
f2 = t[:out].call(met)
return t[:chain].call(f1, f2).call(*args)
elsif method[0...7] == 'out-in-' then
met = t[method[7..-1].to_sym]
f1 = t[:out].call(met)
f2 = met
return t[:chain].call(f1, f2).call(*args)
elsif method[0...4] == 'out-' then
met = t[method[4..-1].to_sym]
return t[:out].call(met).call(*args)
elsif method[0...3] == 'in-' then
met = t[method[3..-1].to_sym]
return met.call(*args)
end
end
#--------------------------------------------------------------------------
# ● UUID
#--------------------------------------------------------------------------
KEY = ('A'..'Z').to_a + (0..9).to_a
def UUID()
i = KEY.shuffle.first(10).join
return (self.timer[i].nil? ? i : UUID())
end
#--------------------------------------------------------------------------
# ● detection disposed?
#--------------------------------------------------------------------------
#~ old = {}
#~ %w(cancel after every during script tween progress clear has? update).each do |name|
#~ sym = name.to_sym
#~ old[sym] = instance_method(sym)
#~ define_method sym do |*args|
#~ raise RGSSError.new "disposed #{self.class} from :#{sym}" if disposed?
#~ old[sym].bind(self).call(*args)
#~ end
#~ end
private :_getResolvedDelay, :tweenCollectPayload, :_tween, :UUID
end
#==============================================================================
# ■ CubicBezier
#==============================================================================
class CubicBezier
private
def initialize(p1x, p1y, p2x, p2y)
@cx = 3.0 * p1x
@bx = 3.0 * (p2x - p1x) - @cx
@ax = 1.0 - @cx - @bx
@cy = 3.0 * p1y
@by = 3.0 * (p2y - p1y) - @cy
@ay = 1.0 - @cy - @by
end
def sampleCurveX(t)
((@ax * t + @bx) * t + @cx) * t
end
def sampleCurveY(t)
((@ay * t + @by) * t + @cy) * t
end
def sampleCurveDerivativeX(t)
(3.0 * @ax * t + 2.0 * @bx) * t + @cx
end
ZERO_LIMIT = 1e-6
def solveCurveX(x)
t2 = x
x2 = 0
derivative = 0
8.times do |i|
x2 = sampleCurveX(t2) - x
return t2 if x2.abs < ZERO_LIMIT
derivative = sampleCurveDerivativeX(t2)
break if derivative.abs < ZERO_LIMIT
t2 -= x2 / derivative
end
t1 = 1
t0 = 0
t2 = x
while t1 > t0
x2 = sampleCurveX(t2) - x
return t2 if x2.abs < ZERO_LIMIT
if x2 > 0
t1 = t2
else
t0 = t2
end
t2 = (t1 + t0) / 2
end
return t2
end
public
def solve(x)
sampleCurveY(solveCurveX(x))
end
end
#~ # bezier = CubicBezier.new(0,0,1,1)
#~ bezier = CubicBezier.new(0.25,0.1,0.25,1)
#~ s = Sprite.new
#~ b = s.bitmap = Bitmap.new(300,300)
#~ b.fill_rect(b.rect, Color.new(255,0,0))
#~ b.width.times do |i|
#~ arr = [i, (b.height-bezier.solve(i/b.width.to_f)*b.height).to_i]
#~ b.set_pixel(*arr, Color.new(255,255,255))
#~ end
#~ loop do
#~ Graphics.update
#~ Input.update
#~ end