Skip to content

Obstacle Course Tutorial: Scripting

Jasdac edited this page Mar 11, 2021 · 8 revisions

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.

Dialog

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.

Code:

#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"

Breakdown

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

Obstacles

Code:

Game

Code:

Clone this wiki locally