Master advanced LSL scripting techniques, learn professional optimization strategies, and build high-performance scripts that scale.
Welcome to the expert level of LSL scripting. This course is designed for experienced scripters who want to take their skills to a professional level.
In virtual worlds, script performance isn't just about your own experience—it affects everyone in the region. Poorly optimized scripts consume server resources and can cause lag for all users.
Before you can optimize, you need to measure. LSL provides llGetTimestamp() for high-precision timing.
// Basic Profiling Template
string startTime;
string endTime;
default
{
state_entry()
{
startTime = llGetTimestamp();
// Your code to profile goes here
integer i;
list myList = [];
for (i = 0; i < 1000; i++)
{
myList += [i];
}
endTime = llGetTimestamp();
float elapsed = (float)endTime - (float)startTime;
llOwnerSay("Operation took: " + (string)elapsed + " seconds");
llOwnerSay("Free Memory: " + (string)llGetFreeMemory() + " bytes");
}
}
Every LSL script has approximately 64KB of memory. This might seem small, but with proper management, it's sufficient for most tasks.
// BAD: Storing boolean as string
string isActive = "true";
// GOOD: Use integer for boolean
integer isActive = TRUE;
// BAD: Storing number as string
string myNumber = "42";
// GOOD: Use integer
integer myNumber = 42;
// BAD: Everything global
integer tempValue1;
integer tempValue2;
integer tempValue3;
default
{
touch_start(integer num)
{
tempValue1 = llDetectedKey(0);
// ... use once and never again
}
}
// GOOD: Use local variables
default
{
touch_start(integer num)
{
key toucherID = llDetectedKey(0);
// Local variable, freed after event ends
}
}
// BAD: Repeated string concatenation
string message = "";
integer i;
for (i = 0; i < 10; i++)
{
message += "Item " + (string)i + "\n"; // Creates new string each time!
}
// GOOD: Use a list and join once
list items = [];
integer i;
for (i = 0; i < 10; i++)
{
items += ["Item " + (string)i];
}
string message = llList2CSV(items); // Join once at the end
list bigList = [];
integer i;
for (i = 0; i < 100; i++)
{
bigList += [i];
}
// Process the list
// ...
// Free the memory!
bigList = [];
States are powerful but often overused. Here's when they make sense:
// Good use of states: Clearly distinct behaviors
default
{
state_entry()
{
llSetText("Door: CLOSED", <1,0,0>, 1.0);
}
touch_start(integer num)
{
llSetText("Door: OPENING", <1,1,0>, 1.0);
llSetPos(llGetPos() + <0,0,2>); // Move up
state open;
}
}
state open
{
state_entry()
{
llSetText("Door: OPEN", <0,1,0>, 1.0);
llSetTimerEvent(10.0); // Auto-close after 10 seconds
}
touch_start(integer num)
{
llSetText("Door: CLOSING", <1,1,0>, 1.0);
llSetPos(llGetPos() - <0,0,2>); // Move down
state default;
}
timer()
{
llSetText("Door: CLOSING", <1,1,0>, 1.0);
llSetPos(llGetPos() - <0,0,2>);
state default;
}
}
// Alternative: Single state with flags
// Better when states don't need different event handlers
integer STATUS_CLOSED = 0;
integer STATUS_OPEN = 1;
integer gStatus = STATUS_CLOSED;
closeDoor()
{
llSetText("Door: CLOSED", <1,0,0>, 1.0);
llSetPos(llGetPos() - <0,0,2>);
llSetTimerEvent(0); // Stop timer
gStatus = STATUS_CLOSED;
}
openDoor()
{
llSetText("Door: OPEN", <0,1,0>, 1.0);
llSetPos(llGetPos() + <0,0,2>);
llSetTimerEvent(10.0); // Auto-close
gStatus = STATUS_OPEN;
}
default
{
touch_start(integer num)
{
if (gStatus == STATUS_CLOSED)
{
openDoor();
}
else
{
closeDoor();
}
}
timer()
{
closeDoor();
}
}
Listeners are one of the most common sources of lag. Every active listener processes every chat message in range, even if the message isn't relevant.
// LESSON 4: Efficient Listener Management
// This vendor script demonstrates professional listener usage
string NOTECARD_NAME = "Product Info";
string NOTECARD_NAME = "Product Info";
integer LISTEN_CHANNEL = 123;
float LISTEN_TIMEOUT = 30.0;
integer gListenHandle = -1; // Store the handle globally
default
{
state_entry()
{
llSetText("Touch for Info", <1,1,1>, 1.0);
}
touch_start(integer total_number)
{
key avatarKey = llDetectedKey(0);
llSay(0, "Hello! To get your notecard, say '/123 buy' in chat.");
llOwnerSay("Listener activated for " + llKey2Name(avatarKey));
// CRITICAL OPTIMIZATION: Filter by speaker
// Only listen to the person who touched!
gListenHandle = llListen(LISTEN_CHANNEL, "", avatarKey, "buy");
// Set timeout to prevent eternal listeners
llSetTimerEvent(LISTEN_TIMEOUT);
}
listen(integer channel, string name, key id, string message)
{
llOwnerSay("Command received from " + name);
llGiveInventory(id, NOTECARD_NAME);
llSay(0, "Thank you! Info sent.");
// CRITICAL: Clean up immediately
llListenRemove(gListenHandle);
gListenHandle = -1;
llSetTimerEvent(0);
}
timer()
{
// Timeout: User didn't respond
llSetTimerEvent(0);
if (gListenHandle != -1)
{
llListenRemove(gListenHandle);
gListenHandle = -1;
}
llOwnerSay("Listener timed out.");
}
}
Like listeners, sensors consume resources. Use them efficiently:
// BAD: Continuous sensing when not needed
default
{
state_entry()
{
// This runs FOREVER, checking every 1 second!
llSensorRepeat("", NULL_KEY, AGENT, 20.0, PI, 1.0);
}
sensor(integer num)
{
llSay(0, "Found " + (string)num + " avatars");
}
}
// GOOD: Sense on demand
default
{
touch_start(integer num)
{
// Only sense when touched
llSensor("", NULL_KEY, AGENT, 20.0, PI, 0);
}
sensor(integer num)
{
llSay(0, "Found " + (string)num + " avatars");
// Sensor automatically stops after one scan
}
no_sensor()
{
llSay(0, "No avatars detected");
}
}
// BAD: Fast timer doing too much work
default
{
state_entry()
{
llSetTimerEvent(0.1); // 10 times per second!
}
timer()
{
// Heavy operation
llSensor("", NULL_KEY, AGENT, 96.0, PI, 0);
}
}
// GOOD: Appropriate timer interval
default
{
state_entry()
{
llSetTimerEvent(5.0); // Once every 5 seconds
}
timer()
{
// Same operation, much less frequent
llSensor("", NULL_KEY, AGENT, 96.0, PI, 0);
}
}
| Method | Best For | Performance | Readability |
|---|---|---|---|
| Custom Strings | Simple, single values | Fast | Poor |
| Lists | Ordered data, iteration | Medium | Good |
| JSON | Complex, nested data | Good | Excellent |
// BAD: Using chat for internal communication
// Root prim script
default
{
touch_start(integer num)
{
llSay(-99, "TURN_ON"); // Broadcasts to entire region!
}
}
// Child prim script
default
{
state_entry()
{
llListen(-99, "", NULL_KEY, "");
}
listen(integer chan, string name, key id, string msg)
{
if (msg == "TURN_ON")
{
// Turn on light
}
}
}
// GOOD: Using linked messages
// Root prim script
integer CMD_TURN_ON = 1001;
default
{
touch_start(integer num)
{
llMessageLinked(LINK_SET, CMD_TURN_ON, "", NULL_KEY);
}
}
// Child prim script
integer CMD_TURN_ON = 1001;
default
{
link_message(integer sender, integer num, string str, key id)
{
if (num == CMD_TURN_ON)
{
// Turn on light
}
}
}
Let's build a complete, scalable light system using JSON and linked messages.
// LSL-401: Professional Controller Script
// Place in root prim
// Command constants
integer CMD_TURN_ON = 1001;
integer CMD_TURN_OFF = 1002;
integer CMD_SET_COLOR = 1003;
integer CMD_SET_BRIGHTNESS = 1004;
default
{
state_entry()
{
llOwnerSay("Light Controller Ready");
llListen(0, "", llGetOwner(), "");
}
listen(integer chan, string name, key id, string msg)
{
msg = llToLower(llStringTrim(msg, STRING_TRIM));
if (msg == "on")
{
llMessageLinked(LINK_SET, CMD_TURN_ON, "", NULL_KEY);
llOwnerSay("Lights ON");
}
else if (msg == "off")
{
llMessageLinked(LINK_SET, CMD_TURN_OFF, "", NULL_KEY);
llOwnerSay("Lights OFF");
}
else if (llGetSubString(msg, 0, 5) == "color ")
{
string colorName = llGetSubString(msg, 6, -1);
vector color;
if (colorName == "red") color = <1,0,0>;
else if (colorName == "green") color = <0,1,0>;
else if (colorName == "blue") color = <0,0,1>;
else if (colorName == "white") color = <1,1,1>;
else if (colorName == "yellow") color = <1,1,0>;
else if (colorName == "purple") color = <1,0,1>;
else if (colorName == "cyan") color = <0,1,1>;
else
{
llOwnerSay("Unknown color. Try: red, green, blue, white, yellow, purple, cyan");
return;
}
// Package data as JSON
string jsonData = llList2Json(JSON_OBJECT, ["color", (string)color]);
llMessageLinked(LINK_SET, CMD_SET_COLOR, jsonData, NULL_KEY);
llOwnerSay("Color set to " + colorName);
}
else if (llGetSubString(msg, 0, 10) == "brightness ")
{
float brightness = (float)llGetSubString(msg, 11, -1);
if (brightness < 0.0) brightness = 0.0;
if (brightness > 1.0) brightness = 1.0;
string jsonData = llList2Json(JSON_OBJECT, ["brightness", (string)brightness]);
llMessageLinked(LINK_SET, CMD_SET_BRIGHTNESS, jsonData, NULL_KEY);
llOwnerSay("Brightness set to " + (string)((integer)(brightness * 100)) + "%");
}
}
}
// LSL-401: Professional Node Script
// Place in each child prim that should be a light
integer CMD_TURN_ON = 1001;
integer CMD_TURN_OFF = 1002;
integer CMD_SET_COLOR = 1003;
integer CMD_SET_BRIGHTNESS = 1004;
vector gColor = <1,1,1>;
float gBrightness = 1.0;
integer gIsOn = FALSE;
updateLight()
{
if (gIsOn)
{
llSetPrimitiveParams([
PRIM_COLOR, ALL_SIDES, gColor, 1.0,
PRIM_FULLBRIGHT, ALL_SIDES, TRUE,
PRIM_GLOW, ALL_SIDES, gBrightness * 0.3
]);
}
else
{
llSetPrimitiveParams([
PRIM_FULLBRIGHT, ALL_SIDES, FALSE,
PRIM_GLOW, ALL_SIDES, 0.0
]);
}
}
default
{
state_entry()
{
updateLight();
}
link_message(integer sender, integer num, string str, key id)
{
if (num == CMD_TURN_ON)
{
gIsOn = TRUE;
updateLight();
}
else if (num == CMD_TURN_OFF)
{
gIsOn = FALSE;
updateLight();
}
else if (num == CMD_SET_COLOR)
{
// Parse JSON data
if (llJsonValueType(str, ["color"]) != JSON_INVALID)
{
gColor = (vector)llJsonGetValue(str, ["color"]);
updateLight();
}
}
else if (num == CMD_SET_BRIGHTNESS)
{
if (llJsonValueType(str, ["brightness"]) != JSON_INVALID)
{
gBrightness = (float)llJsonGetValue(str, ["brightness"]);
updateLight();
}
}
}
}
Don't compute values until you need them.
// BAD: Computing everything upfront
vector gPosition;
vector gSize;
float gDistance;
default
{
state_entry()
{
gPosition = llGetPos();
gSize = llGetScale();
gDistance = llVecMag(gPosition - <128,128,0>);
// What if we never use these?
}
}
// GOOD: Compute on demand
default
{
touch_start(integer num)
{
// Only compute when needed
vector position = llGetPos();
float distance = llVecMag(position - <128,128,0>);
llOwnerSay("Distance: " + (string)distance);
}
}
// BAD: Repeated expensive calls
default
{
timer()
{
key owner = llGetOwner();
llInstantMessage(llGetOwner(), "Timer fired");
llGiveInventory(llGetOwner(), "Object");
// llGetOwner() called 3 times!
}
}
// GOOD: Cache the result
default
{
timer()
{
key owner = llGetOwner();
llInstantMessage(owner, "Timer fired");
llGiveInventory(owner, "Object");
// Cached, only called once
}
}
// BAD: Multiple llSetPrimitiveParams calls
default
{
touch_start(integer num)
{
llSetColor(<1,0,0>, ALL_SIDES);
llSetAlpha(0.5, ALL_SIDES);
llSetTexture("texture", ALL_SIDES);
// 3 separate server calls!
}
}
// GOOD: Single batched call
default
{
touch_start(integer num)
{
llSetPrimitiveParams([
PRIM_COLOR, ALL_SIDES, <1,0,0>, 0.5,
PRIM_TEXTURE, ALL_SIDES, "texture", <1,1,0>, <0,0,0>, 0.0
]);
// Single server call!
}
}
// BAD: Deep nesting
default
{
touch_start(integer num)
{
if (llDetectedKey(0) == llGetOwner())
{
if (llGetAttached() == 0)
{
if (llGetPermissions() & PERMISSION_TRIGGER_ANIMATION)
{
// Do something
}
}
}
}
}
// GOOD: Early exit
default
{
touch_start(integer num)
{
// Exit early if conditions not met
if (llDetectedKey(0) != llGetOwner()) return;
if (llGetAttached() != 0) return;
if (!(llGetPermissions() & PERMISSION_TRIGGER_ANIMATION)) return;
// Clean, readable main logic
llStartAnimation("wave");
}
}
Apply what you've learned with these practical challenges:
Task: Create a script that profiles list operations. Compare the performance of building a list with 1000 entries using += operator vs building it with proper list slicing.
Goal: Learn to measure and compare different approaches.
Use llGetTimestamp() before and after each operation. The difference will show which is faster.
Task: Write a script that monitors its own memory usage every 10 seconds. If free memory drops below 10,000 bytes, it should alert the owner on the DEBUG_CHANNEL.
Goal: Practice memory monitoring and timer management.
Use llGetFreeMemory() in a timer event. DEBUG_CHANNEL is a constant.
Task: Create a greeter that scans for avatars in a 10m radius when touched, says hello to the nearest one, then stops scanning.
Goal: Learn to use sensors efficiently (one-shot vs repeating).
Use llSensor() (not llSensorRepeat()) inside touch_start. The sensor event will fire once with results.
Task: Create a script that reads configuration from a notecard containing JSON. The notecard should have: {"channel": 25, "message": "Hello", "active": true}
Goal: Master JSON parsing and notecard reading.
Use llGetNotecardLine() in state_entry, handle it in dataserver event, then use llJsonGetValue() to extract values.
Task: Create a 2-prim object. Root prim sends commands ("on", "off", "burst") via linked messages. Child prim controls a particle system based on commands received.
Goal: Practice multi-script architecture and linked messages.
Use integer constants for commands (e.g., CMD_ON = 1, CMD_OFF = 2, CMD_BURST = 3). Child script uses llParticleSystem() in link_message event.
Task: Take a simple 3-state security orb (disabled, arming, armed) and refactor it to use a single state with integer flags instead.
Goal: Understand when states add value vs add complexity.
Create constants: STATUS_DISABLED = 0, STATUS_ARMING = 1, STATUS_ARMED = 2. Use a global variable gStatus and if/else blocks.
llGetTimestamp() - Returns high-precision UTC timestamp stringllGetFreeMemory() - Returns available script memory in bytesllGetUsedMemory() - Returns used script memory in bytesllGetSPMaxMemory() - Returns maximum memory available to scriptllListen(integer channel, string name, key id, string msg) - Opens a listener, returns handlellListenRemove(integer handle) - Removes a specific listenerllSetTimerEvent(float sec) - Starts/stops timer (0 = stop)llSensor() - Single sensor scanllSensorRepeat() - Repeating sensor scanllSensorRemove() - Stops repeating sensorllMessageLinked(integer link, integer num, string str, key id) - Sends message to scripts in linksetllRegionSay(integer channel, string msg) - Broadcasts to entire region on channelllOwnerSay(string msg) - Sends message to owner onlyllJsonSetValue(string json, list specifiers, string value) - Sets value in JSONllJsonGetValue(string json, list specifiers) - Gets value from JSONllJsonValueType(string json, list specifiers) - Returns type of JSON valuellList2Json(string type, list values) - Converts list to JSONllJson2List(string json) - Converts JSON to listllSetPrimitiveParams(list params) - Batches multiple prim parameter changesllGetPrimitiveParams(list params) - Batches multiple prim parameter readsCreate a complete vendor system that demonstrates all the techniques learned in this course:
You've completed the LSL Expert course on Optimization, Performance & Best Practices.
You now have the knowledge to write professional, efficient LSL scripts that respect server resources and provide excellent user experiences.