SLIP INTO RUBY - UNDER THE HOOD PART 24: THE FINAL EPISODE
In which we complete a journey started 8 years ago, and say our fond farewells.
Trihan
- 06/25/2021 04:56 PM
- 2225 views
Hello, sports fans! We're so close to the end of this code breakdown so let's just jump right into the next fun-filled episode of
Today: Name a debugged battle
Scene_Name
This is one that's not used by every game, but is invaluable for the ones that do. Scene_Name handles the scene where you rename actors, and it inherits from Scene_MenuBase.
For our prepare method, we take actor_id and max_char as parameters. We simply set the instance variables @actor_id and @max_char to the values passed in.
In the start method, first we call the parent method with super. Then we set the instance variable @actor to the instance of $game_actors at index @actor_id. We set @edit_window to a new instance of Window_NameEdit passing in @actor and @max_char as the "actor" and "max_char" arguments, and @input_window to a new instance of Window_NameInput passing in @edit_window as the "edit_window" argument. Then we call set_handler on @input_window to set the :ok handler to call the on_input_ok method.
And in the ok handler, we set @actor's name to the name from @edit_window, and then call return_scene to go back to the scene we were on previously.
That's it for Scene_Name! The beauty is that all the other legwork has been covered either by the parent class or the window classes themselves, so nothing else is needed.
Scene_Debug
Definitely one of the most important scenes from a developer perspective; this class handles the debugging menu which opens when you hit F9 in test play, and it also inherits from Scene_MenuBase.
For the start method, we're just calling the parent method and then a number of window creation methods, namely create_left_window to create the switch/variable list, create_right_window to create the value display/edit list, and create_debug_help_window to create the help window down the bottom of the screen.
The terminate method only calls the parent method; it used to refresh $game_map as well, but for whatever reason the devs commented that line out. Technically since "super" is the only line that's here now, we could remove this entirely as it doesn't really do anything.
In this method, we set @left_window to a new instance of Window_DebugLeft passing in 0 for the x coordinate and 0 for the y. Then we set the :ok handler to on_left_ok, and the :cancel handler to return_scene.
Here we create the right-hand window. We set wx to the width of @left_window, and ww to the width of the game window minus wx. Then we set @right_window to a new instance of Window_DebugRight passing in wx for the x coordinate, 0 for the y, and ww for the width. This will place the window to the right of the left window, taking up all available remaining horizontal screen space (the window itself defines the height, which is enough to fit 10 lines of text). Then we set the :cancel handler to on_right_cancel, and set the right_window property of @left_window to @right_window, which creates the link between the windows allowing the right window to "know" whether a switch or variable is selected in the left one. (we don't need an :ok handler for this window because the functionality of the enter key is handled already by the window class itself)
This method creates the help window for the debug scene. We set wx to the x of @right_window, wy to its height, ww to its width, and wh to the height of the game window minus wy. Then we set @debug_help_window to a new instance of Window_Base, passing in wx, wy, ww and wh as the x, y, width and height arguments respectively. This will cause the help window to take up the remaining screen space to the right of the left window and underneath the right window.
It may seem odd not to use Window_Help here, but I believe the reason is that usually the help windows are at the top of the screen and it's the first thing drawn so it doesn't need to have any data on other windows, but here it's the last thing so it needs to "know" where the others are, and Window_Help doesn't facilitate that since it needs the number of lines it will display as an argument.
This is the handler method for :ok on the left window. We call refresh_help_window, then activate @right_window and select index 0.
And this is the :cancel handler for the right window; first we activate @left_window, then unselect @right_window, and finally clear the contents of @debug_help_window.
This method refreshes the help window, and to do so we call draw_text_ex on @debug_help_window, passing in 4 for the x coordinate, 0 for the y, and help_text for the text. This will draw help_text at the top left of the window. What is that going to be? Well...
This. So if @left_window's mode is :switch, which it will be if you have a switch selected, it will show that you can hit C (or enter) to turn that switch ON or OFF. Otherwise (meaning a variable is selected) it will show that you can hit left for -1, right for +1, page up for -10 and page down for +10.
And that's all you need for the debug scene! Now on to the mother of all scenes, the Big Kahuna.
Scene_Battle
This one is, obviously, the scene that handles battle processing. And there's a lot of stuff that goes into a battle, as evidenced by the class for it being almost as long as the interpreter itself. It inherits from Scene_Base.
In the start method, we call the parent (which creates the main viewport) and then we call create_spriteset to create our battle spriteset, create_all_windows to create the windows you'll see on screen, and we set BattleManager's method_wait_for_message to the wait_for_message method.
And for post_start, we call the parent method (which performs the transition and updates Input) and then call battle_start, which is what truly gets the ball rolling for the combat processing.
In pre_terminate, we call the parent method (which doesn't actually do anything so this line isn't needed) then if SceneManager's scene_is? method returns true when passing in Scene_Map, we call Graphics' fadeout method passing in 30 for the number of frames argument, and if scene_is? returns true when passing in Scene_Title, we pass in 60 to fadeout instead.
The only reason I can think of for this check is to account for a battle event having a "Return to Title Screen" command in it.
For the terminate method, we once again call the parent method (which freezes Graphics, disposes all windows and disposes of the main viewport) after which we call dispose_spriteset, dispose @info_viewport, and call stop from the RPG::ME module, which will stop any playing ME audio.
Here we overwrite the update method. We call the parent method, then if BattleManager's in_turn? method returns true (which it will if its @phase instance variable is set to :turn) we call process_event and then process_action. Either way, we then call BattleManager's judge_win_loss method.
We also need to overwrite update_basic with a few more bits of functionality. First, as usual, we call the parent method, which updates Graphics, Input and the windows. Then we update $game_timer (which will decrement the timer by 1 frame and call its expire method if it's reached 0) and $game_troop (which just updates the screen) as well as the @spriteset (which updates battlebacks, enemies, actors, pictures, the timer sprite and viewports), and finally call update_info_viewport and update_message_open.
update_for_wait is a method which determines what gets updated when we're "waiting" and here we just call update_basic.
The wait method performs a wait for a number of frames, taking duration as a parameter. We run a loop for as many times as the passed-in value using iteration variable "i", and in the block for it we call update_for_wait is i is less than half duration OR if show_fast? returns false.
Basically this means that while waiting, no actions or events will process and only the timer/graphics will update each frame. If the player is holding the "fast forward" key, the wait will be over in half its duration.
The aforementioned "fast forward" key is either :A or :C (which map to Shift and Space/Enter/Z by default) so we return true if Input's press? method returns true passing in either :A OR :C.
We also have abs_wait, a method that's similar to wait but doesn't allow for fast forwarding. So again we take duration as a parameter, and again we run a loop for as many times as the value passed, but all we do in the block is call update_for_wait.
abs_wait_short is just a wrapper method which performs a short wait with no fast forward, and here we're just calling abs_wait passing in 15 for the duration. This will call nothing but update_basic for 15 frames.
This method waits for a message to finish displaying. We call update on _message_window and then call update_for_wait while the visible property of $game_message is true (this is what stops other stuff from happening until you hit enter to close the message window)
This method waits for an animation to finish playing. First we call update_for_wait and then we call update_for_wait while @spriteset's animation? method returns true (which it will if any battler with a sprite is displaying an animation). The first call just ensures that there will be at least 1 frame where update_for_wait is called when this method is.
This is just the same but for effects rather than animations (effects being things like whiten, blink, appear, disappear, boss collapse etc).
This method updates the info viewport, as the name suggests. We have 3 potential calls to move_info_viewport, each with a condition and the only difference being the argument passed to it. We pass 0 to it if @party_command_window is active, 128 if @actor_command_window is active, and 64 is BattleManager has started processing a turn.
This method is the one that moves the info viewport, as called above, and it takes ox as a parameter. First we set current_ox to the ox (origin X) of @info_viewport. Then if current_ox is less than the passed-in ox, we set it to the minimum value between ox and current_ox plus 16. If current_ox is greater than ox, we set it to the minimum value between ox and current_ox minus 16.
This is what creates the "slide" effect with the command/status windows at the bottom of the battle screen. If you consider that the party command window starts at ox 0, when you switch to the actor command window it moves to 128. When the current ox is 0, this means that current_ox is less than ox and so @info_viewport's ox is set to the minimum between 128 and 0 + 16, which results in 16. The next frame, current_ox is 16 and it's set to the minimum between 128 and 16 + 16, which results in 32. And so on and so forth until it reaches 128, at which point it's no longer less than or greater than ox, so the update won't move it any more.
If you've forgotten, the origin X basically defines which part of the viewport's display area is considered to be at "0" or the far left of the rectangle. So if ox is 128, then the far left of the viewport display will show the pixels 128 pixels to the right of 0 in the actual image being displayed. This causes the party command window to look like it's sliding over to the left when the actor command window activates, but really we're sliding the viewport viewing area itself; the rectangle never moves.
This method updates the opening of the message window. If $game_message's busy? method returns true AND the close? method of @status_window returns false, we set @message_window's opennness to 0 and then close @status_window, @party_command_window and @actor_command_window.
This will prevent the openness of the message window from increasing until the status window has closed.
This method creates the spriteset, which contains all of the graphical elements which will be shown in the battle. All we're doing there is setting @spriteset to a new instance of Spriteset_Battle.
This method creates all the battle windows, and boy howdy are there a lot of them. This set of method calls will create the message window, the scrolling text window, the battle log window, the status window, the information viewport, the party and actor command windows, the help window, the skill and item selection windows, and the actor and enemy selection windows.
We start off fairly simply with the method for creating the message window, in which we just set @message_window to a new instance of Window_Message.
Creating the log window is a little more involved. We set @log_window to a new instance of Window_BattleLog, then set its method_wait property to the "wait" method, and its method_wait_for_effect property to "wait_for_effect".
For creating the status window, we set @status_window to a new instance of Window_BattleStatus, then set its x coordinate to 128 (which places it to the right of the party command window, which is 128 pixels wide)
This method creates the information viewport, which is the section at the bottom of the screen housing the party command window, the status window, the actor command window, and the actor/enemy selection windows.
We set @info_viewport to a new instance of Viewport, then set its rect's y coordinate to the game window height minus the height of @status_window, and its rect's height to @status_window's height. In other words, our viewport rect takes up the entire area below the main battle display. We also set the viewport's z to 100, and its ox to 64, and then set @status_window's viewport to @info_viewport.
The ox being 64 at the start creates a very subtle blink-and-you'll-miss-it effect where the command and status windows slide into place when battle begins due to update_info_viewport gradually moving it to 0.
This method creates the party command window. We set @party_command_window to a new instance of Window_PartyCommand. We set its viewport to @info_viewport and then set a couple of handlers for it. The handler for the :fight symbol is set to the command_fight method, and the handler for the :escape symbol is set to command_escape.
This method creates the actor command window. We set @actor_command_window to a new instance of Window_ActorCommand, and its viewport to @info_viewport. We set the handler for the :attack symbol to command_attack, for the :skill symbol to command_skill, for :guard to command_guard, :item to command_item, and :cancel to prior_command. Finally, we set the window's x coordinate to the width of the game window (which places it off-screen to the right of the status window initially)
This method creates the help window. We set @help_window to a new instance of Window_Help and then set its visible property to false, as we don't want to see it immediately.
This method creates the skill selection window. We set @skill_window to a new instance of Window_BattleSkill, passing in @help_window as the help_window argument, and @info_viewport as the viewport. Then we set the :ok handler to on_skill_ok, and the :cancel handler to on_skill_cancel.
And surprising nobody the item window is exactly the same, only it's a Window_BattleItem instead. Obviously the instance variable name and the handler methods are changed to use item rather than skill.
This method creates the actor selection window. We set @actor_window to a new instance of Window_BattleActor, passing in @info_viewport as the viewport argument. Then we set the :ok handler to on_actor_ok, and the :cancel handler to on_actor_cancel.
And just as skills and items are pretty much the same thing, so are actors and enemies, so to create the enemy selection window we just do the same thing but replacing actor with enemy.
This method refreshes the status display, and all we're doing in it is calling @status_window's refresh method.
This method moves to the next actor's command input. If BattleManager's next_command method returns true, we call start_actor_command_selection. Otherwise, we call turn_start.
Basically, "set BattleManager's actor index to the next number up, and then start the turn if the index is higher than the party member count".
This one is pretty much the same, but when prior_command returns false we call start_party_command_selection, as hitting cancel on the first party member's command input takes us back to the party command window.
And this is the method that does that. Unless scene_changing? returns true, we refresh the status window, then unselect and open it. If BattleManager's input_start method returns true (which it will if the battle isn't a surprise attack and at least one actor is able to input actions) we close @actor_command_window and call the setup method of @party_command_window. Otherwise (meaning it is a surprise attack or that no actor can move) we deactivate @party_command_window and call turn_start to start the turn.
command_fight is the handler method for the "Fight" command, and all we do there is call next_command, which will either open up the command window for the first actor who can act, or start the turn if none of them can.
This method handles the "Escape" command. We call turn_start unless BattleManager's process_escape method returns true. In other words, an attempt will be made to escape from battle and end it, but if that fails the turn will start and allow the enemies a turn without the party doing anything.
This method starts the actor command selection. We call @status_window's select method, passing in BattleManager's actor's index as the index argument, then close @party_command_window and setup on @actor_command_window, passing in BattleManager's actor as the actor argument. This will highlight in the status window the actor who's acting, and show us the actor commands for that actor.
This method handles the "Attack" command for an actor. We call the set_attack method of BattleManager's actor's input, and then select_enemy_selection.
Basically, we get the current actor from BattleManager, then get that actor's current action, and set it up as a normal attack (skill 1). Then we open up the window for them to select a target for the attack.
This method handles the "Skill" command for an actor. We set @skill_window's actor to BattleManager's actor, and its stype_id to the current_ext of @actor_command_window. Then we refresh it, and finally show and activate it.
current_ext is used to create a link between the command and the skill type it represents, which was passed when creating the command in the Window_ActorCommand class. This is how "Special" opens up the list of special skills, "Magic" shows magic skills and so on. Refreshing the window means it will show the skill list for the current actor.
This method handles the "Guard" command. All we're doing here is calling set_guard on BattleManager's actor's input action, and then calling next_command.
command_item handles the "Item" command. Here, we refresh @item_window and then show and activate it.
This method begins the selection of an actor as a target (for healing spells, buffs etc.) We refresh @actor_window, then show and activate it.
This is the method that handles hitting the :ok key with an actor selected in the actor selection window.
We set BattleManager's actor's input action's target_index to @actor_window's index. Then we hide @actor_window, @skill_window and @item_window (easier to just hide all 3 since we don't have a reference here to which one was open when the actor selection came into play; we could check, but there's no downside to hiding a window that's already hidden so there's no point) and then we call next_command to move to the next actor.
In the cancel handler, though, we do need to check. First of all, no matter what, we hide @actor_window. Then we have a case statement branching based on the current_symbol of @actor_command_window; if the symbol was :skill (meaning we got to the actor selection window from picking a skill to use) we activate @skill_window. It it's :item, we activate @item_window instead.
Not much to say here. The method for starting enemy selection is exactly the same as the actor one but using a different window.
Same deal here. Other than using the enemy window, this is identical to on_actor_ok.
And this one is aaaaalmost the same, but we have to add in an additional when clause in the case statement for the :attack symbol, which activates @actor_command_window (we didn't need this in on_actor_cancel because party members can't attack allies...at least not in the default engine).
This method handles hitting :ok while highlighting a skill in the skill list. We set @skill to @skill_window's item. Then we call set_skill on BattleManager's actor's input action, passing in @skill's id, and we also set the actor's last_skill's object to @skill. If @skill's need_selection? method returns false, we hide @skill_window and call next_command. Otherwise, if @skill's for_opponent? method returns true, we call select_enemy_selection to start selecting an enemy. And otherwise, we call select_actor_selection, as the only possibility remaining is that the skill targets an actor.
So let's say the player is choosing the turn action for their healer, and they've chosen "Heal" (skill 26 in the default database). @skill_window's item is the Game_Item containing the data for the Heal skill, and this is assigned to the @skill variable. We pass 26 to set_skill, meaning the current action will also point to the Heal skill, and last_skill's object is set to that Game_Item. need_selection? will return true, since Heal targets one ally. for_opponent? will return false, so we start actor selection.
This method handles hitting cancel while selecting a skill. Here, we hide @skill_window and activate @actor_command_window.
This one is very similar to on_skill_ok but with a couple of differences. Since items belong to the entire party rather than a specific actor, we're setting $game_party's last_item object. Besides that and using "item" rather than "skill", it's functionally identical.
As is the cancel handler, in which we just hide @item_window and activate @actor_command_window.
battle_start is the method called...well, when the battle starts. You may remember the call for this was way back in the beginning of the class, in post_start. Anyway, what are we doing here? Well, first we call BattleManager's battle_start method, which increases the number of battles the player has fought, processes on battle start for the actors and enemies, creates the enemy names if there are duplicates, and shows messages for pre-emptive/surprise attacks. Then we call process_event (which we'll see very soon) and finally we call start_party_command_selection, which either opens up on the party command window or immediately starts the turn if it's a surprise attack or the party contains no actors who can select actions.
This method begins a turn once all applicable actions have been chosen. We close @party_command_window and @actor_command_window as we don't need them any more, and unselect @status_window to remove the highlight on party members. We set @subject to nil since we're no longer choosing actions, then call BattleManager's turn_start method, which increases the turn count and determines the battler order. Finally, we wait for the log window to finish displaying its text and then clear it.
This method processes the end of a turn. First, we run an each loop on all_battle_members using iteration variable "battler" and in that loop we call each battler's on_turn_end method, then refresh_status. We call display_auto_affected_status in @log_window passing in battler, and then we call its wait_and_clear method to wait for the text to finish and then clear the log.
After the loop, we call BattleManager's turn_end method, then process_event, and finally we call start_party_command_selection to return us to the party commands.
Here we see how all_battle_members is formed: we're returning $game_party's members plus $game_troop's members. The beauty of Ruby arrays is that you can perform arithmetic operations on them just like you can with numbers, so this is just adding two arrays to each other to form one made up of all elements of both.
This method processes events, and is integral to battle events working the way they do.
While scene_changing? is false, we update $game_troop's interpreter, and call its setup_battle_event method (which checks all troop event pages for events which have met their conditions). We call wait_for_message in case text was being shown, then wait_for_effect if $game_troop's all_dead? method returns true (meaning the entire troop has been killed). Then we call process_forced_action to process any forced actions, then BattleManager's judge_win_loss method to figure out if the battle was won or lost yet and then we break out of the loop unless $game_troop's interpreter's running? method returns true. Finally, we call update_for_wait.
This method processes a forced action. If BattleManager's action_forced? method returns true (meaning an action is being forced) we set last_subject to @subject, and @subject to BattleManager's action_forced_battler, which will return the battler whose action is being forced. We call BattleManager's clear_action_force to clear the action force flag, then call process_action to actually process the forced action, and finally we set @subject to last_subject to return to the subject who was going to act before the forced action happened.
This method processes an action. We return if scene_changing? returns true, as there's no point in continuing to process actions if we're leaving the battle scene. If @subject returns nil OR @subject has no current action, we set @subject to BattleManager's next_subject (basically, if there isn't a current acting battler or the current battler has no action to take, we move to the next one). We return turn_end unless there is now a @subject, since that would mean all applicable battlers have acted and we need to finish the turn now. If @subject's current_action is not nil, we call its prepare method. Then if the valid? method of @subject's current_action returns true, we open @status_window and call execute_action. After this check, we call remove_current_action on @subject to remove that action from their pool since it's been executed now. Then finally we call process_action_end unless @subject has a current_action, meaning that even after removing the one that just got executed they have more actions to take.
This method processes the end of an action. First we call @subject's on_action_end method and refresh_status. Then we call @log_window's display_auto_affected_status method passing in @subject to display any states which were automatically applied by the action, then wait_and_clear to clear it afterwards. After that we call display_current_state to update the messages of any states present, followed by another call to wait_and_clear. Finally, we call BattleManager's judge_win_loss method to check whether the action resulted in winning or losing the battle.
This method executes an action. We set @subject's sprite_effect_type to :whiten, which will cause its sprite to flash white briefly, then we call use_item, and finally we call wait_and_clear on @log_window to give any logged text a chance to display before disappearing.
This method uses an item (though it also applies to skills, so here we mean item in the general sense rather than in the "potion" sense). We set item to @subject's current_action's item object, and call @log_window's display_use_item method passing in @subject as the subject and item as the item, which will show the name of the skill/item being used. We then call @subject's use_item method passing in item, which will pay any costs, consume the item if a consumable item was used, and call any common events associated with the item. We refresh_status to reflect changes in resources etc., and set targets to the compacted result of calling @subject's current_action's make_targets method, which gives us an array of the action's targets without any nil entries. We call show_animation passing in targets as the targets and the item's animation_id as the animation ID, and then for each target we call invoke_item a number of times equal to the item's repeats property, passing in target as the target and item as the item.
And here we have invoke_item, which as we've just seen takes target and item as parameters.
If a random number from 0 to 1 is less than the result of target's item_cnt method when @subject and item are passed to it (in other words, the chance of the target counterattacking) we call invoke_counter_attack, passing in target as the target and item as the item. Otherwise, if a random number from 0 to 1 is less than the result of target's item_mrf with @subject and item passed to it, we call invoke_magic_reflection passing in target and item, as this means the target reflected a magic attack. Otherwise, we call apply_item_effects, passing in as target the result of calling apply_substitute passing in target and item, and item as the item. This ensures that the target is already replaced with a substitute battler when effects are applied, in cases where a substitute is going to take the target's place. Finally, we set @subject's last_target_index to target's index.
This method applies item effects, taking target and item as parameters. First we call target's item_apply method, passing in @subject as the user and item as the item. We refresh_status to reflect any chances in HP, MP etc. Then we call @log_window's display_action_results method, passing in target as the target and item as the item.
This method invokes a counterattack, taking target and item as parameters. First we call the display_counter method of @log_window, passing in target as the target and item as the item, which will display in the battle log that a counterattack has occurred. We set attack_skill to the element of $data_skills with an ID corresponding to target's attack_skill_id (which is almost always going to be 1), then call @subject's item_apply method, passing in target as the user and attack_skill as the item (which will apply a normal attack from the target on the subject). We call refresh_status to reflect HP changes etc. and then once again call display_action_results from @log_window.
This method invokes magic reflection, whereby the target reflects a spell back at the user, and takes target and item as parameters. We set @subject's magic_reflection property to true, then call display_reflection on @log_window passing in target as the target and item as the item, which will show that a magic reflection occurred. We call apply_item_effects passing in @subject as the target and item as the item, then set @subject's magic_reflection to false.
So why do we set this flag? Following the chain of method called from apply_item_effects onwards, you'll see that this is used to determine opposite? for the purposes of applying things like states etc. In other words, the flag temporarily tells the battler that they count as a hostile unit, which means state rates and luck effects will influence the reflected spell as if it had been cast on an enemy rather than an ally.
This method applies the substitute effect, taking target and item as parameters.
If check_substitute returns true with target and item passed to it, we set substitute to the result of target's friends_unit's substitute_battler method, which gets the first party member who has a "substitute" trait. If there is a substitute and the substitute isn't the target, we call @log_window's display_substitute method passing in substitute and target, and return substitute. This will result in the substitute actor standing in to take damage and other effects on behalf of the original target.
And in this method, we check whether a substitute can occur. It takes target and item as parameters.
We return true if target's HP is less than a quarter of their maximum AND there is either no item or the item is not "certain hit".
This tells us two important things: one, that an actor will only substitute for allies at 25% HP or less. Secondly, they cannot do this against a "certain hit" effect; it will only happen for physical or magical skills/items.
This method shows an animation on a set of targets, taking targets and animation_id as parameters. If animation_id is less than 0 (meaning it's -1, which denotes it as a normal attack) we call show_attack_animation passing in targets. Otherwise, we call show_normal_animation passing in targets and animation_id. Either way, we call @log_window's wait method and then call wait_for_animation.
This method shows the attack animation on a set of targets, taking targets as a parameter. If @subject is an actor, we call show_normal_animation on targets, passing in @subject's atk_animation_id1 and false for the mirror argument, and then call it again passing in targets, @subject's atk_animation_id2, and true for the mirror argument. Otherwise, we call the Sound module's play_enemy_attack method and then abs_wait_short to wait 15 frames.
The dual call to show_normal_animation is what causes a dual wielding actor to flip the animation of their second hit, which makes it a little more visually interesting.
And finally, we have show_normal_animation, which shows a normal animation on a set of targets, taking target, animation_id and mirror as parameters. mirror defaults to false if not supplied.
We set animation to the element of $data_animations at an index of animation_id. If animation contains data, we run an each loop on targets using iteration variable "target". In this loop, we set target's animation_id to the passed-in animation_id, its animation_mirror to the mirror argument, and then call abs_wait_short to wait 15 frames unless the animation's to_screen? method returns true, which it will if the animation is set to play on the whole screen rather than each target. After each target has been processed, we then call abs_wait_short if the animation plays on the whole screen.
What this will do is create a 15-frame wait between animations on each target when the animation hits each target individually, and otherwise it will just wait 15 frames at the end.
We are finally done! The behemoth that is Scene_Battle has been slain, and no mysteries remain to us. That leaves just one class.
Scene_Gameover
And it's a pretty short one. This scene is the one that comes up when your party fails miserably in battle and everyone is dead. It inherits from Scene_Base.
First we overwrite the start method, starting by calling the parent method. We also call play_gameover_music, fadeout_frozen_graphics and create_background.
The terminate overwrite calls the parent method and then the dispose_background method.
In the frame update method, we call the parent method and then call goto_title if the :C Input is triggered (meaning the player has hit enter).
Here we overwrite the base perform_transition method, and we're still calling Graphics' transition method but we pass in fadein_speed rather than transition_speed, so that the game over screen fades in slightly slower than a regular scene change.
This method plays the game over music. First, we call the stop methods of both RPG::BGM and RPG::BGS to cut out any currently-playing background music or sound. Then we call the play method of $data_system's gameover_me.
This method fades out the frozen graphics from the previous scene. We call Graphics' transition method passing in fadeout_speed, and then its freeze method.
This method creates the background. We set @sprite to a new instance of Sprite, and then set its bitmap to the cached GameOver system graphic.
This method disposes of the background. Here we simply dispose @sprite's bitmap and then dispose @sprite itself.
This method determines the fade out speed. Here we return 60, meaning the scene prior to the game over one takes 1 second to fade out fully.
And here we have the fade in speed. We return 120, so that fading the game over screen in takes 2 seconds.
This method returns the player to the title screen. First we call fadeout_all, and then we call SceneManager's goto method passing in Scene_Title.
That's all for Scene_Gameover, and indeed for the default scripts! We've gone through every Module, every Game Object, every Window and every Scene. The only thing left is "main" with the line
And that kicks off the whole process of the game running.
That's it. From the first episode of Slip into Ruby in July 2012, we've now covered every single line of code that makes up the base RPG Maker VX Ace engine. It's been a hell of a journey, and I can't tell you how grateful I am to everyone who's given me feedback and let me know how important this series was to their own journeys as indie game developers. I couldn't have predicted its impact or importance when I started, and the fact that people are still reading today something I began 8 years ago is frankly amazing.
So thank you, each and every reader who's made writing this series worthwhile. You are what has kept me going, and hopefully the series will be demystifying the engine for generations of users to come.
For those who jumped ship to RPG Maker MV and MZ, I still have Jump into Javascript going, but unless I think of something else to cover on the Ruby end, this series has reaches its conclusion. If anyone has any questions about any aspect of any Slip into Ruby episode or any of the code we've covered over the years, do feel free to email me at guitarherotrihan@gmail.com. If I get enough questions, I'll do a final Q&A episode to round things off.
But if that doesn't happen, I guess that's it from me, at least as far as Slip into Ruby is concerned.
Until...now.

Today: Name a debugged battle
Scene_Name
This is one that's not used by every game, but is invaluable for the ones that do. Scene_Name handles the scene where you rename actors, and it inherits from Scene_MenuBase.
def prepare(actor_id, max_char) @actor_id = actor_id @max_char = max_char end
For our prepare method, we take actor_id and max_char as parameters. We simply set the instance variables @actor_id and @max_char to the values passed in.
def start super @actor = $game_actors[@actor_id] @edit_window = Window_NameEdit.new(@actor, @max_char) @input_window = Window_NameInput.new(@edit_window) @input_window.set_handler(:ok, method(:on_input_ok)) end
In the start method, first we call the parent method with super. Then we set the instance variable @actor to the instance of $game_actors at index @actor_id. We set @edit_window to a new instance of Window_NameEdit passing in @actor and @max_char as the "actor" and "max_char" arguments, and @input_window to a new instance of Window_NameInput passing in @edit_window as the "edit_window" argument. Then we call set_handler on @input_window to set the :ok handler to call the on_input_ok method.
def on_input_ok @actor.name = @edit_window.name return_scene end
And in the ok handler, we set @actor's name to the name from @edit_window, and then call return_scene to go back to the scene we were on previously.
That's it for Scene_Name! The beauty is that all the other legwork has been covered either by the parent class or the window classes themselves, so nothing else is needed.
Scene_Debug
Definitely one of the most important scenes from a developer perspective; this class handles the debugging menu which opens when you hit F9 in test play, and it also inherits from Scene_MenuBase.
def start super create_left_window create_right_window create_debug_help_window end
For the start method, we're just calling the parent method and then a number of window creation methods, namely create_left_window to create the switch/variable list, create_right_window to create the value display/edit list, and create_debug_help_window to create the help window down the bottom of the screen.
def terminate super #$game_map.refresh end
The terminate method only calls the parent method; it used to refresh $game_map as well, but for whatever reason the devs commented that line out. Technically since "super" is the only line that's here now, we could remove this entirely as it doesn't really do anything.
def create_left_window @left_window = Window_DebugLeft.new(0, 0) @left_window.set_handler(:ok, method(:on_left_ok)) @left_window.set_handler(:cancel, method(:return_scene)) end
In this method, we set @left_window to a new instance of Window_DebugLeft passing in 0 for the x coordinate and 0 for the y. Then we set the :ok handler to on_left_ok, and the :cancel handler to return_scene.
def create_right_window wx = @left_window.width ww = Graphics.width - wx @right_window = Window_DebugRight.new(wx, 0, ww) @right_window.set_handler(:cancel, method(:on_right_cancel)) @left_window.right_window = @right_window end
Here we create the right-hand window. We set wx to the width of @left_window, and ww to the width of the game window minus wx. Then we set @right_window to a new instance of Window_DebugRight passing in wx for the x coordinate, 0 for the y, and ww for the width. This will place the window to the right of the left window, taking up all available remaining horizontal screen space (the window itself defines the height, which is enough to fit 10 lines of text). Then we set the :cancel handler to on_right_cancel, and set the right_window property of @left_window to @right_window, which creates the link between the windows allowing the right window to "know" whether a switch or variable is selected in the left one. (we don't need an :ok handler for this window because the functionality of the enter key is handled already by the window class itself)
def create_debug_help_window wx = @right_window.x wy = @right_window.height ww = @right_window.width wh = Graphics.height - wy @debug_help_window = Window_Base.new(wx, wy, ww, wh) end
This method creates the help window for the debug scene. We set wx to the x of @right_window, wy to its height, ww to its width, and wh to the height of the game window minus wy. Then we set @debug_help_window to a new instance of Window_Base, passing in wx, wy, ww and wh as the x, y, width and height arguments respectively. This will cause the help window to take up the remaining screen space to the right of the left window and underneath the right window.
It may seem odd not to use Window_Help here, but I believe the reason is that usually the help windows are at the top of the screen and it's the first thing drawn so it doesn't need to have any data on other windows, but here it's the last thing so it needs to "know" where the others are, and Window_Help doesn't facilitate that since it needs the number of lines it will display as an argument.
def on_left_ok refresh_help_window @right_window.activate @right_window.select(0) end
This is the handler method for :ok on the left window. We call refresh_help_window, then activate @right_window and select index 0.
def on_right_cancel @left_window.activate @right_window.unselect @debug_help_window.contents.clear end
And this is the :cancel handler for the right window; first we activate @left_window, then unselect @right_window, and finally clear the contents of @debug_help_window.
def refresh_help_window @debug_help_window.draw_text_ex(4, 0, help_text) end
This method refreshes the help window, and to do so we call draw_text_ex on @debug_help_window, passing in 4 for the x coordinate, 0 for the y, and help_text for the text. This will draw help_text at the top left of the window. What is that going to be? Well...
def help_text if @left_window.mode == :switch "C (Enter) : ON / OFF" else "← (Left) : -1\n" + "→ (Right) : +1\n" + "L (Pageup) : -10\n" + "R (Pagedown) : +10" end end
This. So if @left_window's mode is :switch, which it will be if you have a switch selected, it will show that you can hit C (or enter) to turn that switch ON or OFF. Otherwise (meaning a variable is selected) it will show that you can hit left for -1, right for +1, page up for -10 and page down for +10.
And that's all you need for the debug scene! Now on to the mother of all scenes, the Big Kahuna.
Scene_Battle
This one is, obviously, the scene that handles battle processing. And there's a lot of stuff that goes into a battle, as evidenced by the class for it being almost as long as the interpreter itself. It inherits from Scene_Base.
def start super create_spriteset create_all_windows BattleManager.method_wait_for_message = method(:wait_for_message) end
In the start method, we call the parent (which creates the main viewport) and then we call create_spriteset to create our battle spriteset, create_all_windows to create the windows you'll see on screen, and we set BattleManager's method_wait_for_message to the wait_for_message method.
def post_start super battle_start end
And for post_start, we call the parent method (which performs the transition and updates Input) and then call battle_start, which is what truly gets the ball rolling for the combat processing.
def pre_terminate super Graphics.fadeout(30) if SceneManager.scene_is?(Scene_Map) Graphics.fadeout(60) if SceneManager.scene_is?(Scene_Title) end
In pre_terminate, we call the parent method (which doesn't actually do anything so this line isn't needed) then if SceneManager's scene_is? method returns true when passing in Scene_Map, we call Graphics' fadeout method passing in 30 for the number of frames argument, and if scene_is? returns true when passing in Scene_Title, we pass in 60 to fadeout instead.
The only reason I can think of for this check is to account for a battle event having a "Return to Title Screen" command in it.
def terminate super dispose_spriteset @info_viewport.dispose RPG::ME.stop end
For the terminate method, we once again call the parent method (which freezes Graphics, disposes all windows and disposes of the main viewport) after which we call dispose_spriteset, dispose @info_viewport, and call stop from the RPG::ME module, which will stop any playing ME audio.
def update super if BattleManager.in_turn? process_event process_action end BattleManager.judge_win_loss end
Here we overwrite the update method. We call the parent method, then if BattleManager's in_turn? method returns true (which it will if its @phase instance variable is set to :turn) we call process_event and then process_action. Either way, we then call BattleManager's judge_win_loss method.
def update_basic super $game_timer.update $game_troop.update @spriteset.update update_info_viewport update_message_open end
We also need to overwrite update_basic with a few more bits of functionality. First, as usual, we call the parent method, which updates Graphics, Input and the windows. Then we update $game_timer (which will decrement the timer by 1 frame and call its expire method if it's reached 0) and $game_troop (which just updates the screen) as well as the @spriteset (which updates battlebacks, enemies, actors, pictures, the timer sprite and viewports), and finally call update_info_viewport and update_message_open.
def update_for_wait update_basic end
update_for_wait is a method which determines what gets updated when we're "waiting" and here we just call update_basic.
def wait(duration) duration.times {|i| update_for_wait if i < duration / 2 || !show_fast? } end
The wait method performs a wait for a number of frames, taking duration as a parameter. We run a loop for as many times as the passed-in value using iteration variable "i", and in the block for it we call update_for_wait is i is less than half duration OR if show_fast? returns false.
Basically this means that while waiting, no actions or events will process and only the timer/graphics will update each frame. If the player is holding the "fast forward" key, the wait will be over in half its duration.
def show_fast? Input.press?(:A) || Input.press?(:C) end
The aforementioned "fast forward" key is either :A or :C (which map to Shift and Space/Enter/Z by default) so we return true if Input's press? method returns true passing in either :A OR :C.
def abs_wait(duration) duration.times {|i| update_for_wait } end
We also have abs_wait, a method that's similar to wait but doesn't allow for fast forwarding. So again we take duration as a parameter, and again we run a loop for as many times as the value passed, but all we do in the block is call update_for_wait.
def abs_wait_short abs_wait(15) end
abs_wait_short is just a wrapper method which performs a short wait with no fast forward, and here we're just calling abs_wait passing in 15 for the duration. This will call nothing but update_basic for 15 frames.
def wait_for_message @message_window.update update_for_wait while $game_message.visible end
This method waits for a message to finish displaying. We call update on _message_window and then call update_for_wait while the visible property of $game_message is true (this is what stops other stuff from happening until you hit enter to close the message window)
def wait_for_animation update_for_wait update_for_wait while @spriteset.animation? end
This method waits for an animation to finish playing. First we call update_for_wait and then we call update_for_wait while @spriteset's animation? method returns true (which it will if any battler with a sprite is displaying an animation). The first call just ensures that there will be at least 1 frame where update_for_wait is called when this method is.
def wait_for_effect update_for_wait update_for_wait while @spriteset.effect? end
This is just the same but for effects rather than animations (effects being things like whiten, blink, appear, disappear, boss collapse etc).
def update_info_viewport move_info_viewport(0) if @party_command_window.active move_info_viewport(128) if @actor_command_window.active move_info_viewport(64) if BattleManager.in_turn? end
This method updates the info viewport, as the name suggests. We have 3 potential calls to move_info_viewport, each with a condition and the only difference being the argument passed to it. We pass 0 to it if @party_command_window is active, 128 if @actor_command_window is active, and 64 is BattleManager has started processing a turn.
def move_info_viewport(ox) current_ox = @info_viewport.ox @info_viewport.ox = [ox, current_ox + 16].min if current_ox < ox @info_viewport.ox = [ox, current_ox - 16].max if current_ox > ox end
This method is the one that moves the info viewport, as called above, and it takes ox as a parameter. First we set current_ox to the ox (origin X) of @info_viewport. Then if current_ox is less than the passed-in ox, we set it to the minimum value between ox and current_ox plus 16. If current_ox is greater than ox, we set it to the minimum value between ox and current_ox minus 16.
This is what creates the "slide" effect with the command/status windows at the bottom of the battle screen. If you consider that the party command window starts at ox 0, when you switch to the actor command window it moves to 128. When the current ox is 0, this means that current_ox is less than ox and so @info_viewport's ox is set to the minimum between 128 and 0 + 16, which results in 16. The next frame, current_ox is 16 and it's set to the minimum between 128 and 16 + 16, which results in 32. And so on and so forth until it reaches 128, at which point it's no longer less than or greater than ox, so the update won't move it any more.
If you've forgotten, the origin X basically defines which part of the viewport's display area is considered to be at "0" or the far left of the rectangle. So if ox is 128, then the far left of the viewport display will show the pixels 128 pixels to the right of 0 in the actual image being displayed. This causes the party command window to look like it's sliding over to the left when the actor command window activates, but really we're sliding the viewport viewing area itself; the rectangle never moves.
def update_message_open if $game_message.busy? && !@status_window.close? @message_window.openness = 0 @status_window.close @party_command_window.close @actor_command_window.close end end
This method updates the opening of the message window. If $game_message's busy? method returns true AND the close? method of @status_window returns false, we set @message_window's opennness to 0 and then close @status_window, @party_command_window and @actor_command_window.
This will prevent the openness of the message window from increasing until the status window has closed.
def create_spriteset @spriteset = Spriteset_Battle.new end
This method creates the spriteset, which contains all of the graphical elements which will be shown in the battle. All we're doing there is setting @spriteset to a new instance of Spriteset_Battle.
def create_all_windows create_message_window create_scroll_text_window create_log_window create_status_window create_info_viewport create_party_command_window create_actor_command_window create_help_window create_skill_window create_item_window create_actor_window create_enemy_window end
This method creates all the battle windows, and boy howdy are there a lot of them. This set of method calls will create the message window, the scrolling text window, the battle log window, the status window, the information viewport, the party and actor command windows, the help window, the skill and item selection windows, and the actor and enemy selection windows.
def create_message_window @message_window = Window_Message.new end
We start off fairly simply with the method for creating the message window, in which we just set @message_window to a new instance of Window_Message.
def create_log_window @log_window = Window_BattleLog.new @log_window.method_wait = method(:wait) @log_window.method_wait_for_effect = method(:wait_for_effect) end
Creating the log window is a little more involved. We set @log_window to a new instance of Window_BattleLog, then set its method_wait property to the "wait" method, and its method_wait_for_effect property to "wait_for_effect".
def create_status_window @status_window = Window_BattleStatus.new @status_window.x = 128 end
For creating the status window, we set @status_window to a new instance of Window_BattleStatus, then set its x coordinate to 128 (which places it to the right of the party command window, which is 128 pixels wide)
def create_info_viewport @info_viewport = Viewport.new @info_viewport.rect.y = Graphics.height - @status_window.height @info_viewport.rect.height = @status_window.height @info_viewport.z = 100 @info_viewport.ox = 64 @status_window.viewport = @info_viewport end
This method creates the information viewport, which is the section at the bottom of the screen housing the party command window, the status window, the actor command window, and the actor/enemy selection windows.
We set @info_viewport to a new instance of Viewport, then set its rect's y coordinate to the game window height minus the height of @status_window, and its rect's height to @status_window's height. In other words, our viewport rect takes up the entire area below the main battle display. We also set the viewport's z to 100, and its ox to 64, and then set @status_window's viewport to @info_viewport.
The ox being 64 at the start creates a very subtle blink-and-you'll-miss-it effect where the command and status windows slide into place when battle begins due to update_info_viewport gradually moving it to 0.
def create_party_command_window @party_command_window = Window_PartyCommand.new @party_command_window.viewport = @info_viewport @party_command_window.set_handler(:fight, method(:command_fight)) @party_command_window.set_handler(:escape, method(:command_escape)) @party_command_window.unselect end
This method creates the party command window. We set @party_command_window to a new instance of Window_PartyCommand. We set its viewport to @info_viewport and then set a couple of handlers for it. The handler for the :fight symbol is set to the command_fight method, and the handler for the :escape symbol is set to command_escape.
def create_actor_command_window @actor_command_window = Window_ActorCommand.new @actor_command_window.viewport = @info_viewport @actor_command_window.set_handler(:attack, method(:command_attack)) @actor_command_window.set_handler(:skill, method(:command_skill)) @actor_command_window.set_handler(:guard, method(:command_guard)) @actor_command_window.set_handler(:item, method(:command_item)) @actor_command_window.set_handler(:cancel, method(:prior_command)) @actor_command_window.x = Graphics.width end
This method creates the actor command window. We set @actor_command_window to a new instance of Window_ActorCommand, and its viewport to @info_viewport. We set the handler for the :attack symbol to command_attack, for the :skill symbol to command_skill, for :guard to command_guard, :item to command_item, and :cancel to prior_command. Finally, we set the window's x coordinate to the width of the game window (which places it off-screen to the right of the status window initially)
def create_help_window @help_window = Window_Help.new @help_window.visible = false end
This method creates the help window. We set @help_window to a new instance of Window_Help and then set its visible property to false, as we don't want to see it immediately.
def create_skill_window @skill_window = Window_BattleSkill.new(@help_window, @info_viewport) @skill_window.set_handler(:ok, method(:on_skill_ok)) @skill_window.set_handler(:cancel, method(:on_skill_cancel)) end
This method creates the skill selection window. We set @skill_window to a new instance of Window_BattleSkill, passing in @help_window as the help_window argument, and @info_viewport as the viewport. Then we set the :ok handler to on_skill_ok, and the :cancel handler to on_skill_cancel.
def create_item_window @item_window = Window_BattleItem.new(@help_window, @info_viewport) @item_window.set_handler(:ok, method(:on_item_ok)) @item_window.set_handler(:cancel, method(:on_item_cancel)) end
And surprising nobody the item window is exactly the same, only it's a Window_BattleItem instead. Obviously the instance variable name and the handler methods are changed to use item rather than skill.
def create_actor_window @actor_window = Window_BattleActor.new(@info_viewport) @actor_window.set_handler(:ok, method(:on_actor_ok)) @actor_window.set_handler(:cancel, method(:on_actor_cancel)) end
This method creates the actor selection window. We set @actor_window to a new instance of Window_BattleActor, passing in @info_viewport as the viewport argument. Then we set the :ok handler to on_actor_ok, and the :cancel handler to on_actor_cancel.
def create_enemy_window @enemy_window = Window_BattleEnemy.new(@info_viewport) @enemy_window.set_handler(:ok, method(:on_enemy_ok)) @enemy_window.set_handler(:cancel, method(:on_enemy_cancel)) end
And just as skills and items are pretty much the same thing, so are actors and enemies, so to create the enemy selection window we just do the same thing but replacing actor with enemy.
def refresh_status @status_window.refresh end
This method refreshes the status display, and all we're doing in it is calling @status_window's refresh method.
def next_command if BattleManager.next_command start_actor_command_selection else turn_start end end
This method moves to the next actor's command input. If BattleManager's next_command method returns true, we call start_actor_command_selection. Otherwise, we call turn_start.
Basically, "set BattleManager's actor index to the next number up, and then start the turn if the index is higher than the party member count".
def prior_command if BattleManager.prior_command start_actor_command_selection else start_party_command_selection end end
This one is pretty much the same, but when prior_command returns false we call start_party_command_selection, as hitting cancel on the first party member's command input takes us back to the party command window.
def start_party_command_selection unless scene_changing? refresh_status @status_window.unselect @status_window.open if BattleManager.input_start @actor_command_window.close @party_command_window.setup else @party_command_window.deactivate turn_start end end end
And this is the method that does that. Unless scene_changing? returns true, we refresh the status window, then unselect and open it. If BattleManager's input_start method returns true (which it will if the battle isn't a surprise attack and at least one actor is able to input actions) we close @actor_command_window and call the setup method of @party_command_window. Otherwise (meaning it is a surprise attack or that no actor can move) we deactivate @party_command_window and call turn_start to start the turn.
def command_fight next_command end
command_fight is the handler method for the "Fight" command, and all we do there is call next_command, which will either open up the command window for the first actor who can act, or start the turn if none of them can.
def command_escape turn_start unless BattleManager.process_escape end
This method handles the "Escape" command. We call turn_start unless BattleManager's process_escape method returns true. In other words, an attempt will be made to escape from battle and end it, but if that fails the turn will start and allow the enemies a turn without the party doing anything.
def start_actor_command_selection @status_window.select(BattleManager.actor.index) @party_command_window.close @actor_command_window.setup(BattleManager.actor) end
This method starts the actor command selection. We call @status_window's select method, passing in BattleManager's actor's index as the index argument, then close @party_command_window and setup on @actor_command_window, passing in BattleManager's actor as the actor argument. This will highlight in the status window the actor who's acting, and show us the actor commands for that actor.
def command_attack BattleManager.actor.input.set_attack select_enemy_selection end
This method handles the "Attack" command for an actor. We call the set_attack method of BattleManager's actor's input, and then select_enemy_selection.
Basically, we get the current actor from BattleManager, then get that actor's current action, and set it up as a normal attack (skill 1). Then we open up the window for them to select a target for the attack.
def command_skill @skill_window.actor = BattleManager.actor @skill_window.stype_id = @actor_command_window.current_ext @skill_window.refresh @skill_window.show.activate end
This method handles the "Skill" command for an actor. We set @skill_window's actor to BattleManager's actor, and its stype_id to the current_ext of @actor_command_window. Then we refresh it, and finally show and activate it.
current_ext is used to create a link between the command and the skill type it represents, which was passed when creating the command in the Window_ActorCommand class. This is how "Special" opens up the list of special skills, "Magic" shows magic skills and so on. Refreshing the window means it will show the skill list for the current actor.
def command_guard BattleManager.actor.input.set_guard next_command end
This method handles the "Guard" command. All we're doing here is calling set_guard on BattleManager's actor's input action, and then calling next_command.
def command_item @item_window.refresh @item_window.show.activate end
command_item handles the "Item" command. Here, we refresh @item_window and then show and activate it.
def select_actor_selection @actor_window.refresh @actor_window.show.activate end
This method begins the selection of an actor as a target (for healing spells, buffs etc.) We refresh @actor_window, then show and activate it.
def on_actor_ok BattleManager.actor.input.target_index = @actor_window.index @actor_window.hide @skill_window.hide @item_window.hide next_command end
This is the method that handles hitting the :ok key with an actor selected in the actor selection window.
We set BattleManager's actor's input action's target_index to @actor_window's index. Then we hide @actor_window, @skill_window and @item_window (easier to just hide all 3 since we don't have a reference here to which one was open when the actor selection came into play; we could check, but there's no downside to hiding a window that's already hidden so there's no point) and then we call next_command to move to the next actor.
def on_actor_cancel @actor_window.hide case @actor_command_window.current_symbol when :skill @skill_window.activate when :item @item_window.activate end end
In the cancel handler, though, we do need to check. First of all, no matter what, we hide @actor_window. Then we have a case statement branching based on the current_symbol of @actor_command_window; if the symbol was :skill (meaning we got to the actor selection window from picking a skill to use) we activate @skill_window. It it's :item, we activate @item_window instead.
def select_enemy_selection @enemy_window.refresh @enemy_window.show.activate end
Not much to say here. The method for starting enemy selection is exactly the same as the actor one but using a different window.
def on_enemy_ok BattleManager.actor.input.target_index = @enemy_window.enemy.index @enemy_window.hide @skill_window.hide @item_window.hide next_command end
Same deal here. Other than using the enemy window, this is identical to on_actor_ok.
def on_enemy_cancel @enemy_window.hide case @actor_command_window.current_symbol when :attack @actor_command_window.activate when :skill @skill_window.activate when :item @item_window.activate end end
And this one is aaaaalmost the same, but we have to add in an additional when clause in the case statement for the :attack symbol, which activates @actor_command_window (we didn't need this in on_actor_cancel because party members can't attack allies...at least not in the default engine).
def on_skill_ok @skill = @skill_window.item BattleManager.actor.input.set_skill(@skill.id) BattleManager.actor.last_skill.object = @skill if !@skill.need_selection? @skill_window.hide next_command elsif @skill.for_opponent? select_enemy_selection else select_actor_selection end end
This method handles hitting :ok while highlighting a skill in the skill list. We set @skill to @skill_window's item. Then we call set_skill on BattleManager's actor's input action, passing in @skill's id, and we also set the actor's last_skill's object to @skill. If @skill's need_selection? method returns false, we hide @skill_window and call next_command. Otherwise, if @skill's for_opponent? method returns true, we call select_enemy_selection to start selecting an enemy. And otherwise, we call select_actor_selection, as the only possibility remaining is that the skill targets an actor.
So let's say the player is choosing the turn action for their healer, and they've chosen "Heal" (skill 26 in the default database). @skill_window's item is the Game_Item containing the data for the Heal skill, and this is assigned to the @skill variable. We pass 26 to set_skill, meaning the current action will also point to the Heal skill, and last_skill's object is set to that Game_Item. need_selection? will return true, since Heal targets one ally. for_opponent? will return false, so we start actor selection.
def on_skill_cancel @skill_window.hide @actor_command_window.activate end
This method handles hitting cancel while selecting a skill. Here, we hide @skill_window and activate @actor_command_window.
def on_item_ok @item = @item_window.item BattleManager.actor.input.set_item(@item.id) if !@item.need_selection? @item_window.hide next_command elsif @item.for_opponent? select_enemy_selection else select_actor_selection end $game_party.last_item.object = @item end
This one is very similar to on_skill_ok but with a couple of differences. Since items belong to the entire party rather than a specific actor, we're setting $game_party's last_item object. Besides that and using "item" rather than "skill", it's functionally identical.
def on_item_cancel @item_window.hide @actor_command_window.activate end
As is the cancel handler, in which we just hide @item_window and activate @actor_command_window.
def battle_start BattleManager.battle_start process_event start_party_command_selection end
battle_start is the method called...well, when the battle starts. You may remember the call for this was way back in the beginning of the class, in post_start. Anyway, what are we doing here? Well, first we call BattleManager's battle_start method, which increases the number of battles the player has fought, processes on battle start for the actors and enemies, creates the enemy names if there are duplicates, and shows messages for pre-emptive/surprise attacks. Then we call process_event (which we'll see very soon) and finally we call start_party_command_selection, which either opens up on the party command window or immediately starts the turn if it's a surprise attack or the party contains no actors who can select actions.
def turn_start @party_command_window.close @actor_command_window.close @status_window.unselect @subject = nil BattleManager.turn_start @log_window.wait @log_window.clear end
This method begins a turn once all applicable actions have been chosen. We close @party_command_window and @actor_command_window as we don't need them any more, and unselect @status_window to remove the highlight on party members. We set @subject to nil since we're no longer choosing actions, then call BattleManager's turn_start method, which increases the turn count and determines the battler order. Finally, we wait for the log window to finish displaying its text and then clear it.
def turn_end all_battle_members.each do |battler| battler.on_turn_end refresh_status @log_window.display_auto_affected_status(battler) @log_window.wait_and_clear end BattleManager.turn_end process_event start_party_command_selection end
This method processes the end of a turn. First, we run an each loop on all_battle_members using iteration variable "battler" and in that loop we call each battler's on_turn_end method, then refresh_status. We call display_auto_affected_status in @log_window passing in battler, and then we call its wait_and_clear method to wait for the text to finish and then clear the log.
After the loop, we call BattleManager's turn_end method, then process_event, and finally we call start_party_command_selection to return us to the party commands.
def all_battle_members $game_party.members + $game_troop.members end
Here we see how all_battle_members is formed: we're returning $game_party's members plus $game_troop's members. The beauty of Ruby arrays is that you can perform arithmetic operations on them just like you can with numbers, so this is just adding two arrays to each other to form one made up of all elements of both.
def process_event while !scene_changing? $game_troop.interpreter.update $game_troop.setup_battle_event wait_for_message wait_for_effect if $game_troop.all_dead? process_forced_action BattleManager.judge_win_loss break unless $game_troop.interpreter.running? update_for_wait end end
This method processes events, and is integral to battle events working the way they do.
While scene_changing? is false, we update $game_troop's interpreter, and call its setup_battle_event method (which checks all troop event pages for events which have met their conditions). We call wait_for_message in case text was being shown, then wait_for_effect if $game_troop's all_dead? method returns true (meaning the entire troop has been killed). Then we call process_forced_action to process any forced actions, then BattleManager's judge_win_loss method to figure out if the battle was won or lost yet and then we break out of the loop unless $game_troop's interpreter's running? method returns true. Finally, we call update_for_wait.
def process_forced_action if BattleManager.action_forced? last_subject = @subject @subject = BattleManager.action_forced_battler BattleManager.clear_action_force process_action @subject = last_subject end end
This method processes a forced action. If BattleManager's action_forced? method returns true (meaning an action is being forced) we set last_subject to @subject, and @subject to BattleManager's action_forced_battler, which will return the battler whose action is being forced. We call BattleManager's clear_action_force to clear the action force flag, then call process_action to actually process the forced action, and finally we set @subject to last_subject to return to the subject who was going to act before the forced action happened.
def process_action return if scene_changing? if !@subject || !@subject.current_action @subject = BattleManager.next_subject end return turn_end unless @subject if @subject.current_action @subject.current_action.prepare if @subject.current_action.valid? @status_window.open execute_action end @subject.remove_current_action end process_action_end unless @subject.current_action end
This method processes an action. We return if scene_changing? returns true, as there's no point in continuing to process actions if we're leaving the battle scene. If @subject returns nil OR @subject has no current action, we set @subject to BattleManager's next_subject (basically, if there isn't a current acting battler or the current battler has no action to take, we move to the next one). We return turn_end unless there is now a @subject, since that would mean all applicable battlers have acted and we need to finish the turn now. If @subject's current_action is not nil, we call its prepare method. Then if the valid? method of @subject's current_action returns true, we open @status_window and call execute_action. After this check, we call remove_current_action on @subject to remove that action from their pool since it's been executed now. Then finally we call process_action_end unless @subject has a current_action, meaning that even after removing the one that just got executed they have more actions to take.
def process_action_end @subject.on_action_end refresh_status @log_window.display_auto_affected_status(@subject) @log_window.wait_and_clear @log_window.display_current_state(@subject) @log_window.wait_and_clear BattleManager.judge_win_loss end
This method processes the end of an action. First we call @subject's on_action_end method and refresh_status. Then we call @log_window's display_auto_affected_status method passing in @subject to display any states which were automatically applied by the action, then wait_and_clear to clear it afterwards. After that we call display_current_state to update the messages of any states present, followed by another call to wait_and_clear. Finally, we call BattleManager's judge_win_loss method to check whether the action resulted in winning or losing the battle.
def execute_action @subject.sprite_effect_type = :whiten use_item @log_window.wait_and_clear end
This method executes an action. We set @subject's sprite_effect_type to :whiten, which will cause its sprite to flash white briefly, then we call use_item, and finally we call wait_and_clear on @log_window to give any logged text a chance to display before disappearing.
def use_item item = @subject.current_action.item @log_window.display_use_item(@subject, item) @subject.use_item(item) refresh_status targets = @subject.current_action.make_targets.compact show_animation(targets, item.animation_id) targets.each {|target| item.repeats.times { invoke_item(target, item) } } end
This method uses an item (though it also applies to skills, so here we mean item in the general sense rather than in the "potion" sense). We set item to @subject's current_action's item object, and call @log_window's display_use_item method passing in @subject as the subject and item as the item, which will show the name of the skill/item being used. We then call @subject's use_item method passing in item, which will pay any costs, consume the item if a consumable item was used, and call any common events associated with the item. We refresh_status to reflect changes in resources etc., and set targets to the compacted result of calling @subject's current_action's make_targets method, which gives us an array of the action's targets without any nil entries. We call show_animation passing in targets as the targets and the item's animation_id as the animation ID, and then for each target we call invoke_item a number of times equal to the item's repeats property, passing in target as the target and item as the item.
def invoke_item(target, item) if rand < target.item_cnt(@subject, item) invoke_counter_attack(target, item) elsif rand < target.item_mrf(@subject, item) invoke_magic_reflection(target, item) else apply_item_effects(apply_substitute(target, item), item) end @subject.last_target_index = target.index end
And here we have invoke_item, which as we've just seen takes target and item as parameters.
If a random number from 0 to 1 is less than the result of target's item_cnt method when @subject and item are passed to it (in other words, the chance of the target counterattacking) we call invoke_counter_attack, passing in target as the target and item as the item. Otherwise, if a random number from 0 to 1 is less than the result of target's item_mrf with @subject and item passed to it, we call invoke_magic_reflection passing in target and item, as this means the target reflected a magic attack. Otherwise, we call apply_item_effects, passing in as target the result of calling apply_substitute passing in target and item, and item as the item. This ensures that the target is already replaced with a substitute battler when effects are applied, in cases where a substitute is going to take the target's place. Finally, we set @subject's last_target_index to target's index.
def apply_item_effects(target, item) target.item_apply(@subject, item) refresh_status @log_window.display_action_results(target, item) end
This method applies item effects, taking target and item as parameters. First we call target's item_apply method, passing in @subject as the user and item as the item. We refresh_status to reflect any chances in HP, MP etc. Then we call @log_window's display_action_results method, passing in target as the target and item as the item.
def invoke_counter_attack(target, item) @log_window.display_counter(target, item) attack_skill = $data_skills[target.attack_skill_id] @subject.item_apply(target, attack_skill) refresh_status @log_window.display_action_results(@subject, attack_skill) end
This method invokes a counterattack, taking target and item as parameters. First we call the display_counter method of @log_window, passing in target as the target and item as the item, which will display in the battle log that a counterattack has occurred. We set attack_skill to the element of $data_skills with an ID corresponding to target's attack_skill_id (which is almost always going to be 1), then call @subject's item_apply method, passing in target as the user and attack_skill as the item (which will apply a normal attack from the target on the subject). We call refresh_status to reflect HP changes etc. and then once again call display_action_results from @log_window.
def invoke_magic_reflection(target, item) @subject.magic_reflection = true @log_window.display_reflection(target, item) apply_item_effects(@subject, item) @subject.magic_reflection = false end
This method invokes magic reflection, whereby the target reflects a spell back at the user, and takes target and item as parameters. We set @subject's magic_reflection property to true, then call display_reflection on @log_window passing in target as the target and item as the item, which will show that a magic reflection occurred. We call apply_item_effects passing in @subject as the target and item as the item, then set @subject's magic_reflection to false.
So why do we set this flag? Following the chain of method called from apply_item_effects onwards, you'll see that this is used to determine opposite? for the purposes of applying things like states etc. In other words, the flag temporarily tells the battler that they count as a hostile unit, which means state rates and luck effects will influence the reflected spell as if it had been cast on an enemy rather than an ally.
def apply_substitute(target, item) if check_substitute(target, item) substitute = target.friends_unit.substitute_battler if substitute && target != substitute @log_window.display_substitute(substitute, target) return substitute end end target end
This method applies the substitute effect, taking target and item as parameters.
If check_substitute returns true with target and item passed to it, we set substitute to the result of target's friends_unit's substitute_battler method, which gets the first party member who has a "substitute" trait. If there is a substitute and the substitute isn't the target, we call @log_window's display_substitute method passing in substitute and target, and return substitute. This will result in the substitute actor standing in to take damage and other effects on behalf of the original target.
def check_substitute(target, item) target.hp < target.mhp / 4 && (!item || !item.certain?) end
And in this method, we check whether a substitute can occur. It takes target and item as parameters.
We return true if target's HP is less than a quarter of their maximum AND there is either no item or the item is not "certain hit".
This tells us two important things: one, that an actor will only substitute for allies at 25% HP or less. Secondly, they cannot do this against a "certain hit" effect; it will only happen for physical or magical skills/items.
def show_animation(targets, animation_id) if animation_id < 0 show_attack_animation(targets) else show_normal_animation(targets, animation_id) end @log_window.wait wait_for_animation end
This method shows an animation on a set of targets, taking targets and animation_id as parameters. If animation_id is less than 0 (meaning it's -1, which denotes it as a normal attack) we call show_attack_animation passing in targets. Otherwise, we call show_normal_animation passing in targets and animation_id. Either way, we call @log_window's wait method and then call wait_for_animation.
def show_attack_animation(targets) if @subject.actor? show_normal_animation(targets, @subject.atk_animation_id1, false) show_normal_animation(targets, @subject.atk_animation_id2, true) else Sound.play_enemy_attack abs_wait_short end end
This method shows the attack animation on a set of targets, taking targets as a parameter. If @subject is an actor, we call show_normal_animation on targets, passing in @subject's atk_animation_id1 and false for the mirror argument, and then call it again passing in targets, @subject's atk_animation_id2, and true for the mirror argument. Otherwise, we call the Sound module's play_enemy_attack method and then abs_wait_short to wait 15 frames.
The dual call to show_normal_animation is what causes a dual wielding actor to flip the animation of their second hit, which makes it a little more visually interesting.
def show_normal_animation(targets, animation_id, mirror = false) animation = $data_animations[animation_id] if animation targets.each do |target| target.animation_id = animation_id target.animation_mirror = mirror abs_wait_short unless animation.to_screen? end abs_wait_short if animation.to_screen? end end
And finally, we have show_normal_animation, which shows a normal animation on a set of targets, taking target, animation_id and mirror as parameters. mirror defaults to false if not supplied.
We set animation to the element of $data_animations at an index of animation_id. If animation contains data, we run an each loop on targets using iteration variable "target". In this loop, we set target's animation_id to the passed-in animation_id, its animation_mirror to the mirror argument, and then call abs_wait_short to wait 15 frames unless the animation's to_screen? method returns true, which it will if the animation is set to play on the whole screen rather than each target. After each target has been processed, we then call abs_wait_short if the animation plays on the whole screen.
What this will do is create a 15-frame wait between animations on each target when the animation hits each target individually, and otherwise it will just wait 15 frames at the end.
We are finally done! The behemoth that is Scene_Battle has been slain, and no mysteries remain to us. That leaves just one class.
Scene_Gameover
And it's a pretty short one. This scene is the one that comes up when your party fails miserably in battle and everyone is dead. It inherits from Scene_Base.
def start super play_gameover_music fadeout_frozen_graphics create_background end
First we overwrite the start method, starting by calling the parent method. We also call play_gameover_music, fadeout_frozen_graphics and create_background.
def terminate super dispose_background end
The terminate overwrite calls the parent method and then the dispose_background method.
def update super goto_title if Input.trigger?(:C) end
In the frame update method, we call the parent method and then call goto_title if the :C Input is triggered (meaning the player has hit enter).
def perform_transition Graphics.transition(fadein_speed) end
Here we overwrite the base perform_transition method, and we're still calling Graphics' transition method but we pass in fadein_speed rather than transition_speed, so that the game over screen fades in slightly slower than a regular scene change.
def play_gameover_music RPG::BGM.stop RPG::BGS.stop $data_system.gameover_me.play end
This method plays the game over music. First, we call the stop methods of both RPG::BGM and RPG::BGS to cut out any currently-playing background music or sound. Then we call the play method of $data_system's gameover_me.
def fadeout_frozen_graphics Graphics.transition(fadeout_speed) Graphics.freeze end
This method fades out the frozen graphics from the previous scene. We call Graphics' transition method passing in fadeout_speed, and then its freeze method.
def create_background @sprite = Sprite.new @sprite.bitmap = Cache.system("GameOver") end
This method creates the background. We set @sprite to a new instance of Sprite, and then set its bitmap to the cached GameOver system graphic.
def dispose_background @sprite.bitmap.dispose @sprite.dispose end
This method disposes of the background. Here we simply dispose @sprite's bitmap and then dispose @sprite itself.
def fadeout_speed return 60 end
This method determines the fade out speed. Here we return 60, meaning the scene prior to the game over one takes 1 second to fade out fully.
def fadein_speed return 120 end
And here we have the fade in speed. We return 120, so that fading the game over screen in takes 2 seconds.
def goto_title fadeout_all SceneManager.goto(Scene_Title) end
This method returns the player to the title screen. First we call fadeout_all, and then we call SceneManager's goto method passing in Scene_Title.
That's all for Scene_Gameover, and indeed for the default scripts! We've gone through every Module, every Game Object, every Window and every Scene. The only thing left is "main" with the line
rgss_main { SceneManager.run }
And that kicks off the whole process of the game running.
That's it. From the first episode of Slip into Ruby in July 2012, we've now covered every single line of code that makes up the base RPG Maker VX Ace engine. It's been a hell of a journey, and I can't tell you how grateful I am to everyone who's given me feedback and let me know how important this series was to their own journeys as indie game developers. I couldn't have predicted its impact or importance when I started, and the fact that people are still reading today something I began 8 years ago is frankly amazing.
So thank you, each and every reader who's made writing this series worthwhile. You are what has kept me going, and hopefully the series will be demystifying the engine for generations of users to come.
For those who jumped ship to RPG Maker MV and MZ, I still have Jump into Javascript going, but unless I think of something else to cover on the Ruby end, this series has reaches its conclusion. If anyone has any questions about any aspect of any Slip into Ruby episode or any of the code we've covered over the years, do feel free to email me at guitarherotrihan@gmail.com. If I get enough questions, I'll do a final Q&A episode to round things off.
But if that doesn't happen, I guess that's it from me, at least as far as Slip into Ruby is concerned.
Until...now.
Posts 

Pages:
1
Thank you for making this, I'm definitely saving each one of these for offline viewing and reviewing, As a beginner who is using Ace way after it's heyday, I'm glad that there's still comprehensive tutorials for it out there.
Alright Trihan, you've finally done it
You don't know how grateful I am for you to finishing your whole tutorial series, I began reading it when I was learning to code and nowadays I'm a developer with a job and everything :)
Never made a game with rpg maker vx ace though, but now it's the time for it! Thank you for finishing your tutorial series, it will help me a lot when I get the free time to develop something in vx ace
You don't know how grateful I am for you to finishing your whole tutorial series, I began reading it when I was learning to code and nowadays I'm a developer with a job and everything :)
Never made a game with rpg maker vx ace though, but now it's the time for it! Thank you for finishing your tutorial series, it will help me a lot when I get the free time to develop something in vx ace
Pages:
1