Using the Linden Script Language

This page is a short tutorial on using the Linden Script Language (LSL). It includes a collection of examples that illustrate basic LSL capabilities in graphics, "physics," communication between users and scripts, and object creation. Examples are chosen to present fairly isolated "building blocks" that may be combined later to create more complex scripts, and to better understand the Examples scripts and Script Library presented on the Wiki.

This work was originally done for the Kan-ed network within the Kansas Board of Regents, but is now being maintained by the author.

Topics
This page is not an introduction to LSL language syntax or programming in general. For necessary background you may want to consult one of these tutorials:

In addition, the LSL Wiki homepage links to many LSL features, including a list of LSL functions by category that will probably be helpful as you read this tutorial, since almost every section introduces at least one new LSL function.

Caveat: This page covers neither vehicles nor collisions, and is still under development.

Running LSL scripts

SecondLife includes a facility for defining scripts within existing objects. To create an object, start SecondLife, log in, right-click on virtual ground or water and left-click "Create" on the circular menu that appears. When you do this, an "object manipulation menu" will open on the left side of your screen and your cursor will change to a "magic wand" when you hover over the ground again.

Left-click again on the ground. You will then hear a mysterious rumble and an object will appear. It will be some 3-dimensional geometric figure, such as a cube, cone or sphere. The colored lines emanating from that object are used for making certain positional changes in the object.

Return to the object manipulation menu, and if you see a "More>>" button, press it once, until the extended version of the menu opens.

Within the extended menu, left-click on the "Object" tab, and you will see options for changing the overall shape and dimensions of the currently selected object.

Left-click on the "Content" tab and you will see a list of scripts associated with the object. The "Contents" directory displayed within the "Content" tab will most likely be empty.

Left-click on the "New Script" button, and a "Script" editing window will open. The editing window has a "script editing area" where you can type or paste in a new script, and buttons for saving the script, undoing changes made, etc. It will look something like this:

The script editing area should contain the following script:

default
{
    state_entry()
    {
        llSay( 0, "Hello, Avatar!");
    }

    touch_start(integer total_number)
    {
        llSay( 0, "Touched.");
    }
}

Left-click on the "Save" button. You should see the message "Compile successful, saving..." appear in the box below the script editing area. After a short pause you should see the message "Save complete." appear in that box as well.

Once the script is saved, you will see the message "Hello, Avatar!" appear in the lower-left-hand corner of your screen. This message indicates that the script has started and waiting for touch events.

To test the object's responsiveness to "touch," close both the script editing and object manipulation windows, and right-click on the object. The circular object menu will appear from which you can select "Touch." You should then see "Object: Touched." appear in the lower-left-hand corner as well.

Entering and running a simple script

Here is a very simple program that changes the color and size of the object every time the object is touched.

integer counter;

default
{
    state_entry()
    {
        llSay( 0, "Hello, Avatar! Touch to change color and size.");
        counter = 0;
    }

    touch_start(integer total_number)
    {   // do these instructions when the object is touched.
        counter = counter + 1;

        // choose three random RGB color components between 0. and 1.0.
        float redness = llFrand( 1.0 );
        float greenness = llFrand( 1.0 );
        float blueness = llFrand( 1.0 );

        // combine color components into a vector and use that vector
        // to set object color.
        vector prim_color = < redness, greenness, blueness >;
        llSetColor( prim_color, ALL_SIDES );   // set object color to new color.

        // choose a random number between 0. and 10. for use as a scale factor.
        float new_scale = llFrand(10.0) + 1.0;
        llSetScale(< new_scale, new_scale, new_scale > ); // set object scale.        
        llSay( 0, "Touched by angel number " + (string)counter);
    }
}
Here is a snapshot showing the author next to one of the boxes created by this program.

Note that your boxes will have different colors and sizes, and that this program also counts the number of times the object has been touched, and writes a message in the lower-left-hand corner of the screen giving that number.

To insert this script into the object just created, right click on the object, reopen the Script editing window, paste this script into the Script editing area, and click "Save". When the script has been saved, test it by right-clicking on the object and choosing "Touch," as before.

You may have to do some debugging to correct errors, and when you finally get things running correctly, you may want to right-click on the object and choose "Take" to move the object into your Inventory. Objects in your inventory will remain there even after you log off, and will be available when you log back in. Later, you may move copies of these objects back into SecondLife, and reuse them to avoid recreating them.

Changing an object's appearance over time

Here is a script that changes the color and size of an object every two seconds for about 40 seconds beginning when the object is touched.

integer counter;
integer second;

default
{
    state_entry()
    {
        llSay( 0, "Hello, Avatar! Touch to change color and size.");
        counter = 0;
    }

    touch_start(integer total_number)
    {
        counter = counter + 1;        
        
        llSay( 0, "Touched by angel number " + (string)counter);

        llSetTimerEvent( 2 );  // create a "timer event" every 2 seconds.
    }

    timer()  // do these instructions every time the timer event occurs.
    {
        second++;

        // choose three random RGB color components between 0. and 1.0.
        float red = llFrand( 1.0 );  
        float green = llFrand( 1.0 );
        float blue = llFrand( 1.0 );
        
        // combine color components into a vector and use that vector
        // to set object color.
        vector prim_color = < red, green, blue >;  
        llSetColor( prim_color, ALL_SIDES );   // set object color to new color.
        
        // a choose random number between 0. and 10 for use as a scale factor.
        float new_scale = llFrand( 10.0 );  
        llSetScale(< new_scale, new_scale, new_scale > ); // set object scale.
       
        if ( second > 19 )  // then time to wrap this up.
        {         
            // turn object black, print "resting" message, and reset object....
            llSetColor( < 0, 0, 0 >, ALL_SIDES );
            
            llSay( 0, "Object now resting and resetting script." );
            llResetScript();  // return object to ready state.
        }
    }
}

When a touch_start event occurs, the program starts a timer that "runs out" every 2 seconds, causing a "timer event." Whenever a timer event occurs, the script segment within the brackets below the string "timer()" is executed. The timer() section counts the number of times it is activated, and resets the script after 20 or so activations.

If multiple avatars touch the object at the same time, strange behavior may occur. Such an event may be monitored and controlled by using the total_number variable passed to touch_start.

Changing an object's position over time

A script may change the position of its object by using the llSetPos() function. Here is a script that repositions an object using llSetPos() about once a second for 20 or so seconds.

The new positions are chosen by incrementing the starting position using a vector constructed during each iteration from 3 random values between 0.0 and 10.0 meters.

integer counter;
integer second;
vector startPosition;

default
{
    state_entry()
    {
        llSay( 0, "Hello, Avatar! Touch to change position.");
        counter = 0;
        startPosition = llGetPos();
    }

    touch_start(integer total_number)
    {
        counter = counter + 1;        
        
        llSay( 0, "Touched by angel number " + (string)counter);

        llSetTimerEvent( 1 );  // arrange for a "timer event" every second.
    }

    timer()  // do these instructions every time the timer event occurs.
    {
        second++;

        // choose three random distances between 0. and 10.0.
        float X_distance = llFrand( 10.0 );  
        float Y_distance = llFrand( 10.0 );
        float Z_distance = llFrand( 10.0 );
        
        // combine these distance components into a vector and use it
        // to increment the starting position and reposition the object.
        vector increment = < X_distance, Y_distance, Z_distance >;  
        vector newPosition = startPosition + increment;
        llSetPos( newPosition );   // reposition object.
        
        if ( second > 19 )  // then time to wrap this up.
        {    
            // move object back to starting position...
            while ( llVecDist( llGetPos(), startPosition ) > 0.001) 
            {
                llSetPos( startPosition );
            }
     
            llSay( 0, "Object now resting and resetting script." );
            llResetScript();  // return object to ready state.
        }
    }
}

To help you keep track of the object, it is repositioned to the starting position before the script reinitializes.

The motion generated by random repositioning is, of course, quite eratic. (One might even call it "random.") To cause an object to move more "naturally" one must choose subsequent positions more carefully. New positions must lie along a movement track, and be sufficiently close to one another that jerkiness is either not apparent or minimized. This is sometimes difficult in SecondLife due to network delays, server congestion, and/or poor client CPU or graphics card performance.

A 3 GHz CPU paired with a high-end 3D graphics card, such as the nVidia 6800 Ultra, will usually deliver very good performance operating over a network connection with a 50ms roundtrip ping time to a SL server. A 3 GHz CPU hosting an nVidia Quadro NVS (designed primarily for 2D graphics) and operating with a 200ms ping time provides only moderately satisfactory results.

To see an example attempting to portray smooth object movement under script control, look at this script to move an object in a contained volume.

A simple particle system

It is possible to arrange for an object to generate and emit "particles" under script control. For example, an object can be set up to emit bubbles, sparks, fire, etc.

To see this in action, you can modify the script in previous section to emit particles while the object is moving.

To do that insert a call to llParticleSystem() in touch_event() and another call to turn it off when the object has been returned to its starting position and the script is about to be reset.

The first call should like this one from the Wiki:

llParticleSystem( 
   [
    PSYS_PART_FLAGS,         PSYS_PART_WIND_MASK | PSYS_PART_EMISSIVE_MASK,
    PSYS_SRC_PATTERN,        PSYS_SRC_PATTERN_EXPLODE, 
    PSYS_PART_START_COLOR,   <1,0,0>
   ] );
and the second call should look like:
llParticleSystem( [] );
Here is a snapshot of the author standing next to a a moving object emitting glowing red balls as specified in the call above:

The unusual layout shown in the llParticle System() call above is meant to emphasize how parameters in the call to llParticleSystem() are sent as a collection of element pairs within an LSL list. Each line in the parameter list contains two elements separated by a single comma. The first element is the parameter name and the second element is the value to be associated with that parameter. For example, the particle color is controlled by the parameter PSYS_PART_START_COLOR, and the value sent is < 1, 0, 0 >, which specifies the color "red".

Some parameters can take multiple settings at the same time. For example, the parameter-value pair

PSYS_PART_FLAGS, PSYS_PART_WIND_MASK | PSYS_PART_EMISSIVE_MASK
sets both the PSYS_PART_WIND_MASK and PSYS_PART_EMISSIVE_MASK flags within the PSYS_PART_FLAGS parameter at the same time. (You can set as many flags as necessary by separating them with logical OR characters (|) as shown above.)

The Wiki page for llParticleSystem() lists all the accessible parameters. There are many parameters and some have difficult names, but it is fairly easy to experiment with them to find out how they work, since they usually have directly observable effects.

Note that particles are generated locally on each client within range. That means they do not put much load on the server and can be quite realistic.

Rotating an object

There are several ways to represent rotational movement in LSL. The Euler representation uses a (3-part) vector where each component specifies movement in one of 3 dimensions. For example, the components of a vector, V, holding an Euler rotation, is composed of 3 angular components describing:
   Component   Movement in plane    Around axis     Known as
   ---------   -----------------   ------------     --------
     V.x             Y-Z                X             roll

     V.y             X-Z                Y             pitch

     V.z             X-Y                Z             yaw
While the Euler representation is fairly intuitive and is supported within LSL, rotational movments may also be represented as "quaternions."

Quaternions are implemented within LSL via a built-in variable type, called a "rotation," which is composed of 4 components, x, y, z, and s, all of type float. The first 3 components may be thought of as defining an axis around which rotation occurs, while the 4th describes the amount of rotation.

A rotation can be defined using an Euler representation and then converted to quaternion representation via a call to llEuler2Rot(). For example, < 0, 10, 0 > defines a rotation of 10 degrees around the Y axis, and can be converted to quaternion form using the call

llEuler2Rot( < 0, 10 * DEG_TO_RAD, 0 >)
yielding a value of < 0.00000, 0.08716, 0.00000, 0.99619 >.

When the rotation describing the "current orientation" of an object is known, a new orientation can be derived simply by multiplying one rotation by another.

Here is an example script that illustrates the use of a for loop to simulate continuous rotation of an object in one place. It uses llGetRot() to acquire the rotation describing the current orientation, modifies the returned value and then uses llSetRot() to reorient the object.

default
{
    state_entry()
    {
        llSay( 0, "Hello, Avatar!");
        vector startPoint = llGetPos();
    }

    touch_start(integer total_number)
    {
        llSay( 0, "Touched." );

        // Define a rotation of 10 degrees around the Y-axis.
        rotation Y_10 = llEuler2Rot( < 0, 10 * DEG_TO_RAD, 0 > );
       
        // now rotate the object 10 degrees in the X-Z plane during
        // each loop iteration. note that each call to llSetRot 
        // causes a .2 second delay.
        integer i;
        for( i = 1; i < 100; i++ )  
        { 
            // rotate object in the X-Z plane around its own Y-axis.
            rotation newRotation = llGetRot() * Y_10; 

            llSetRot( newRotation ); 
        }  
        llSay( 0, "Rotation stopped" );              
    }
}  
Each pass through the for loop calculates a new orientation relative to the current orientation by multiplying the object's current rotational orientation by the 10 degree rotation defined during the state_entry event.

If you right-click on the object while it is rotating, the object axes will appear, and you can verify it is rotating around its Y-axis.

Note that operand order is significant when multiplying by variables of type rotation. The statement:

rotation newRot = llGetRot() * Y_15;
will rotate the object with respect to global coordinates, while the statement:
rotation newRot = Y_15 * llGetRot();
will rotate with respect to the object's local axes.

Rotation local to each client

Rotation may also be simulated by using the llTargetOmega() function within a script. Movement produced by this function is produced independently by each SecondLife client that can observe the moving object. As a result, movement is much smoother, but behavior will differ slightly on each client.

Here is an example that rotates an object around both the Y and Z axes (as specified using < 0, 1, 1 >) at a rate of .2 * Pi radians per second:

default
{
    state_entry()
    {
        llTargetOmega( < 0, 1, 1 >, .2 * PI, 1.0 );
    }
}
Note that this approach is less flexible than rotational movement produced using llSetRot().

Rotating an object in an orbit

Here is a script that makes an object orbit in the X-Y plane around a point < 3, 3, 3 > from its starting position.

This script makes use of the effect of multiplying a position vector by a rotation. The Wiki page describing this operation says:

"An object can be rotated around an arbitrary point by multiplying a vector by a rotation.... The vector should be the difference between the object's current position and the desired "center-point" of rotation. Take the result of the multiplication and add it to the object's current position. This vector will be the "new location" the object should be moved to."

The orbiting motion is simulated via a for loop that repositions the object by 15 degrees during each iteration. Note that llSetPos() is used in place of llSetRot() during this process.

vector rotationCenter;  // point to rotate around.

default
{
    state_entry()
    {
        llSay( 0, "Hello, Avatar!");
        vector startPoint = llGetPos();
        rotationCenter = startPoint + < 3, 3, 3 >; 
        // distance to the point of rotation should probably be a
        // function of the max dimension of the object.
    }

    touch_start(integer total_number)
    {
        llSay( 0, "Touched." );

        // Define a "rotation" of 10 degrees around the z-axis.
        rotation Z_15 = llEuler2Rot( < 0, 0, 15 * DEG_TO_RAD > );
        
        integer i;
        for( i = 1; i < 100; i++ )   // limit simulation time in case of
        {                            // unexpected behavior.
            vector currentPosition = llGetPos();

            vector currentOffset = currentPosition - rotationCenter;
           
            // rotate the offset vector in the X-Y plane around the 
            // distant point of rotation. 
            vector rotatedOffset = currentOffset * Z_15;
            vector newPosition = rotationCenter + rotatedOffset;

            llSetPos( newPosition ); 
        }  
        llSay( 0, "Orbiting stopped" );              
    }
}  

Here is a snapshot of the author riding a large orange cone rotating under control of a hybrid version of the script above using code from both of the rotation examples just presented. The cone rotates around the Y-axis while simultaneously orbiting the distant center point.

And here is what the experience looks like from the author's point of view just as the snapshot above was taken:

Note that it is also possible to combine this orbit motion with local rotation produced by using llSetTargetOmega(), as shown in this script. This is accomplished by starting local rotation along with a timer that periodically executes instructions to move the rotating object in an orbit. This approach can be used to simulate lunar movement around an orbiting planet (if the orbiting object is two linked spheres). Note, however, that the planet will rotate at the same rate as the moon, which is not realistic.

Two simple examples using LSL "physics"

LSL includes the ability to simulate some of the physical laws governing energy, gravity, torque, etc. as they affect objects in-world. This ability derives from a set of built-in functions that implements the effects of these laws.

To see "physics" in action, create an object, open the Object tab, check the "Physics" box, change the Z-axis height of your object to make it several meters above the ground, and then close the Object manipulation window. Your object should then drop to the ground, and if the ground is not level and your object is sufficiently spherical, it may even roll downhill. (In fact, you may have to chase it to regain control of it.)

The following script sets physics "on" using the function call

llSetStatus( 1, TRUE )
and then, when the object is touched, uses the command
llApplyImpulse( < 0.0, 0.0, z_force >, FALSE );
to apply an instantaneous vertical impulse:
default
{
    state_entry()
    {
        llSay( 0, "Hello, Avatar! Touch to launch me straight up.");
        llSetStatus( 1, TRUE );  // turn on physics.
    }

    touch_start(integer total_number)
    {  
        vector start_color = llGetColor( ALL_SIDES ); // save current color. 
        llSetColor( < 1.0, 0.0, 0.0 > , ALL_SIDES );  // set color to red.
        
        float objMass = llGetMass();
        float Z_force = 20.0 * objMass;
        
        llApplyImpulse( < 0.0, 0.0, Z_force >, FALSE );
        
        llSay( 0, "Impulse of " + (string)Z_force + " applied." );
        llSetColor( start_color , ALL_SIDES ); // set color to green.
    }
}
The next example applies a continuous upward vertical force using the command
llSetForce( < 0.0, 0.0, Z_force >, FALSE ) 
on the object. This function call sets up a force of 0.0 Newtons along the positive X-axis, 0.0 Newtons along the positive Y-axis, and Z_force Newtons along the positive Z-axis.

Since the weight of an object is equal to the mass of the object times the force of gravity, or 9.8 meters per second per second, a force greater than 9.8 * objMass should overcome the force of gravity and raise the object. In this script, Z_force is set to "10.2 * objMass", which should raise the object slowly. You can edit the script to change this parameter and observe different object behaviors.

vector startPos;
vector curPos;
vector curForce;
integer second;

default
{
    state_entry()
    {
        llSay( 0, "Hello, Avatar! Touch to launch me straight up.");
        llSetStatus( 1, TRUE );
        startPos = < 0, 0, 0 >;
    }

    touch_start(integer total_number)
    {
        startPos = llGetPos();
        curPos = startPos;
        curForce = < 0, 0, 0 >;
        second = 0;
        
        llSetColor( < 1.0, 0.0, 0.0 > , ALL_SIDES ); // set color to red.
        
        float objMass = llGetMass();
        float Z_force = 10.2 * objMass;
        
        llSetForce( < 0.0, 0.0, Z_force >, FALSE );
        
        llSay( 0, "Force of " + (string)Z_force + " being applied." );
        llSetTimerEvent(1);
    }
    
    timer()
    {
        second++;
        curPos = llGetPos();
        float curDisplacement = llVecMag( curPos - startPos );
        
        if( ( curDisplacement > 30. ) &&  // then object is too far away, and
          ( llGetForce() != < 0.0, 0.0, 0.0 > ) ) // force not already zero, 
        {   // then let gravity take over, and change color to green.
            llSetForce( < 0.0, 0.0, 0.0 >, FALSE ); 
            llSetColor( < 0, 1.0, 0 >, ALL_SIDES ); 
            llSay( 0, "Force removed; object in free flight." );
        } 
        
        if ( second > 19 )  // then time to wrap this up.
        {
            // turn object blue and zero force to be safe....
            llSetColor( < 0, 0, 1.0 >, ALL_SIDES ); // change color to blue.
            llSetForce( < 0, 0, 0 >, FALSE );

            // ...move object back to starting position...
            // ...after saving current status of Physics attribute.
            integer savedStatus = llGetStatus( 1 );
            llSetStatus( 1, FALSE );  // turn physics off.
            while ( llVecDist( llGetPos(), startPos ) > 0.001) 
            {
                llSetPos( startPos );
            }
            llSetStatus( 1, savedStatus );  // restore Physics status.
            
            //...and then turn color to black and Reset the script.          
            llSetColor( < 1, 1, 1 >, ALL_SIDES );
            llSetTimerEvent( 0 );  // turn off timer events.
            llSay( 0, "Done and resetting script." );
            llResetScript();  // return object to ready state.
        }
    }
}

Note that it can be difficult to control objects in motion in the Second Life environment. For example, avatars apparently lose control of objects more than 10 meters away, and objects set in motion may continue across many lands before they eventually leave the simulated space. (When they do leave, they are usually returned to the "lost and found" area of your Inventory.)

The script above has two features to help maintain control. First, it uses a timer event, as illustrated in a previous program, to monitor the position of the object, and to take corrective action. In particular, if the object moves more than 30 meters from its starting position, the timer event script eliminates applied force by using the function call llSetForce( < 0, 0, 0 >, FALSE ).

Second, the script stops the simulation and returns the object to its starting position after a fixed number of timer events.

Note that this script deliberately manipulates object susceptibility to physical forces. In particular, physics is turned "off" while the object is repositioned to its starting location using script functions that won't work when physics is enabled.

Controlling an object through commands embedded in chat text

A script can be set up to "listen" to ongoing chat dialog and respond to commands issued via chat. Here is a script that listens for chat messages coming from the owner of the object in which the script resides. If a chat line starts with the strings "up" or "down" (separated from the other text on the line by a blank), the script will assume the owner is giving a command to move the object up or down. It will then look for a second blank-delimited string, convert it to a number, and use that number to decide how far up or down to move the object.

vector startPosition;
float groundLevel;

default
{
    state_entry() 
    {
        llListen( 0, "", llGetOwner(), "");

        startPosition = llGetPos();
        groundLevel = llGround( startPosition );
  
        llSay( 0, "Control this object with chat commands like:" );
        llSay( 0, "'up' or 'down' followed by a distance." );
    }  
    
    listen( integer channel, string name, key id, string message ) 
    {
        // separate the input into blank-delmited tokens.
        list parsed = llParseString2List( message, [ " " ], [] );

        // get the first part--the "command".
        string command = llList2String( parsed, 0 );
        
        // get the second part--the "distance".
        string distance_string = llList2String( parsed, 1 );
        float distance = ( float )distance_string;

        vector position = llGetPos();

        if( command == "up" )
        {
            if( ( position.z + distance ) < (startPosition.z + 10.0 ) )
            {
               llSetPos( llGetPos() + < 0, 0, distance > ); // move up
               llSetText( "Went up " + (string)distance, < 1, 0, 0 >, 1 );
            }
            else
            {
               llSetText( "Can't go so high.", < 1, 0, 0 >, 1 );
            }
        }
        else if( command == "down" )
        {
            if( ( position.z - distance ) > groundLevel ) 
            {
               llSetPos( llGetPos() + < 0, 0, -distance > ); // move down
               llSetText( "Went down " + (string)distance, < 1, 0, 0 >, 1 );
            }
            else
            {
               llSetText( "Can't go so low.", < 1, 0, 0 >, 1 );
            }
        }    
    }
}

llListen() is used to set up an event trigger, so the "listen" event will be invoked whenever a chat message is received from the owner. The listen event receives the content of the message in its "message" argument, and "parses" that message into its blank-delimited parts. The first part is taken as a "command" and the second part as a "distance", as described above.

Note that this technique can also be used by objects to communicate with one another, if they are close enough to one another.

Controlling an object through a Dialog box

It is possible for an object to present a set of options to an avatar that can be used to control its behavior. Here is a program that moves an object up or down along its Z axis under dialog control:

integer dialog_channel= 427; // set a dialog channel
list menu = [ "Go up", "Go down" ]; 
vector startPosition;
float groundLevel;

default 
{
    state_entry() 
    {
        // arrange to listen for dialog answers (from multiple users)
        llListen( dialog_channel, "", NULL_KEY, ""); 

        startPosition = llGetPos();
        groundLevel = llGround( startPosition );
    }
    
    touch_start(integer total_number) 
    {
        llDialog( llDetectedKey( 0 ), "What do you want to do?", menu, 
                 dialog_channel );
    }
    
    listen(integer channel, string name, key id, string choice )
    {
        vector position = llGetPos();
    
        // if a valid choice was made, implement that choice if possible.
        // (llListFindList returns -1 if choice is not in the menu list.)
        if ( llListFindList( menu, [ choice ]) != -1 )  
        { 
            if ( choice == "Go up" )
            {
               if( position.z < ( startPosition.z + 10.0 ) )
               {
                  llSetPos( llGetPos() + < 0, 0, 1.0 > ); // move up
               } 
            }
            else if( choice == "Go down" )
            { 
               if( position.z > ( groundLevel + 1.0 ) ) 
               {
                  llSetPos( llGetPos() + < 0, 0, -1.0 > ); // move down
               }
            }
        }
        else
        {
            llSay( 0, "Invalid choice: " + choice );
        }
    }
}

Whenever the object is touched it will ask the user to move it up or down.

Note that a dialog box can include an option to request "Additional options" which will appear on a cascaded Dialog box.

Controlling an object through avatar movement control keys

It is also possible to allow an object to respond to keyboard keys that normally control your avatar. For example, the next script will ask for permission to reassign your avatar forward and backward movement keys to move the object up and down.

vector startPosition;
float groundLevel;

default 
{
    state_entry() 
    { 
       // get permission to take over the avatar's control inputs.
       llRequestPermissions( llGetOwner(), PERMISSION_TAKE_CONTROLS );
       
       startPosition = llGetPos();
       groundLevel = llGround( startPosition );
    }

    run_time_permissions( integer perm )  // event for processing 
                                          // permission dialog.
    {
        if ( perm & PERMISSION_TAKE_CONTROLS )  // permission has been given.
        { 
            // go ahead and take over the forward and backward controls.
            llTakeControls( CONTROL_FWD | CONTROL_BACK, TRUE, FALSE ); 
        }
    }
    
    control( key id, integer held, integer change )  // event for processing 
                                                     // key press.
    { 
        vector position = llGetPos();
        
        if ( change & held & CONTROL_FWD ) 
        {   // the "move forward" control has been activated.
            if( position.z < (startPosition.z + 10.0) )
            {
               llSetPos( llGetPos() + < 0, 0, 1.0 >); // move up
            }
        } 
        else if ( change & held & CONTROL_BACK ) 
        {   // the "move backward" key has been activated.
            if( position.z > groundLevel + 1.0 ) 
            {
               llSetPos( llGetPos() + < 0, 0, -1.0 >); // move down
            }
        }
    }
}

Note that this script takes over both the up- and down-arrow keys, as well as the forward and backward control buttons provided by "View/Movement controls".

Rezzing objects from within scripts

"Rezzing" is the process of creating an object. Avatars can create objects as described earlier. However, it is not possible for a script to create an object in this fashion. Scripts can only "instantiate" (create instances of) objects already created and residing in the inventory of the object in which the script is located.

To work with this feature, create an object, give it the name "Object1", and allow it to be copied by selecting "Allow anyone to copy" and "Next owner can copy" within the Object manipulation window.

Then right-click on the object and "Take" it into your avatar's inventory.

Now create a second object with the name "Object2", and open its Object manipulation window "Content" tab. Then open your avatar's inventory by clicking "Inventory" at the bottom of your SecondLife window, open the "Objects" folder, left-click on the "Object1" entry, and drag it to the Object2 "Contents" folder area.

Now edit the following script into Object2:

default
{
    state_entry()
    {
        llSay( 0, "Hello, Avatar!");
    }

    touch_start(integer total_number)
    {
        llSay( 0, "Touched.");
        
        llRezObject("Object1", llGetPos() + < 0, 0, 2 >, ZERO_VECTOR, 
            ZERO_ROTATION, 42);
    }
}

Now when you touch Object2, llRezObject() will instantiate Object1, and you should see a copy of Object1 appear 2 meters above Object2.

The last parameter in the llRezObject() call ("42" in this example) is passed to the new object. It is presented as the single parameter to the on_rez event in the new object. An "intelligent" version of Object1 could use such a parameter to make immediate changes (such as color or size) under its control.

llRezObject() triggers the object_rez event and passes the new object's key along for subsequent use within Object2. Commands within object_rez can then link the newly created object and/or send messages to the new object, etc. to affect its appearance or behavior.

Creating linked sets of objects

LSL provides a set of functions for linking and managing sets of primitive objects, sometimes called "link_sets." Primitive objects that are linked together will behave like a single object in some ways. For example, they will move as a single object if repositioned or made physical, and editing commands will apply to the collection. They can also be set up to communicate with one another and pass certain event information among themselves.

Here is a version of the script above that asks for permission to link other objects into a linked set and creates two child objects only if it receives permission to link them once they have been created.

Once linkage is established, the script then uses llSetLinkColor() to make the newly created objects red. The newly created and linked objects are identified using link numbers "2" and "3" during message exchanges within the linkset, but they may also be addressed via LSL constants like "LINK_ALL_CHILDREN" or "LINK_SET".

The parent object is identified as link number "1" and may be addressed as "LINK_ROOT" (although primitive objects without linked children are identified as link number "0").

integer createdObjectCounter;
integer linkedObjectCounter;

default
{
    state_entry()
    {
        llSay( 0, "Hello, Avatar!");
        linkedObjectCounter = 0;  // zero the linked object counter.
    }

    touch_start(integer total_number)
    {
        if( createdObjectCounter <= 0 )  // nothing has yet been linked,
        {                               // begin object creation sequence...
           // ask for permissions now, since it will be too late later.
           llRequestPermissions( llGetOwner(), PERMISSION_CHANGE_LINKS );
        }
        else   // just do whatever should be done upon touch without
        {      // creating new objects to link. 
            // insert commands here to respond to a touch.
        }
    }

    run_time_permissions( integer permissions_granted )
    {
       if( permissions_granted == PERMISSION_CHANGE_LINKS )
       {   // create 2 objects.
           llRezObject("Object1", llGetPos() + < 1, 0, 2 >, 
                       ZERO_VECTOR, ZERO_ROTATION, 42);
           createdObjectCounter = createdObjectCounter + 1;

           llRezObject("Object1", llGetPos() + < -1, 0, 2 >, 
                       ZERO_VECTOR, ZERO_ROTATION, 42);
           createdObjectCounter = createdObjectCounter + 1;

       }
       else
       {
           llOwnerSay( "Didn't get permission to change links." );
           return;
       }
    }

    object_rez( key child_id )
    {
        llOwnerSay( "rez happened and produced object with key " + 
                     (string)child_id );

        // link as parent to the just created child.
        llCreateLink( child_id, TRUE ); 

        // if all child objects have been created then the script can
        // continue to work as a linked set of objects.
        linkedObjectCounter++;
        if( linkedObjectCounter >= 2 ) 
        {
            // Change all child objects in the set to red (including parent).
            llSetLinkColor( LINK_ALL_CHILDREN, < 1, 0, 0 >, ALL_SIDES );  

            // Make child object "2" half-tranparent.
            llSetLinkAlpha( 2, .5, ALL_SIDES );

            // Insert commands here to manage subsequent activity of the 
            // linkset, like this command to rotate the result:
            // llTargetOmega( < 0, 1, 1 >, .2 * PI, 1.0 );
        }
    }
}
Activities within this script are coordinated via a cascade of events. The run_time_permissions event runs only after the owner answers the prompt to allow object links to be established. It checks to see if permission has been granted and either rezzes the new objects or says it can't.

The object_rez event runs only when a rez succeeds, at which point the new object is linked in and the script checks to see if all objects have been successfully created. If so, the child objects are ordered to change their color, and one is made semi-transparent.

It is here that additional commands could be added to direct subsequent behavior of the linkset. For example, the entire set could be made to rotate by inserting portions of the rotation script presented earlier. Alternatively, this script could change control to a different script state (by using the "state" command) that would continue to direct linkset behavior.

Members of a linkset can send messages to other members of the linkset by using the llMessageLinked() function. To receive messages, member objects must have a link_message_event event handler defined within their scripts.

Note that objects can only be created within 10 meters distance of the object containing the creating script.

Also, while objects can be created from within loops (although there is a short delay with each instantiation), you should be very careful about interative or recursive object creation to avoid system catastrophes.

A simple texture animation

It is possible to create texture files that hold multiple individual images that may be used as animation frames. When a series of such frames is displayed upon the side of an object many different effects are possible. For example, an object may be made to appear as if it is rotating even though it is not. Here is a snapshot of the author next to a flame simulated using texture animation:

The following script will produce a similar flame when applied to an appropriate object:

default
{
    state_entry()
    {
        llSetStatus(STATUS_PHANTOM,TRUE);
        llSetTexture("lit_texture", ALL_SIDES);
        llSetTextureAnim (ANIM_ON | LOOP, ALL_SIDES, 4, 4, 0, 0, 15.0);
    }
} 
This script works on a flattened cube with dimensions of approximately 8.0 by 8.0 by 0.0 meters, and requires a texture like this
flame_texture. To get this texture into an object you must first get it into your SecondLife inventory and then move it into the object. To get it into your inventory:

During the upload process you will see that the texture file is composed of 16 slightly different flame images laid out in a 4 by 4 grid, as shown below:

The script calls llSetStatus() to make the object a "phantom" and then specifies the texture for llSetTextureAnimation() to use with a call to llSetTexture().

Finally, llSetTextureAnimation() is called to loop through each of the 16 flame images. The third and fourth parameters in the parameter list (namely, "4, 4") specify the 4 by 4 grid. The fifth and sixth parameters (namely, "0, 0") specify the starting frame and the number of frames to include in the loop, where "0" in parameter 5 specifies the first frame, and where "0" in parameter 6 specifies "all frames." The last parameter specifies the number of frames to display per second.

Note that this image will be almost 2-dimensional. To create a more convincing illusion, animate the sides of a composite object made from two flat panels intersecting orthogonally at their vertical midlines. To do that, create the two panels, position them to intersect, and link them together by using the "Link" option under the "Tools" menu option (or simply Control-l). (It would also be possible to rez and link the second panel for this purpose using the technique presented earlier.)

Note that llSetTextureAnim() executes locally, so that it will appear slightly different on different SecondLife clients. Note also that vertical yellow lines will sometimes be seen next to the flame. These are actually the very narrow sides of the cube glowing with the flame image. (Perhaps careful specification of object sides to be used during animation could prevent these artifacts.)

Using a sensor to define a (moving) center of rotation

An earlier section showed how to cause an object to rotate around a fixed center. This section introduces sensors and shows how they can be used to identify the location of an avatar so that an object may be made to rotate around it.

Sensors allow objects to scan their environment to establish the presence of other objects or agents in their vicinity. Sensors are invoked by using the llSensor() or llSensorRepeat() functions to scan selected portions of the environment. llSensor() makes a single scan, while llSensorRepeat() can be set up to scan the environment at regular intervals. Note that an object may have only one active sensor at a time.

When a sensor conducts a scan, it induces the "sensor" event which can process the results of the scan. Within the script to process the sensor event, a set of detection functions can be used to acquire information about sensed objects.

Here is a script that uses a repeating sensor scan to identify the location of its owner, and rotates its object around its owner.

// This is a script designed to orbit its owner.
vector startPos;
vector curPos;

vector offset;             // offset from Agent
integer iteration;
float rotationRate;       // degrees of rotation per iteration
float sensorInterval;     // seconds between sensor scan.

default
{
    state_entry()
    {
        llOwnerSay( "Hello, Avatar! Touch to start orbiting." );
        llSetStatus( 1, FALSE );  // turn Physics off.
        offset = < 2, 2, 1 >;
        iteration = 0;
        rotationRate = .5;
        sensorInterval = .3;
    }

    touch_start(integer total_number)
    {
        startPos = llGetPos();
        curPos = startPos;
        
        llSleep( .1 );

        key id = llGetOwner();
        llSensorRepeat( "", id, AGENT, 96, PI, sensorInterval );
    }

    sensor(integer total_number)
    {
        iteration++;

        if( iteration > 300 )
        {
           llResetScript();
        }

        if( llDetectedOwner( 0 ) == llGetOwner() )  
        {        // the detected Agent is my owner.
           vector position = llDetectedPos(0);  // find Owner position.

           // calculate next object position relative both to the Owner's 
           // position and the current time interval counter.  That is, 
           // use the iteration counter to define a rotation, multiply 
           // the rotation by the constant offset to get a rotated offset 
           // vector, and add that rotated offset to the current position
           // to defne the new position. 
          
           float degreeRotation = llRound( rotationRate * iteration ) % 360;
           rotation Rotation = 
                llEuler2Rot( < 0, 0, degreeRotation * DEG_TO_RAD > );
           vector rotatedOffset = offset * Rotation;
           position += rotatedOffset;
          
           // change the location of the object and save the current (rotated) 
           // offset for use during the next iteration.
           llSetPos( position ); 
           offset = rotatedOffset; 
        }  
    }
}
Sensor scans in this script are set to search for the owner agent within 96 meters of the object and within PI radians to either side of the object's forward vector (which is to say all around the object).

Investigating some example scripts

This tutorial has presented a variety of annotated example scripts to help readers learn to use LSL. By using the basic structures presented so far, it should be possible to begin to understand some more detailed examples. Of particular interest are: To use the example gun script, create an object to be used as a gun. A default cube will do, but you may prefer something more realistic. Copy the gun script into the newly created object, lower the bullet velocity by replacing the line
float gBulletSpeed = 75.0;
with the line
float gBulletSpeed = 7.5;
and "Save" the script. This change will make it possible to see the bullet as it emerges from the gun and moves through space.

Now create an object suitable for rezzing as a "bullet". Once again, a default sphere will do, but you may choose a more (or less) realistic object. Put the bullet script into the newly created bullet object, "Save" it, and then "Take" it object into your Inventory.

When the bullet object is in your inventory, you can move a copy to the gun object's Contents collection, to "load" your gun. "Attach" the gun to your Right Hand, enter "Mouselook" and every time you left-click, you will "fire" a bullet.

Note that both gun and bullet have "physics". Each shot will cause a recoil, and bullets will fall under the influence of gravity. Using a slow muzzle velocity allows you to observe these effects more easily.

To use the floater script, move it to an object and "Save" it as usual. When the Save is complete, right-click the object, and select "Get in". You will see the "Stand up" button appear at the bottom of your screen. When you are safely seated, right-click and choose "Touch". The script will take over your keyboard and you will see the "Release Keys" and "Mouselook" buttons appear at the bottom of your screen, as well as a list of control commands in the Chat area. You may also type "menu" to ask the script to repeat the menu of control commands at any time. (Note that "Touch" will toggle script control over your keyboard.)

The floater and cross-leg-sit scripts can be used together to allow your avatar to sit cross-legged on the object being driven. To use these scripts together just put both of them separately into an object's Content collection. Aside from the fact that your avatar will sit in a more aestheically pleasing position, the floater script will work the same.

Summary

This tutorial has presented a collection of examples illustrating many of the basic facilities available to LSL scripters. By using these basic structures, it should be possible to construct scripts for a wide variety of purposes. For example, you should be able to script simple games, dynamic gadgets and kinetic art. It should even be possible to investigate and/or illustrate some physical phenomena in a more or less realistic fashion by using the "physics" available in-world.

Please send suggestions for improvement and report problems and successes encountered using this document to Michael Grobe at grobe@ku.edu.

Bon voyage!

First version: August 5, 2005
Second version: Sept 19, 2005
Current version: April 7, 2006