SLIP INTO RUBY - UNDER THE HOOD PART 12: RAINBOW SPRITE

In which we crack open a cold can of Sprite_Base and finish it off with a Sprite_Character chaser.

  • Trihan
  • 01/22/2017 03:18 AM
  • 2153 views
Hello, sports fans! I know I always said this would be a weekly series, but the way I see it I've got about 2 years of late submissions to make up for, so luckily for you it's time for another exciting episode of



With us finally being done with the game object classes, we can move on to a new category: sprites. Sprites comprise...pretty much literally everything you see on the screen when you play an RPG Maker VX Ace game. Without sprite classes, you'd just be staring at blank emptiness, and that wouldn't be much fun for anyone.

Without further ado, let's meet the grandfather of all sprites.

Sprite_Base

This is, as the name suggests, the base class for all other sprite classes, and as with all other base classes contains properties and methods common to each of them. It's not actually the top-level class though; it inherits from the Sprite class, which is built-in to Ace. I won't go over the specifics of Sprite as you can read all about it in the help file.

Sprite_Base has three class variables:

@@ani_checker = []
  @@ani_spr_checker = []
  @@_reference_count = {}


ani_checker is a class-wide array containing the unique elements from $data_animations that need to be processed during animation updates. ani_spr_checker is a class-wide array used to make sure screen-wide animations are only created once. _reference_count is a class-wide hashmap which stores how many copies of a given bitmap exist in memory.

def initialize(viewport = nil)
    super(viewport)
    @use_sprite = true        # Sprite use flag
    @ani_duration = 0         # Remaining time of animation
  end


The constructor for the class; takes one parameter, viewport, which defaults to nil. A viewport is just a defined section of the screen on which sprites can be drawn (for example, in battle viewport1 is for the battle background and enemies, viewport2 is for timers, pictures and screen effects, and viewport3 is for fade overlays). First we call the initialize method of the parent class (Sprite) which creates the new object. @use_sprite is set to true (a flag which marks this sprite instance as being in use) and @ani_duration is set to 0 (the number of frames until an animation targeting the sprite is finished).

def dispose
    super
    dispose_animation
  end


The disposal method for sprites. Calls the dispose method of the parent class, then calls the dispose_animation method, which we'll cover later.

def update
    super
    update_animation
    @@ani_checker.clear
    @@ani_spr_checker.clear
  end


The update method for sprites. Calls the update method of the parent class, then calls the update_animation method, which we'll cover later. Finally, calls the clear method of @@ani_checker and @@ani_spr_checker; as they're both arrays, this just removes all elements from them.

def animation?
    @animation != nil
  end


This is a method that checks whether an animation is currently being displayed on the sprite; returns true if @animation is not nil, and false otherwise.

def start_animation(animation, mirror = false)
    dispose_animation
    @animation = animation
    if @animation
      @ani_mirror = mirror
      set_animation_rate
      @ani_duration = @animation.frame_max * @ani_rate + 1
      load_animation_bitmap
      make_animation_sprites
      set_animation_origin
    end
  end


This method starts playing an animation over the sprite and takes two parameters: animation, which is the element of $data_animations corresponding to the animation being played, and a mirror flag to determine whether the animation is being mirrored. The only place mirror is currently used is for Dual Wield attack animations.

First we call dispose_animation to free up the animation variables. @animation is set to the animation parameter; if the variable now contains data, @ani_mirror is set to the mirror parameter, we call set_animation_rate (which just sets @ani_rate (animation speed) to 4, as we'll see in a second), set @ani_duration to the animation's frame_max property (which is just the total number of frames assigned to it in the Animations tab of the database) multiplied by the animation speed + 1. For example, the Slash Physical animation is 6 frames long, so when it's playing, @ani_duration will be (6 * 4) + 1 = 25 update frames to display it. We then call the load_animation_bitmap, make_animation_sprites, and set_animation_origin methods, which we'll cover shortly.

def set_animation_rate
    @ani_rate = 4     # Fixed value by default
  end


Enterbrain sure do love their fixed values. This method simply sets @ani_rate to 4, as I explained above. If you want your animations to go faster or slower, this is the number to change.

def load_animation_bitmap
    animation1_name = @animation.animation1_name
    animation1_hue = @animation.animation1_hue
    animation2_name = @animation.animation2_name
    animation2_hue = @animation.animation2_hue
    @ani_bitmap1 = Cache.animation(animation1_name, animation1_hue)
    @ani_bitmap2 = Cache.animation(animation2_name, animation2_hue)
    if @@_reference_count.include?(@ani_bitmap1)
      @@_reference_count[@ani_bitmap1] += 1
    else
      @@_reference_count[@ani_bitmap1] = 1
    end
    if @@_reference_count.include?(@ani_bitmap2)
      @@_reference_count[@ani_bitmap2] += 1
    else
      @@_reference_count[@ani_bitmap2] = 1
    end
    Graphics.frame_reset
  end


This is the method which reads (loads) the animation graphics. There are a few things going on here.

First, we set animation1_name and animation1_hue to the matching properties of the @animation object. These are, obviously, the name of the animation graphic file and the hue setting you chose on the slider in the tab. animation2_name and animation2_hue are set the same way.

@ani_bitmap1 is set to the result of calling the animation method of the Cache module with animation1_name and animation1_hue as arguments. If you remember from the Cache breakdown, this calls the load_bitmap method for the file and hue, which either adds a new instance of Bitmap to the cache for that file, or retrieves the already-cached Bitmap. We do the same thing for @ani_bitmap2.

If @@_reference_count includes the Bitmap for @ani_bitmap1, we add 1 to the value under the key matching that bitmap, otherwise we set it to 1. This basically equates to either "We have 1 more of this bitmap now!" or "We only have 1 of this bitmap". Again, the same thing is done for @ani_bitmap2.

Finally, we call the frame_reset method of the Graphics module. As per the help file: "Resets the screen refresh timing. Call this method after a time-consuming process to prevent excessive frame skipping."

def make_animation_sprites
    @ani_sprites = []
    if @use_sprite && !@@ani_spr_checker.include?(@animation)
      16.times do
        sprite = ::Sprite.new(viewport)
        sprite.visible = false
        @ani_sprites.push(sprite)
      end
      if @animation.position == 3
        @@ani_spr_checker.push(@animation)
      end
    end
    @ani_duplicated = @@ani_checker.include?(@animation)
    if !@ani_duplicated && @animation.position == 3
      @@ani_checker.push(@animation)
    end
  end


This method creates the animation sprites.

First, we initialise @ani_sprites as an empty array. If the animation target's @use_sprite flag is true and @@ani_spr_checker does not include the animation, we enter a 16-iteration loop (16 is the maximum number of cels an animation frame can contain). The local sprite variable is set to a new instance of Sprite with the associated viewport as its argument (this just means the new Sprite will use the same viewport as the sprite the animation is displaying over). The visible property of the new sprite is set to false, and we push the new sprite to @ani_sprites.

Following this loop, if the position property of @animation is 3 (this is "Screen" on the Position setting in the tab), we push @animation to @@ani_spr_checker.

Regardless of whether the target sprite is being used, @ani_duplicated is set to true if @@ani_checker includes @animation, and false otherwise. Then, if @ani_duplicated is false and the animation affects the whole screen, we push @animation to @@ani_checker.

def set_animation_origin
    if @animation.position == 3
      if viewport == nil
        @ani_ox = Graphics.width / 2
        @ani_oy = Graphics.height / 2
      else
        @ani_ox = viewport.rect.width / 2
        @ani_oy = viewport.rect.height / 2
      end
    else
      @ani_ox = x - ox + width / 2
      @ani_oy = y - oy + height / 2
      if @animation.position == 0
        @ani_oy -= height / 2
      elsif @animation.position == 2
        @ani_oy += height / 2
      end
    end
  end


This method sets the animation origin.

If the animation position is 3 (screen), we check whether the viewport is nil. If it is, @ani_ox is set to half the width of the whole screen, and @ani_oy is set to half its height. If there is a viewport associated with the sprite, they're set to half the viewport rect's width and height instead.

If the animation position is anything else, @ani_ox is set to the target's x position, minus the x of its starting point, plus half its width. @ani_oy is set to the target's y position, minus the y of its starting point, plus half its height. If the position is 0 (head), @ani_oy is increased by half the target's height. If the position is 2 (feet), @ani_oy is increased by half the target's height.

Let's say we have a 136x47 battler sprite which is currently at (196, 288), and the animation is positioned at head height. @ani_ox will be 196 - 68 + (136 / 2) = 196, and @ani_oy will be 288 - 47 + (47 / 2) = 264. The reason for this is that as we'll see in a bit, battler sprites have their ox set to half the bitmap's width (136 / 2 = 68) and oy to the full bitmap height (47). As the position is 0, oy will be decreased by 47 / 2 = 23, so its y starting point will be 23 pixels higher than usual.

def dispose_animation
    if @ani_bitmap1
      @@_reference_count[@ani_bitmap1] -= 1
      if @@_reference_count[@ani_bitmap1] == 0
        @ani_bitmap1.dispose
      end
    end
    if @ani_bitmap2
      @@_reference_count[@ani_bitmap2] -= 1
      if @@_reference_count[@ani_bitmap2] == 0
        @ani_bitmap2.dispose
      end
    end
    if @ani_sprites
      @ani_sprites.each {|sprite| sprite.dispose }
      @ani_sprites = nil
      @animation = nil
    end
    @ani_bitmap1 = nil
    @ani_bitmap2 = nil
  end


This method frees up animation resources.

If @ani_bitmap1 contains data, we decrement its key in @@_reference_count by 1, then if the reference count is 0 (meaning there are no more copies of that bitmap in memory), we call dispose on the bitmap. The same thing is done for @ani_bitmap2.

If @ani_sprites still contains data, we iterate through each element using the iteration variable "sprite" and call dispose on it. Then, @ani_sprites is set to nil, as is @animation.

Finally, @ani_bitmap1 and @ani_bitmap2 are also set to nil.

def update_animation
    return unless animation?
    @ani_duration -= 1
    if @ani_duration % @ani_rate == 0
      if @ani_duration > 0
        frame_index = @animation.frame_max
        frame_index -= (@ani_duration + @ani_rate - 1) / @ani_rate
        animation_set_sprites(@animation.frames[frame_index])
        @animation.timings.each do |timing|
          animation_process_timing(timing) if timing.frame == frame_index
        end
      else
        end_animation
      end
    end
  end


This is the method that updates animation.

We return unless an animation is playing, because obviously there's no point in updating an animation if there isn't one to update. @ani_duration is decremented by 1. We check whether the remainder after dividing @ani_duration by @ani_rate is 0 (basically, whether it's divisible by 4, since every 4 update frames will display 1 animation frame. In other words, a 15-frame animation will show in 1 second). If so, we then check whether @ani_duration is greater than 0. If it is, frame_index is set to the animation's frame_max, then decremented by (duration + speed - 1) divided by the speed.

Taking our 6-frame animation from before as an example, the duration will go down to 24, which is divisible by 4 so we'll hit the if statement. frame_index is set to 6, then decremented by (24 + 4 - 1) / 4 = 27 / 4 = 6, so it's now 0. The next time it hits, when duration is 20, will make the equation (20 + 4 - 1) / 4 = 23 / 4 = 5, so the index will be 1. This is the way the animation works through each frame.

We then call animation_set_sprites, passing in the element of @animation's frames property with index frame_index. The frames property is an array of RPG::Animation::Frame objects, which contain the cel data. We'll look at this method in a bit.

After that, we iterate through each element of the animation's timings property, which is an array of RPG::Animation::Timing objects which contain timing data for sound and flash effects, using the iteration variable "timing". In each iteration, we call the animation_process_timing method passing in timing as the argument, but only if the timing's frame number matches the current frame index (because we don't want to play effects for other frames, do we?)

If the animation duration is not greater than 0, we just call end_animation.

def end_animation
    dispose_animation
end


The method for ending an animation simply calls dispose_animation. They could have just called it directly in the two places in the scripts that do so, but Sprite_Character has an extra line in its version of end_animation that wouldn't have worked in the dispose method for any other sprite.

def animation_set_sprites(frame)
    cell_data = frame.cell_data
    @ani_sprites.each_with_index do |sprite, i|
      next unless sprite
      pattern = cell_data[i, 0]
      if !pattern || pattern < 0
        sprite.visible = false
        next
      end
      sprite.bitmap = pattern < 100 ? @ani_bitmap1 : @ani_bitmap2
      sprite.visible = true
      sprite.src_rect.set(pattern % 5 * 192,
        pattern % 100 / 5 * 192, 192, 192)
      if @ani_mirror
        sprite.x = @ani_ox - cell_data[i, 1]
        sprite.y = @ani_oy + cell_data[i, 2]
        sprite.angle = (360 - cell_data[i, 4])
        sprite.mirror = (cell_data[i, 5] == 0)
      else
        sprite.x = @ani_ox + cell_data[i, 1]
        sprite.y = @ani_oy + cell_data[i, 2]
        sprite.angle = cell_data[i, 4]
        sprite.mirror = (cell_data[i, 5] == 1)
      end
      sprite.z = self.z + 300 + i
      sprite.ox = 96
      sprite.oy = 96
      sprite.zoom_x = cell_data[i, 3] / 100.0
      sprite.zoom_y = cell_data[i, 3] / 100.0
      sprite.opacity = cell_data[i, 6] * self.opacity / 255.0
      sprite.blend_type = cell_data[i, 7]
    end
  end


This method sets up the animation sprites and takes one parameter, frame. This is an instance of RPG::Animation::Frame, which contains the data for the cels.

First, we set cell_data to the cell_data property of frame. As per the help file:

"2-dimensional array containing cell contents (Table).

Generally takes the form cell_data.

data_index ranges from 0 to 7 and represents a variety of information about a cell (0: pattern, 1: x-coordinate, 2: y-coordinate, 3: zoom level, 4: angle of rotation, 5: horizontal flip, 6: opacity, 7: blending mode). Patterns are 1 less than the number displayed in RPG Maker. -1 indicates that the cell is not in use."

We iterate through each element of @ani_sprites with an index, which as you'll remember currently contains 16 blank Sprite instances, one for each possible cel in the frame. We're using two iteration variables, sprite for the current element, and i for the index.

We go to the next element unless the sprite object exists (this prevents processing of nonexistent sprites).

The local variable pattern is set to cell_data[i, 0], which is to say the pattern of the cel for the current iteration. If there is no pattern or the pattern is less than 0, the visible property of the sprite is set to false and we move on to the next one.

The bitmap property of the sprite is set to @ani_bitmap1 if the pattern is less than 100, or @ani_bitmap2 otherwise. (100 being the maximum number of individual patterns that an animation file can have), its visible property is set to true, its src_rect has its set method called with the arguments (pattern % 5 * 192), (pattern % 100 / 5 * 192), 192, and 192. These determine the x, y, width and height of the rect. (each cel is 192x192 pixels, so performing these algorithms will tell us the position of the rect that covers the 192x192 section of the graphic file corresponding to the appropriate pattern that was placed in the animation editor. For example, if we'd placed the first bitmap's fourth pattern, x would be (3 % 5 * 192) = (3 * 192) = 576, so the rect for this sprite will start slicing the source bitmap at the 576th x pixel. y would be (3 % 100 / 5 * 192) = (0 * 192) = 0, so we start slicing y at position 0. Because the graphic only has 5 cels horizontally, at cel 6 this will end up being 1 * 192, at cel 11 it's 2 * 192 and so on. The % 100 is to make sure that cels from the second file are sliced at the same positions, as the pattern numbers start at 100).

If @ani_mirror is true, the sprite's x is set to @ani_ox - the x coordinate of the current cell data. The sprite's y is set to @ani_oy + the y coordinate of the current cell data. The sprite's angle is set to 360 - the rotation angle of the current cell data. The sprite's mirror property is set to the result of (flip flag of cell data == 0), which will be true if the flip flag is "no" and false otherwise. (because the whole animation is being mirrored, any cels that were being mirrored originally now won't be and vice versa). If @ani_mirror is false, the only differences are that the sprite's x has its x coordinate added to @ani_ox instead of subtracted, the rotation angle isn't being subtracted from 360, and the logic check for mirror uses 1 instead of 0, so that cels will be flipped if you chose "Yes" instead.

The sprite's z coordinate is set to the target's z + 300 + the index of the cel. This means that all cels will appear above the target, and cels with higher numbers will be on top. Its ox and oy are both set to 96 (since the full cel is 192x192 and the origin is in the centre). zooom_x and zoom_y are set to the cell data's zoom value divided by 100 (so if you've set a cel to 120% x zoom, the value will be 120 / 100.0 = 1.2). Opacity is set to the cell data's opacity value multiplied by the target's opacity divided by 255. This means that animation cels will be fainter when displayed on targets with higher opacity. For example, a cel with opacity 50 displaying over a target with opacity 100 will end up with opacity 50 * 100 / 255.0, which is just over 19. As opacity is an integer, it will round to 19. Finally, the sprite's blend type property is set to the blend type in the cell data (0 for normal, 1 for additive, 2 for subtractive). Additive blending is where the RGB values of the target and animation layers are added together, resulting in a lighter cel and subtle glowing effect. Subtractive slending is where the values are subtracted, making the cels darker.

def animation_process_timing(timing)
    timing.se.play unless @ani_duplicated
    case timing.flash_scope
    when 1
      self.flash(timing.flash_color, timing.flash_duration * @ani_rate)
    when 2
      if viewport && !@ani_duplicated
        viewport.flash(timing.flash_color, timing.flash_duration * @ani_rate)
      end
    when 3
      self.flash(nil, timing.flash_duration * @ani_rate)
    end
  end


This method processes sound effect and flash timing, and takes one parameter: timing, which is an instance of RPG::Animation::Timing.

We call the play method of the timing object's se property (which is an instance of RPG::SE) unless the animation is duplicated. I'm honestly not sure under what circumstances an animation -will- be duplicated; I've tried pretty much every setting and as far as I can see no matter what you do, the ani checker will have cleared before any other animation is created, even if it's the same one. They must have put that in for a reason, though.

There's then a case statement for the flash scope of the timing object. When 1 (target), we call the flash method of the target sprite, passing in the flash colour and duration * rate as arguments. When 2 (screen), we check to see if there's a viewport associated with the target and make sure the animation is not duplicated. If so, we call the flash method of the viewport, passing in the same arguments. When 3 (hide target), we call the flash method of the target but pass in nil for the colour, so the target will disappear.

That's the basics of sprites, and the end of Sprite_Base. Now let's have a look at the classes which inherit from it.

Sprite_Character

This is the class used to display characters. It's hooked up to an instance of Game_Character, and will automatically change the sprite state in accordance with the character settings, as we'll see.

It has a single public instance variable:

attr_accessor :character


:character, as you may have guessed, is the Game_Character object the sprite represents.

def initialize(viewport, character = nil)
    super(viewport)
    @character = character
    @balloon_duration = 0
    update
  end


The class constructor takes two parameters: viewport and character, which defaults to nil. First, we call the parent class's constructor with the viewport as an argument. Then, we set @character to the Game_Character instance that was passed in. @balloon_duration is set to 0, and finally we call the update method, which we'll see in a bit.

def dispose
    end_animation
    end_balloon
    super
  end


The destructor for character sprites calls its own version of end_animation, which we'll see soon, its end_balloon method, which we'll also see soon, and then the dispose method of its parent class.

def update
    super
    update_bitmap
    update_src_rect
    update_position
    update_other
    update_balloon
    setup_new_effect
  end


The update method calls the update method of its parent class (which calls update_animation and clears the checkers), then also calls update_bitmap, update_src_rect, update_position, update_other, update_balloon, and setup_new_effect. If you guessed that we'll be looking at those soon, you win a cookie.

def tileset_bitmap(tile_id)
    Cache.tileset($game_map.tileset.tileset_names[5 + tile_id / 256])
  end


This method gets the tileset image that corresponds to the passed-in tile ID. It returns the tileset from Cache which matches the element of tileset_names (which is a property of tileset, which is a property of $game_map) with ID 5 + the tile's ID divided by 256 (the maximum number of tiles a single B-E tileset graphic can contain). For example, if we passed in tile ID 585, the index would be (5 + 585 / 256) = 7, and if we look at the tileset_names section of the help file, we can see that this will load up the TileD graphic (which is correct, as that's the one tile 585 is in).

def update_bitmap

if graphic_changed?
@tile_id = @character.tile_id
@character_name = @character.character_name
@character_index = @character.character_index
if @tile_id > 0
set_tile_bitmap
else
set_character_bitmap
end
end
end


This method updates the character's bitmap when the graphic changes.

If graphic_changed? returns true (which we'll look at next), @tile_id is set to the character's tile_id property, @character_name is set to the character's character name property, and @character_index is set to the character's character index property. If the tile ID is greater than 0 (meaning the graphic for the Game_Character was chosen from a tileset B-E tile) we call set_tile_bitmap, otherwise it's a standard charset and we call set_character_bitmap.

def graphic_changed?
    @tile_id != @character.tile_id ||
    @character_name != @character.character_name ||
    @character_index != @character.character_index
  end


This method determines whether the graphic has changed. Returns true if @tile_id, @character_name, or @character_index are different from that of the linked character (which obviously means they've changed since the last update).

def set_tile_bitmap
    sx = (@tile_id / 128 % 2 * 8 + @tile_id % 8) * 32;
    sy = @tile_id % 256 / 8 % 16 * 32;
    self.bitmap = tileset_bitmap(@tile_id)
    self.src_rect.set(sx, sy, 32, 32)
    self.ox = 16
    self.oy = 32
  end


This method sets the tile bitmap. sx is set to (the remainder of (the remainder of half of (tile id / 128) multiplied by 8 + the tile ID) divided by 8) multiplied by 32. Phew, that's a handful! Let's take our previous example of tile 585 and plug that in to the algorithm.

So why are we jumping through all these hoops? Well, it's to do with the way the tile IDs are assigned vs the layout of the actual tileset graphic. The graphic itself is a 512x512 image, broken down into a 16x16 grid of 32x32 tiles. However, internally the image is split in half and the bottom half is put underneath the top, making an 8x32 tileset from which you highlight the tile you want when you're setting the character graphic in the event editor.

Let's take the third tile in the second row of the tileset E graphic. Its ID is 778. The second tile of the second "half" is 897. However, if you look at the actual image and treat each 32x32 section as a tile, those two tiles are only 7 apart, so we need to do some mathematical jiggery-pokery to make the numbers work.

So first, we divide the tile ID by 128. When all four B-E tilesets are put together, they essentially form eight "sections" of 128 tiles, two per tileset, and we need to know which one our chosen tile is in. So taking our example tiles, 778 / 128 = 6, and 896 / 128 = 7, so we know that those tiles are in section 6 and section 7 respectively (bearing in mind it starts at 0). We modulus this number by 2 to find out whether the tile is on the left or right hand side of the section (0 = left, 1 = right) and then multiply that by 8 (so it's either going to be 0 or 8). We then add the @tile_id % 8, which tells us the index of the tile (778 % 8 = 2, 897 % 8 = 1).

Finally, we multiply the total by 32 to find out the pixel value of the starting X for our tile. So for 778, it's (0 + 2) * 32 = 64, and for 896 it's (8 + 1) * 32 = 288. So we can see that regardless of which half of the internal image the tile comes from, we still end up with the correct X coordinate.

Getting the Y coordinate is a bit more straightforward: it's just the remainder of dividing (the remainder of dividing the tile ID by 256, divided by 8) by 16, then multiplied by 32.

The modulus by 256 is to determine the individual tile index, as the tilesets each contain 256 tiles. We then divide by 8 and take the remainder of dividing the result by 16 because there are 16 tiles horizontally but due to the right half of the graphic being appended to the bottom of the left half we need to halve it first to get the appropriate value for the later IDs. This gives us the "row" the tile is on, which we then multiply by 32 to get the appropriate Y value for the slice.

Taking our two example tiles again, the first ends up being 778 % 256 (10) / 8 (1) % 16 (1) * 32 = 32, and the second is 896 % 256 (128) / 8 (16) % 16 (0) * 32 = 0.

Putting the two calculations together, for tile 778, the starting x/y coordinate will be (64, 32) and for tile 896, the starting x/y coordinate will be (288, 0). If you look up those coordinates on the tileset graphic in an image editing program, you'll see that they correspond to the top left corners of the tiles with those IDs.

So after all of that wonderful mathematical jiggery-pokery, we set the bitmap property of the sprite to the result of calling tileset_bitmap passing in @tile_id, which grabs us the appropriate tileset image from the Cache, sets the sprite's src_rect to (sx, sy, 32, 32) (which will slice a 32x32 portion of the tileset image, starting at sx as the x coordinate and sy as the y) and the origin x and y to 16 and 32 respectively (as the x origin is the centre of the sprite, and the y origin is at the bottom).

def set_character_bitmap
    self.bitmap = Cache.character(@character_name)
    sign = @character_name[/^[\!\$]./]
    if sign && sign.include?('$')
      @cw = bitmap.width / 3
      @ch = bitmap.height / 4
    else
      @cw = bitmap.width / 12
      @ch = bitmap.height / 8
    end
    self.ox = @cw / 2
    self.oy = @ch
  end


This method sets the bitmap for a non-tile character.

First, we set the sprite's bitmap property to the character from Cache matching @character_name. A local variable called sign is set to the result of a regex pattern which looks for !$ at the beginning of the character name (which will either be "!$", "!", "$" or nil). We check to see if sign is not nil and whether it includes "$"; if it does, @cw is set to a third of the bitmap's width, and @ch a quarter of the bitmap's height. Otherwise, @cw is set to a 12th of the bitmap's width, and @ch an eighth of the bitmap's height.

The reason for this is explained by the help file:

"A character can be of any size, and a total of twelve patterns (four directions (down, left, right, up) × 3 patterns) are arranged in the designated order. In each file, arrange characters two down and four across, for a total of eight. The size of this character is calculated based on one-twelfth the width and one-eighth the height of this file.

Note that RPG Maker VX Ace displays characters offset four pixels from tiles so as to more naturally portray them with buildings.

Adding an exclamation point (!) to the beginning of a file name cancels the application of the four-pixel offset, and also turns off the translucent effect applied by the bush attribute. This is used mainly for object-type characters on maps, such as doors and treasure chests. It can also be used in combination with the dollar sign ($) special character.
Adding a dollar sign ($) to the beginning of a file name allows you to treat one character as one file. In this case, the size of the character will be one-third of the width and one-fourth of the height of the file. It can also be used in combination with the exclamation point (!) special character."

Finally, we set the sprite's ox to half of @cw (again, since the x origin is in the centre) and its oy to @ch (and again, since the y origin is the bottom).

def update_src_rect
    if @tile_id == 0
      index = @character.character_index
      pattern = @character.pattern < 3 ? @character.pattern : 1
      sx = (index % 4 * 3 + pattern) * @cw
      sy = (index / 4 * 4 + (@character.direction - 2) / 2) * @ch
      self.src_rect.set(sx, sy, @cw, @ch)
    end
  end


This is the method which updates animation rects (in other words, how the engine knows which frame of a charset is being displayed at any given time. Basically, what happens behind the scenes during animation is that a different part of the overall graphic is "sliced" based on the pattern).

If the tile ID is 0 (in other words, we're dealing with a charset and not a tile graphic), index is set to the character_index property of @character, which tells us which of the eight possible characters this sprite represents. pattern is set to the character's pattern property if it's less than 3, or 1 otherwise. (if you think back to Game_CharacterBase, there's a method there which adds 1 to @pattern every frame and then gets the remainder of dividing it by 4).

For a practical example of how this results in looping animation, consider that @character.pattern starts at 1 (standing still, as per the initialize method of Game_CharacterBase). On the first update, it's increased to 2, divided by 4, and set to the remainder, which is 2. This is < 3, so pattern here will be 2 (left leg forward). Then @character.pattern is increased to 3, divided by 4, and set to the remainder, which is 3. This is not less than 3, so pattern is set to 1 (standing still). Then @character.pattern is increased to 4, divided by 4, and set to the remainder, which is 0. This is less than 3, so pattern is set to 0 (right leg forward). Next update it resets, and repeat ad infinitum.

Once the pattern has been established, sx is set to (the index mod 4, multiplied by 3, plus the pattern) multiplied by @cw. This gives us the starting X coordinate of the pattern to be sliced. sy is set to (a quarter of the index, multiplied by 4, plus (character's direction - 2) divided by 2) multiplied by @ch. Then the sprite's source rect is set to (sx, sy, @cw, @ch).

Let's look at this in practice. Say the character graphic's been set to the red-haired girl from Actor2.png in the RTP. This graphic is 384x256, and the filename doesn't have a $ at the beginning, so @cw is 32, and @ch is also 32. The index for this character is 5, and to begin with she'll be standing still facing down, so pattern will be 1. sx will be (5 % 4 * 3 + 1) * 32, which is 128. sy will be (5 / 4 * 4 + (2 - 2) / 2) * 32, which is also 128. So we know that the slice for her downward-facing standing still frame is going to start at (128, 128) on the sheet, and be 32 pixels wide by 32 pixels high. If you look up this coordinate on the image in an editing program, you'll see that it matches up. This is pretty much how most spritesheet-based animation works, so understanding the principles behind this has applications that go far beyond RPG Maker.

def update_position
    move_animation(@character.screen_x - x, @character.screen_y - y)
    self.x = @character.screen_x
    self.y = @character.screen_y
    self.z = @character.screen_z
  end


This method updates the sprite's screen position. First, we call move_animation, passing in the character's screen x minus the sprite's x coordinate, and the character's y minus the sprite's y coordinate. We'll look at what that does in just a bit. Then, we set the sprite's x, y and z to that of the character.

def update_other
    self.opacity = @character.opacity
    self.blend_type = @character.blend_type
    self.bush_depth = @character.bush_depth
    self.visible = !@character.transparent
  end


This method updates other aspects of the sprite. This simply sets the sprite's opacity, blend type, bush depth and visibility to the character's corresponding properties (in the case of visible, it's true if the character is not transparent, and false otherwise).

def setup_new_effect
    if !animation? && @character.animation_id > 0
      animation = $data_animations[@character.animation_id]
      start_animation(animation)
    end
    if !@balloon_sprite && @character.balloon_id > 0
      @balloon_id = @character.balloon_id
      start_balloon
    end
  end


This method sets up new animation or balloon effects.

First, if an animation is not currently displaying and the character has one set (i.e. the ID is greater than 0), we set the sprite's animation property to the animation data corresponding to the character's animation ID, then call start_animation passing in the animation as an argument.

Then, if the sprite does not have a balloon sprite set and the character does, we set @balloon_id to the character's and call start_balloon, which we'll look at in a bit.

def move_animation(dx, dy)
    if @animation && @animation.position != 3
      @ani_ox += dx
      @ani_oy += dy
      @ani_sprites.each do |sprite|
        sprite.x += dx
        sprite.y += dy
      end
    end
  end


This method moves an animation to follow the character, and as you may recall is called by update_position. It takes two parameters, dx and dy, which were the character's screen X minus the sprite's X, and the character's screen Y minus the sprite's Y. This gives us the difference between where the sprite is and where the character is now.

If an animation is playing and its scope is not "screen" (that is, it's either targeted at the sprite's head, centre or feet), we add dx and dy to the sprite's @ani_ox and @ani_oy (basically, make their origin values the same as the character's), then iterate through each of the animation cel sprites using the iteration variable sprite, and add those values to each sprite's x and y values as well. This results in all 16 cels moving along with the character they're playing over.

def end_animation
    super
    @character.animation_id = 0
  end


This method ends the current animation. We call the method of the parent class, which disposes the animation bitmaps and sprites, and then set the character's animation ID to 0.

def start_balloon
    dispose_balloon
    @balloon_duration = 8 * balloon_speed + balloon_wait
    @balloon_sprite = ::Sprite.new(viewport)
    @balloon_sprite.bitmap = Cache.system("Balloon")
    @balloon_sprite.ox = 16
    @balloon_sprite.oy = 32
    update_balloon
  end


This method starts the display of a balloon icon. First, we call dispose_balloon in case there's already one showing. We set the balloon's duration instance variable to 8 multiplied by the balloon speed plus the balloon wait time. Speed and wait are, by default, hardcoded as 8 and 12, so basically this value will always end up being 76 unless you change it. The reason behind these values is pretty simple: each balloon is 8 frames long, each frame shows for 8 update frames, and the last frame will stay visible for 12 update frames (a fifth of a second) before disappearing.

The balloon's sprite is set to a new instance of Sprite, using the same viewport as the character. That sprite's bitmap is set to the system graphic in Cache called "Balloon". The sprite's ox is set to 16, and the oy to 32. As with most other origin settings we've seen so far, this is because balloon icons are 32x32 and their origin is at bottom centre.

Finally, we call update_balloon, which we'll look at shortly.

def dispose_balloon
    if @balloon_sprite
      @balloon_sprite.dispose
      @balloon_sprite = nil
    end
  end


This method disposes of the balloon and frees up its resources in memory. If there's a balloon sprite present, calls its dispose method and sets the variable that held the object to nil.

def update_balloon
    if @balloon_duration > 0
      @balloon_duration -= 1
      if @balloon_duration > 0
        @balloon_sprite.x = x
        @balloon_sprite.y = y - height
        @balloon_sprite.z = z + 200
        sx = balloon_frame_index * 32
        sy = (@balloon_id - 1) * 32
        @balloon_sprite.src_rect.set(sx, sy, 32, 32)
      else
        end_balloon
      end
    end
  end


This method updates the balloon animation if one is playing.

If the balloon duration is greater than 0, we subtract 1 from it. If it's still greater than 0, we set the balloon sprite's x to the character sprite's x, the balloon sprite's y to the character sprite's y minus its height (so that the balloon appears just above the character's head) and its z to the character sprite's z plus 200 (to ensure that the balloon appears on top of characters). For the source "slice", sx is set to the balloon's frame index multiplied by 32 (we'll see frame index a few methods from now) and sy is set to 1 less than the balloon's ID multiplied by 32. The balloon sprite's source rect is then set to (sx, sy, 32, 32) which as we've seen previously slices a 32x32 portion of the bitmap starting at (sx, sy).

If the balloon duration is not greater than 0 after decrementing it, we just call end_balloon instead.

Given the other bitmap-slicing algorithms we've looked at so far, the balloon one is so simplistic you should understand what it does without an explanation. The only thing that might throw you off is the -1 on the balloon ID; this is because the IDs in the drop-down list you choose from when using a "Show Balloon" command start at 1, but obviously in terms of what Y coordinate to start slicing a balloon frame at, the first one is 0.

def end_balloon
    dispose_balloon
    @character.balloon_id = 0
  end


The method for ending a balloon simply disposes it by calling dispose_balloon and sets the character's balloon ID to 0.

def balloon_speed
    return 8
  end


Icky hardcoding method for balloon speed, or how many frames each animation frame lasts.

def balloon_wait
    return 12
  end


Similarly icky hardcoding method for the number of frames the final animation frame of the balloon sticks around.

def balloon_frame_index
    return 7 - [(@balloon_duration - balloon_wait) / balloon_speed, 0].max
  end


Last but not least comes the method that determines the index of a balloon animation frame.

We return 7 minus either (the remaining duration of the balloon less the final frame wait time) divided by speed, or 0, whichever is higher.

So for example, taking the first frame of the balloon, at the point this method is called duration will be 75 (since it's already been decremented) so the return value is

7 - [(75 - 12) / 8, 0].max


63 / 8 is 7, which is obviously higher than 0, so it ends up being 7 - 7, giving us a return value of 0. Which funnily enough is exactly the frame that should be displayed because it's the start of the animation.

Now let's look at the point where the fourth frame of the animation shows, when we have 51 frames left (76 - 25):

7 - [(51 - 12) / 8, 0].max


39 / 8 is 4, which again is higher than 0, so it ends up being 7 - 4, giving us a return value of 3. Bearing in mind that the frames are 0-indexed, this is the first point at which we transition from the 3rd frame to the 4th.

When we hit 19 frames left is when the last animation frame is hit as that's the first time dividing the duration - wait by 8 results in 0, meaning we show index 7. It'll show for the 8 frames required to take us down to 12, after which point the division is giving a negative value and 0 becomes the greater value, so it becomes the one returned by the call to .max until the duration hits 0 and the animation finishes.

That's it for Sprite_Base and Sprite_Character! The article size is getting a bit high now so I think I'll leave it there. The next ones we're going to look at are Sprite_Battler, Sprite_Picture and Sprite_Timer, which will round off the sprites before we look at the Spriteset classes.

As usual, comments, corrections, criticisms and death threats are encouraged and welcomed.

Until next time.