DragonRuby: Smoothly Growing and Shrinking Sprites

This post is about DragonRuby, a Ruby implementation for writing games. Check it out!

Starting Off

Let’s start simply by rendering a sprite in the middle of the window. For convenience, the program also renders guidelines marking the center of the window (for brevity, not showing this in later code). Also for convenience, this code uses a sprite included with the DragonRuby distribution.

def tick args
  # Double size of the original sprite, in pixels
  width = 80 * 2
  height = 80 * 2

  # Output sprite in middle of the window
  args.outputs.sprites << {
    x: args.grid.center_x, # Horizontal center
    y: args.grid.center_y, # Vertical center
    w: width,
    h: height,
    path: 'sprites/hexagon/red.png',
  }

  # Add grid lines to mark center of window
  args.outputs.lines << {
    x: 0,
    y: args.grid.center_y,
    x2: args.grid.w,
    y2: args.grid.center_y
  } <<
  {
    x: args.grid.center_x,
    y: 0,
    x2: args.grid.center_x,
    y2: args.grid.h
  }
end

Off-center rendering of a red hexagon

As you can see, we’ve perfectly rendered a sprite in the center of the -

Wait, that’s not quite right. What happened?

Anchoring

In DragonRuby, the (0, 0) coordinate is the bottom-left of the window. Similarly, when setting x and y location for a sprite, those correspond to the bottom-left of the sprite.

This could be solved with some math like x = x + (sprite.w / 2) to adjust the sprite appropriately. But this will cause complications later when resizing the sprite.

Preview of the problem:

Hexagon growing and shrinking off-center

This might be what you want, but for this post we want the sprite to expand/shrink from the center.

A simpler approach that trying to move x and y around is to instead use the anchor_x and anchor_y attributes.

Important note: anchor_* attributes were introduced in DragonRuby 4.8.

These are very similar to the angle_anchor_* attributes discussed previously.

The values used for the anchors are a percentage of the width or height of the sprite. This diagram from the earlier post might help:

Angle anchors on a black rectangle

The default “anchors” are essentially at (0, 0) (technically, they are nil, but never mind that).

The center of the sprite is at (0.5, 0.5).

In code:

def tick args
  # Double size of the original sprite
  width = 80 * 2
  height = 80 * 2

  # Actually render sprite in middle of the window
  args.outputs.sprites << {
    x: args.grid.center_x,
    y: args.grid.center_y,
    w: width,
    h: height,
    anchor_x: 0.5,
    anchor_y: 0.5,
    path: 'sprites/hexagon/red.png',
  }
end

Red hexagon centered in window

There we go! Digression over, back to squishing and stretching this sprite.

“Simple” Approach

For the first approach, let’s do this:

  • Set a rate of growth (e.g. 1 pixel per tick)
  • Set a target size
  • Grow (or shrink) the size on each tick
  • When the target size is met, reverse direction

To avoid some duplication, this code uses the same size for width and height. Adjust as desired.

def tick args
  # How big to make the sprite
  target_size = 80 * 2

  # Current size of the sprite
  args.state.size ||= 0

  # How fast to grow
  args.state.growth_rate ||= 1

  # If the target size is reached, reverse
  if args.state.size >= target_size
    args.state.growth_rate = -1
  elsif args.state.size <= 0
    args.state.growth_rate = 1
  end

  # Grow (or shrink) the size
  args.state.size += args.state.growth_rate

  args.outputs.sprites << {
    x: args.grid.center_x,
    y: args.grid.center_y,
    w: args.state.size,
    h: args.state.size,
    path: 'sprites/hexagon/red.png',
    anchor_x: 0.5,
    anchor_y: 0.5,
  }
end

Result:

Red hexagon growing and shrinking in the center of the window

Nailed it. Post over..?

Easing In

Instead of calculating the growth rate “by hand,” wouldn’t it be nice if a function could do that for us? Maybe even have the ability to vary the growth rate over time?

args.easing.ease is here for that very reason.

(This video, linked in the DragonRuby docs, is really good. Plus it also will explain the names of the easing functions used below.)

args.easing.ease will return a “percentage” value (between 0 and 1) which can be multiplied against the target value to get the current value. For the example here, that means it will calculate the current percentage of the size of the the sprite.

def tick args
  target_size = 80 * 2
  duration = 60

  args.state.start_time ||= 0
  args.state.easing_function = :identity

  # Calculate percentage (0 to 1) of progress based
  # on the start time, current time, duration, and easing function
  percentage = args.easing.ease args.state.start_time,
                                args.state.tick_count,
                                duration,                         
                                args.state.easing_function

  # Output the scaled image
  args.outputs.sprites << {
    x: args.grid.center_x,
    y: args.grid.center_y,
    w: target_size * percentage,
    h: target_size * percentage,
    path: 'sprites/hexagon/red.png',
    anchor_x: 0.5,
    anchor_y: 0.5,
  }
end

This code uses the :identity function which is linear - essentially the same as the earlier code that adds a constant “growth rate” on each tick.

Here is the result:

Red hexagon growing, but not shrinking

Oops, forgot to shrink it back down!

Easing Out

The code is going to get slightly more complicated now. Instead of passing in a single easing function, the new code uses an array of function names. The array is “splatted” into args.ease.easing.

When the set duration is up, the code adds :flip to the list of functions. Instead of going from 0 to 1, the percentage will now go from 1 to 0.

When that’s over, :flip is removed from the list and it starts all over.

def tick args
  target_size = 80 * 2
  duration = 60

  args.state.start_time ||= 0
                                                                               
  # List of easing functions                                                   
  args.state.easing_functions ||= [:identity]                                  
                                                                               
  # Calculate percentage (0 to 1) of progress based                            
  # on the start time, current time, duration, and easing function(s)          
  percentage = args.easing.ease args.state.start_time,                         
                                      args.state.tick_count,                   
                                      duration,                                
                                      *args.state.easing_functions             
                                                                               
  # When we reach the end of the duration, switch direction                    
  if args.state.tick_count == args.state.start_time + duration                 
    # Reset the start time for the easing function
    args.state.start_time = args.state.tick_count                              
                                                                               
    if args.state.easing_functions == [:identity]                              
      args.state.easing_functions = [:identity, :flip]                         
    else                                                                       
      args.state.easing_functions = [:identity]                                
    end                                                                        
  end                                                                          
                                                                               
  # Output the scaled image                                                    
  args.outputs.sprites << {                                                    
    x: args.grid.center_x,                                                     
    y: args.grid.center_y,                                                     
    w: target_size * percentage,                                               
    h: target_size * percentage,                                               
    path: 'sprites/hexagon/red.png',                                           
    anchor_x: 0.5,                                                             
    anchor_y: 0.5,                                                             
  }
end

Red hexagon growing and shrinking

One way to think of [:identity, :flip] is like this:

:identity is f(x) = x and :identity, :flip is g(x) = 1 - f(x). :flip can be used to “reverse” any function.

More Easing

This may not be very exciting, but keep in mind there are several pre-defined easing functions:

  • :identity (f(x) = x)
  • :quad (f(x) = x^2)
  • :cube (f(x) = x^3)
  • :quint (f(x) = x^4)
  • :smooth_start_quad (same as :quad)
  • :smooth_start_cube (same as :cube)
  • :smooth_start_quart (same as :quart)
  • :smooth_start_quint (same as :quint)
  • :smooth_stop_quad (f(x) = 1 - (1 - x)^2)
  • :smooth_stop_cube (f(x) = 1 - (1 - x)^3)
  • :smooth_stop_quart (f(x) = 1 - (1 - x)^4)
  • :smooth_stop_quint (f(x) = 1 - (1 - x)^5)

Mix and match as you’d like… for example, here’s growing with :cube but shrinking with :quint:

def tick args
  target_size = 80 * 2
  duration = 60

  args.state.start_time ||= 0
                                                                               
  # List of easing functions                                                   
  args.state.easing_functions ||= [:cube]                                  
                                                                               
  # Calculate percentage (0 to 1) of progress based                            
  # on the start time, current time, duration, and easing function(s)          
  percentage = args.easing.ease args.state.start_time,                         
                                args.state.tick_count,                   
                                duration,                                
                                *args.state.easing_functions             
                                                                               
  # When we reach the end of the duration, switch direction                    
  if args.state.tick_count == args.state.start_time + duration                 
    args.state.start_time = args.state.tick_count                              
                                                                               
    if args.state.easing_functions == [:cube]                              
      args.state.easing_functions = [:quint, :flip]                         
    else                                                                       
      args.state.easing_functions = [:cube]                                
    end                                                                        
  end                                                                          
                                                                               
  # Output the scaled image                                                    
  args.outputs.sprites << {                                                    
    x: args.grid.center_x,                                                     
    y: args.grid.center_y,                                                     
    w: target_size * percentage,                                               
    h: target_size * percentage,                                               
    path: 'sprites/hexagon/red.png',                                           
    anchor_x: 0.5,                                                             
    anchor_y: 0.5,                                                             
  }
end

Hexagon growing, but then shrinking faster than it grew

Closing Out

This post demonstrates two concepts: changing the anchors for a sprite, and using “easing” to set the size of a sprite.

Easing is a general-purpose concept that can be used for any applications, such as smooth movement.

Keep in mind, you can also:

Have fun!

DragonRuby: Moving in Arbitrary Directions

In a previous post we looked at rotating rectangles in DragonRuby.

Now let’s take that one step further to try turning and moving!

In this post, we’ll look at very simple movement of a spaceship.

Setup

First, let’s get the state all ready.

The code below puts the sprite of a ship in the middle of the screen.

def tick args
  # Setting up initial state
  args.state.ship.w ||= 50
  args.state.ship.h ||= 50
  args.state.ship.x ||= args.grid.center_x - (args.state.ship.w / 2)
  args.state.ship.y ||= args.grid.center_y - (args.state.ship.h / 2)
  args.state.ship.angle ||= 0
  args.state.ship.speed ||= 0

  # Show the ship
  args.outputs.sprites << {
    x: args.state.ship.x,
    y: args.state.ship.y,
    w: args.state.ship.w,
    h: args.state.ship.h,
    path: 'sprites/ship.png',
    angle: args.state.ship.angle,
  }
end

Simple ship in the middle of the window

Not too exciting thus far, but we’ll make it better.

Rotation

For rotation, we’ll turn the ship 2.5 degrees when the left or right arrow keys are pressed or held down.

def tick args
  # Setting up initial state
  args.state.ship.w ||= 50
  args.state.ship.h ||= 50
  args.state.ship.x ||= args.grid.center_x - (args.state.ship.w / 2)
  args.state.ship.y ||= args.grid.center_y - (args.state.ship.h / 2)
  args.state.ship.angle ||= 0 
  args.state.ship.speed ||= 0 

  # Turn left and right
  if args.inputs.keyboard.right
    args.state.ship.angle += 2.5
  elsif args.inputs.keyboard.left
    args.state.ship.angle -= 2.5
  end

  # Keep angle between 0 and 360
  args.state.ship.angle %= 360

  # Show the ship
  args.outputs.sprites << {
    x: args.state.ship.x,
    y: args.state.ship.y,
    w: args.state.ship.w,
    h: args.state.ship.h,
    path: 'sprites/ship.png',
    angle: args.state.ship.angle,
  }
end

Rotating spaceship

Important to note DragonRuby puts 0 straight to the right or “due east”, with increasing angles rotating counter-clockwise.

Angles in DragonRuby

Acceleration

Still keeping things simple, let’s accelerate the ship when the up arrow is held down and decelerate otherwise.

For now we won’t worry about what direction the ship is heading.

def tick args
  # Setting up initial state
  args.state.ship.w ||= 50
  args.state.ship.h ||= 50
  args.state.ship.x ||= args.grid.center_x - (args.state.ship.w / 2)
  args.state.ship.y ||= args.grid.center_y - (args.state.ship.h / 2)
  args.state.ship.angle ||= 0
  args.state.ship.speed ||= 0

  # Accelerate with up arrow, otherwise decelerate
  if args.inputs.keyboard.up
    args.state.ship.speed += 0.2
  else
    args.state.ship.speed -= 0.1
  end

  # Keep speed between 0 and 10
  args.state.ship.speed = args.state.ship.speed.clamp(0, 10)

  # Turn left and right
  if args.inputs.keyboard.right
    args.state.ship.angle -= 2.5
  elsif args.inputs.keyboard.left
    args.state.ship.angle += 2.5
  end

  # Keep angle between 0 and 360
  args.state.ship.angle %= 360

  # Go?
  args.state.ship.x += args.state.ship.speed

  # Show the ship
  args.outputs.sprites << {
    x: args.state.ship.x,
    y: args.state.ship.y,
    w: args.state.ship.w,
    h: args.state.ship.h,
    path: 'sprites/ship.png',
    angle: args.state.ship.angle,
  }
end

Ship moving to the right

Moving in Arbitrary Directions

We are almost there!

In the code above, we only update the ship’s x position. This is just to make sure we have the acceleration/deceleration working how we’d like.

But what we really want is to go in the direction the ship is pointing!

To do so, we need to update both the x and y position, proportional to the angle and speed… ugh that sounds like we might need some math! Trigonometry even??

Actually, DragonRuby comes to the rescue here! Integer#vector will return a unit vector ([x, y] where x and y are between -1 and 1) corresponding to the angle.

For example:

0.vector # => [1.0, 0.0]

So at an angle of 0 degrees, only the x direction is affected.

But all we really need to know is that angle.vector_x and angle.vector_y will give us the “magnitude” we need to convert speed and angle to x and y distance:

  args.state.ship.x += args.state.ship.speed * args.state.ship.angle.vector_x
  args.state.ship.y += args.state.ship.speed * args.state.ship.angle.vector_y

Putting that into context:

def tick args
  # Setting up initial state
  args.state.ship.w ||= 50
  args.state.ship.h ||= 50
  args.state.ship.x ||= args.grid.center_x - (args.state.ship.w / 2)
  args.state.ship.y ||= args.grid.center_y - (args.state.ship.h / 2)
  args.state.ship.angle ||= 0
  args.state.ship.speed ||= 0

  # Accelerate with up arrow, otherwise decelerate
  if args.inputs.keyboard.up
    args.state.ship.speed += 0.2
  else
    args.state.ship.speed -= 0.1
  end

  # Keep speed between 0 and 10
  args.state.ship.speed = args.state.ship.speed.clamp(0, 10)

  # Turn left and right
  if args.inputs.keyboard.right
    args.state.ship.angle -= 2.5
  elsif args.inputs.keyboard.left
    args.state.ship.angle += 2.5
  end

  # Keep angle between 0 and 360
  args.state.ship.angle %= 360

  # Go in the correct direction!
  args.state.ship.x += args.state.ship.speed * args.state.ship.angle.vector_x
  args.state.ship.y += args.state.ship.speed * args.state.ship.angle.vector_y

  # Show the ship
  args.outputs.sprites << {
    x: args.state.ship.x,
    y: args.state.ship.y,
    w: args.state.ship.w,
    h: args.state.ship.h,
    path: 'sprites/ship.png',
    angle: args.state.ship.angle,
  }
end

Spaceship flying around

Staying Within Bounds

Just to round this out, let’s do Astroids-style wrap-around to keep the ship on the screen.

args.grid.right is the width of the screen and args.grid.top can be used for the height.

def tick args
  # Setting up initial state
  args.state.ship.w ||= 50
  args.state.ship.h ||= 50
  args.state.ship.x ||= args.grid.center_x - (args.state.ship.w / 2)
  args.state.ship.y ||= args.grid.center_y - (args.state.ship.h / 2)
  args.state.ship.angle ||= 0
  args.state.ship.speed ||= 0

  # Accelerate with up arrow, otherwise decelerate
  if args.inputs.keyboard.up
    args.state.ship.speed += 0.2
  else
    args.state.ship.speed -= 0.1
  end

  # Keep speed between 0 and 10
  args.state.ship.speed = args.state.ship.speed.clamp(0, 10)

  # Turn left and right
  if args.inputs.keyboard.right
    args.state.ship.angle -= 2.5
  elsif args.inputs.keyboard.left
    args.state.ship.angle += 2.5
  end

  # Keep angle between 0 and 360
  args.state.ship.angle %= 360

  # Go in the right direction!
  args.state.ship.x += args.state.ship.speed * args.state.ship.angle.vector_x
  args.state.ship.y += args.state.ship.speed * args.state.ship.angle.vector_y

  # Wrap around to keep the ship on the screen
  args.state.ship.x %= args.grid.right
  args.state.ship.y %= args.grid.top

  # Show the ship
  args.outputs.sprites << {
    x: args.state.ship.x,
    y: args.state.ship.y,
    w: args.state.ship.w,
    h: args.state.ship.h,
    path: 'sprites/ship.png',
    angle: args.state.ship.angle,
  }
end

Spaceship flying around but warping between edges

Conclusion

And there we go. In less than 50 lines of code and with no complicated math, we can move an object around in arbitrary directions!

DragonRuby: Basic Sprite Animation

Animating sprites in DragonRuby is fairly simple, but it does require putting a couple ideas together.

First, it’s best to have a single image with all frames of the animation together, equally spaced apart. I prefer the frames are arranged horizontally from left-to-right, so that is what we will use here.

Here is an example, borrowed from here:

Animation frames of a walking adventurer

The first frame can be displayed like this:

def tick args
  height = 195
  width = 192

  args.outputs.sprites << {
    x: args.grid.center_x - (width / 2),
    y: args.grid.center_y - (height / 2),
    h: height,
    w: width,
    source_x: 0,
    source_y: 0,
    source_w: width,
    source_h: height,
    path: 'sprites/walking.png',
  }
end

source_x and source_y set the bottom left corner of a “tile” or basically a slice of the image. (To use the top left instead, set tile_x and tile_y). source_w and source_h set the width and height of the tile. The sprite can be scaled when displayed with w and h.

Single frame of adventurer

If the frames are laid out horizontally, then all one needs to do is update the source_x value (typically by the width of the tile) in order to change the frame.

Here is an illustration for a few frames:

Frame index illustration

We could accomplish this by using the multiplying the width of the tile by the current tick (modulo the number of frames, so it loops):

def tick args
  height = 195
  width = 192
  num_frames = 8

  source_x = width * (args.tick_count % num_frames)

  args.outputs.sprites << {
    x: args.grid.center_x - (width / 2),
    y: args.grid.center_y - (height / 2),
    h: height,
    w: width,
    source_x: source_x,
    source_y: 0,
    source_w: width,
    source_h: height,
    path: 'sprites/walking.png',
  }
end

This works… but it’s a bit fast for a walk!

Very fast walk

This is where DragonRuby helps out. The frame_index method will do the calculation of the current frame for us.

frame_index accepts these arguments:

  • count: total number of frames in the animation
  • hold_for: how many ticks to wait between frames
  • repeat: whether or not to loop

frame_index can be called on any integer, but typically uses the tick number on which the animation started. Below, the code sets this to 0 (the first tick). This could instead be when an event happens, based on input, or anything else.

Multiplying the width of the tile by the frame index results in the source_x value for the current frame of the animation:

def tick args
  height = 195
  width = 192
  num_frames = 8
  start_tick = 0
  delay = 4

  source_x = width * start_tick.frame_index(count: num_frames, hold_for: delay, repeat: true)

  args.outputs.sprites << {
    x: args.grid.center_x - (width / 2),
    y: args.grid.center_y - (height / 2),
    h: height,
    w: width,
    source_x: source_x,
    source_y: 0,
    source_w: width,
    source_h: height,
    path: 'sprites/walking.png',
  }
end

Slower walk

And that’s it!

But With Ruby Classes

Once a game starts to get moderately complex, I like to arrange behavior into classes. It’s also convenient to use attr_gtk to avoid passing args around and to save on some typing (e.g. args.outputs becomes just outputs).

class MyGame
  attr_gtk

  def initialize(args)
    @my_sprite = MySprite.new(args.grid.center_x, args.grid.center_y)
    args.outputs.static_sprites << @my_sprite
  end

  def tick
    if inputs.mouse.click
      if @my_sprite.running?
        @my_sprite.stop
      else
        @my_sprite.start(args.state.tick_count)
      end
    end

    @my_sprite.update
  end
end

class MySprite
  attr_sprite

  def initialize x, y
    @x = x
    @y = y

    @w = 192
    @h = 195
    @source_x = 0
    @source_y = 0
    @source_w = @w
    @source_h = @h
    @path = 'sprites/walking.png'

    @running = false
  end

  # Set @running to the current tick number
  # this is so the frame_index can use that as the
  # start of the animation timing.
  def start(tick_count)
    @running = tick_count
  end

  def stop
    @running = false
  end

  def running?
    @running
  end

  # Update source_x based on frame_index
  # if currently running
  def update
    if @running
      @source_x = @source_w * @running.frame_index(count: 8, hold_for: 4, repeat: true)
    end
  end
end

def tick args
  $my_game ||= MyGame.new(args)
  $my_game.args = args
  $my_game.tick
end

This example essentially follows my Object-Oriented Starter approach and moves the logic into a game class and a sprite class.

When the mouse is clicked, the sprite starts moving (using the current tick_count as the starting tick). When the mouse is clicked again, the sprite stops.

Source vs. Tile

To use just a piece of an image (for animations or otherwise), there are two options: source_(x|y|h|w) or tile_(x|y|h|w).

These options are nearly identical, except source_y is bottom left and tile_y is top left.

The source_ options were added in DragonRuby 1.6 and are more consistent with the rest of DragonRuby where the origin is the bottom left. On the other hand, the tile_ options align easier with image editors.

Either option works, depending on what is important to you.

Go!

Now that’s really it! Get moving!