BSL:Tutorial/Scratch: Difference between revisions

From OniGalore
Jump to navigation Jump to search
(added info on usage of restore_game)
 
(22 intermediate revisions by 7 users not shown)
Line 1: Line 1:
This page is a short tutorial on how to get started on simple scripts for those who have had no experience with scripting before.
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]].
It will teach you, step by step, how to write a simple script. By the end of this tutorial, you will be able to move on to more advanced scripts.
{{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".


== Part 1 : Knowing your formats ==
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).


The game will accept 2 types of syntax for scripts, and they are based on different styles of writing. Choose the one that suits you best.
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 types are :
The file's contents should look like this:
func void main(void)
{
    chr_teleport 0 7045
}


Shell-Style:
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!
This is compiled much like some System-Shells. It is written in a "one-function-call-a-line" format, and is the easiest to begin with if you have had no coding experience, as the code is neatly displayed line after line. The downside of this it, that once you get into more complex scripts or scripts with many function calls, the file becomes very hard to read and very large. Also, an great tool, passing information through a command, cannot be done using Shell-Style.


Example: [[Shell-Style]]
==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.


C-Style:
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.
This is compiled much like the popular coding style C. The syntax can be placed into multiple-function lines and functions are divided by a ''';''' . In advanced scripts, this style will allow you to condense your script and the parts will be easier to read/debug. It also allows you to utilize some of the more advanced features easily. The problem with this is, that starting off, it is harder to write if you have had no previous experience with coding.


Example: [[C-Style]]
To recap, it should look like this:
func void main(void)
{
    chr_teleport 0 7045
}


Hybrid Style:
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.
This is a combination between C-Style and Shell-Stlye. It keeps the easy-to-read format of Shell-Style, yet allows you to use the more advanced techniques of C-Style.


For the purpose of this article, we will use Shell-Style syntax for simplicity.
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.


== Part 2: Starting Off ==
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.


Now, I suggest reading the original script files to get a basic idea for how scripts are laid out. This will allow us to jump-start a bit.
==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.


Open up your IGMD and look for the following folder: EnvWarehouse.
==Spawning weapons on the ground==
Open it up, and we'll take a look at how it it laid out. There should be several files in here. Open using NOTEPAD, the one titled '''warehouse_main.bsl'''. It should look like this:
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:
func void main(void)<br>
* <tt>shield</tt>: Gives a force shield
{<br>
* <tt>invis</tt>: Gives a phase cloak
    env_show 2010 0<br>
* <tt>ammo</tt>: Gives a ballistic ammo clip
    gl_fog_blue=.15<br>
* <tt>cell</tt>: Gives an energy cell
    gl_fog_red=.15<br>
    gl_fog_green=.15<br>
    gl_fog_start=.99<br>
    gs_farclipplane_set 5000<br>
    obj_create 20 20<br>
    level_start<br>
<br>
}</tt>


This is an example of a '''main''' file. It contains the information of how the level starts.
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
}


Ignore the contents of the script for now, and notice that the contents of the function <tt>main</tt> are enclosed by a set of brackets ('''{''' and '''}''').
==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.


All functions must be enclosed in this set of brackets or it will not be executed.
==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.


Close the file, and rename the folder "EnvWarehouse" "EnvWarehouse_original". Then, create a folder called "EnvWarehouse" and create a script file inside and name it "Tutorial". We will be using this file to show various examples of scripts and finally, write a small script of your own.
==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.


== Part 3: Basic Commands ==
==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>.


This is just a small section explaining the commands that will be used in this tutorial. For a complete list of commands, see [http://www.fh-eberswalde.de/user/dkriesch/onistuff/commands.htm Ssg's Site] for more information.
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
}


ai2_spawn (name) - creates and starts an AI from a character object<br>
==Delays==
chr_teleport (name) (flag) - teleports a character to a flag<br>
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:
chr_givepowerup (name) (powerup) (amount) - gives a character a powerup<br>
.
.
.


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:


== Part 4: First Script ==
chr_wait_health Top_Comguy_1 0
ai2_spawn dum_train_block
chr_teleport dum_train_block 7010


This section will teach you how to write a very basic script (fighting script). It will start you off in the Final Training Room, where you practiced firing weapons in the original game. With this script, the room will appear very empty.
==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:


Let's open up the "Tutorial.bsl" file and start writing the script.
func void main(void)
{
    start_fight
}
func start_fight
{
    ...
}


First, declare it the "main" portion of the script by typing "<tt>func void main(void)</tt> at the very beginning of the file. This tells the game which function it is. The "main" function is the function that the game looks at first to determine what is to be executed.
…with "..." being whatever you had in main() before.


Below that line, type '''{'''. This tells the game that the function has started.
==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:


Add a space, and then type <tt>chr_teleport 0 7010</tt>. This function teleports Konoko (her id is always 0) to the flag 7010. It is located in the final training room.
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
}


Finish the function "main" by typing '''}''' on the next line.
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.


It should look like this:
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.


<tt>func void main(void)<br>
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.
{<br>
chr_teleport 0 7010<br>
}</tt>


==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.


Save the file, load up Oni, and click "New Game". You should be standing in the center of the final combat training room.
func void level_start(string ai_name)
{
    if (save_point eq 0)
    {
      ...
    }
    if (save_point eq 1)
    {
      ...
    }
    ...
}


You are now on your way!
<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]]
 
== Part 5: Adding NPCs (AI) ==
 
Before we begin, notice that there are two types of AI. AI created from character objects, and AI created from memory. AI created from character objects are created by the function call <tt>ai2_spawn (name)</tt>, where (name) is the name of the AI. AI created from memory are created by the function call <tt>chr_create (ai_number) start,</tt>. These AI do not have combat logic, and thus will not attack you physically but can still use weapons.
 
Now, we are going to add 2 enemy AI to our script that we wrote in the last section. Open it up, and lets add some enemies for Konoko to fight.
 
To recap, it should look like this :
 
<tt>func void main(void)<br>
{<br>
chr_teleport 0 7010<br>
}</tt>
 
After the function call <tt>chr_teleport 0 7010</tt>, add the following 2 commands on separate lines: <tt>ai2_spawn Top_Striker</tt> and <tt>ai2_spawn Top_Comguy</tt>. These two commands tell the game to create 2 characters: Top_Striker, and Top_Comguy.
 
Since that these characters do not spawn in the room, they must be teleported there. Using the <tt>chr_teleport</tt> command used earlier to teleport yourself, modify it to teleport the 2 AI using their names instead of "0". Teleport them to the flags "7008" and "7009" respectively. Refer to the example above or the list of function calls above to write this in.
 
It should now look like this :
 
<tt>func void main(void)<br>
{<br>
chr_teleport 0 7010<br>
ai2_spawn Top_Striker<br>
ai2_spawn Top_Comguy<br>
chr_teleport Top_Striker 7008<br>
chr_teleport Top_Comguy 7009<br>
}</tt>
 
This script will now teleport you into the final training room and face you off against 2 Strikers.
 
This fight too easy? In the next section, we will learn how to spice up the fight, and let you develop your own script.
 
 
----
 
== Part 6: Customization==
 
Here, you will be able to split off and customize your script.
Each subsection will represent a way to customize the above script.
 
===Weapons and Powerups===
These are the easiest things to add into a battle.
 
Weapons can be spawned on the ground or given to characters.
 
====Giving Characters Weapons====
 
Giving characters weapons is very easy. Simply use the command <tt>chr_giveweapon</tt> to give a character a weapon.
 
[[OSL:Inventory|Here]] are the list of the weapon codes. For the purpose of this example, we will use <tt>w3_phr</tt>, the plasma rifle. This command will give the two strikers plasma rifles.
 
Add the commands <tt>chr_giveweapon Top_Striker w3_phr</tt> and <tt>chr_giveweapon Top_Comguy w3_phr</tt> on separate lines beneath what you have already. Be sure to add it ''BEFORE'' the '''}''' or it will not execute.
 
====Spawning Weapons on the ground====
 
Spawning weapons is simple. Use the command <tt>weapon_spawn</tt> to spawn a weapon.
 
Add the commands <tt>weapon_spawn w3_phr 7008</tt> and <tt>weapon_spawn w3_phr 7009</tt> on separate lines beneath what you have already. Be sure to add it ''BEFORE'' the '''}''' or it will not execute. This command will spawn 2 plasma rifles, one under each striker's feet. AI will automatically pick up weapons with ammo or empty weapons they have ammo for.
 
====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.
 
The powerups you can give a character are :
* shield : Gives a force shield
* invis : Gives a phase cloak
* ammo : Gives a ballistic ammo
* cell : Gives an energy cell.
 
For the purpose of this example, we will give the characters invisibilities.
 
Add the commands <tt>chr_givepowerup Top_Striker invis -1</tt> and <tt>chr_givepowerup Top_Comguy invis -1</tt> on separate lines beneath what you have already. Be sure to add it ''BEFORE'' the '''}''' or it will not execute. This command will make the strikers permanently invisible. To make then visible after 30 seconds, erase "-1".
 
====Spawning Powerups on the Ground====
 
Spawning powerups are simple. Use the command <tt>powerup_spawn</tt> to spawn a powerup.
 
Add the commands <tt>powerup_spawn invis 7008</tt> and <tt>powerup_spawn invis 7009</tt> on separate lines beneath what you have already. Be sure to add it ''BEFORE'' the '''}''' or it will not execute. This command will spawn 2 phase cloaks beneath the striker's feet. Since AI  will not pick up powerups, this is for you only.
 
===Extra Characters===
 
Addubg extra characters to the script is easy. Just follow the steps for adding the first AI and follow the procedure, look for AI names [[OSL: AI names|Here]].
 
====Adding Enemies====
 
Look at the chart for what teams the AI are on. Most of the time, adding enemies simply involve spawning and teleporting the AI. As long as the team in "Syndicate", it will be an enemy. If you want to add an emeny from an allied team, use the <tt>chr_changeteam</tt> command to changet their team to "Syndicate" like this:
 
<tt>chr_changeteam griffin Syndicate</tt>
 
====Adding Allies====
 
Look at the chart for what teams the AI are on. As long as the team is "TCTF" or "Konoko", it will be an ally for you in this script. If you want to add an ally from the enemy team, use the <tt>chr_changeteam</tt> command to changet their team to "TCTF" or "Konoko" like this:
 
<tt>chr_changeteam WH_Striker_D TCTF</tt>
 
===Overpower===
 
Use the <tt>chr_set_health</tt> command to set a character's health. Note that each character has a base health that may not be the same. For example, konoko's base health is 200, while a brown striker's is only 50. Keep this in mind when setting character's health. Setting it over their base health will send them into overpower - like using a hypo at full health. The health will drain at 1 every second even for AI. This example sets konoko's health to 400 - overpower.
 
<tt>chr_set_health 0 400</tt>
 
 
This following example shows everything listed above added.
 
<tt>func void main(void)<br>
{<br>
<br>
chr_teleport 0 7010<br>
powerup_spawn hypo 7010<br>
<br>
ai2_spawn Top_Striker<br>
ai2_spawn Top_Comguy<br>
<br>
chr_teleport Top_Striker 7008<br>
chr_teleport Top_Comguy 7009<br>
<br>
chr_givepowerup Top_Striker invis -1<br>
chr_givepowerup Top_Comguy invis -1<br>
powerup_spawn invis 7007<br>
powerup_spawn invis 7009<br>
<br>
chr_giveweapon Top_Striker w7_scc<br>
chr_giveweapon Top_Comguy w3_phr<br>
chr_givepowerup Top_Striker ammo 15<br>
chr_givepowerup Top_Comguy cell 30<br>
<br>
weapon_spawn w2_sap 7045<br>
powerup_spawn ammo 7008<br>
<br>
ai2_spawn OutroTCTF01<br>
chr_teleport OutroTCTF01 7007<br>
<br>
ai2_spawn WH_Thug_A<br>
chr_changeteam WH_Thug_A TCTF<br>
chr_teleport WH_Thug_A 7007<br>
<br>
ai2_spawn griffin<br>
chr_changeteam griffin Syndicate<br>
chr_teleport griffin 7008<br>
chr_set_health griffin 3900<br>
}</tt>
 
 
----
 
== Part 7: Delay ==
 
<tt>sleep</tt><br>
<tt>chr_wait_health</tt>
 
To wait for some time before executing a command, use <tt>sleep</tt>. To wait for approx 5 seconds before spawning an extra drone, go like this:
 
<tt>sleep 300<br>
ai2_spawn dum_train_block<br>
chr_teleport dum_train_block 7010<br>
</tt>
 
To wait for a striker to die and then spawn some drones, go like this:
 
<tt>chr_wait_health Top_Striker 0<br>
ai2_spawn dum_train_block<br>
chr_teleport dum_train_block 7010<br></tt>
 
 
== Part 8: Forking and Separate Functions==
 
This is the part where we start moving commands out of "main". Create a new function called "logic" like this : type "func logic" two lines below the '''}''' of "main". Type '''{''' nad copy out everything inside "main" into "logic". Type <tt>fork logic</tt> in main. It should look like this :
 
<tt>func void main(void)<br>
{<br>
fork logic<br>
}<br>
<br>
func logic<br>
{<br>
...<br>
}</tt>
 
... being whatever you had in "main" before. Add <tt>chr_wait_health griffin 0</tt> at the end of "logic", then type <tt>fork logic2</tt>. Create the function "logic2". In "logic2" you can write whatever you want that you have learned before. For the prupose of this example, '''...''' will represent whatever you had, '''..''' will represent whatever you want, and typed text will represent thing that must be included.
 
<tt>func void main(void)<br>
{<br>
fork logic<br>
}<br>
<br>
func logic<br>
{<br>
...<br>
chr_wait_health griffin 0<br>
fork logic2<br>
}<br>
<br>
func logic2 {<br>
..<br>
}</tt>
 
===Extra: Multiple Forking===
 
More than one function can be forked in the same function. Look at ths example :
 
<tt>func void main(void)<br>
{<br>
fork logicA<br>
fork logicB<br>
}<br>
 
func logicA {<br>
..<br>
}<br>
 
func logicB {<br>
..<br>}</tt>
 
Both "logicA" and "logicB" are executed at the same time. If <tt>chr_wait_health griffin 0</tt> were to be removed, likewise, both "logic" and "logic2" are executed at the same time.
 
== Part 9: Variables and Save Points ==
 
Every wondered why there is no folder for basic training? The truth is, it is actually part of the Warehouse level and is managed by save points.
 
This is the beginning of the "level_start" which is forked from "main" in the original level scripts.
 
<tt>func void level_start(string ai_name)<br>
{<br>
<br>
<br>
    if (save_point eq 0)<br>
    {    <br>
        ...<br>
    }<br>
<br>
    if (save_point eq 1)<br>
    {<br>
        ...<br>
    }<br>
<br>
}</tt>
 
Using forking, you can link save points to functions. Like so :
 
<tt>func void main(void)<br>
{<br>
<br>
    if (save_point eq 0)<br>
    {    <br>
        fork logic<br>
    }<br>
<br>
    if (save_point eq 1)<br>
    {<br>
        fork logic2<br>
    }<br>
<br>
}</tt>
 
When save point 0 is loaded (TCTF Training), the function "logic" is executed. If save point 1 is loaded (Syndicate Warehouse), "logic2" is executed. Note that in Warehouse, the save point number in the script is one higher than in the level load list, because "TCTF Training" is SP0.
 
 
 
 
 
                                                              - Your_Mom

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.