Adam Berger - Advanced expert with 20+ years of experience in virtual worlds
All classes take place in Alife Virtual World at our dedicated Alife Virtual School region
Explore the immersive 3D world of Alife Virtual - Your free virtual world alternative
High-resolution image (1920×1080 pixels) from Alife Virtual World School
Course Code: LSL-401 | The School of Creation
Difficulty Level: Expert
Duration: 10 Weeks
Prerequisites: LSL-301 (Advanced Scripting) and real-world scripting experience
Lead Instructor: Adam Berger
Format: Live in-world classes, self-paced web content, hands-on exercises
Cost: Absolutely FREE
Hello and welcome to LSL-401! I'm Adam Berger, your lead instructor for this course. With nearly two decades of experience building and scripting in virtual worlds, I've seen firsthand how the difference between a good script and a great script lies in its performance. A script that works is one thing; a script that works efficiently, respecting the shared resources of the simulator, is the mark of a true professional.
This course is designed for scripters who are already comfortable with the LSL syntax and have built complex projects. Now, we're going to peel back the layers and look under the hood. We'll move beyond just making things work and into the realm of making them work brilliantly. You'll learn to write code that is not only powerful but also lean, fast, and robust.
By the end of this 10-week course, you will be able to:
In a shared environment like Alife Virtual or Second Life, every script contributes to the overall performance of the region. A single poorly-written script can cause lag for dozens of people. As an expert scripter, you have the power not only to create amazing things but also to ensure the virtual world remains a smooth and enjoyable experience for everyone. These skills are what separate hobbyists from professional developers and are highly sought after for large-scale projects.
This is an expert-level course. It is essential that you have completed LSL-301: Advanced Scripting or have equivalent, demonstrable experience. You should be completely comfortable with: complex flow control (loops, if/else), lists, linked messages, all common event handlers, and have built multi-script objects before. We will be building on this foundation, not reviewing it.
To optimize a script, you must first understand how the simulator runs it. LSL is single-threaded and event-driven. This means a script can only do one thing at a time, and it only does something in response to an event (like `touch_start`, `timer`, or `listen`).
When an event is triggered, it's placed in the script's event queue. The simulator gives each script a tiny slice of time, called a time slice, to execute the code within that event. In OpenSim, this is typically around 20 milliseconds (0.02 seconds) per event. If your code takes longer than that to run, the event will be aborted and you'll get a "Script run-time error," or worse, it will be forcefully paused and resumed later, causing unpredictable behavior and lag.
The single most important metric for performance is Script Time. This is the amount of real-world time the simulator spends executing your script's code, averaged over several seconds. You can see this value in the "Script Info" window (Right-Click object -> Edit -> Tools -> Script Info). Our goal as optimizers is to keep this number as low as possible, ideally under 0.010ms for most scripts.
You can't optimize what you can't measure. Let's build a simple tool to measure the execution time of any event. We do this by getting a high-precision timestamp at the beginning and end of the event and calculating the difference.
// LSL-401: Profiler Script
// This script demonstrates how to measure the execution time of an event.
// Drop this into an object to test its performance.
// The llGetTimestamp() function returns a high-resolution timestamp as a string.
// We must cast it to a float to perform calculations.
default
{
touch_start(integer total_number)
{
// Get the timestamp at the very beginning of the event.
float startTime = (float)llGetTimestamp();
// --- YOUR CODE TO BE PROFILED GOES HERE ---
// For this example, we'll run a simple but computationally
// expensive loop to simulate a "laggy" operation.
integer i;
for (i = 0; i < 5000; ++i)
{
// This loop does nothing useful, but it takes time to execute,
// which is what we want to measure.
}
// --- END OF PROFILED CODE ---
// Get the timestamp at the very end of the event.
float endTime = (float)llGetTimestamp();
// Calculate the difference. The result is in seconds.
float executionTime = endTime - startTime;
// To display the result in a more readable format (microseconds),
// we multiply by 1,000,000.
// 1 second = 1,000,000 microseconds (µs)
float executionTimeMicroseconds = executionTime * 1000000.0;
// Report the result to the owner. llOwnerSay is our primary debugging tool.
llOwnerSay("Touch event executed in: " + (string)executionTimeMicroseconds + " µs");
}
}
States are a powerful organizational tool in LSL, but they are not free. Every time a script executes a `state NewState;` command, the simulator has to:
Furthermore, every script in OpenSim is limited to 64KB of memory. This includes the script text itself, plus memory for all global and local variables at runtime. Long strings, large lists, and numerous global variables can quickly consume this memory, leading to a "Stack-Heap Collision" error, which crashes the script. Efficient memory use is paramount.
Let's compare two ways to script a simple rotating door. The first uses two states (`closed` and `open`). The second uses a single state with a boolean (integer) flag.
// LSL-401: Inefficient Multi-State Door
// This version uses two states, which is less performant for simple logic.
rotation ROT_CLOSED = ZERO_ROTATION;
rotation ROT_OPEN;
default
{
state_entry()
{
// We calculate the open rotation once at the start.
ROT_OPEN = llGetRot() * llEuler2Rot(<0, 0, 90.0> * DEG_TO_RAD);
llSetRot(ROT_CLOSED); // Ensure door is closed.
state closed; // Immediately jump to the 'closed' state.
}
}
state closed
{
state_entry()
{
// This event runs every time we enter this state.
llSetRot(ROT_CLOSED);
}
touch_start(integer total_number)
{
// On touch, we change to the 'open' state.
// This is a "costly" operation.
state open;
}
}
state open
{
state_entry()
{
// This event also runs every time we enter this state.
llSetRot(ROT_OPEN);
// We use a timer to close the door automatically.
llSetTimerEvent(3.0);
}
touch_start(integer total_number)
{
// On touch, we change back to the 'closed' state.
state closed;
}
timer()
{
llSetTimerEvent(0); // Always turn off the timer when done.
state closed; // Change state again.
}
}
// LSL-401: Optimized Single-State Door
// This version uses one state and a flag variable, which is much more efficient.
rotation ROT_CLOSED = ZERO_ROTATION;
rotation ROT_OPEN;
integer gIsOpen = FALSE; // Our flag variable. FALSE is 0, TRUE is 1.
default
{
state_entry()
{
// Calculate the open rotation once.
ROT_OPEN = llGetRot() * llEuler2Rot(<0, 0, 90.0> * DEG_TO_RAD);
llSetRot(ROT_CLOSED);
gIsOpen = FALSE;
}
touch_start(integer total_number)
{
// Instead of changing state, we just check our flag variable.
if (gIsOpen == FALSE)
{
// Open the door
gIsOpen = TRUE;
llSetRot(ROT_OPEN);
llSetTimerEvent(3.0); // Set a timer to auto-close.
}
else // if (gIsOpen == TRUE)
{
// Close the door
gIsOpen = FALSE;
llSetRot(ROT_CLOSED);
llSetTimerEvent(0); // Turn off the timer.
}
}
timer()
{
// The timer simply closes the door if it's open.
llSetTimerEvent(0); // Turn off the timer.
if (gIsOpen == TRUE)
{
gIsOpen = FALSE;
llSetRot(ROT_CLOSED);
}
}
}
By avoiding `state` changes, the second script uses significantly less script time for each interaction.
Some of the biggest sources of region-wide lag are scripts that are constantly "listening" for something when they don't need to be. The three main culprits are:
The key to optimization is to only activate these events when they are absolutely needed, and deactivate them immediately afterward.
Let's create a vendor that gives a notecard. A naive approach would be to have an `llListen` active all the time. A professional approach is to activate the listener only when a customer touches the vendor, and turn it off after a short time.
// LSL-401: Smart Vendor with On-Demand Listener
// This script only listens for commands for a short period after being touched.
// --- CONFIGURATION ---
string NOTECARD_NAME = "My Product Info"; // Name of the notecard in inventory.
integer LISTEN_CHANNEL = 123; // The channel to listen on.
float LISTEN_TIMEOUT = 30.0; // Seconds to wait for a command before turning listener off.
// --- END CONFIGURATION ---
default
{
state_entry()
{
// On startup, we are not listening. The script is idle and using zero time.
llSetText("Touch for Info", <1,1,1>, 1.0);
}
touch_start(integer total_number)
{
// When a user touches the vendor, we activate the listener.
key avatar_key = llDetectedKey(0); // Get the key of the user who touched.
// Announce options in local chat.
llSay(0, "Hello! To get your notecard, please say '/123 buy' in chat.");
llOwnerSay("Activated listener for " + llKey2Name(avatar_key) + " for " + (string)LISTEN_TIMEOUT + " seconds.");
// Activate the listener, but ONLY for the person who touched.
// This is a critical optimization!
integer handle = llListen(LISTEN_CHANNEL, "", avatar_key, "buy");
// Set a timer to turn the listener off automatically.
// This prevents the listener from staying on forever if the user walks away.
llSetTimerEvent(LISTEN_TIMEOUT);
}
listen(integer channel, string name, key id, string message)
{
// We received the 'buy' command.
llOwnerSay("Received command from " + name);
// Give the notecard.
llGiveInventory(id, NOTECARD_NAME);
llSay(0, "Thank you, " + name + "! I've sent you the info card.");
// IMPORTANT: We are done. Turn off the listener and the timer immediately.
llListenRemove(llGetListenHandle()); // Best practice to remove by handle if you stored it.
llSetTimerEvent(0); // Stop the timeout timer.
llSetText("Touch for Info", <1,1,1>, 1.0);
}
timer()
{
// If this timer event fires, it means the user did not say the command in time.
llSetTimerEvent(0); // Turn off the timer.
llListenRemove(llGetListenHandle()); // Turn off the listener.
llOwnerSay("Listener timed out. Deactivating.");
llSay(0, "Timed out. Please touch me again to restart.");
llSetText("Touch for Info", <1,1,1>, 1.0);
}
}
How you store and transmit data between scripts has a huge impact on performance.
Let's build a professional, multi-prim light system. A single "Controller" script in the root prim will send commands to multiple "Node" scripts in the child prims using `llMessageLinked` and JSON. This pattern is scalable and extremely efficient.
// LSL-401: Controller Script
// Place this in the root prim of a linked object.
// It will send commands to 'Node' scripts in the child prims.
// Command Constants for clarity and easy maintenance
integer CMD_TURN_ON = 1001;
integer CMD_TURN_OFF = 1002;
integer CMD_SET_COLOR = 1003;
default
{
state_entry()
{
llOwnerSay("Controller ready. Touch to cycle lights.");
}
touch_start(integer total_number)
{
// On touch, we'll demonstrate sending different commands.
// We use llFrand to pick a random action.
float choice = llFrand(3.0);
if (choice < 1.0)
{
// --- Turn all lights ON ---
llOwnerSay("Sending command: TURN ON");
// The string parameter is empty as the command is in the integer parameter.
llMessageLinked(LINK_SET, CMD_TURN_ON, "", NULL_KEY);
}
else if (choice < 2.0)
{
// --- Turn all lights OFF ---
llOwnerSay("Sending command: TURN OFF");
llMessageLinked(LINK_SET, CMD_TURN_OFF, "", NULL_KEY);
}
else
{
// --- Set all lights to a random color using JSON ---
// Create a random color vector
vector randomColor = <llFrand(1.0), llFrand(1.0), llFrand(1.0)>;
llOwnerSay("Sending command: SET COLOR to " + (string)randomColor);
// Package the data into a JSON string.
// This is clean and easily extensible if we need to send more data later.
string jsonData = "[]"; // Start with an empty JSON array/object.
jsonData = llJsonSetValue(jsonData, ["color"], (string)randomColor);
// Send the command and the JSON data.
llMessageLinked(LINK_SET, CMD_SET_COLOR, jsonData, NULL_KEY);
}
}
}
// LSL-401: Node Script
// Place this in one or more child prims.
// It listens for commands from the 'Controller' script.
// Command Constants - must match the controller!
integer CMD_TURN_ON = 1001;
integer CMD_TURN_OFF = 1002;
integer CMD_SET_COLOR = 1003;
// Turn the light on.
setLightOn()
{
// Make prim full bright and set glow.
llSetPrimitiveParams([ PRIM_FULLBRIGHT, ALL_SIDES, TRUE, PRIM_GLOW, ALL_SIDES, 0.3 ]);
}
// Turn the light off.
setLightOff()
{
// Turn off full bright and glow.
llSetPrimitiveParams([ PRIM_FULLBRIGHT, ALL_SIDES, FALSE, PRIM_GLOW, ALL_SIDES, 0.0 ]);
}
default
{
state_entry()
{
// Initialize the light to an off state.
setLightOff();
// The script is now idle, waiting for a linked message.
}
link_message(integer sender_num, integer num, string str, key id)
{
// This event only fires when a linked message is received.
// It's extremely efficient.
if (num == CMD_TURN_ON)
{
setLightOn();
}
else if (num == CMD_TURN_OFF)
{
setLightOff();
}
else if (num == CMD_SET_COLOR)
{
// The command is to set color. The color data is in the 'str' parameter as JSON.
// First, validate the JSON.
if (llJsonValueType(str, ["color"]) != JSON_INVALID)
{
// Extract the color vector from the JSON string.
vector newColor = (vector)llJsonGetValue(str, ["color"]);
// Apply the color and turn the light on.
llSetColor(newColor, ALL_SIDES);
setLightOn();
}
}
}
}
Apply your knowledge! Create scripts to perform the following tasks. Focus on efficiency and best practices.