BSL:Tutorial/Scratch

From OniGalore
< BSL:Tutorial
Revision as of 20:40, 18 May 2024 by Iritscen (talk | contribs) (added info on usage of restore_game)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

This page is a short tutorial for those who have no experience with scripting on how to get started creating a new level script. It's recommended that you read the BSL:Introduction page first to get a grasp of BSL's syntax. Each significant command in this tutorial should have a link behind it the first time it's referenced; click that link to learn more about the command. For a complete list of commands, see BSL:Functions and BSL:Variables.

The "main" function

Open up your IGMD folder and look for EnvWarehouse/. Inside this folder you should see seven .bsl files. Using a plain-text editor (such as Notepad or Notepad++ in Windows, and TextEdit or BBEdit in macOS), open the file warehouse_main.bsl. It should look like this:

#
# warehouse_main.bsl
#

var int my_save_point;

func void main(void)
{
    env_show 2010 0
    gl_fog_blue=.15
    gl_fog_red=.15
    gl_fog_green=.15
    gl_fog_start=.99
    gs_farclipplane_set 5000
    obj_create 20 20
    level_start
}

This is an example of a level's main script. It contains the main() function, which is always where the level starts. The script file can technically be called anything, but there must be a function called "main" in one of the level's script files. Oni will always look for and start with this function when a level loads.

Creating a script

Close warehouse_main.bsl if you still have it open, and rename the containing folder "EnvWarehouse-original". Create a new folder called "EnvWarehouse" and create a new plain-text file inside it called "tutorial.bsl".

First we're just going to place you in the final room of TCTF Training where you practice firing weapons. The room will be empty since no enemies have been spawned by the script. In case you're wondering how you could be in TCTF Training when this is a script inside EnvWarehouse/, be aware that CHAPTER 00 . COMBAT TRAINING is actually part of the same level data files as CHAPTER 01 . TRIAL RUN and only appears to be separate in the Load Game menu (this is the only time Oni combines two levels into one set of files).

Are you editing the file tutorial.bsl? Good. First, declare the "main" function for the level by typing "func void main(void)" at the very beginning of the file. Inside of curly braces, type this one command: chr_teleport 0 7045. This function call will teleport Konoko (her ID is always 0) to flag 7045, which is located in the final training room.

The file's contents should look like this:

func void main(void)
{
   chr_teleport 0 7045
}

Save the file, open up Oni and click "New Game" (you can also click "Load Game" and choose "TCTF Training" or any save point for "Syndicate Warehouse" and get the same result). You should be standing on the desk in the final training room. Now to start populating the level!

Adding NPCs (AIs)

Before we begin, notice that there are two types of AIs: AIs created from character resources (CHAR) and AIs created at runtime. AIs stored in CHARs are spawned by the function call ai2_spawn name, where "name" is the name of the AI. AIs can be created at runtime by the function call chr_create ai_number start. These AIs do not have combat logic (MELE) and thus will not attack you physically, but can still use weapons. Below we will only work with CHAR AIs since they are already set up and ready to use.

Now we are going to add 2 enemy AIs to our script that we started in the last section. Open it up, and let's add some enemies for Konoko to fight.

To recap, it should look like this:

func void main(void)
{
    chr_teleport 0 7045
}

After the call to chr_teleport add the following two commands on separate lines: ai2_spawn Top_Striker_1 and ai2_spawn Top_Comguy_1. These commands tell the game to spawn two characters already named in the game data, Top_Striker_1 and Top_Comguy_1.

Since these characters do not spawn in the final room they must be teleported there. This time we'll use the chr_teleport command to teleport the 2 AIs, and we'll use their names to identify them instead of numeric IDs. Teleport them to the flags "7008" and "7009" respectively. When you're done, main() should now look like this:

func void main(void)
{
   chr_teleport 0 7045

   ai2_spawn Top_Striker_1
   ai2_spawn Top_Comguy_1

   chr_teleport Top_Striker_1 7008
   chr_teleport Top_Comguy_1 7009
}

The script will now teleport you into the final training room and face you off against two Strikers. Try it out.

This fight too easy? In the next section we will spice up the fight. P.S.: You'll notice that one of the two Strikers is called a Comguy. There's no guarantee that Bungie West labeled their AIs correctly. Trust no one.

Giving characters weapons

Giving characters weapons is very easy: simply use the command chr_giveweapon. See HERE for the list of weapon names. Let's go with w3_phr, the plasma rifle. Give the two Strikers this weapon by adding the commands chr_giveweapon Top_Striker_1 w3_phr and chr_giveweapon Top_Comguy_1 w3_phr to the main() function. Try out the fight again.

Spawning weapons on the ground

Spawning weapons is done with the command weapon_spawn. Change your previous chr_giveweapon commands into two weapon_spawn commands in order to spawn plasma rifles at flags 7008 and 7009. You should end up with the commands weapon_spawn w3_phr 7008 and weapon_spawn w3_phr 7009. This will place plasma rifles at each Striker's feet. Each spawned weapon comes with a full clip. An AI will often pick up a weapon with ammo or a weapon which they have ammo for. Try out the fight. You may need to coax the AIs to run past the weapons in order for them to pick them up.

Preventing scripting overload

Before we continue, we have a boring matter of some housekeeping to attend to. There are trigger volumes in this room which can fire events during training: one is for supplying the player with fresh ammo if they run out and one is for monitoring whether the glass targets are shot out. Normally the level's scripting will respond to and then disable these "TV"s, but because we wiped out that level scripting, the TVs will continually fire until they overwhelm the scripting system. This is marked by the message "FATAL SCRIPTING ERROR: exceeded maximum scripting parameter base size of 32768" which is visible when Developer Mode is active. If you have Dev Mode enabled (through the Daodan DLL), press F1 to bring up the Comlink screen and type 'x' or "thedayismine" to enable Dev Mode, then play the level for about half a minute to see this message appear. This overload doesn't matter for this script yet because it runs all its commands at level start, but for a normal level script which plays out over time, it would disable all commands issued after the overload occurs. To fix this, call trigvolume_enable tv74 0 and trigvolume_enable tv75 0.

Giving characters powerups

Giving characters powerups is much like giving characters weapons. Use the command chr_givepowerup to give a character a powerup.

The powerups you can give a character are:

  • shield: Gives a force shield
  • invis: Gives a phase cloak
  • ammo: Gives a ballistic ammo clip
  • cell: Gives an energy cell

Use this command to grant an energy cell to each Striker. You should end up with the commands chr_givepowerup Top_Striker_1 cell 1 and chr_givepowerup Top_Comguy_1 cell 1. This will allow them to reload their weapons once. To review, your script should now say:

func void main(void)
{
  chr_teleport 0 7045

  ai2_spawn Top_Striker_1
  ai2_spawn Top_Comguy_1

  chr_teleport Top_Striker_1 7008
  chr_teleport Top_Comguy_1 7009

  weapon_spawn w3_phr 7008
  weapon_spawn w3_phr 7009

  trigvolume_enable tv74 0
  trigvolume_enable tv75 0

  chr_givepowerup Top_Striker_1 cell 1
  chr_givepowerup Top_Comguy_1 cell 1
}

Spawning powerups on the ground

Spawning powerups is easy with the command powerup_spawn. Add the commands powerup_spawn invis 7008 and powerup_spawn hypo 7009. This command will spawn a phase cloak and a hypo at the Strikers' feet, but since AIs will not pick up powerups, these are for you only.

Adding an ally

Adding extra characters to the script can be done in the same way you added the Strikers. You can see all the AI names for this level here. Look up the name for Griffin in this level (he's available because he appears in Chapter 1's opening cutscene) and spawn him at flag 7007. You should end up with ai2_spawn griffin and chr_teleport griffin 7007. Your boss will now assist you in the fight against the armed Strikers.

Making an enemy

Look back at ssg's table to see what teams the AIs are on. Most of the time, adding enemies simply involve spawning and teleporting the AI to your location, and as long as its team is "Syndicate" it will be an enemy. If you want to make an enemy from someone who would normally be on an allied team, use the chr_changeteam command to change their team to Syndicate like this:

chr_changeteam griffin Syndicate

The same principle would work in reverse if you spawned an AI who's on team Syndicate and changed their team to "TCTF" or "Konoko" with this command.

Overpower

The fight is becoming too slanted against Konoko; let's give her a helping hand. We can use the chr_set_health command to set a character's health points (HP). Note that each character has a base health that is unique to them. For example, Konoko's base HP is 200 while a green Striker's might be only 50. Keep this in mind when setting characters' health. Setting it over their base HP will work like using a hypo at full health. A character in an "overhealth" state will lose 1 HP every 10 ticks (1/6 of a second), whether they're the player character or an AI. However only Konoko and Muro get stronger and glow while they have overhealth. Let's set Konoko's health to 400 so she enters overpower mode: chr_set_health 0 400.

Let's recap. If you've added everything above, you should have:

func void main(void)
{
   chr_teleport 0 7045

   ai2_spawn Top_Striker_1
   ai2_spawn Top_Comguy_1

   chr_teleport Top_Striker_1 7008
   chr_teleport Top_Comguy_1 7009

   weapon_spawn w3_phr 7008
   weapon_spawn w3_phr 7009

   trigvolume_enable tv74 0
   trigvolume_enable tv75 0

   chr_givepowerup Top_Striker_1 cell 1
   chr_givepowerup Top_Comguy_1 cell 1

   powerup_spawn invis 7008
   powerup_spawn hypo 7009

   ai2_spawn griffin
   chr_teleport griffin 7007
   chr_changeteam griffin Syndicate

   chr_set_health 0 400
}

Delays

To wait for a fixed amount of time before executing a command, use sleep, which takes an argument in ticks (1/60 of a second). Add this to wait for 5 seconds before spawning a training drone:

sleep 300
ai2_spawn dum_train_block
chr_teleport dum_train_block 7010

You can also wait for a character's health to reach a certain target before proceeding. Change your sleep command to this chr_wait_health command in order to wait for the first Striker to die before spawning the drone:

chr_wait_health Top_Comguy_1 0
ai2_spawn dum_train_block
chr_teleport dum_train_block 7010

Separate functions

It's bad practice to put all your level scripting code in main(). Using multiple functions is not only clearer but allows you to run different code in parallel. Create a new function called "start_fight" and move all the code from main() into start_fight(). Then call start_fight() from main(). It should look like this:

func void main(void)
{
   start_fight
}

func start_fight
{
   ...
}

…with "..." being whatever you had in main() before.

Forking

Now that you get the basic idea of functions, we can use multiple functions to run code in parallel. Change tutorial.bsl to read:

func void main(void)
{
   start_fight
   fork spawn_dummy
   sound_music_start mus_ot 0.75
   fork kill_music
}

func start_fight
{
   chr_teleport 0 7045

   ai2_spawn Top_Striker_1
   ai2_spawn Top_Comguy_1

   chr_teleport Top_Striker_1 7008
   chr_teleport Top_Comguy_1 7009

   weapon_spawn w3_phr 7008
   weapon_spawn w3_phr 7009

   trigvolume_enable tv74 0
   trigvolume_enable tv75 0

   chr_givepowerup Top_Striker_1 cell 1
   chr_givepowerup Top_Comguy_1 cell 1

   powerup_spawn invis 7008
   powerup_spawn hypo 7009

   ai2_spawn griffin
   chr_teleport griffin 7007
   chr_changeteam griffin Syndicate

   chr_set_health 0 400
}

func spawn_dummy
{
   chr_wait_health Top_Comguy_1 0
   ai2_spawn dum_train_block
   chr_teleport dum_train_block 7010
}

func kill_music
{
   chr_wait_health Top_Striker_1 0
   chr_wait_health Top_Comguy_1 0
   chr_wait_health dum_train_block 0
   chr_wait_health griffin 0
   sound_music_volume mus_ot 0 5.0
}

First main() calls start_fight(), which runs in its entirety before returning control to main(), and then main() forks spawn_dummy(), which waits until one Striker is dead before spawning the training dummy.

So far there's nothing different from how the script ran before, but note the third line in main(), which calls sound_music_start. When you play the level, the music begins immediately. If we had called spawn_dummy() without "fork", we would be stuck waiting for the dummy to spawn before control returned to main() and it could play the music.

After that, we fork one more function, kill_music(), which waits for all four characters to be defeated before fading out the music. We could have simply called this function without "fork" since it's the last line of code in main(), but forking it is good practice so that more code can be added to main() in the future without needing to remember to add "fork" to this line.

Save points

As a final note, remember how TCTF Training and Syndicate Warehouse are the same level, just separated by save point? If you look back at the original main() function that was in warehouse_main.bsl, its last line called the function level_start(). You'll find this function in warehouse_level_scripts.bsl. This function runs whenever a save point from either Training or Warehouse is loaded.

func void level_start(string ai_name)
{
   if (save_point eq 0)
   {
      ...
   }

   if (save_point eq 1)
   {
      ...
   }

   ...
}

save_point is a built-in variable that can be read but not set, and which tells you the save point that the player loaded, so you can set up your level accordingly (clicking on the name of the level in Load Game registers as save point 0). To record the player reaching a save point in your level, you call save_game n, where 'n' is 1-10.

To handle the player restoring a saved game, you use logic like the snippet above to read save_point and then set up the level accordingly by doing things such as turning trigger volumes on/off, spawning additional AIs, and whatever other changes are necessary to take the level from its default starting state to the state it should have as of the save point from which the player is loading. After making these changes, you call restore_game to restore the player's state. This function retrieves the player state that was saved in persist.dat for the current save point: Konoko's position in a level, her health, and her inventory. persist.dat does not store information on world state, which is why you are responsible for handling that manually with your own BSL calls.