Thursday, November 30, 2006

Scripting: the basics, part 1

LSL, the Linden Scripting Language, is very similar to many modern scripting languages, such as JavaScript or PHP (which themselves have a lot in common with the C programming language and its descendants).

But not a whole lot of people write scripts in Second Life, maybe because it's a bit scary with all those curly brackets and parentheses and semicolons. I thought I'd try my hand at being a teacher in SL, since I learned so much in classes my first couple of weeks there, and wanted to "give back" to the community, so I'll use this blog to start organizing my thoughts for what and how I'd like to teach!

For this first post, let's look at the default "New Script" which gets created when you click the New Script… button in the Content tab of the edit window. (If you've never tried this before, rez a prim and select the Edit button on the Focus/Move/Edit/Create/Land panel that pops up. If it's a short window, click on the More » button in the lower right-hand corner. Then click on the rightmost tab labeled Content, and press the New Script… button.)

When you create a new script, your prim will say something like "Object: Hello, Avatar!" (assuming you haven't renamed it from its default name of "Object"). Double-click on the script (named, aptly enough, "New Script"), and an edit window will pop up. The script will look something like this:

llSay(0, "Hello, Avatar!");

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

The first line in the script is "default". This means that the following code pertains to the object's default state. (States are an easy way of making the object react different ways to the same stimuli. If you don't have need of different states, as this script doesn't, then everything happens in the default state.)

The second line is an opening curly bracket (or curly brace). Curly braces are used in LSL, as in several other languages, for grouping multiple statements together. All states and functions must enclose their contents in curly brackets, even if those contents are empty. (They'll be more useful later on, when we get into conditionals and flow control.)

The third line is "state_entry()" - this is a function called every time the script enters a state. If it's located in the default state, it gets called every time the script is saved or reset. The parentheses appear after every function definition, and enclose the parameters that are passed to the function. The state_entry() function has no parameters, so there's nothing between the parentheses.

The fourth line is another curly bracket, this time enclosing the contents of the state_entry() function. (You'll notice that things are indented, and each time there's a set of curly brackets, everything inside them gets indented another level; this isn't required, but it does make it a lot easier for a human to read if the contents of every nested container are indented by a tab's worth of spaces.)

The fifth line — the actual contents of the state_entry() function — is a call to a predefined LSL function, llSay(). (Many predefined LSL functions start with "ll", for Linden Lab.) This particular function call causes the object to say "Hello, Avatar!" on chat channel 0 (zero), the one players talk on and the only one they can "hear" without some sort of scripted listening device. The line ends with a semicolon (;), which is put at the end of every statement.

The sixth line is a closing curly bracket, and as we can see by the way it's indented, it lines up with the opening brace for the state_entry() function. If you find a script where things are not indented like this, you may have to count the opening and closing braces to find out where things begin and end!

After a blank line (again, not strictly necessary, but good for readability), we have a new function, touch_start(). This one is called whenever someone begins touching the object or prim containing the script, and takes one parameter, an integer (whole number, with no decimal point) which indicates the total number of agents (avatars, or players) who have started touching it since the last time it was called. This is usually just one, unless people touched it simultaneously or the sim is very lagged. This particular script doesn't do anything with that parameter, but we still need to include it.

Handy Tip: When you type a word that the script editor recognizes as a valid LSL keyword, it changes color. You can hover your mouse pointer over a colored word to see more information about it — for example, if you type the name of a function and hover the mouse pointer over it, it will pop up a little tooltip telling you what parameters it takes, and what kind of data they are.

Within the curly brackets for touch_start(), we have another llSay() function. In this particular script, when someone touches the prim or object containing the script, it will say "Touched."

After the closing curly bracket for the touch_start() function is the closing curly bracket for the default state... and that's the end of the script!

Scripting: the basics, part 2

In the first part, we wrote a couple of functions, and used another one a couple of times, but we didn't really go over what a function is, how it works, or why you would want to use one. Here we will look at functions, and also at the types of data which Second Life scripts can use.

In LSL, the Linden Scripting Language (and in programming in general), a function is a modular unit of code which can optionally be passed a number of parameters, and which can optionally return a value.

float quintuple( float theNumber )
return theNumber * 5.0; // Multiply by 5

A silly little example, but it should serve to illustrate the main points of a function.

First, we have the function declaration. This consists of three parts: the optional return type (in this case, a "float", or floating-point number — a number which can have a decimal point, as opposed to an integer which must be a whole number), the function name, and the optional parameter list, which shows what kind of data the function expects, and gives it a (hopefully) descriptive name.

If your function doesn't return a value, leave off the return type. If it does return a value, the return type must indicate the type of data the function will return (we'll cover data types later on in this post). The function name is required (otherwise there's no way to call the function). The parentheses which enclose the parameter list must be present in every function declaration, even if you have no actual parameters.

Next we have the curly brackets which enclose the statements of the function itself. Curly brackets are used to group multiple statements into a larger unit. They are required to enclose the body of a function, even if the body consists of only one statement (as this one does).

And inside the curly brackets we have a statement which multiplies the parameter by 5, and returns that value. The statement ends with a semicolon. After the semicolon are two forward slashes (//) — this starts a comment. You can put anything you want in a comment, but usually comments are used to explain what's going on (either so that somebody else reading your script can understand what your intent is, or when you come back to modify your script months later, you can understand what your intent was).

And finally we have the closing curly bracket, which terminates the function declaration.

If we were to call this function from elsewhere in the script, it might look something like this:
float result = quintuple( 3.0 );
This line creates a new floating-point variable named "result", and assigns the return value of our "quintuple" function to it. Since we pass it the number 3.0 as the parameter, and the function multiplies the parameter by 5.0, we should end up with 15.0 in the variable named result.

Why would you want to use this as a function, rather than just multiplying values directly in your code? Well, you wouldn't; as I said, this was just a silly example. But if you have a case where you perform multiple actions on some data, and you have to perform these actions many times from different places in your code, you might turn the actions into a function. You can also improve the readability of complex code by moving logical groupings of instructions into their own functions.

And now a word about variables, and the types of data they can represent.

A variable is a way of storing values for later use. You can store seven types of values in LSL:

  • integer - a whole number, with no decimal point. (examples: 0, 1, -27, 42, 2147483647)

  • float - a floating-point number, or one with a decimal point. (examples: 0.0, 3.141592654, 17.6, -57.0)

  • string - text data. String values are enclosed in quotes when you define them. (examples: "Hello, Avatar!", "yes", "no", "I'm sorry, Dave, but I'm afraid I can't do that.", "1942"1)

  • key - A special kind of string, which represents something within Second Life. Key values are also enclosed in quotes when you define them. (example: "c541c47f-e0c0-058b-ad1a-d6ae3a4584d9")

  • vector - a three-part number which represents a 3D position in space. You can also use vectors to store Euler rotations, but we won't get into those quite yet. The components of a vector are each float numbers. (examples: <0,0,0>2, <17.8,32.5,16.0>)

  • rotation - similar to a vector, only with four components. These are used for quaternion rotations, and will make your brain hurt. (examples: <0,0,0,1>, <3.141592654,0.0,0.0,1.0>)

  • list - a special kind of data type which can contain zero or more elements of any other data type. Lists are signified by square brackets surrounding their elements, which are separated by commas. We'll get into lists much later. (examples: [0,1,2,3,4,5], ["Bob", "Mary", "Joe", "Sue"], [], [1, 3.141592654, "Isn't this fun?", <0,0,0>])

1. Even though 1942 is an integer, you can have a numeric value in a string. It's the quotes that make all the difference.

2. If a float number has nothing but zeroes after the decimal point, you can usually leave off the decimal point; LSL will usually promote it to a float. Usually. This is not, however, always the case; certain functions will produce errors if you pass an integer in a list where a float is expected.

Scripting: the basics, part 3

This script is one I wrote for a barrier which prohibits entry unless the barrier's owner touches or collides with it; when this happens, it will set itself to PHANTOM and turn partially transparent for five seconds, and then return to its previous opaque solidity.

It's also a good demonstration of how to modify prim parameters, set timers, and use multiple states.

default {
collision_start(integer total_number) {
if (llDetectedKey( 0 ) == llGetOwner()) state open;

touch_start(integer total_number) {
if (llDetectedKey( 0 ) == llGetOwner()) state open;

state open {
state_entry() {
llSetPrimitiveParams( [PRIM_PHANTOM,TRUE,
PRIM_COLOR,2,<1,1,1>,0.75] );
llSetTimerEvent( 5 );

timer() {
llSetPrimitiveParams( [PRIM_PHANTOM,FALSE,
PRIM_COLOR,2,<1,1,1>,1.0] );
llSetTimerEvent( 0 );
state default;

Okay, now the first line in this script is the default state. States are a way for the script to behave differently based on certain criteria; without states, we would have to use variables to keep track of things. In this script, I have two states: the default state (in which a touch or collision by the owner will "open" the barrier), and the "open" state, in which it opens itself, waits five seconds, and then closes again; in this state, further touches and collisions will do nothing.

We have two functions in the default state: collision_start and touch_start. These two functions are called, if they exist, when an avatar or object starts to collide with the prim containing the script, or when an avatar starts touching the prim containing the script.

These two functions are passed a parameter, an integer value indicating how many objects or avatars have started colliding with (or touching) the prim. This number is usually one (it's theoretically possible for two avatars to start colliding with or touching the prim at the exact same instant, but for the sake of simplicity we're going to assume that it's only one).

As it happens, I want both of these conditions to do exactly the same thing, so the code within each function is identical:

       if (llDetectedKey( 0 ) == llGetOwner()) state open;

llDetectedKey() gets the UUID of the object detected by the function. Index number 0, which we're using here, is the first object or avatar detected. (We could check the "total_number" variable, and if it's greater than one, we could loop through and check all of them, using llDetectedKey( 1 ), llDetectedKey( 2 ), and so on.)

The statement then compares the value returned by llDetectedKey() against the key of the person who owns the barrier, which is returned by the llGetOwner() function. If they're equal, then it switches into the state I've named "open".

Tip: The default state must occur first in the script. If you try to insert another state above default, the LSL compiler will produce a syntax error when you try to save the script.

The "open" state contains a function named state_entry. This gets called, if it exists, whenever the script enters the state the function is contained within. All states may have a state_entry function; like with all predefined functions, whichever one exists in the currently active state is the one that gets called.

The state_entry function calls llSetPrimitiveParams(). This is a function which can change nearly every feature of a prim, including what shape it is! It takes a list of things you want to change, and what you want to change them to. (The wiki has a great page describing all the things it can do.)

Here, we're changing the PRIM_PHANTOM parameter, and setting it to TRUE. I also decided to give a little visual feedback, so I'm setting the front and back faces of the barrier to partially transparent. With llSetPrimitiveParams(), you have to change both the color and the alpha setting simultaneously. Fortunately, I know that the faces should be completely white, so for each face I pass the PRIM_COLOR keyword (which tells llSetPrimitiveParams() what I want to change), the number of the first face to change, the RGB value <1,1,1> (white), and the alpha value of 0.75, (75% opaque). I then pass the same sequence again for the other face.

Tip: To find out what face you want to change, edit the prim and click on the "Select Texture" radio button in the edit dialog. You should see crosshairs appear on all faces of the prim. Click on the face you want to find the number for, and hit Ctrl-Alt-Shift-T. Your chat window will give you certain information about the texture on that face, including the face number.

After setting the prim parameters, I then set a timer event to trigger five seconds later (and subsequently in five-second intervals until the timer is killed), and that's the end of the state_entry function.

The next function is the timer function, which gets called when the timer event is triggered. In this function, I call llSetPrimitiveParams() again to reverse the changes I made earlier (I set the PRIM_PHANTOM parameter back to FALSE, and set the alpha on the front and back faces to 1.0 (fully opaque).

The next line kills the timer events (by passing a parameter of 0), and then the next line after that sets the script back into the default state again so that it will "listen" for collisions and touches again.