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
- 3387 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 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.
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).
The disposal method for sprites. Calls the dispose method of the parent class, then calls the dispose_animation method, which we'll cover later.
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.
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.
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.
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.
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."
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.
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.
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.
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.
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.
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.
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:
:character, as you may have guessed, is the Game_Character object the sprite represents.
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.
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.
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.
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).
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.
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).
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).
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).
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.
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.
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).
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.
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.
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.
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.
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.
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.
The method for ending a balloon simply disposes it by calling dispose_balloon and sets the character's balloon ID to 0.
Icky hardcoding method for balloon speed, or how many frames each animation frame lasts.
Similarly icky hardcoding method for the number of frames the final animation frame of the balloon sticks around.
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
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):
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.
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.