SLIP INTO RUBY - UNDER THE HOOD PART 13: I BATTLED A TIMED PICTURE

In which Sprite_Battler, Sprite_Picture and Sprite_Timer go under the microscope.

  • Trihan
  • 01/23/2017 03:04 PM
  • 2570 views
Hello, sports fans! It's a new day, and that means that owing to my vastly-accelerated release schedule (allowing for time taken to approve the submissions) it's time for another exciting episode of



We're going to finish off the breakdown of the Sprite classes today, meaning we'll be covering Sprite_Battler, Sprite_Picture and Sprite_Timer. I'm going to be playing with a new idea, which is using images for examples rather than massive walls of text. Let me know if it works better or not. Without further ado, let's begin!

Sprite_Battler

This sprite, oddly enough, is used to display battlers. In battle. This is rocket science, right here. Much like Sprite_Character, these sprites are hooked up with an instance of Game_Battler and the sprite state changes automatically depending on the properties of the associated battler. Through a thorough explanation of each method, we'll see exactly how this works.

There's only one public instance variable in this class:

attr_accessor :battler


It's an attr_accessor (which if you'll remember from previous lessons means that you can assign to it or read it without having to explicitly code a method to do so) and it contains the Game_Battler object that the sprite represents.

def initialize(viewport, battler = nil)
    super(viewport)
    @battler = battler
    @battler_visible = false
    @effect_type = nil
    @effect_duration = 0
  end


The constructor, much like that of the character sprites, takes a viewport as a parameter, and also a parameter to pass in the associated battler, which defaults to nil.

First we call the constructor of the parent class, Sprite_Base. We set @battler to the passed-in battler object, and @battler_visible to false so initially it won't be visible on the screen. @effect_type is set to nil and @effect_duration to 0. We'll see these in action soon, but for now it's enough to know that effect type deals with the effect types we looked at in the breakdown of Game_Battler: appearing, disappearing, whitening, blinking, collapsing, boss collapse, and instant collapse. The duration is, obviously, how many frames the effect will last.

def dispose
    bitmap.dispose if bitmap
    super
  end


The dispose method disposes of the sprite's bitmap if it has one in memory, then calls the dispose method of the parent class. Characters don't do this because it's more likely that you'll need to reuse a bitmap on the map, but generally in battle once you dispose of a sprite you're not going to need it again, so this avoids memory leaks from retaining references that aren't in use.

def update
    super
    if @battler
      @use_sprite = @battler.use_sprite?
      if @use_sprite
        update_bitmap
        update_origin
        update_position
      end
      setup_new_effect
      setup_new_animation
      update_effect
    else
      self.bitmap = nil
      @effect_type = nil
    end
  end


The frame update method first calls the update method of the parent class, which updates animation and clears the class-wide checker arrays. @use_sprite is set to false if the battler is an actor, and true if it's an enemy (we looked at this back in the Game_Actor and Game_Enemy breakdowns). Then, if it's true, we call update_bitmap, update_origin and update_position, which we'll be looking at soon.

Regardless of whether the sprite is in use, we call setup_new_effect, setup_new_animation and update_effect. Those will be covered a little later.

If there is no battler, we set the battler sprite's bitmap and @effect_type to nil to free up memory from references, as we clearly no longer need them.

def update_bitmap
    new_bitmap = Cache.battler(@battler.battler_name, @battler.battler_hue)
    if bitmap != new_bitmap
      self.bitmap = new_bitmap
      init_visibility
    end
  end


This method updates the bitmap in the event that it changes (which it will if, say, we use a Transform Enemy command).

new_bitmap is set to the cached battler with the battler object's battler_name and battler_hue. If this object differs from that held in "bitmap", we set the sprite's bitmap property to new_bitmap, and call init_visibility, which we'll look at next.

def init_visibility
    @battler_visible = @battler.alive?
    self.opacity = 0 unless @battler_visible
  end


Pretty simple method to initialise visibility. @battler_visible is set to true if the battler is alive, then the sprite's opacity is set to 0 unless the battler is now visible.

def update_origin
    if bitmap
      self.ox = bitmap.width / 2
      self.oy = bitmap.height
    end
  end


This method updates the sprite's origin coordinates. If there's a bitmap set, we set the sprite's ox to half the bitmap's width, and oy to the bitmap's height, as shown below.



def update_position
    self.x = @battler.screen_x
    self.y = @battler.screen_y
    self.z = @battler.screen_z
  end


This method updates the battler sprite's position, and simply sets its x, y and z coordinates to match those of its associated battler.

def setup_new_effect
    if !@battler_visible && @battler.alive?
      start_effect(:appear)
    elsif @battler_visible && @battler.hidden?
      start_effect(:disappear)
    end
    if @battler_visible && @battler.sprite_effect_type
      start_effect(@battler.sprite_effect_type)
      @battler.sprite_effect_type = nil
    end
  end


This method sets up new effects as mentioned above.

If the battler is not visible, and it is alive, we call start_effect, passing in the symbol :appear. Otherwise, if the battler is visible and its hidden property is true, we call start_effect passing in the symbol :disappear.

If the battler is visible, and it has a sprite effect type set, we call start_effect, passing in that symbol. We then set the battler's sprite_effect_type to nil, as the setup for the effect will have been done now and we no longer need the symbol referencing it.

def start_effect(effect_type)
    @effect_type = effect_type
    case @effect_type
    when :appear
      @effect_duration = 16
      @battler_visible = true
    when :disappear
      @effect_duration = 32
      @battler_visible = false
    when :whiten
      @effect_duration = 16
      @battler_visible = true
    when :blink
      @effect_duration = 20
      @battler_visible = true
    when :collapse
      @effect_duration = 48
      @battler_visible = false
    when :boss_collapse
      @effect_duration = bitmap.height
      @battler_visible = false
    when :instant_collapse
      @effect_duration = 16
      @battler_visible = false
    end
    revert_to_normal
  end


This is the method that sets duration and visibility variables for the various effects, and takes one parameter: effect_type.

First, we set @effect_type to the passed-in parameter, then there's a case statement checking its value. It will be one of seven symbols: when :appear, the effect duration is set to 16 frames, with visibility being true. When :disappear, duration is 32 and visibility is false. When :whiten, duration is 16 and visibility is true. When :blink, duration is 20 and visibility is true. When :collapse, duration is 48 and visibility is false. When :boss_collapse, the effect duration is as many frames as there are pixels in the bitmap's height and visibility is false. When :instant_collapse, duration is 16 and visibility is false. After the case statement, we call revert_to_normal, which we'll look at next.

def revert_to_normal
    self.blend_type = 0
    self.color.set(0, 0, 0, 0)
    self.opacity = 255
    self.ox = bitmap.width / 2 if bitmap
    self.src_rect.y = 0
  end


This method reverts the sprite to normal settings, setting the blend type to 0 (normal), colour to (0, 0, 0, 0), opacity to 255, x origin to half the bitmap's width if one is set, and the y of the source rect to 0. This basically removes any settings on the sprite that might have been applied to it by other effects in battle, so that the appropriate effect will display correctly.

def setup_new_animation
    if @battler.animation_id > 0
      animation = $data_animations[@battler.animation_id]
      mirror = @battler.animation_mirror
      start_animation(animation, mirror)
      @battler.animation_id = 0
    end
  end


This method sets up an animation to display over the battler. If the battler's animation ID is greater than 0, animation is set to the data for the animation with the matching index, mirror is set to the animation_mirror value of the battler (which will pretty much always be false unless an enemy is displaying the animation for the off-hand weapon of a dual-wielding hero), we call start_animation passing in the animation and mirror variables, and the battler's animation ID is set to 0 so that the process will only happen once (since the call to this method is part of update).

def effect?
    @effect_type != nil
  end


This method determines whether an effect is currently executing. It returns true if @effect_type is not nil, and false otherwise.

def update_effect
    if @effect_duration > 0
      @effect_duration -= 1
      case @effect_type
      when :whiten
        update_whiten
      when :blink
        update_blink
      when :appear
        update_appear
      when :disappear
        update_disappear
      when :collapse
        update_collapse
      when :boss_collapse
        update_boss_collapse
      when :instant_collapse
        update_instant_collapse
      end
      @effect_type = nil if @effect_duration == 0
    end
  end


This method updates effects. If the effect duration is greater than 0, it's decremented by 1. We then have a case statement for the effect type. When it's :whiten, we call update_whiten. When :blink, we call update_blink. When :appear, we call update_appear. When :disappear, we call update_disappear. When :collapse, we call update_collapse. When :boss_collapse, we call update_boss_collapse. When :instant_collapse, we call update_instant_collapse. Finally, following the case statement, we set @effect_type to nil if it's duration is now equal to 0 (as we know the effect is over).

def update_whiten
    self.color.set(255, 255, 255, 0)
    self.color.alpha = 128 - (16 - @effect_duration) * 10
  end


This method updates the "whiten" effect. Sets the sprite's color property to (255, 255, 255, 0) (white with 0 alpha, so it will be completely transparent to begin with) then sets the alpha to 128 minus (16 minus the duration) multiplied by 10. In accordance with BEDMAS or BODMAS or whichever version you learned at school, the brackets happen first, then (16 - duration) is multiplied by 10, then the result of that is subtracted from 128. You've done maths, I don't have to tell you how it works, but let's look at an example anyway to see exactly how the battler changes from the first frame to the last:



def update_blink
    self.opacity = (@effect_duration % 10 < 5) ? 255 : 0
  end


This is the method that updates the blink effect. Sets the sprite's opacity to 255 if the duration mod 10 is less than 5, and 0 otherwise.

On the first frame, duration will be 19. 19 mod 10 is 9, which isn't less than 5, so opacity is 0 and the graphic is transparent. This will be the case up until duration is down to 14, at which point opacity will go back up to 255. This means the actual blink lasts for 5 of the 20 frames.

def update_appear
    self.opacity = (16 - @effect_duration) * 16
  end


This method updates the appearance of an enemy, which is why they kind of gradually fade in when a battle starts. I'll stop explaining the exact mathematics behind it as even if you didn't get it to begin with it should be clicking now. Suffice to say that this will make the battler's opacity start at 16 and increase by 16 for 16 frames, bringing it up to 255 (which is the maximum value opacity can be).

def update_disappear
    self.opacity = 256 - (32 - @effect_duration) * 10
  end


This method updates the disappearing effect. Disappearing is twice the duration of an appear, so opacity will start at 246 and decrease by 10 for 32 frames; the battler will have completely disappeared around frame 26.

def update_collapse
    self.blend_type = 1
    self.color.set(255, 128, 128, 128)
    self.opacity = 256 - (48 - @effect_duration) * 6
  end


This method updates the collapse effect. Sets the sprite's blend type to "additive" to give a glowy sort of effect, its color to (255, 128, 128, 128) which is a half-visible pinkish sort of hue, and opacity to 256 - (48 - duration) * 6. This means opacity starts at 250 and decreases by 6 per frame over 48 frames; the battler becomes completely invisible around frame 43.

def update_boss_collapse
    alpha = @effect_duration * 120 / bitmap.height
    self.ox = bitmap.width / 2 + @effect_duration % 2 * 4 - 2
    self.blend_type = 1
    self.color.set(255, 255, 255, 255 - alpha)
    self.opacity = alpha
    self.src_rect.y -= 1
    Sound.play_boss_collapse2 if @effect_duration % 20 == 19
  end


This is the most complex of the effects, the boss collapse. Basically what it does is makes the battler kind of "melt" down the screen with a shake effect and a whitish glow. Let's see how that works.

First, a local variable called alpha is set to the duration (which is initially set as the bitmap's height) multiplied by 120, divided by the bitmap's height. Let's take the "Darklord" battler as an example. It's 359x286, so on frame 1 it'll be 285 * 120 / 286 = 119. It'll continue to reduce at a proportional rate for the duration of the effect.

Then we set the x origin of the sprite to half the bitmap's width plus @effect_duration mod 2 multiplied by 4 minus 2. Using the same example again, order of operations means that first we divide 359 by 2 to get 179. Then we calculate 285 mod 2 to get 1, multiply that by 4 to get 4, add the 179 to 4 to get 183, then subtract 2 to get 181. As the original x origin was 179, this will effectively move the battler graphic 2 pixels to the left from where it started. On the next frame, ox will be set to 177, moving it 2 pixels to the right of its starting point. Moving the graphic 2 frames left on odd frames and 2 frames right on even ones is how the shake effect is achieved. On an interesting sidenote, they could just as easily have done this with the line

self.ox = bitmap.width / 2 + (@effect_duration % 2 == 0 ? -2 : 2)


which would just add 2 to or subtract 2 from ox depending on whether the duration was odd or even. Not sure why they made it any more complicated than that, but it is what it is.

Then blend type is set to 1, which as we already saw before makes the blending additive, and the colour is set to (255, 255, 255, 255 - alpha) which gives us white with an alpha value based on the duration (in other words, the glow fades as it progresses). Opacity is also set to alpha, so the graphic itself will fade as the effect progresses too.

Then, the y of the source rect is reduced by 1. If you'll remember, currently the y of the source rect starts at 0, so it's going to end up being negative. "But Trihan!" I hear you cry, "0 is the very top of the image! Whatever will happen if you go higher than the top?" Well let's look at the result:



As you can see, the graphic takes up the same size of rect, but because the y coordinate is starting higher, there are more blank rows of pixels at the top, and subsequently there are some rows of pixels at the bottom that don't get drawn any more. Over the duration of the effect, this will cause the sprite to appear to sink into the ground.

Finally, we play the boss collapse 2 effect set in the system tab if the remaining duration of the effect mod 20 is equal to 19 (in other words, just after every 20th frame).

def update_instant_collapse
    self.opacity = 0
  end


This method updates the instant collapse effect, and simply sets the sprite's opacity to 0.

Sprite_Picture

This kind of sprite displays pictures. It links up with an instance of Game_Picture and changes state automatically depending on the associated instance. Unlike character and battler sprites, Sprite_Picture inherits directly from Sprite.

def initialize(viewport, picture)
    super(viewport)
    @picture = picture
    update
  end


The constructor takes two parameters, viewport and picture. We call the constructor of the parent class, passing in viewport, set @picture to the passed-in picture instance, then call update.

def dispose
    bitmap.dispose if bitmap
    super
  end


As with battlers, the dispose method of a picture disposes of the bitmap if there is one, then calls the parent class's dispose method.

def update
    super
    update_bitmap
    update_origin
    update_position
    update_zoom
    update_other
  end


The frame update method is just a wrapper for calls to other methods: first we call the parent class's update method, then update_bitmap, update_origin, update_position, update_zoom and update_other.

def update_bitmap
    if @picture.name.empty?
      self.bitmap = nil
    else
      self.bitmap = Cache.picture(@picture.name)
    end
  end


This method updates the bitmap if it changes. If the linked picture's name is an empty string, we set the sprite's bitmap property to nil. Otherwise, we set it to the picture file from the Cache matching the picture's name.

def update_origin
    if @picture.origin == 0
      self.ox = 0
      self.oy = 0
    else
      self.ox = bitmap.width / 2
      self.oy = bitmap.height / 2
    end
  end


This method updates the origin of the picture. If the object's origin property is 0 (this equates to "Upper Left" in the Origin part of the Display Position settings on the Show Picture command), then ox and oy of the sprite are both set to 0. Otherwise, ox is set to half the bitmap's width, and oy is set to half the bitmap's height (in other words, in the centre).

def update_position
    self.x = @picture.x
    self.y = @picture.y
    self.z = @picture.number
  end


This method updates the picture sprite's position: sets x and y to the x and y of the associated picture, and z to the picture's number. This means that higher picture numbers appear on top.

def update_zoom
    self.zoom_x = @picture.zoom_x / 100.0
    self.zoom_y = @picture.zoom_y / 100.0
  end


This method updates zoom values. zoom_x and zoom_y are both set to the picture's properties divided by 100 (as the picture properties are stored as actual percentages while the sprite's are decimals). This means that 100% zoom on both axes will have the properties each set to 1, while 120% on x and 115 on y will result in 1.2 and 1.15.

def update_other
    self.opacity = @picture.opacity
    self.blend_type = @picture.blend_type
    self.angle = @picture.angle
    self.tone.set(@picture.tone)
  end


This method updates other aspects of the picture, namely opacity, blend type, angle and tone.

Sprite_Timer

This sprite displays the timers you use on screen. It links to $game_timer and automatically changes sprite states depending on its properties. Like picture sprites, Sprite_Timer inherits from Sprite rather than Sprite_Base.

def initialize(viewport)
    super(viewport)
    create_bitmap
    update
  end


Pretty standard constructor by now. Takes a viewport as a parameter, calls the parent class method passing in the same parameter value, calls create_bitmap, then calls update.

def dispose
    self.bitmap.dispose
    super
  end


Also standard dispose method. Disposes of the sprite's bitmap, then calls the parent class method.

def create_bitmap
    self.bitmap = Bitmap.new(96, 48)
    self.bitmap.font.size = 32
    self.bitmap.font.color.set(255, 255, 255)
  end


This method creates the timer bitmap. Sets the bitmap property to a new instance of Bitmap, passing in 96 and 48 as arguments. These are the X and Y dimensions of the created bitmap, so we basically end up with a 96x48 blank canvas. The bitmap's font size is set to 32 (default is 24, so this is a bit bigger than text usually would be when drawn on a bitmap) and the colour of the font is set to (255, 255, 255), or white.

def update
    super
    update_bitmap
    update_position
    update_visibility
  end


Another case of the frame update method being a wrapper for other method calls. First we call the parent class's update method, then update_bitmap, then update_position, and finally update_visibility.

def update_bitmap
    if $game_timer.sec != @total_sec
      @total_sec = $game_timer.sec
      redraw
    end
  end


This method updates the bitmap. If the number of seconds on the game timer differs from the @total_sec instance variable, we set that variable to the number of seconds on the timer, and call the redraw method. This obviously means the timer will only be redrawn when the sprite's recorded number of seconds is no longer the same as the timer's, and avoids redrawing it on every frame which would eat up resources.

def redraw
    self.bitmap.clear
    self.bitmap.draw_text(self.bitmap.rect, timer_text, 1)
  end


This method redraws the timer text. First, clears the bitmap, so it's once again a blank canvas. Then we call the bitmap's draw_text method, passing in the bitmap's rect, the result of calling timer_text (which we'll see in a second), and 1. Look up draw_text under Bitmap in the help file and you'll see that this is the overload that takes a rect, the string str to write, and an optional value for alignment. When the align value is 1, the text will be centred.

def timer_text
    sprintf("%02d:%02d", @total_sec / 60, @total_sec % 60)
  end


This method creates the text for the timer. sprintf is a string format method: "%02d:%02d" means that we're going to be outputting an integer 2 digits long, followed by a colon, followed by another integer 2 digits long. The arguments following the format string contain the values that will be substituted into the positions of the "d"s: the first is @total_sec / 60, which gives us the number of minutes in the timer, and the second is @total_sec % 60, which gives us the number of seconds left in the current minute.

def update_position
    self.x = Graphics.width - self.bitmap.width
    self.y = 0
    self.z = 200
  end


This method updates the position of the timer sprite. The sprite's x is set to the width of the screen minus the width of the bitmap, meaning it'll be drawn at the far right. y is set to 0, so it will be drawn at the top. z is set to 200, so it will appear above most other sprites.

def update_visibility
    self.visible = $game_timer.working?
  end


This method updates the visibility of the timer sprite. The visible property will be set to true if the game timer is running, and false otherwise. Pretty simple really.

And that's it for sprites! In only two articles, we've basically just looked at how RPG Maker VX Ace draws literally everything you see on your screen during a game. Next up we'll look at the Spriteset classes, which just bring together various other sprites to achieve weather, game maps, and battles.

Please let me know whether using images for some of the examples helped to explain the concept more clearly, or whether it was better when it was text-only.

Until next time.