-
Notifications
You must be signed in to change notification settings - Fork 2
Obstacle Course Tutorial: Scripting
The scripting might be a little overwhelming since there's a lot of it. But it's fairly straightforward if you're working with premade assets. The biggest issue is the amount of code needed due to the philosophy of "every stage is its own mini game". This means that the Game Controller holds all the cards and makes all decisions for how things work in your world.
Let's go through the 3 user defined Level Controller scripts from the split obstacle course.
The dialog manager controls the dialog popups when you click the Level Controller. But don't fret, there's template code that you can use to significantly reduce the amount of code you need to write. You are, however, free to not use that if you want to make something completely custom.
In these tutorials, I will first paste the full code from the level. And then break that down into sections, explaining how it works. If you haven't already seen the module breakdown. Please check that out as well, as it will help you understand the main parts of an ObstacleScript module.
#define USE_STATE_ENTRY
#define USE_LISTEN
#define USE_PLAYERS
#define USE_TIMER
#include "ObstacleScript/index.lsl"
#include "ObstacleScript/resources/DialogHelper.lsl"
// Custom menus
#define MENU_TEAM -1
integer TEAM_PAGE;
// In the configuration global, first value contains team settings
#define GCONF_TEAMS 0
integer TEAMS; // Each bit corresponds to the
list onDialogOpen( integer dialog ){
string text; list buttons;
if( dialog == MENU_MAIN ){
text = "TEAMS:";
forPlayer( i, player )
text += "\n "+llGetSubString(llGetDisplayName(player), 0, 8)+". "+(str)((TEAMS&(1<<i))>0);
end
if( ~GSETTINGS & GS_GAME_STARTED ){
buttons += "Teams";
buttons += "Shuffle Teams";
}
}
else if( dialog == MENU_TEAM ){
buttons += "BACK";
if( count(PLAYERS) > 10 )
buttons += ">>";
if( TEAM_PAGE > count(PLAYERS)/10 )
TEAM_PAGE = 0;
integer i;
for( i = TEAM_PAGE*10; i < TEAM_PAGE*10+10 && i < count(PLAYERS); ++i ){
key player = l2k(PLAYERS, i);
text += "\n "+llGetDisplayName(player)+". "+(str)((TEAMS&(1<<i))>0);
buttons += llGetSubString(llGetDisplayName(player), 0, 8);
}
}
// Put your custom dialog open buttons and text here
return (list)text + buttons;
}
onDialogButton( int menu, string button ){
//qd("Button pressed:" + menu + button);
if( menu == MENU_MAIN ){
if( button == "Teams" ){
TEAM_PAGE = 0;
openDialog(MENU_TEAM);
}
else if( button == "Shuffle Teams" ){
list shuffle;
int i;
for( ; i < count(PLAYERS); ++i )
shuffle += i < count(PLAYERS)/2;
shuffle = llListRandomize(shuffle, 1);
TEAMS = 0;
for( i = 0; i < count(shuffle); ++i )
TEAMS = TEAMS | (l2i(shuffle, i) << i);
// Save to gconf
GCONF = llListReplaceList(GCONF, (list)TEAMS, GCONF_TEAMS, GCONF_TEAMS);
_dtxt();
openDialog(MENU_MAIN);
}
}
else if( menu == MENU_TEAM ){
if( button == ">>" ){
++TEAM_PAGE;
openDialog(MENU_TEAM);
}
else if( button == "BACK" ){
openDialog(MENU_MAIN);
}
else{
forPlayer( index, player )
if( llGetSubString(llGetDisplayName(player), 0, 8) == button ){
TEAMS = TEAMS^(1<<index);
// Save to gconf
GCONF = llListReplaceList(GCONF, (list)TEAMS, GCONF_TEAMS, GCONF_TEAMS);
_dtxt();
openDialog(MENU_TEAM);
return;
}
end
}
}
}
// Lets you override the hover text
// Use the global GSCORE to get the score list from #GAME
string onTextUpdate(){
if( GSETTINGS & GS_RECENT_GAME_END ){
// Return winner text here
integer winner = l2f(GSCORE, 1) > 0 && l2f(GSCORE, 1) < l2f(GSCORE, 0);
string text = "TEAM "+(str)winner+" WINS!\n";
text += "Average times:\n";
text += "Team 0: "+l2s(GSCORE, 0)+" sec\n";
text += "Team 1: "+l2s(GSCORE, 1)+" sec\n";
return text;
}
else if( GSETTINGS & GS_GAME_STARTED && GSETTINGS & GS_GAME_LOADED ){
// Output the game mode text here
return "Lowest average time of each team wins!";
}
else if( ~GSETTINGS & GS_GAME_STARTED ){
string txt = "-- PLAYERS --\n";
if( count(PLAYERS) > 6 )
txt += (str)count(PLAYERS)+" Joined\n";
else{
forPlayer( index, player )
txt += "["+(string)((TEAMS&(1<<index)) > 0)+"] "+llGetDisplayName(player)+"\n";
end
}
return txt;
}
return "";
}
#include "ObstacleScript/begin.lsl"
// Sets up the click handler
dialogHelperHandler()
onStateEntry()
dialogHelperSetup();
end
#include "ObstacleScript/end.lsl"
#define USE_STATE_ENTRY
#define USE_LISTEN
#define USE_PLAYERS
#define USE_TIMER
#include "ObstacleScript/index.lsl"
#include "ObstacleScript/resources/DialogHelper.lsl"
Setup dependencies and also include the DialogHelper script, which automates a lot for you.
#define MENU_TEAM -1
integer TEAM_PAGE;
Here we define a custom menu page for setting the team of a player, since the DialogHelper isn't setup to handle teams. And also creates a global value TEAM_PAGE since the nr of players may exceed 12, which means we'd run out of dialog options otherwise. Any custom menus should use a negative integer.
#define GCONF_TEAMS 0
DialogHelper includes a global list called GCONF. This list contains configuration data to send to the #Game script when the owner picks the START GAME option. You don't have to use it if you don't need the game to support any custom configuration. But in our case we will store the player teams in a single integer (bitwise operation), and it will be passed as the first element in the GCONF list.
integer TEAMS;
Sets up a global variable to save the player teams. Each bit corresponds to the team of that player. This works because there's only 2 teams in this level.
list onDialogOpen( integer dialog ){
string text; list buttons;
if( dialog == MENU_MAIN ){
text = "TEAMS:";
forPlayer( i, player )
text += "\n "+llGetSubString(llGetDisplayName(player), 0, 8)+". "+(str)((TEAMS&(1<<i))>0);
end
if( ~GSETTINGS & GS_GAME_STARTED ){
buttons += "Teams";
buttons += "Shuffle Teams";
}
}
else if( dialog == MENU_TEAM ){
buttons += "BACK";
if( count(PLAYERS) > 10 )
buttons += ">>";
if( TEAM_PAGE > count(PLAYERS)/10 )
TEAM_PAGE = 0;
integer i;
for( i = TEAM_PAGE*10; i < TEAM_PAGE*10+10 && i < count(PLAYERS); ++i ){
key player = l2k(PLAYERS, i);
text += "\n "+llGetDisplayName(player)+". "+(str)((TEAMS&(1<<i))>0);
buttons += llGetSubString(llGetDisplayName(player), 0, 8);
}
}
// Put your custom dialog open buttons and text here
return (list)text + buttons;
}
onDialogOpen is a function that's called before a dialog is opened on the user. It should return a list where the first element is a string containing the text that should be put in the dialog (or "" if you want to use default). Followed by any custom buttons to append to the dialog page. The DialogHelper header file contains 3 default menus (as of writing): MENU_MAIN, MENU_MAINTENANCE, MENU_INVITE_PLAYER.
Since we want to present teams in the default menu, we'll set a custom text which shows the players and teams. We also want two custom buttons to manage or shuffle teams, so we add those to buttons.
If the dialog is our custom team menu, we add a button for each player, and also add some rudimentary pagination in case the nr of players are too many for one page. We also output a list of players and their teams again.
onDialogButton( int menu, string button ){
//qd("Button pressed:" + menu + button);
if( menu == MENU_MAIN ){
if( button == "Teams" ){
TEAM_PAGE = 0;
openDialog(MENU_TEAM);
}
else if( button == "Shuffle Teams" ){
list shuffle;
int i;
for( ; i < count(PLAYERS); ++i )
shuffle += i < count(PLAYERS)/2;
shuffle = llListRandomize(shuffle, 1);
TEAMS = 0;
for( i = 0; i < count(shuffle); ++i )
TEAMS = TEAMS | (l2i(shuffle, i) << i);
// Save to gconf
GCONF = llListReplaceList(GCONF, (list)TEAMS, GCONF_TEAMS, GCONF_TEAMS);
_dtxt();
openDialog(MENU_MAIN);
}
}
else if( menu == MENU_TEAM ){
if( button == ">>" ){
++TEAM_PAGE;
openDialog(MENU_TEAM);
}
else if( button == "BACK" ){
openDialog(MENU_MAIN);
}
else{
forPlayer( index, player )
if( llGetSubString(llGetDisplayName(player), 0, 8) == button ){
TEAMS = TEAMS^(1<<index);
// Save to gconf
GCONF = llListReplaceList(GCONF, (list)TEAMS, GCONF_TEAMS, GCONF_TEAMS);
_dtxt();
openDialog(MENU_TEAM);
return;
}
end
}
}
}
onDialogButton is called when the user presses any button. Here we can add custom behavior. Other than the bitwise operations, this should be fairly straight forward. We check what the menu is, and what button was pressed. Then handle that.
string onTextUpdate(){
if( GSETTINGS & GS_RECENT_GAME_END ){
// Return winner text here
integer winner = l2f(GSCORE, 1) > 0 && l2f(GSCORE, 1) < l2f(GSCORE, 0);
string text = "TEAM "+(str)winner+" WINS!\n";
text += "Average times:\n";
text += "Team 0: "+l2s(GSCORE, 0)+" sec\n";
text += "Team 1: "+l2s(GSCORE, 1)+" sec\n";
return text;
}
else if( GSETTINGS & GS_GAME_STARTED && GSETTINGS & GS_GAME_LOADED ){
// Output the game mode text here
return "Lowest average time of each team wins!";
}
else if( ~GSETTINGS & GS_GAME_STARTED ){
string txt = "-- PLAYERS --\n";
if( count(PLAYERS) > 6 )
txt += (str)count(PLAYERS)+" Joined\n";
else{
forPlayer( index, player )
txt += "["+(string)((TEAMS&(1<<index)) > 0)+"] "+llGetDisplayName(player)+"\n";
end
}
return txt;
}
return "";
}
onTextUpdate is called whenever the hover text should be refreshed. GSETTINGS is a global integer that's handled automatically for you when using DialogHelper. It contains stat information like GS_GAME_STARTED, GS_RECENT_GAME_END, GS_GAME_LOADED, to help you output the correct text. See DialogHelper.lsl for more information. If G_RECENT_GAME_END is set, we want to present the scores. Scores are passed as a list to the global GSCORE from #Game whenever the game ends. You control what should go in this list in #Game when ending a game. In this case the list contains 2 elements: Team 0 total time, team 1 total time. We use these two values to show what team won, and what times they had.
The other two states are for when the level is loaded, and when waiting for the game to start. The loading messages while loading a level is handled automatically by returning "".
#include "ObstacleScript/begin.lsl"
// Sets up the click handler
dialogHelperHandler()
onStateEntry()
dialogHelperSetup();
end
#include "ObstacleScript/end.lsl"
Finally we setup the event handler. In it we call dialogHelperHandler() to allow DialogHelper to setup and handle any events it may need, and in onStateEntry we call dialogHelperSetup to activate it when the script starts.
The purpose of Obstacles is to handle any obstacles that don't need any settings from #Game to run. For an instance crusher walls that should run automatically.
#define USE_STATE_ENTRY
#define USE_TIMER
#include "ObstacleScript/index.lsl"
#include "ObstacleScript/begin.lsl"
onSpawnerGameLoad()
_CRUSHERS_setup(6, 2.25);
end
onStateEntry()
setInterval("TENT", 2);
end
onTimer( id )
_CRUSHERS_onTimer(id);
end
onRezzerRezzed( obj )
_CRUSHERS_onSpawn(obj);
end
handleTimer("TENT")
Trap$attackAll( "*", [] );
end
onStairSeated( hud, stair, seated )
key player = llGetOwnerKey(hud);
if( llKey2Name(stair) == "TumblingBeam" ){
if( seated )
Qte$start( hud, QteConst$QTE_GAUGE, "TUMBL" );
else
Qte$end( hud, TRUE );
}
end
onPlayerQteEnded( hud, success, callback )
if( callback == "TUMBL" && !success ){
Rlv$unSit( hud, TRUE );
}
end
#include "ObstacleScript/end.lsl"
#define USE_STATE_ENTRY
#define USE_TIMER
#include "ObstacleScript/index.lsl"
#include "ObstacleScript/begin.lsl"
Setup features we want to use, include the main header file (index.lsl) and start the event handler.
onSpawnerGameLoad()
_CRUSHERS_setup(6, 2.25);
end
onSpawnerGameLoad is raised when you're rezzing the level. We'll use my algorithm for crusher walls defined in CrusherWall.lsh instead of writing a custom algorithm for them by calling _CRUSHERS_setup(6, 2.25); where 6 indicate the nr of walls, and 2.25 is the time between movements.
onStateEntry()
setInterval("TENT", 2);
end
When the script loads, we'll setup a repeating timer every 2 sec called "TENT" and will be used to trigger the lashing tentacles.
onTimer( id )
_CRUSHERS_onTimer(id);
end
Here we pass the timer handler for my crusher wall algorithm, allowing it to handle the wall timers automatically.
onRezzerRezzed( obj )
_CRUSHERS_onSpawn(obj);
end
The crusher walls need to send message to them directly by ID, so when an object is rezzed, let the call this function to let the CrusherWall helper automatically check if it was a wall and whether to store it in the script or not.
handleTimer("TENT")
Trap$attackAll( "*", [] );
end
onStairSeated( hud, stair, seated )
key player = llGetOwnerKey(hud);
if( llKey2Name(stair) == "TumblingBeam" ){
if( seated )
Qte$start( hud, QteConst$QTE_GAUGE, "TUMBL" );
else
Qte$end( hud, TRUE );
}
end
When a player sits on a stair (ladder, shimmy wall, tumbler beam etc), this event is raised. We check if the object name was TumblingBeam and in that case start a quick time event on their HUD. We'll specify that it should use the GAUGE type event, and callback "TUMBL" when the quicktime event finishes or fails. If the player is unseated, we clear the event.
onPlayerQteEnded( hud, success, callback )
if( callback == "TUMBL" && !success ){
Rlv$unSit( hud, TRUE );
}
end
When the quicktime event ends, and if the callback matches ours and was not successful, we unsit the player to let them fall off.
#include "ObstacleScript/end.lsl"
Ends the event handler and the script.
The game script is where most of the game logic takes place. Todo...