How to write an atb system script

DoubleX

Just a nameless weakling
Veteran
Joined
Jan 2, 2014
Messages
1,787
Reaction score
939
First Language
Chinese
Primarily Uses
N/A
I'm going to talk about atb system script notetag management now, but before that, I want to inform you that I've added invariants into the mix in the previous reply(#20 which is about time performance optimizations). From now on, I'll use more powerful atb system scripts as examples, so be prepared to read more complicated codes lol

Degrees of control and freedom given to users

I think that it's the 1st thing to consider: How much control and freedom your notetags will give to your users? To me, there are basically 5 such levels:

Level 1 - Everything's hardcoded, meaning notetags don't exist at all

Level 2 - The notetag values are always constant literals, meaning they can't be changed once they're set

Level 3 - The notetag values are that of the switches/variables with the specified ids, meaning those values can be changed by changing those of the associated switches/variables

Level 4 - The notetag values are always constant RGSS3 codes, meaning those codes themselves can't be changed once they're set(but what they deliver can change depending on how those codes are written)

Level 5 - The notetag values are RGSS3 codes that can always be changed on the fly

You'll generally want to stay away from Level 4 and even Level 5 unless you really, really feel/think that you're already just that skilled. So I'll stick to Level 3 until this topic reaches the advanced scripting proficiency level XD
Notetag Value Priority And Stackability

In atb system scripts, there are normally 2 types of notetag values: Those that can be stacked, and those that can't. For example:

- The atb gain rate modifier can be defined to be stacked by specifying how they're stacked(like additions, multiplications, etc) and the orders they'll be stacked(e.g.: applying those from the states first before applying those from the battlers themselves)

- The atb bar color modifiers basically can't be stacked, so such notetag with the highest priority will be the effective one(e.g.: those from states always have the highest precedence)

For example, the cast cancel notetag of Victor Engine - Active Time Battle can be stacked:

#-------------------------------------------------------------------------- # * New method: setup_cast_cancel #-------------------------------------------------------------------------- def setup_cast_cancel(user, item) rate = item.cast_cancel rate += user.cast_cancel if item.physical? rate *= cast_protection execute_cast_cancel if rand < rate && cast_action? end
Code:
  #--------------------------------------------------------------------------  # * New method: cast_cancel  #--------------------------------------------------------------------------  def cast_cancel    regexp = /<CAST CANCEL: (\d+)%?>/i    get_all_notes.scan(regexp).inject(0.0) {|r| r += ($1.to_f / 100) }  end
get_all_notes collects all notes from the battlers, then their classes and equips, and finally states.
In this case, the item's notetag takes precedence over the battlers' ones, which takes precedence over the classes', equips' and finally states' ones. The notetag values are stacked via additions.

On the other hand, the color notetags of DoubleX RMVXA Color Addon to YSA Battle System: Classical ATB can't be stacked:

  #----------------------------------------------------------------------------|  #  New method: custom_catb_state_colors                                      |  #----------------------------------------------------------------------------|  def custom_catb_state_colors    return if @state_temp_catb && @state_temp_catb == states    @state_temp_catb = states    @custom_catb_state_color = false    @state_temp_catb.each { |state|      next if !state.catb_color && !state.catb_rgba      if state.catb_color        @custom_catb_state_color1 = state.catb_color1        @custom_catb_state_color2 = state.catb_color2      else        @custom_catb_state_color1 = state.catb_rgba1        @custom_catb_state_color2 = state.catb_rgba2      end      @custom_catb_state_color = true    }  end # custom_catb_state_colors
In this case, the notetag value from the state with the lowest priority will be used.


Caching And Reading Notetag Values

Typically, there are 2 ways to extract notetag values: Using rpg data instance variables to cache them upon game start, and reading those notetag right before using the values.

For example, the values of the skill/item charge rate notetags of YSA Battle System: Classical ATB are cached using a new RPG::UsableItem instance variable upon game start:

class RPG::UsableItem < RPG::BaseItem  #--------------------------------------------------------------------------  # public instance variables  #--------------------------------------------------------------------------  attr_accessor :charge_rate  attr_accessor :charge_on  #--------------------------------------------------------------------------  # common cache: load_notetags_catb  #--------------------------------------------------------------------------  def load_notetags_catb    @charge_rate = 100    @charge_on = false    #---    self.note.split(/[\r\n]+/).each { |line|      case line      #---      when YSA::REGEXP::USABLEITEM::CHARGE_RATE        @charge_on = true        @charge_rate = $1.to_i      #---      end    } # self.note.split    #---    @charge_rate = 100 if @charge_rate <= 0  endend # RPG::UsableItem
On the other hand, the values of the use_atb notetag of Akea Active Time Battle are read right before being used:

class Game_Battler < Game_BattlerBasealias :akea_atb_use_Item :use_item  #--------------------------------------------------------------------------  # ? Usando habilidade/item  #     item :  habilidade/item  #--------------------------------------------------------------------------  def use_item(item)    note = /<use_atb *(\d+)?>/i    item.note =~ note ? @atb -= $1.to_i : @atb = 0    akea_atb_use_Item(item)  endend
If the notetag values can be used frequently(like always used in some atb system hotspots), you'll want to cache them upon game start for the sake of time performance. While such caching adds memory usage thorough the entire game executions, the more frequently the notetag values are used, the more time performance outweighs the memory usage, and vice versa.

Nevertheless, use_atb notetag of Akea Active Time Battle doesn't need to be cached for this alone, as its values are only used when an action's executed, which shouldn't be frequent.


Optimizing Notetag Time Performance

Apart from preferring caching notetag values over reading them right before using them, which is a relatively minor time performance boost, you'll want to avoid the below crystal clear and obvious pitfall if you do value time performance(YSA Battle Add-on: Lunatic CATB Rate):

  def lunatic_catb_rate_formula    formulas = []    formulas = self.actor.catb_rate + self.class.catb_rate if self.actor?    formulas = self.enemy.catb_rate if self.enemy?    value = real_gain_catb    value_percent = 100    if self.actor?      if self.equips        self.equips.each { |a| formulas += a.catb_rate if a }      end      if self.skills        self.skills.each { |a| formulas += a.catb_rate if a }      end    end    self.states.each { |state| formulas += state.catb_rate }    for formula in formulas      case formula.upcase            #----------------------------------------------------------------------      # ATB Rate Formula No.1: BOOST PERCENT      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -      # Boost ATB rate by x percents.      #      # Formula notetag:      #   <custom catb rate: boost percent x%>      #----------------------------------------------------------------------      when /BOOST PERCENT[ ](\d+)([%?])/i        value_percent += $1.to_i              #----------------------------------------------------------------------      # ATB Rate Formula No.2: REDUCE PERCENT      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -      # Reduce ATB rate by x percents.      #      # Formula notetag:      #   <custom catb rate: reduce percent x%>      #----------------------------------------------------------------------      when /REDUCE PERCENT[ ](\d+)([%?])/i        value_percent = [value_percent - $1.to_i, 1].max              #----------------------------------------------------------------------      # ATB Rate Formula No.3: SET PERCENT      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -      # Set ATB rate to x percents.      #      # Formula notetag:      #   <custom catb rate: set percent x%>      #----------------------------------------------------------------------      when /SET PERCENT[ ](\d+)([%?])/i        value_percent = [$1.to_i, 1].max              #----------------------------------------------------------------------      # ATB Rate Formula No.4: SET VALUE      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -      # Set base ATB filled speed to x.      #      # Formula notetag:      #   <custom catb rate: set value x>      #----------------------------------------------------------------------      when /SET VALUE[ ](\d+)/i        value = [$1.to_i, 1].max              #----------------------------------------------------------------------      # ATB Rate Formula No.5: ADD VALUE      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -      # Increase base ATB filled speed by x.      #      # Formula notetag:      #   <custom catb rate: add value x>      #----------------------------------------------------------------------      when /ADD VALUE[ ](\d+)/i        value += $1.to_i              #----------------------------------------------------------------------      # ATB Rate Formula No.6: DEC VALUE      # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -      # Decrease base ATB filled speed by x.      #      # Formula notetag:      #   <custom catb rate: dec value x>      #----------------------------------------------------------------------      when /DEC VALUE[ ](\d+)/i        value = [value - $1.to_i, 1].max              #----------------------------------------------------------------------      # ATB Starter Formula Default: COMMON RATE      #----------------------------------------------------------------------      when /COMMON RATE/i        # Do nothing              #----------------------------------------------------------------------      # Stop editting past this point.      #----------------------------------------------------------------------      end # End case    end # End for        #----------------------------------------------------------------------    # Return value.    #----------------------------------------------------------------------    return value * value_percent / 100      end # End defIt'll be run whenever the battler's atb or charge value can be changed, which can happen on any atb frame update.
Although I personally treat Yami as a superbly awesome scripting deity, the above method's undoubtedly pure madness in terms of time performance. The only reason I can think of why scripting giants like Yami will write such a complete mess is that it'll be more user-friendly.

While including skills there seems to be simply an understandably silly mistake(even though it's the greatest contributor to severe average fps drops when the battlers have lots of skills), all the others are much, much more serious code smells:

1. The notetag values are read right before being used instead of cached upon game start, even when they're normally used several(or up to dozens of) times per frame

2. The whole design doesn't try to cache which notetags(and hence their values) will be used as the above method always recheck which notetags will be used whenever it's called

There are some others, but these 2 are the biggest time performance killers, so I'll focus on them, especially the 2nd point as the 1st one are almost merely technical affairs(although advanced scripting proficiency might be needed there).

As mentioned in the last reply(#20 which is about time performance optimizations), invariants can be used to boost time performance. In this case, the basic invariant to be found is: When will value and/or value_percent change?

Right now the attempted answer's "per method call", which is the loosest and also the most meaningless and useless invariant possible.

It appears to be the strictest invariant possible, as the notetag values are RGSS3 codes that can return different values per frame(e.g.: rand is added into the mix). However, that's indeed just dead wrong, even though it seems to be 100% correct.

The real answer's this: When the effective notetag list changes(stacking order change, adding new effective notetag, removing notetag that becomes ineffective) or any effective notetag value changes without the former changes(e.g.: rand is added into the mix).

That seems to be the same as the above apparent answer, but in fact it isn't, as we can continue to ask: When will the effective notetag list change? When will any effective notetag value change without the former change?

The answer to the 1st question's easier and more straightforward: When the battler itself/battler class/battler equips/battler skill lists/battler states changes(adding/removing data or changing the existing ordering).

The answer to the 2nd question's harder and trickier: When the rest of the battler's internal states(program states rather than RPG::State) change, or the users added something else(like rand) into the mix.

That still looks like effectively the same as "per method call" as scripters have absolutely no control of what users might add into the mix, but again it's wrong although it's natural to come up with that.

If users add anything that can change the notetag values in ways scripters can't control anyway, those scripts can let those users to use script calls to explicitly inform them that those notetag values can change at specific moments, and assume that it's those users' responsibility to use those script calls properly. While it'll slightly harm those that use insane notetag values, it'll drastically benefit those who're not that crazy.

Implementing those answers can be done via battler change notification flags at all existing battler internal state changing methods(e.g.: refresh under Game_BattlerBase), and notetag change notification flags that are raised by users explicitly(e.g.: via script calls). The change notification flags can be analogous to this forum's user notifications, as they both need to be raised if there are changes, and reset if those notifications are already read.

While any entire answer to this case might need advanced scripting proficiency to be thoroughly comprehended(but if you still insist, you can try to thoroughly comprehend DoubleX RMVXA Enhanced YSA Battle System: Classical ATB which gives Level 5 control and freedom to users and needs advanced scripting proficiency to be thoroughly comprehended), the answer would be significantly simpler if those notetags only give Level 3 control and freedom to users instead(that's 1 solid reason why you'll want to stay away from Level 4 and even Level 5 unless you've advanced scripting proficiency). For instance, the CATB rate addon can be rewritten into something like this(I don't think there's any way to implement the answer without major refactoring):

# Lunatic CATB Rate Formulas calculates actors/enemies ATB rate.# Use the following notetag to assign the formulas to be used.# NOTE: You can use this with Actor, Enemy, Equipment, Class or State.# NOTE2: MAX_CATB_VALUE = 100000.0# Notetag: <custom catb rate: operator, val, id_flag># operator is the operator with the current value at the left hand side and# val as the right hand side# val is:# - the actual value which is a float or integer if id_flag is val# - the id of the variable whose value's to be used if id_flag is var# Battler script call: lcatbra_note_change = true# Notifies that at least 1 effective notetag value changed# This includes the changes of the variable with id FILL_TIME_VARIABLEclass << DataManager # Edit alias load_database_lcatbra load_database  def load_database(*argv, &argb)    load_database_lcatbra(*argv, &argb)    load_notetags_lcatbra # Added  end # load_database  def load_notetags_lcatbra # New    [$data_actors, $data_classes, $data_enemies, $data_weapons, $data_armors,     $data_states].each { |data|      data.each { |obj| obj.load_notetags_lcatbra if obj }    }  end # load_notetags_lcatbraend # DataManagerclass RPG::BaseItem # Edit  #----------------------------------------------------------------------------|  #  New public instance variable                                              |  #----------------------------------------------------------------------------|  attr_accessor :catb_rate # The complete catb rate notetag list  def load_notetags_lcatbra # New    @catb_rate = { operator: [], val: [] }    @note.split(/[\r\n]+/).each { |line|      case line      when /<(?:CUSTOM_CATB_RATE|custom catb rate):\s*.+,\s*.+,\s*\w+>/i        @catb_rate[:operator] << $1.to_sym        next @catb_rate[:val] << $2.to_f if $3.to_sym == :val        @catb_rate[:val] << -> { $game_variables[$2.to_i] }      end    }  end # load_notetags_lcatbraend # RPG::BaseItemclass Game_BattlerBase # Edit  alias erase_state_lcatbra erase_state  def erase_state(state_id, &argb)    erase_state_lcatbra(state_id, &argb)    @lcatbra_battler_change = true # Added to notify possible notetag change  end # erase_state  alias refresh_lcatbra refresh  def refresh(*argv, &argb)    refresh_lcatbra(*argv, &argb)    @lcatbra_battler_change = true # Added to notify possible notetag change  end # refreshend # Game_BattlerBaseclass Game_Battler < Game_BattlerBase # Edit  #----------------------------------------------------------------------------|  #  New public instance variable                                              |  #----------------------------------------------------------------------------|  attr_writer :lcatbra_note_change # The notetag value change notification flag  alias clear_states_lcatbra clear_states  def clear_states(*argv, &argb)    clear_states_lcatbra(*argv, &argb)    @lcatbra_battler_change = true # Added to notify possible notetag change  end # clear_states  alias add_new_state_lcatbra add_new_state  def add_new_state(state_id, &argb)    add_new_state(state_id, &argb)    @lcatbra_battler_change = true # Added to notify possible notetag change  end # add_new_state  alias make_first_catb_value_lcatbra make_first_catb_value  def make_first_catb_value(pre = 0, &argb)    make_first_catb_value_lcatbra(pre, &argb)    @lcatbra_battler_change = true # Added to run lunatic_catb_rate_formula  end # make_first_catb_value  MAX_CATB_VALUE = 100000.0  def lunatic_catb_rate_formula # New    # Reevaluates the cached value only if the notetag value list may be changed    return @lcatbra_val unless @lcatbra_battler_change || @lcatbra_note_change    @lcatbra_battler_change = @lcatbra_note_change = false    if actor?      notes = actor.catb_rate + self.class.catb_rate      equips.each { |equip| notes += equip.catb_rate if equip } if equips    elsif enemy?      notes = enemy.catb_rate    else      notes = []    end    states.each { |state| notes += state.catb_rate }    @lcatbra_val = real_gain_catb    notes.each { |note|      val = note[:val].respond_to?:)call) ? note[:val].call : note[:val]      @lcatbra_val.send(note[:operator], cal)    }    @lcatbra_val    #  end # lunatic_catb_rate_formulaend # Game_Battler
@lcatbra_val is the cached value, @lcatbra_battler_change is the battler change notification flag, and @lcatbra_note_change is the notetag change notification flags that are raised by users explicitly.

In this case, there are 3 reasons to change @lcatbra_val:

1. The battler's agi changes

2. The effective notetag list changes(stacking order change, adding new effective notetag, removing notetag that becomes ineffective)

3. Any effective notetag value changes without the 2nd changes(e.g.: rand is added into the mix)

The 1st and 2nd reasons should be caught by refresh and clear_states(adding erase_state and add_new_state into the mix are just to play safe in case add_state or remove_state is bypassed).

The 3rd reason should be caught by users by properly using the battler script call lcatbra_note_change = true. It's assumed to be their responsibility.

Note that "there should be no other reason to change @lcatbra_val" is an invariant used here.

Nevertheless, the original implementation does have at least 1 merit - being more user-friendly by minimizing the users' responsibilities. But since it's for advanced users in the first place, they should be able to use that added notification flag properly(whether they'll be willing to do so is another story though).

The average benchmark of these cases in my machine(WinXP 32 bit + i7-3820 without overclock + ASUS GTX550 Ti without overclock + 2 * 2GB DDR3-1333 without overclock) are as follows(Both the Graphics.frame_rate and the monitor refresh rate are set as 120, 4 actors and 8 enemies are included, the average number of skills for each battler's roughly 10, and only the time interval where no battler can act are tested):

1. The original CATB Rate Addon(Without removing the skills portion)

- About 20 graphics redraws per frame(average)

- About 20 graphics updates per frame(average)

2. The original CATB Rate Addon(With the skills portion removed)

- About 60 graphics redraws per frame(average)

- About 60 graphics updates per frame(average)

3. The edited CATB Rate Addon(With Level 4 to Level 5 control and freedom given to users and the skills portion removed)

- About 120 graphics redraws per frame(average)

- About 120 graphics updates per frame(average)

I don't want to be harsh nor rude, but the original design's utterly unforgivable in terms of time performance.


P.S.: Those who do want their atb system scripts' notetags to give Level 4 to Level 5 control and freedom to their users will want to learn this trick:

data.note = eval("-> battler { battler.instance_exec { #{code} } }")It's almost as flexible as direct eval while it's still much faster than direct eval(up to something like 8 times faster in my machine).

This trick will be heavily used when this topic reaches advanced scripting proficiency, so you may want to practice it now if you really want to aim that high(like trying to make the most powerful atb system script ever).

P.S.S.: More convoluted and complicated notetag management(architectures, generics, etc) will be needed if an atb system script's an advanced complex one. It'll be covered when this topic reaches advanced scripting proficiency.
 
Last edited by a moderator:

DoubleX

Just a nameless weakling
Veteran
Joined
Jan 2, 2014
Messages
1,787
Reaction score
939
First Language
Chinese
Primarily Uses
N/A
More on optimizing performance:

Some ways to come up with less performant algorithms

Now I'm going to talk about some other code qualities when writing atb system scripts. You're assumed to have a solid understanding to the default RMVXA battle flow implementations(which is vital to debugging atb system scripts and implementing changing battle systems on the fly).

Conforming to the MVC architecture

Disclaimer: It's not necessary to conform to MVC. Doing so's just an option to write atb system scripts.

Applying MVC to atb system scripts:

- Global and battler atb clock, action input and execution flow implementations(including action validity), and the added failed escape cost, atb frame update and wait condition implementations are almost purely Model

- Battler atb clock display is almost purely View

- Action input and execution flows definitions, window setup and deactivation, and the added atb frame update and wait condition definitions are almost purely Control

Also, the default RGSS3 script architecture at least closely resembles to(if not just uses) MVC. For instance:

- BattleManager is mainly Model although it's some Controller features

- Game Objects(except Game_Interpreter) closely resemble to Model

- Sprites closely resemble to View

- Windows are mainly View although it's some Model features

- Scenes closely resemble to Controller

They imply that:

- Global atb clock, action input and execution flow implementations, and the added atb frame update and wait condition implementations should be written under BattleManager whenever feasible

- Action validity should be written under Game_Action whenever feasible

- Battler atb clock and the added failed escape cost should be written under Game_BattlerBase, Game_Battler, Game_Actor, Game_Enemy, Game_Party and/or Game_Troop whenever feasible

- Battler atb clock display should be written under Window_BattleStatus and/or some new Sprite/Viewport/Window classes whenever feasible

- Action input and execution flows definitions, window setup and deactivation, and the added atb frame update and wait condition definitions should be written under Scene_Battle whenever feasible

As the major change to the default RMVXA battle system by atb system scripts is the battle flow, which is almost purely Control, it's normally desirable to write those scripts in ways that fellow scripters can quickly grasp the big picture just by reading the Control part of those scripts. Applying the 80-20 rule, we'll want them to fathom 80% of the script implementations just by reading 20% of the codes.

This means atb system scripts conforming to MVC should cleanly presents the overall framework of the battle flow changes under Scene_Battle, and leave their details into BattleManager. Most of the rest of those scripts should be under Game_Objects and Sprite/Viewport/Window if they belong to Model and View respectively.

On a side note: To ensure your script to be highly readable, you'll have to assume the readers know nothing on the basic concepts and knowledge of implementing atb system scripts.

Disclaimer(again): It's not necessary to conform to MVC. Doing so's just an option to write atb system scripts.
Cohesion and Coupling

Disclaimer: Never treat what I'm saying as dogmas. Think about them instead of blindly following them.

As best practices, high cohesion and loose coupling are generally preferred.

To achieve high cohesion, try to strive for functional cohesion whenever feasible.

To achieve loose coupling, try to eliminate as many couplings that shouldn't even exist(reaching no coupling at all) as feasible(sometimes via delegation), and strive for data coupling and even message coupling whenever feasible.

High cohesion and loose coupling should be first applied to feature set design(addons in the core addon approach or components in the single script approach), which is the highest level of their applications in writing atb system scripts.

To achieve functional cohesion, each feature should have just 1 well defined function(Single responsibility principle).

To achieve loose coupling, each feature should be defined in ways that are as independent from as many other features as possible.

To eliminate couplings that shouldn't even exist, features that can be implemented as completely standalone scripts should be implemented this way(those features are delegated to those completely standalone scripts).

For instance, the below feature achieves functional cohesion:

Battler atb clock display - Displays all battlers' atb clocks("Displays" is a sufficiently imperative verb and "all battlers' atb clocks" is a sufficiently specific noun)

Whereas the coupling between:

- The global and the battler atb clock can be as loose as no coupling, as they're always 100% independent from each other(unless some features specifically change this).

- The atb frame update method and the battler atb clock display can be as loose as message coupling, as the latter's almost always called by the former(except some extreme edge cases of course) without passing anything(i.e., no arguments).

- The battler atb clock and battler atb clock display can be as loose as data coupling, as the latter always only need to know the current battler atb clock and nothing else.

On a lower level, high cohesion and loose coupling can be applied on deciding writing which implementation parts into which classes.

1 way to achieve function cohesion and loose coupling here is to conform to the default RMVXA codebase's class designs, by having a solid understanding on the functions of each class and the couplings among those classes(bear in mind that it also stick to "composition over inheritance").

On the lowest level, high cohesion and loose coupling can be applied on adding new instance variables(add class variables only with very, very solid reasons), and writing new methods as well as aliasing and rewriting existing ones.

High cohesion can be achieved here by ensuring every new method and variable does just 1 thing(so writing short methods are highly recommended although methods shouldn't be too short either).

Loose coupling can be achieved here by utilizing object composition(just like what the default RMVXA codebase does).

Disclaimer(again): Never treat what I'm saying as dogmas. Think about them instead of blindly following them.
Writing modular features

Disclaimer: Never treat what I'm saying as dogmas. Think about them instead of blindly following them.

Modular design is the key to write high quality atb system scripts. Both the addons in the core addon approach and the optional components in the single script approach should be modular enough that enabling/disabling/modifying any of them shouldn't break anything else(Separation of concerns).

Programming to the interface can help here. When dealing with a feature, try to only expose parts that are needed by some of the rest of the atb system, and isolate the rest into that feature only.

For example, the battler atb clock display only needs to(unless there are some features that specifically change that):

- Know the battler atb clock and nothing else.

- Be known by the atb frame update and nothing else.

This ensures that enabling/disabling/modifying the battler atb clock display won't break anything else, as it's as simple as enabling/disabling/modifying the battler atb clock display method contents, which has nothing to do with anything else.

It also means modules should interact by telling them what to do rather than how to do, as the how part should be completely private to the module that actually does the jobs.

For example, the atb frame update method should only ask all battlers to update their atb clock, but will never actually update their atb clock for them, as it's those battlers' jobs that should be done by themselves only.

While sometimes a module has to pass some data to another module in order for the latter to do its job, those data should only be the absolutely necessary ones(and they shouldn't be used for controls inside the latter module to avoid control coupling).

For example, the battler should only "pass" the battler atb clock(via getters) into the battler atb clock display and nothing else, as that's the only data the latter needs.

An excellent counterexample on modularity is how I screwed up YSA Battle System: Classical ATB by writing too many too poorly written addons with too many antipatterns(the combined technical debt is so deep that they almost lead to technical bankruptcy). Just bear in mind that reading them all can be extremely painful(even for myself). I'll talk about this counterexample later.

Disclaimer(again): Never treat what I'm saying as dogmas. Think about them instead of blindly following them.
 
Last edited by a moderator:

DoubleX

Just a nameless weakling
Veteran
Joined
Jan 2, 2014
Messages
1,787
Reaction score
939
First Language
Chinese
Primarily Uses
N/A
Someone asked me to make a template on making an atb system script, showing all its necessary steps in detail(the difficulty in this reply's back to the basic level, like those in #1 to #19).

Although I've covered one for DoubleX RMVXA Basic ATB in #17, that request caused me to think that maybe that's still too convoluted, so an even easier, simpler and smaller example might help more.

I'll use DoubleX RMVXA Min ATB, which is the easiest, simplest and smallest atb system script I've ever written, as an example(I never published this as I don't feel/think it's any practical use).

Although it's actually maximally minimized from DoubleX RMVXA Basic ATB, here I'll pretend that I'd written it from scratch instead:

The feature set

Script Calls:

# * Battler manipulations |# 1. matb_val |# - Returns the battler's atb fill percentage |# 2. reset_matb_val |# - Clears all battler's actions and empties the battler's atb bar also |
Configurations:

# Sets the base atb fill time from empty to full as BASE_FILL_T seconds # If BASE_FILL_T_VAR_ID is a natural number, the value of variable with id # BASE_FILL_T_VAR_ID will be used instead of using BASE_FILL_T BASE_FILL_T = 5 BASE_FILL_T_VAR_ID = 0 # Sets the maximum turn clock unit as MAX_TURN_UNIT # If MAX_TURN_UNIT_VAR_ID is a natural number, the value of variable with id # MAX_TURN_UNIT_VAR_ID will be used instead of using MAX_TURN_UNIT MAX_TURN_UNIT = 5 MAX_TURN_UNIT_VAR_ID = 0 # Sets the 1st atb bar color as text color ATB_BAR_COLOR1 # If ATB_BAR_COLOR1_VAR_ID is a natural number, the value of variable with # id ATB_BAR_COLOR1_VAR_ID will be used instead of using ATB_BAR_COLOR1 # The value of variable with id ATB_BAR_COLOR1_VAR_ID should remain the same # during battles to ensure proper atb bar color displays ATB_BAR_COLOR1 = 7 ATB_BAR_COLOR1_VAR_ID = 0 # Sets the 2nd atb bar color as text color ATB_BAR_COLOR2 # If ATB_BAR_COLOR2_VAR_ID is a natural number, the value of variable with # id ATB_BAR_COLOR2_VAR_ID will be used instead of using ATB_BAR_COLOR2 # The value of variable with id ATB_BAR_COLOR2_VAR_ID should remain the same # during battles to ensure proper atb bar color displays ATB_BAR_COLOR2 = 8 ATB_BAR_COLOR2_VAR_ID = 0

The atb frame update

First, I alias update under Scene_Battle:

alias update_matb update def update update_matb matb_update if matb_update? # Added end # update
matb_update is the atb frame update method while matb_update? checks against the atb wait conditions.

Next, I write matb_update under Scene_Battle:

def matb_update # New turn_end if (@matb_turn_clock += 1) >= DoubleX_RMVXA::MATB.max_turn_unit BattleManager.matb_update list = BattleManager.action_battlers.select { |b| b.inputable? }.collect! { |b| b.index } matb_update_windows(list) unless list.include?(@status_window.index) @status_window.refresh_matb_bars end # matb_update
It controls what needs to be updated in an atb frame update.

BattleManager.matb_update updates all battler atb clocks

matb_update_windows uses the list of indices of all actors that can input actions to update the status window to ensure it's always selecting the correct actor or correctly unselected.

@status_window.refresh_matb_bars refreshes all actors' atb bars.

To sum up, it updates the underlying data before updating their visuals to ensure those visuals always correctly displays those underlying data.


The ATB wait condition

I write matb_update?(which is called by update under Scene_Battle) under Scene_Battle:

def matb_update? # New return false if scene_changing? || @spriteset.animation? || @actor_window.active || @enemy_window.active || @skill_window.active || @item_window.active || @actor_command_window.active || @party_command_window.active BattleManager.matb_update? end # matb_update?
As that script always use the loosest wait condition, which is wait whenever players can input commands or the atb frame update simple can't take place, which is covered by BattleManager.matb_update?:

def matb_update? # New return false if $game_party.all_dead? || $game_troop.all_dead? || $game_message.visible @phase && @phase != :init end # matb_update?
When the game party or troop's all dead, or the battle's about to be aborted, the battle will end, causing further atb frame updates to be redundant(and harmful in some cases).

At the start of a battle, some extra preparations needs to be done(like applying the battler atb clock values at the start of a battle) before the atb frame update can run.

As updating the atb frame can trigger animation displays and/or open windows, and doing those can interfere with the game messages(or even just crash or freeze the game), updating the atb frame at that moment can be bug prone.


The battler atb clock update

I write matb_update under BatlteManager to update all alive battlers' atb clock:

def matb_update # New ($game_party.alive_members + $game_troop.alive_members).each { |mem| mem.matb_update } end # matb_update
Each battler updates his/her/its atb clock in matb_update under Game_Battler:

def matb_update # New return if @matb_val >= 100.0 || inputable? || restriction > 3 @matb_val_change = @matb_val != @matb_val += agi / 100.0 return unless @matb_val >= 100.0 @matb_val = 100.0 make_actions end # matb_update
If the battler's full atb or can input actions or can't move, there's no need to update the battler atb clock, as that battler will either execute or input actions at that moment, or just can't update atb due to being restricted.

@matb_val_change is used to notify the battler's atb bar to be updated, which should be updated if and only if the atb value's changed(Updating an atb bar is relatively expensive so it should only be updated if it's to).

If the battler's atb becomes full, new empty action slots should be made so that battler can input actions, so I aliased make_actions under Game_Battler to mark that the battler becomes actable:

alias make_actions_matb make_actions def make_actions make_actions_matb # Added return if BattleManager.action_battlers.include?(self) BattleManager.action_battlers << self # end # make_actions

The action execution

I rewrite process_action under Scene_Battle:

def process_action # Rewrite # Rewritten return process_matb_act(true) if @subject BattleManager.action_battlers.reject { |b| b.inputable? }.each { |b| @subject = b process_matb_act @subject = nil } # end # process_action
It checks if any battler can process actions while process_,atb_act actually processes those actions. Only battlers that can act are checked.

If the battler's atb isn't full or is inputable, that battler won't process actions, otherwise that battler will process each action sequentially until all actions are processed.

process_matb_act under Scene_Battle will also call process_action_end under Scene_Battle upon executing all the battler's actions:

# forced: Whether the action's forced def process_matb_act(forced = false) # New while @subject.current_action @subject.current_action.prepare if @subject.current_action.valid? @status_window.open execute_action end @subject.remove_current_action break if forced end process_action_end unless @subject.current_action end # process_matb_act
After executing all the battler's actions, that battler's states with auto removal timing action end needs to be updated and his/her/its atb clock needs to be reset to conform to the default RMVXA battle features. So I aliased on_action_end:

alias on_action_end_matb on_action_end def on_action_end on_action_end_matb # Added matb_update_state_turns(1) reset_matb_val # end # on_action_end
update_state_turns under Game_Battler is rewritten to this:

def update_state_turns # Rewrite matb_update_state_turns(2) # Rewritten end # update_state_turns
matb_update_state_turns is the below new method:

# timing: The state's auto removal timing def matb_update_state_turns(timing) # New states.each { |state| next if state.auto_removal_timing != timing @state_turns[state.id] -= 1 if @state_turns[state.id] > 0 } end # matb_update_state_turns
reset_matb_val under Game_Battler resets the battler's atb clock, removes that battler's action slots, and removes that battler from the actable battler list:

def reset_matb_val # New @matb_val = 0.0 @matb_val_change = true clear_actions BattleManager.action_battlers.delete(self) BattleManager.clear_actor if actor? && BattleManager.actor == self end # reset_matb_val

Action Validity

An actor is inputable only if that actor has at least 1 empty action slot. So I aliased inputable? under Game_Battler:

alias inputable_matb? inputable? def inputable? return false if @actions.all? { |act| act.item } inputable_matb? && @actions.size > 0 && actor? # Rewritten end # inputable?

Actor command window setup and deactivation

I write matb_update_windows under Scene_Battle to handle most window setups and deactivations:

# list: The list of indices of all the currently inputable actors def matb_update_windows(list) # New @status_window.unselect if @status_window.index >= 0 return if list.empty? BattleManager.actor_index = list[0] start_actor_command_selection end # matb_update_windows
As the passed list doesn't include the current status window index, if it's greater than 0(selecting an actor), it'll be certainly wrong, as it should only select an actor with index included in the passed list, meaning the status window should be unselected.

If the list's empty, it means no actor can input actions, so nothing else needs to be done.

If the list isn't empty, then the 1st actor in the list will be selected with his/her/its actor command window being setup.


Starting battler atb clock status

I aliased battle_start under BattleManager:

  alias battle_start_matb battle_start  def battle_start    battle_start_matb    matb_battle_start # Added  end # battle_start
matb_battle_start under BattleManager sets all battlers' starting atb value:

def matb_battle_start # New start = @preemptive ? :preempt : @surprise ? :surprise : :norm ($game_party.battle_members + $game_troop.members).each { |mem| mem.matb_start(start) } end # matb_battle_start
Each battler's matb_start under Game_Battler actually sets his/her/its starting atb value:

# start: The battle start type def matb_start(start) # New @matb_val = movable? && (start == :preempt && actor? || start == :surprise && enemy?) ? 100.0 : 0.0 @matb_val_change = true end # matb_start
If the battler's isn't movable, the starting atb value will be 0.

If the start is preemptive, actors' atb will be full and new action slots will be acquired, while enemies' atb will be empty, and vice versa.

The starting atb value will be 0 for all the other cases.


Battler atb clock display

I rewrite draw_gauge_area_with_tp and draw_gauge_area_without_tp under Window_BattleStatus:

def draw_gauge_area_with_tp(rect, actor) # Rewrite # Rewritten draw_actor_hp(actor, rect.x + 0, rect.y, 60) draw_actor_mp(actor, rect.x + 64, rect.y, 60) draw_actor_tp(actor, rect.x + 128, rect.y, 52) draw_matb_bar(rect, actor, true) if actor # end # draw_gauge_area_with_tp def draw_gauge_area_without_tp(rect, actor) # Rewrite # Rewritten draw_actor_hp(actor, rect.x + 0, rect.y, 72) draw_actor_mp(actor, rect.x + 80, rect.y, 72) draw_matb_bar(rect, actor, false) if actor # end # draw_gauge_area_without_tp
I also write refresh_matb_bars under Window_BattleStatus to refresh all actors' atb bars:

def refresh_matb_bars # New display_tp = $data_system.opt_display_tp item_max.times { |index| next unless (actor = $game_party.battle_members[index]) && actor.matb_val_change draw_matb_bar(gauge_area_rect(index), actor, display_tp) actor.matb_val_change = false } end # refresh_matb_bars
The actor's atb bar will be refreshed only if that actor's atb value changed.

I write draw_matb_bar under Window_BattleStatus to actually draw the actor's atb bar:

# rect: The atb bar's rect # actor: The atb bar's owner # display_tp: The tp bar display flag def draw_matb_bar(rect, actor, display_tp) # New display_tp ? (x, w = rect.x + 184, 36) : (x, w = rect.x + 160, 60) draw_gauge(x, rect.y, w, actor.matb_val / 100.0, text_color(DoubleX_RMVXA::MATB.atb_bar_color1), text_color(DoubleX_RMVXA::MATB.atb_bar_color2)) change_color(system_color) draw_text(x, rect.y, 30, line_height, "AP") end # draw_matb_bar
The actor's atb value change flag will be reset as false right after drawing the atb bar to ensure it'll be drawn only if the atb value's changed.


Battle Turn

I rewrite in_turn? under BattleManager as always returning the atb wait condition:

#----------------------------------------------------------------------------| # Always lets actions to be executed whenever the atb frame update runs | #----------------------------------------------------------------------------| def in_turn? # Rewrite SceneManager.scene.matb_update? # Rewritten end # in_turn?
Now both process_event and process_action will always be called per atb frame update.

I alias input_start under BattleManager to stop recreating new action slots are battlers and opening the party command window:

#----------------------------------------------------------------------------| # Stops making battlers' actions when opening the party command window | #----------------------------------------------------------------------------| alias input_start_matb input_start def input_start @phase = :input # Added input_start_matb end # input_start
I rewrite turn_start under Scene_Battle to stop shifting battle phase, clearing subject, or clearing the log window:

def turn_start # Rewrite @party_command_window.close @actor_command_window.close @status_window.unselect # Removed to stop shifting battle phase nor clearing subject @log_window.wait # Removed to stop clearing the log window end # turn_start
I rewrite turn_end under Scene_Battle to increase the battle turn number by 1 and stop opening thew party command window:

def turn_end # Rewrite ($game_party.battle_members + $game_troop.members).each { |mem| mem.on_turn_end refresh_status @log_window.display_auto_affected_status(mem) @log_window.wait_and_clear } # Added @matb_turn_clock = 0 $game_troop.increase_turn # BattleManager.turn_end process_event # Removed to stop opening the party window end # turn_end

Party Escape

A failed party escape attempt should empty all actors' atb, remove all their action slots, and remove them from the actable battler list. So I aliased clear_actions under Game_Party:

alias clear_actions_matb clear_actions def clear_actions clear_actions_matb members.each { |mem| mem.reset_matb_val } # Added end # clear_actions

Bug Fixes

I alias hide under Game_BattlerBase to ensure no hidden battler will be falsely regarded as actable:

alias hide_matb hide def hide hide_matb reset_matb_val # Added end # hide
I alias initialize under Game_Battler to ensure all actors' action input index will be properly initialized:

alias initialize_matb initialize def initialize initialize_matb # Added to fix nil action input index bugs and edge cases as well clear_actions @matb_val = 0.0 # end # initialize
I alias on_restrict under Game_Battler to ensure no restricted battler will be falsely regarded as actable(except confusion, which will immediately become actable again)

alias on_restrict_matb on_restrict def on_restrict on_restrict_matb # Added to fix nil action battlers bugs and edge cases as well reset_matb_val if BattleManager.action_battlers # end # on_restrict
I alias on_turn_end under Game_Battler to ensure all buff turns will be updated upon turn end:

alias on_turn_end_matb on_turn_end def on_turn_end on_turn_end_matb remove_buffs_auto # Added end # on_turn_end
I alias on_battle_end under Game_Battler to ensure the atb clocks of all battlers leaving the battle before it ends will be properly reset as 0 when they enter the next battle after the start of that battle:

#----------------------------------------------------------------------------| # Ensures battlers added after the battle starts will start with 0 atb value| #----------------------------------------------------------------------------| alias on_battle_end_matb on_battle_end def on_battle_end on_battle_end_matb @matb_val = 0.0 # Added end # on_battle_end
I rewrite update_info_viewport under Scene_Battle to ensure the viewport's always correctly positioned:

def update_info_viewport # Rewrite # Rewritten move_info_viewport(@party_command_window.active ? 0 : matb_update? ? 64 : 128) # end # update_info_viewport
I alias update_message_open under Scene_Battle to ensure the status open will always be corrected opened after the message display's closed:

Code:
  alias update_message_open_matb update_message_open  def update_message_open    update_message_open_matb    # Added    return if $game_message.busy? || $game_troop.all_dead? ||     $game_party.all_dead?     @status_window.open if @status_window.close?    #  end # update_message_open
 
Last edited by a moderator:

Users Who Are Viewing This Thread (Users: 0, Guests: 1)

Latest Threads

Latest Profile Posts

so hopefully tomorrow i get to go home from the hospital i've been here for 5 days already and it's driving me mad. I miss my family like crazy but at least I get to use my own toiletries and my own clothes. My mom is coming to visit soon i can't wait to see her cause i miss her the most. :kaojoy:
Couple hours of work. Might use in my game as a secret find or something. Not sure. Fancy though no? :D
Holy stink, where have I been? Well, I started my temporary job this week. So less time to spend on game design... :(
Cartoonier cloud cover that better fits the art style, as well as (slightly) improved blending/fading... fading clouds when there are larger patterns is still somewhat abrupt for some reason.
Do you Find Tilesetting or Looking for Tilesets/Plugins more fun? Personally I like making my tileset for my Game (Cretaceous Park TM) xD

Forum statistics

Threads
105,868
Messages
1,017,074
Members
137,578
Latest member
JamesLightning
Top