Class: Zif::Actions::Action

Inherits:
Object
  • Object
show all
Includes:
Serializable
Defined in:
lib/zif/actions/action.rb

Overview

Inspired by developer.apple.com/documentation/spritekit/skaction

and Squirrel Eiserloh’s GDC talk on nonlinear transformations www.youtube.com/watch?v=mr5xkf6zSzk

A transition of a set of attributes over time using an easing function (aka tweening, easing) Meant to be applied to an object using the Actionable mixin

Constant Summary collapse

REPEAT_NAMES =

A list of convenient names for repeat counts

{
  once:    1,
  twice:   2,
  thrice:  3,
  forever: Float::INFINITY,
  always:  Float::INFINITY
}.freeze
EASING_FUNCS =
%i[
  immediate linear flip
  smooth_start smooth_start3 smooth_start4 smooth_start5
  smooth_stop smooth_stop3 smooth_stop4 smooth_stop5
  smooth_step smooth_step3 smooth_step4 smooth_step5
].freeze
ROUNDING_FUNCS =
%i[ceil floor round none].freeze

Instance Attribute Summary collapse

1. Public Interface collapse

2. Private-ish methods collapse

3. Easing Functions collapse

Methods included from Serializable

#exclude_from_serialize, #inspect, #serialize, #to_s

Constructor Details

#initialize(node, finish, follow: nil, duration: 1.seconds, easing: :linear, rounding: :round, repeat: 1, &block) ⇒ Action

Note:

Important! Each key in the finish hash must map to an accessible attribute on the node.

(:key getter and :key= setter, as you get with an attr_accessor, but could also be defined manually. See Layers::Camera#pos_x= for an example of a manually defined Actionable attribute)

rubocop:disable Metrics/PerceivedComplexity rubocop:disable Layout/LineLength

Examples:

Detailed explanation

# dragon is a Zif::Sprite (and therefore a Zif::Actions::Actionable, but any class that includes Actionable
# can receive an Action like this.  A non-Sprite example is Zif::Layers::Camera).
# The initial x position is being set to 200 here:
dragon.x = 200

# The ActionService is essential for this.  Every tick, it's going to check the list of registered Actionable
# objects for any running Actions.  If it has one, it will tell that Action a tick has passed.  The Action
# then knows to update the node it was run on, based on the conditions specified by this constructor.
#
# For this example, assume that Zif::Services::ActionService has been set up and is available at:
#   $game.services.named(:action_service)
# Now we need to tell it that our dragon needs to be checked every tick for actions.  This only needs to be
# done once, it will stay in the list of Actionables to check until removed.
$game.services.named(:action_service).register_actionable(dragon)

# Create an action, the plan is to move the dragon to x == 300 over 2 seconds
# Notice that we don't have to specify the start conditions (x==200).
# Action will save the start conditions when created.
move_to_300_action = Zif::Actions::Action.new(
  dragon,
  {x: 300},
  duration: 2.seconds,
  easing: :linear,
  rounding: :round
)

# A plan is no good without execution.  The dragon is a Zif::Sprite, so it has the methods defined by
# Zif::Actions::Actionable, including #run_action.  This tells the action it's starting now (on this tick).
dragon.run_action(move_to_300_action)

# Now we wait.  Every tick, ActionService will inspect dragon, it will find the running action, and slowly
# move the dragon to x == 300.  Because the initial condition is x == 200, and the action should run for 2
# seconds, and the easing function is linear, that means on each tick it needs to move the dragon to:
#
# 200 + (ticks_since_run * (300 - 200)).fdiv(2*60)
#
# So, next tick (ticks_since_run==1) it will be at:
#
# 200 + ((1 * 100) / 120) = 200.833...
#
# We've specified :round as the rounding function, so this gets rounded to 201.
#
# After 120 ticks, the dragon will be at x == 300.  The Action recognizes it's complete and removes itself
# from the list of running actions on dragon.  If we had passed a block to Zif::Actions::Action.new, that
# block would execute at this point.

Parameters:

  • node (Zif::Actions::Actionable)

    The node (Actionable object) the action should be run on

  • finish (Hash)

    A hash representing the end state of the node at the end of the action.

  • follow (Object) (defaults to: nil)

    Another object to follow. The finish condition will be reset each tick by the follow object’s value for the provided keys.

  • duration (Numeric) (defaults to: 1.seconds)
  • easing (Symbol) (defaults to: :linear)

    (see EASING_FUNCS)

  • rounding (Symbol) (defaults to: :round)
  • repeat (Integer, Symbol) (defaults to: 1)

    (see REPEAT_NAMES for valid symbols)

  • block (Block)

    Callback to perform when action completes



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/zif/actions/action.rb', line 133

def initialize(
  node,
  finish,
  follow:   nil,
  duration: 1.seconds,
  easing:   :linear,
  rounding: :round,
  repeat:   1,
  &block
)
  unless node.is_a? Zif::Actions::Actionable
    raise ArgumentError, "Invalid node: #{node}, expected a Zif::Actions::Actionable"
  end

  @node = node
  @follow = follow

  unless EASING_FUNCS.include? easing
    raise ArgumentError, "Invalid easing function: '#{easing}'.  Must be in #{EASING_FUNCS}"
  end

  @easing = easing

  unless ROUNDING_FUNCS.include? rounding
    raise ArgumentError, "Invalid rounding function: '#{rounding}'.  Must be in #{ROUNDING_FUNCS}"
  end

  @rounding = rounding

  @start = {}
  finish.each_key do |key|
    [key, "#{key}="].each do |req_meth|
      unless @node.respond_to?(req_meth)
        raise ArgumentError, "Invalid finish condition: #{@node} doesn't have a method named '##{req_meth}'"
      end
    end
  end

  if @follow
    finish.each do |key, val|
      unless val.is_a? Symbol
        raise ArgumentError, "You provided an object to follow. A Symbol was expected instead of '#{val}' (#{val.class}) for the key-value pair (#{key}: #{val}) in the finish condition. Action needs this symbol to be the name of a method on the followed object (#{@follow.class})"
      end
      unless @follow.respond_to?(val)
        raise ArgumentError, "You provided an object to follow, but it doesn't respond to '##{val}' (for finish '#{key}')"
      end
    end
  end

  @finish = finish
  @finish_early = false
  reset_start

  @repeat = REPEAT_NAMES[repeat] || repeat
  @duration = [duration.to_i, 1].max # in ticks

  @callback = block if block_given?

  # puts "Action: #{@start} -> #{@finish} in #{@duration} using #{@easing}.  Block present? #{block_given?}"
  reset_duration
end

Instance Attribute Details

#callbackProc

Returns A callback to run at the end of the action.

Returns:

  • (Proc)

    A callback to run at the end of the action



25
26
27
# File 'lib/zif/actions/action.rb', line 25

def callback
  @callback
end

#dirtyBoolean (readonly)

Returns True if this action caused a change on the node during this tick.

Returns:

  • (Boolean)

    True if this action caused a change on the node during this tick



40
41
42
# File 'lib/zif/actions/action.rb', line 40

def dirty
  @dirty
end

#durationInteger

Returns The number of ticks it will take to reach the finish condition.

Returns:

  • (Integer)

    The number of ticks it will take to reach the finish condition



34
35
36
# File 'lib/zif/actions/action.rb', line 34

def duration
  @duration
end

#easingSymbol

Returns Method name of the easing function to apply over the duration, (see EASING_FUNCS).

Returns:

  • (Symbol)

    Method name of the easing function to apply over the duration, (see EASING_FUNCS)



28
29
30
# File 'lib/zif/actions/action.rb', line 28

def easing
  @easing
end

#finishHash<Symbol, Object>

Returns Key-value pair of attributes being acted upon, and their final state. The Symbol keys must represent a getter and setter on the node. These can be traditional attributes defined using attr_accessor, or manually defined e.g. def x=(new_x) ..

Returns:

  • (Hash<Symbol, Object>)

    Key-value pair of attributes being acted upon, and their final state. The Symbol keys must represent a getter and setter on the node. These can be traditional attributes defined using attr_accessor, or manually defined e.g. def x=(new_x) ..



19
20
21
# File 'lib/zif/actions/action.rb', line 19

def finish
  @finish
end

#finish_earlyBoolean

Returns Set this to true to override the normal duration and complete this iteration on next tick.

Returns:

  • (Boolean)

    Set this to true to override the normal duration and complete this iteration on next tick



46
47
48
# File 'lib/zif/actions/action.rb', line 46

def finish_early
  @finish_early
end

#followObject

Returns An object being followed. Reset #finish based on this object’s values each tick.

Returns:

  • (Object)

    An object being followed. Reset #finish based on this object’s values each tick.



22
23
24
# File 'lib/zif/actions/action.rb', line 22

def follow
  @follow
end

#repeatNumeric

Returns The number of times this will repeat.

Returns:

  • (Numeric)

    The number of times this will repeat



31
32
33
# File 'lib/zif/actions/action.rb', line 31

def repeat
  @repeat
end

#roundingSymbol

Returns The rounding strategy for values being adjusted during the action (see ROUNDING_FUNCS).

Returns:

  • (Symbol)

    The rounding strategy for values being adjusted during the action (see ROUNDING_FUNCS)



43
44
45
# File 'lib/zif/actions/action.rb', line 43

def rounding
  @rounding
end

#startHash

Returns The start conditions for the keys referenced in #finish.

Returns:

  • (Hash)

    The start conditions for the keys referenced in #finish



13
14
15
# File 'lib/zif/actions/action.rb', line 13

def start
  @start
end

#started_atInteger

Returns Set to $gtk.args.tick_count - 1 when created / started.

Returns:

  • (Integer)

    Set to $gtk.args.tick_count - 1 when created / started



37
38
39
# File 'lib/zif/actions/action.rb', line 37

def started_at
  @started_at
end

Instance Method Details

#complete?Boolean

Returns True if there are no more #repeats left on this action.

Returns:

  • (Boolean)

    True if there are no more #repeats left on this action



226
227
228
229
# File 'lib/zif/actions/action.rb', line 226

def complete?
  # puts "Action#complete?: Action complete! #{self.inspect} #{@node.class}" if @repeat.zero?
  @repeat.zero?
end

#crossfade(a = :linear, b = :linear, x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



309
310
311
# File 'lib/zif/actions/action.rb', line 309

def crossfade(a=:linear, b=:linear, x=progress)
  mix(a, b, x, x)
end

#ease(start_val, finish_val) ⇒ Numeric

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a value between start_val and finish_val based on function specified by #easing

Parameters:

  • start_val (Numeric)
  • finish_val (Numeric)

Returns:

  • (Numeric)

    Returns a value between start_val and finish_val based on function specified by #easing



273
274
275
# File 'lib/zif/actions/action.rb', line 273

def ease(start_val, finish_val)
  ((finish_val - start_val) * send(@easing)) + start_val
end

#finish_early!Object

Forces the easing to finish on the next #perform_tick, ignoring duration



211
212
213
# File 'lib/zif/actions/action.rb', line 211

def finish_early!
  @finish_early = true
end

#flip(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



299
300
301
# File 'lib/zif/actions/action.rb', line 299

def flip(x=progress)
  1 - x
end

#immediate(_x = nil) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



289
290
291
# File 'lib/zif/actions/action.rb', line 289

def immediate(_x=nil)
  1.0
end

#iteration_complete?Boolean

Returns True if #progress is 1.0.

Returns:



221
222
223
# File 'lib/zif/actions/action.rb', line 221

def iteration_complete?
  progress >= 1.0
end

#linear(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



294
295
296
# File 'lib/zif/actions/action.rb', line 294

def linear(x=progress)
  x
end

#mix(a = :linear, b = :linear, rate = 0.5, x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



304
305
306
# File 'lib/zif/actions/action.rb', line 304

def mix(a=:linear, b=:linear, rate=0.5, x=progress)
  (1 - rate) * send(a, x) + rate * send(b, x)
end

#perform_callbackObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Calls #callback with self



279
280
281
282
# File 'lib/zif/actions/action.rb', line 279

def perform_callback
  # puts "Action#perform_callback: Callback triggered"
  @callback.call(self)
end

#perform_tickBoolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Performs one tick’s worth of easing on all attributes specified by #finish conditions. Sets #dirty to true if something changed. Calls #callback if finished.

Returns:



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/zif/actions/action.rb', line 238

def perform_tick
  @dirty = false
  @finish.each do |key, val|
    target = @follow ? @follow.send(val) : val
    start = @node.send(key)
    # puts "  easing #{key} #{start} -> #{val}"
    if start.is_a? Numeric
      change_to = ease(@start[key], target)
      change_to = change_to.send(@rounding) unless @rounding == :none
    else
      change_to = target
    end
    @dirty = true if start != change_to

    # puts "  assigning #{key}= #{change_to}"
    @node.send("#{key}=", change_to)
  end

  # puts "iteration_complete? : #{iteration_complete?}, duration: #{@duration}, repeat: #{@repeat}"

  if iteration_complete?
    @finish_early = false
    @repeat -= 1
    reset_duration
  end

  perform_callback if @callback && complete?

  @dirty
end

#progressFloat

Returns 0.0 -> 1.0 Percentage of duration passed.

Returns:

  • (Float)

    0.0 -> 1.0 Percentage of duration passed.



216
217
218
# File 'lib/zif/actions/action.rb', line 216

def progress
  @finish_early ? 1.0 : ($gtk.args.tick_count - @started_at).fdiv(@duration)
end

#reset_durationObject

Resets #started_at to the current tick



206
207
208
# File 'lib/zif/actions/action.rb', line 206

def reset_duration
  @started_at = $gtk.args.tick_count - 1
end

#reset_startObject

Recalculates the start conditions for the action based on node state. Easing is calculated as difference between start and finish conditions over time.



199
200
201
202
203
# File 'lib/zif/actions/action.rb', line 199

def reset_start
  @finish.each_key do |key|
    @start[key] = @node.send(key)
  end
end

#smooth_start(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



314
315
316
# File 'lib/zif/actions/action.rb', line 314

def smooth_start(x=progress)
  x * x
end

#smooth_start3(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



319
320
321
# File 'lib/zif/actions/action.rb', line 319

def smooth_start3(x=progress)
  x * x * x
end

#smooth_start4(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



324
325
326
# File 'lib/zif/actions/action.rb', line 324

def smooth_start4(x=progress)
  x * x * x * x * x
end

#smooth_start5(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



329
330
331
# File 'lib/zif/actions/action.rb', line 329

def smooth_start5(x=progress)
  x * x * x * x * x * x
end

#smooth_step(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



354
355
356
# File 'lib/zif/actions/action.rb', line 354

def smooth_step(x=progress)
  crossfade(:smooth_start, :smooth_stop, x)
end

#smooth_step3(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



359
360
361
# File 'lib/zif/actions/action.rb', line 359

def smooth_step3(x=progress)
  crossfade(:smooth_start3, :smooth_stop3, x)
end

#smooth_step4(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



364
365
366
# File 'lib/zif/actions/action.rb', line 364

def smooth_step4(x=progress)
  crossfade(:smooth_start4, :smooth_stop4, x)
end

#smooth_step5(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



369
370
371
# File 'lib/zif/actions/action.rb', line 369

def smooth_step5(x=progress)
  crossfade(:smooth_start5, :smooth_stop5, x)
end

#smooth_stop(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



334
335
336
# File 'lib/zif/actions/action.rb', line 334

def smooth_stop(x=progress)
  flip(smooth_start(flip(x)))
end

#smooth_stop3(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



339
340
341
# File 'lib/zif/actions/action.rb', line 339

def smooth_stop3(x=progress)
  flip(smooth_start3(flip(x)))
end

#smooth_stop4(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



344
345
346
# File 'lib/zif/actions/action.rb', line 344

def smooth_stop4(x=progress)
  flip(smooth_start4(flip(x)))
end

#smooth_stop5(x = progress) ⇒ Object

Note:

Meant to be called indirectly via setting #easing



349
350
351
# File 'lib/zif/actions/action.rb', line 349

def smooth_stop5(x=progress)
  flip(smooth_start5(flip(x)))
end