BSL:Tutorial/Scratch: Difference between revisions
(added info on usage of restore_game) |
|||
(21 intermediate revisions by 7 users not shown) | |||
Line 1: | Line 1: | ||
This page is a short tutorial | 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]]. | ||
{{TOClimit|2}} | |||
==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 {{C0}} is actually part of the same level data files as {{C1}} 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 "<tt>func void main(void)</tt>" at the very beginning of the file. Inside of curly braces, type this one command: <tt>[[chr_teleport]] 0 7045</tt>. This function call will teleport Konoko (her ID is always 0) to flag 7045, which is located in the final training room. | |||
The | 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 <tt>[[ai2_spawn]] ''name''</tt>, where "name" is the name of the AI. AIs can be created at runtime by the function call <tt>[[chr_create]] ''ai_number'' start</tt>. 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 <tt>chr_teleport</tt> add the following two commands on separate lines: <tt>ai2_spawn Top_Striker_1</tt> and <tt>ai2_spawn Top_Comguy_1</tt>. 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 <tt>chr_teleport</tt> 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 [[Striker]]s. 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 <tt>[[chr_giveweapon]]</tt>. See [[BSL:Inventory management|HERE]] for the list of weapon names. Let's go with <tt>w3_phr</tt>, the plasma rifle. Give the two Strikers this weapon by adding the commands <tt>chr_giveweapon Top_Striker_1 w3_phr</tt> and <tt>chr_giveweapon Top_Comguy_1 w3_phr</tt> to the main() function. Try out the fight again. | |||
==Spawning weapons on the ground== | |||
Spawning weapons is done with the command <tt>[[weapon_spawn]]</tt>. Change your previous <tt>chr_giveweapon</tt> commands into two <tt>[[weapon_spawn]]</tt> commands in order to spawn plasma rifles at flags 7008 and 7009. You should end up with the commands <tt>weapon_spawn w3_phr 7008</tt> and <tt>weapon_spawn w3_phr 7009</tt>. 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 <tt>[[trigvolume_enable]] tv74 0</tt> and <tt>trigvolume_enable tv75 0</tt>. | |||
==Giving characters powerups== | |||
Giving characters powerups is much like giving characters weapons. Use the command <tt>[[chr_givepowerup]]</tt> to give a character a powerup. | |||
<tt> | The powerups you can give a character are: | ||
* <tt>shield</tt>: Gives a force shield | |||
* <tt>invis</tt>: Gives a phase cloak | |||
* <tt>ammo</tt>: Gives a ballistic ammo clip | |||
* <tt>cell</tt>: Gives an energy cell | |||
Use this command to grant an energy cell to each Striker. You should end up with the commands <tt>chr_givepowerup Top_Striker_1 cell 1</tt> and <tt>chr_givepowerup Top_Comguy_1 cell 1</tt>. 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 <tt>[[powerup_spawn]]</tt>. Add the commands <tt>powerup_spawn invis 7008</tt> and <tt>powerup_spawn hypo 7009</tt>. 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 [http://ssg.oni2.net/subfold/charas/l00_01.htm 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 <tt>ai2_spawn griffin</tt> and <tt>chr_teleport griffin 7007</tt>. 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 <tt>[[chr_changeteam]]</tt> 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 <tt>[[chr_set_health]]</tt> 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: <tt>chr_set_health 0 400</tt>. | |||
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 <tt>[[sleep]]</tt>, 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 <tt>sleep</tt> command to this <tt>[[chr_wait_health]]</tt> 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) | |||
{ | |||
... | |||
} | |||
... | |||
} | |||
<tt>[[save_point]]</tt> 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 <tt>[[save_game]] ''n''</tt>, where 'n' is 1-10. | |||
To handle the player restoring a saved game, you use logic like the snippet above to read <tt>save_point</tt> 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 <tt>[[restore_game]]</tt> 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. | |||
[[Category:Modding tutorials]] | |||
Latest revision as of 20:40, 18 May 2024
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.