There isn’t a lot of documentation on the system that Arc System Works uses to make their games, and I know some people have been wondering how the scripting system (what we call BBScript) works. Here I will try to outline the basic idea of how it functions and why I like it so much. I believe it offers something that I haven’t seen before in a game engine that could be extremely valuable to learn from as a game developer.

Script File Types

First, there is a structure to what each script is and what it contains. These scripts are:

  • The common script (often labeled CMN in filenames)
  • The character script
  • The character effect script

The common script (technically the common effect script, though the actual common script contains practically nothing.) contains all things of universal importance to the game. It contains for example, subroutines which are called by every character that set the universal amount of health, initialize the proper effects from systems like instant guard, just guard, burst, etc. It also contains a subroutine which uses addMove (which I will explain later) to initialize universal options like walking, dashing, jumping, etc.

The character script contains all the information defining a characters actions, frame data/animations, move properties, etc.

The character effect script contains the code which defines all objects spawned by the character. These include projectiles, special particle effects, and even animation smears! It seems to mainly serve as a form of easy code separation.

BBScript Character Patterns

Now that we know where the code is, let’s see what a characters file contains.

Initialization and State metadata

In every ArcSys game I’ve seen thus far, each character initializes all their stats and states in a subroutine called PreInit2nd. This contains the basic stuff, like dash speed, jump height, airdash length, etc. But it also contains the setup for states and how they are accessed.

Lets see one example here in Guilty Gear Strive Sols script, the addMove call for Gunflame:

/* snip... */
  addMove: s32'GunFlame'
    moveType: (SPECIAL)
    characterState: (STANDING)
    moveInput: (INPUT_236)
    moveInput: (INPUT_PRESS_P)
    /* ...Some more unlabeled flags and stuff here... */
    endMove:
/* ...snip */

Adding moves is that easy! Sort of, anyway. This is just the definition of the state and the information that lets the character transition into it. They also register the state with a quick call later on in the init subroutine that just looks like this:

registerMove: s32'Gunflame'

This is nice because it lets you easily add/remove all access to a certain state without deleting the definition of the state and its constraints that allow entering it.

State blocks

Now that we know how to add a states definition, we need to add the real state, this block defines which animations gets accessed, frame data through those accesses, when hitboxes are activated, move properties, etc.

Here is an example of Sols Gunflame state with some annotations:

/* snip... */
beginState: s32'GunFlame'
  upon: (IMMEDIATE)
    Unknown1058: 1
    callSubroutine: s32'BeginingMoveCheck'
    endUpon: 
  sprite: s32'sol400_00', 1 <-- This calls the animation frame + hitbox file of the same name
  callSubroutineWithArgs: s32'cmnAddTensionGG', (STATIC), 3, (STATIC), 0, (STATIC), 0, (STATIC), 0
  Unknown2189: 0, 5
  setCarriedMomentumPercentage: 150 <-- This instruction creates the dash momentum effect
  sprite: s32'sol400_01', 2
  setCarriedMomentumPercentage: 150
  sprite: s32'sol400_02', 2
  setCarriedMomentumPercentage: 120
  Unknown613: s16'vxxx200' <-- This plays a voice sound effect
  sprite: s32'sol400_03', 2
  sprite: s32'sol400_04', 3
  Unknown446: s32'GunFlame_Obj', 0 <-- This one calls the projectile state in the effect script!
  callSubroutineWithArgs: s32'cmn_screenshake', (STATIC), 1, (STATIC), 1, (STATIC), 0, (STATIC), 0
  Unknown615: s32'SE_BTL_SOL_08' <-- Plays a sound effect
  sprite: s32'sol400_05', 3
  Unknown615: s32'SE_BTL_SOL_01'
  sprite: s32'sol400_06', 5
  sprite: s32'sol400_07', 5
  sprite: s32'sol400_08', 5
  sprite: s32'sol400_09', 5
  sprite: s32'sol400_10', 5
  sprite: s32'sol400_11', 3
  Unknown2526: 0, 0, 0, 0, 0, 0
  Unknown2591: 0, 0, 0, 0, 0, 0
  sprite: s32'sol400_12', 3
  sprite: s32'sol400_13', 3
  sprite: s32'sol400_14', 4
  sprite: s32'sol400_15', 3
  endState: 
/* ...snip */

As you can see in this block of code, these states arent too complicated at all! The script creates a nice declarative format for the animation and properties of the player state. There are some limitations to this, such as making it harder to define the limitations of entering states, you will either need to use an engine-intrinsic flag or define some other state which will check a variable and enter the state according to some check.

I wouldn’t be surprised if they had some form of hot-reloading for all these scripts in their development tools. Even without that hot reloading iteration on ideas and new states for characters is incredibly quick and simple.

State Transitions

I think this general approach to scripting for a fighting game is genius. It effectively gives you direct access to the state machine of the player, but with some engine-intrinsic assumptions that make it feel a lot more “batteries included” than needing to redefine the same data for every single state. For example, the acceptable state transitions that are assumed in most ArcSys games work in a hierarchy that looks like this (simplified for the example):

  1. Movement
  2. Normals
  3. Specials/Overdrives

You can only move down the list here, and the engine has this assumption programmed in, therefore when you define a state with moveType: (NORMAL) it will allow that to cancel into states with moveType: (SPECIAL) and moveType: (OVERDRIVE) by default! You are of course allowed to create special cases in these state metadata blocks through special functions, but the safe defaults are great for ensuring consistency.

An important note about frames and the code run between them

Something you will see often in BBScript code is instructions that seemingly are placed too late in a moves animation to make sense. For example a move with 5 active frames, that looks like this:

beginState: s32'PlaceHolderMove'
  sprite: s32'chr_000_00', 2
  hit: <-- `hit` was run here, so shouldnt it have 3 active frames?
  sprite: s32'chr_000_00', 3

This is because when code is placed directly after a sprite call, it is actually run immediately before the first frame of the sprite call directly before it. This is unintuitive but once you learn it the code isn’t too hard to read.

Instructions for Every State Transition

Something very frequently used in ArcSys games is “gatlings” which is essentially just allowing specific normals to cancel into each other. This is not an engine assumption because normals are not meant to be cancelable into every other normal by default. If you want to explicitly add a state transition to a certain state on hit, the addGatlingOption instruction does exactly that, heres an example from Sols cS (lots of the code omitted for readability):

beginState: s32'NmlAtk5CNear'
  upon: (IMMEDIATE)
    callSubroutine: s32'cmn_AtkLv4'
    callSubroutine: s32'cmn_countertype_middle'
    damage: 0, 44
    pushbackX: 50
    Unknown1108: 45
    storeValue: (VARIABLE), 338, (STATIC), 0
    storeValue: (VARIABLE), 339, (STATIC), 0
    storeValue: (VARIABLE), 340, (STATIC), 1
    callSubroutine: s32'cmn_hosei'
    callSubroutineWithArgs: s32'cmn_SameAttack', (STATIC), 3, (STATIC), 0, (STATIC), 0, (STATIC), 0
    callSubroutine: s32'cmn_AtkTemplFloatDamageBody'
    callSubroutine: s32'cmn_fuwafuwafuwa'
    hitAirPushbackX: 0, 10000
    hitAirPushbackY: 0, 20000
    addGatlingOption: s32'NmlAtk6A' <-- Adds the gatling option for 6A (6P in game notation)
    addGatlingOption: s32'NmlAtk5CFar'
    addGatlingOption: s32'NmlAtk5D'
    addGatlingOption: s32'NmlAtk2C'
    addGatlingOption: s32'NmlAtk6C'
    addGatlingOption: s32'NmlAtk2D'
    addGatlingOption: s32'NmlAtk6D'
    addGatlingOption: s32'NmlAtk5E'
    addGatlingOption: s32'NmlAtk2E'
    addGatlingOption: s32'Cancel_FDash'
    enableJumpCancel: 1 <-- They also have a specific instruction for allowing jump cancels
    upon: (HIT_OR_GUARD)
      extendPushboxX: 100000
      endUpon: 
    Unknown953: 2
    storeValue: (VARIABLE), 343, (STATIC), -250000
    storeValue: (VARIABLE), 344, (STATIC), 0
    storeValue: (VARIABLE), 345, (STATIC), 0
    endUpon: 
  /* ...other state stuff here... */
  endState: 

Alright, we know how to add gatlings now. What if we want to add a state transition that can be done regardless of if the move hit, but still restrict it to a specific timing within the move? Theres an instruction for that too, addWhiffCancelOption functions basically the same as addGatlingOption but it adds something thats cancelable without needing to hit the opponent. You can also restrict this with the enableWhiffCancelOptions instruction.

One more instruction related to whiff cancel options which will not affect when theyre cancelable, but will affect when you can input the cancel is whiffCancelOptionBufferTime. This instruction takes the state you added a whiff cancel for, and the amount of frames of buffer you want to have on cancel.

Lets look at an example that combines all 3 of these functions, Axls Rensengeki:

beginState: s32'Rensengeki'
  /* ...upon: (IMMEDIATE) and lots of sprites omitted here... */
  sprite: s32'axl400_21', 3
  enableWhiffCancel: 1 <-- Enable whiff cancels here
  addWhiffCancelOption: s32'Rensen_Bomb' <-- Add the whiff cancel options
  addWhiffCancelOption: s32'Kyokusageki'
  addWhiffCancelOption: s32'Sensageki'
  whiffCancelOptionBufferTime: s32'Rensen_Bomb', 10 <--| Add a 10 frame buffer on all these inputs.
  whiffCancelOptionBufferTime: s32'Kyokusageki', 10    | Note that this considers inputs in the past
  whiffCancelOptionBufferTime: s32'Sensageki', 10      | so it will cancel if you input something 
  sprite: s32'axl400_22', 3                            | before cancels were enabled.
  sprite: s32'axl400_23', 3
  Unknown1657: s32'Rensen_Bomb'
  sprite: s32'axl400_24', 3
  addWhiffCancelOption: s32'Rensen_Bomb_Just' <-- Add the just bomb cancel with a 3 frame buffer time
  whiffCancelOptionBufferTime: s32'Rensen_Bomb_Just', 3
  sprite: s32'axl400_25', 2
  sprite: s32'axl400_26', 2
  Unknown1657: s32'Rensen_Bomb'
  Unknown1657: s32'Kyokusageki'
  Unknown1657: s32'Sensageki'
  Unknown1657: s32'Rensen_Bomb_Just'
  recoveryState: 
  sprite: s32'axl400_27', 2
  sprite: s32'axl400_28', 2
  sprite: s32'axl400_29', 2
  sprite: s32'axl400_30', 3
  sprite: s32'axl400_31', 3
  endState: 

Alright, now we have 2 types of cancels we can easily add to moves, but lets say a move should have some part of the recovery animation only contribute aesthetically if the player allows it to play, but allow that section to be canceled with any action (This is commonly referred to as IASA frames, IASA standing for Interruptible As Soon As). To do this we could add a whiff cancel for every state, but that would be absurd, luckily there is a specific subroutine in Strive that enables canceling into anything (as well as specific instructions for it in the other games). To add this to a move, we just call the subroutine labeled cmnNandemoCancel (nandemo meaning “anything”). Lets take a look in sols Gunflame Feint:

beginState: s32'GunFlameFeint'
  upon: (IMMEDIATE)
    Unknown2173: 
    Unknown1058: 1
    callSubroutine: s32'BeginingMoveCheck'
    endUpon: 
  sprite: s32'sol400_00', 1
  callSubroutineWithArgs: s32'cmnAddTensionGG', (STATIC), 8, (STATIC), 0, (STATIC), 0, (STATIC), 0
  Unknown2189: 0, 5
  setCarriedMomentumPercentage: 150
  sprite: s32'sol400_00', 2
  setCarriedMomentumPercentage: 150
  sprite: s32'sol400_01', 3
  setCarriedMomentumPercentage: 120
  Unknown613: s16'vxxx201'
  sprite: s32'sol400_02', 2
  sprite: s32'sol400_03', 2
  sprite: s32'sol400_04', 2
  Unknown615: s32'SE_BTL_SOL_08'
  sprite: s32'sol400_05', 2
  sprite: s32'sol400_06', 2
  sprite: s32'sol400_07', 2
  sprite: s32'sol400_08', 2
  Unknown613: s16'vxxx202'
  sprite: s32'sol400_09', 2
  sprite: s32'sol400_10', 3
  sprite: s32'sol400_11', 2
  Unknown2526: 0, 0, 0, 0, 0, 0
  Unknown2591: 0, 0, 0, 0, 0, 0
  sprite: s32'sol400_12', 2
  sprite: s32'sol400_13', 1
  sprite: s32'sol400_14', 3
  callSubroutine: s32'cmnNandemoCancel' <- Make cancelable here for a total of 5 interruptable frames
  sprite: s32'sol400_15', 2
  endState: 

Event Handlers

Another interesting thing about the scripting system is the frequent usage of events and easy creation of event handlers. Most states begin with an upon: (IMMEDIATE) block which contains the setup for move properties and the creation of other event handlers. Some examples of other events used commonly will execute code upon:

  • The character touching the ground
  • Every frame step
  • Exiting the state
  • Hitting or counterhitting the opponent
  • Being guarded by the opponent

Any event handlers can run arbitrary script code making them very versatile for creating interesting move properties.

The script system also gives you direct access to slots of memory in the engine, these aren’t labeled in the syntax currently but there is a table of the known ones on the BBScript wiki. These variables can be either important parts of a characters state like health, position, offset from the opponents position, combo counter, etc. They can also be unused which allows you to read/write whatever numbers you want to them! This effectively turns BBScript into a real programming language with probably-turing-completeness (I have not tested this but it seems possible, maybe not in practice because memory space is far from infinite though).

All of these qualities together make BBScript an excellent framework for creating and modifying fighting game character designs quickly and efficiently.