Introduction
In this tutorial, you will learn the basics of Hitoshina - Language (HL) by building a Counter HUD for use in Second Life.
What You Will Build
The completed HUD will have the following features:
- + button to increment the counter by 1
- - button to decrement the counter by 1
- Menu button to display a dialog menu for resetting or viewing the current value
It is a simple HUD, but it covers all the major concepts in HL.
What You Will Learn
| Chapter | Concepts |
|---|---|
| Project Setup | script declaration, #init event, sending messages |
| HUD Structure | prim definition, properties |
| Touch Handling | #touch_start event, prims groups |
| Messaging | -> operator, link messages |
| State Management & Dialogs | context, #listen, dialog syntax |
Prerequisites
- Basic Second Life operations (creating objects, attaching HUDs)
- A text editor (VS Code, Neovim, etc.)
- The HL compiler (
hl) installed
For compiler installation, refer to the download page.
Project Setup
Start by creating an HL project, writing your first script, and compiling it.
Creating the Project
Run the following command in your terminal to create the project:
hl new counter_hud
This generates the following directory structure:
counter_hud/
├── lib/ # Library files (.hlib)
├── prc/ # Procedure library files (.hprc)
├── msg/ # Message handler files (.hmsg)
├── output/ # Output files (.lsl)
├── tests/ # Test files (.hltest)
└── main.hl # Main script (initial template)
lib/— holds.hlibfiles of reusable functions and constantsprc/— holds.hprcfiles of reusable proceduresmsg/— holds.hmsgfiles that define message-receive handlersoutput/— the output directory for compiled LSL filestests/— holds unit tests (.hltestfiles)main.hl— the project’s entry point, generated with an initial template ("Hello, World!" -> world;)
Your First Script
Open the generated counter_hud/main.hl and replace its contents with the following:
script main;
main #init {
"Counter HUD Ready" -> owner;
}
This code has three parts:
script main;— declares a script namedmain. Compiling it producesoutput/main.lslmain #init { ... }— the script’s initialization event, which runs when the script is loaded or reset in Second Life"Counter HUD Ready" -> owner;— sends a message to the owner (yourself)
Compiling and Running
Run the compiler from inside the project directory:
hl main.hl
output/main.lsl is generated. Drop this LSL file into a Second Life object and test it.
If “Counter HUD Ready” appears in chat, you’re all set.
Message Sending Basics
In HL, the -> operator sends messages. Several destinations are available:
"Hello" -> owner; // Send to owner only
"Hello" -> world; // Broadcast to nearby area (chat)
"Hello" -> debug; // Send to the debug channel
Using -> owner during HUD development is convenient because only you can see the messages.
HUD Structure
Set up prim definitions and properties so that the HUD buttons can be controlled from HL code.
Preparing the Object in Second Life
Create the following in Second Life:
- Create one root prim (the HUD body)
- Link three child prims to it (the +, -, and Menu buttons)
- Set each child prim’s Name to
btn_plus,btn_minus, andbtn_menurespectively
Important: HL
primidentifiers must match the prim names in Second Life. Touch events are dispatched to the correct handler based on the prim name.
Prim Definitions
Add prim declarations to main.hl. A prim represents a child prim in the linkset:
script main;
prim btn_plus, btn_minus, btn_menu;
main #init {
"Counter HUD Ready" -> owner;
}
prim btn_plus, btn_minus, btn_menu; declares three prims whose names must match the corresponding Second Life prim names.
Setting Properties
To use prim methods such as set_color, the script must cache link numbers at startup. Add the #cache_linknum property:
script main;
prim btn_plus, btn_minus, btn_menu;
main #cache_linknum;
main #init {
"Counter HUD Ready" -> owner;
}
Since a HUD is worn as an attachment, #init should also run when it is attached. Add the #attachable property:
script main;
prim btn_plus, btn_minus, btn_menu;
main #cache_linknum;
main #attachable;
main #init {
"Counter HUD Ready" -> owner;
}
Property Reference
| Property | Effect |
|---|---|
#cache_linknum | Scans the linkset at startup, matches prim names against prim identifiers, and records link numbers. Required for prim method calls such as set_color, name(), and desc() |
#attachable | Re-runs #init (and #animable if set) when attached to an avatar |
The code is now ready to control the button prims.
Touch Handling
Implement the behavior for when HUD buttons are touched.
The touch_start Event
In HL, the #touch_start event detects when a prim is touched. Event handlers are written in the form <prim-name> #touch_start @<script-name>:
script main;
prim btn_plus, btn_minus, btn_menu;
main #cache_linknum;
main #attachable;
main #init {
"Counter HUD Ready" -> owner;
}
btn_plus #touch_start @main |prim obj| {
"Plus tapped" -> owner;
}
btn_minus #touch_start @main |prim obj| {
"Minus tapped" -> owner;
}
btn_menu #touch_start @main |prim obj| {
"Menu tapped" -> owner;
}
Elements of each event handler:
btn_plus— the prim to detect touches on#touch_start— the touch-start event@main— the script that handles this event|prim obj|— parameter that receives information about the touched prim
Event Dispatch by Prim Name
HL automatically dispatches touch events to the correct handler by matching prim names against prim identifiers. Writing btn_plus #touch_start @main means the handler runs only when the prim named btn_plus is touched — no manual conditional branching needed.
prims Groups
When there are many buttons, you can use a prims group to handle them together. For example, if the prim names share a suffix of the same length:
prim btn_add, btn_sub, btn_cfg;
prims btn_*** = btn_add, btn_sub, btn_cfg;
btn_*** #touch_start @main |prim obj| {
obj.name() -> owner;
}
prims btn_*** = btn_add, btn_sub, btn_cfg; groups three prims together. The wildcard *** represents three characters and matches the part after btn_ (add, sub, cfg).
Wildcard rule: The number of
*characters must equal the length of the suffix. All members must have suffixes of the same length. In this tutorial,btn_plus(4),btn_minus(5), andbtn_menu(4) have suffixes of different lengths, so they cannot be grouped withprims.
Since each button has different logic and the suffix lengths are uneven, individual handlers are used here.
Code So Far
script main;
prim btn_plus, btn_minus, btn_menu;
main #cache_linknum;
main #attachable;
main #init {
"Counter HUD Ready" -> owner;
}
btn_plus #touch_start @main |prim obj| {
"Plus tapped" -> owner;
}
btn_minus #touch_start @main |prim obj| {
"Minus tapped" -> owner;
}
btn_menu #touch_start @main |prim obj| {
"Menu tapped" -> owner;
}
Counter increment and decrement are not implemented yet. After learning about messaging in the next chapter, they will be completed together with state management.
Compile the script, add it to your HUD, and confirm that touching each button displays the corresponding message.
Messaging
Let’s take a closer look at HL’s message sending feature.
Message Sending Options
The -> operator in HL supports multiple destinations:
// Built-in targets
"msg" -> owner; // Visible to owner only (llOwnerSay)
"msg" -> world; // Broadcast to nearby area (llSay)
"msg" -> debug; // Debug channel (llDebugSay)
// Inter-script communication (link messages)
"msg":100 -> sub; // Send to the sub script on channel 100
Link Messages
In objects with multiple scripts, link messages are used for inter-script communication. As a HUD grows larger, splitting logic across scripts becomes common practice.
Sender:
"update":100 -> sub;
The receiver listens with the #linkmsg event:
script sub;
sub #linkmsg :100 |str msg| {
msg -> owner;
}
:100 is the channel number. Using the same number on both sides filters messages to only those on that channel.
Choosing Channels
Link message channels can be freely assigned, but organizing them by purpose makes the code easier to maintain:
const int CH_UPDATE = 100; // For display updates
const int CH_COMMAND = 200; // For sending commands
Constants defined with const can be used anywhere in the code.
In the Counter HUD
This counter HUD fits within a single script, so link messages are not used here. However, they are an important concept when a HUD grows more complex — for example, when display control is split into a separate script.
The next chapter introduces state management with context and implements counter increment and decrement.
State Management & Dialogs
Implement counter value storage and a menu dialog triggered by the Menu button.
State Management with context
In HL, context is used to manage script state. Variables declared inside a context retain their values across event handler calls:
context @main {
int count = 0;
}
context @main is the state associated with the main script. int count = 0; sets the initial value of the counter to 0.
To access the context in an event handler, add context to the parameter list:
main #init |context ctx| {
ctx.count = 0;
"Counter HUD Ready: " + ctx.count.to_str() -> owner;
}
Implementing the Counter
Using context, complete the code so that button touches increment and decrement the count:
script main;
prim btn_plus, btn_minus, btn_menu;
main #cache_linknum;
main #attachable;
context @main {
int count = 0;
}
main #init |context ctx| {
ctx.count = 0;
"Counter HUD Ready" -> owner;
}
btn_plus #touch_start @main |prim obj, context ctx| {
ctx.count += 1;
"Count: " + ctx.count.to_str() -> owner;
}
btn_minus #touch_start @main |prim obj, context ctx| {
ctx.count -= 1;
"Count: " + ctx.count.to_str() -> owner;
}
Multiple parameters can be received as in |prim obj, context ctx|. ctx.count.to_str() converts the integer to a string.
Displaying a Dialog
Show a dialog when the Menu button is touched. The send and receive sides have to share the same channel number, so use the const keyword introduced in chapter 5 to give that number a name. Sprinkling the same literal in multiple places is a classic source of mismatches when only one side is updated.
Use a negative channel for dialog responses. Avatars can only chat on channel 0 and positive channels through the normal chat bar, so negative channels stay free of user input and won’t get triggered by stray messages.
const int CH_DIALOG = -101;
HL has dedicated syntax for dialogs that lets you visually arrange button layouts directly in code:
btn_menu #touch_start @main |prim obj, context ctx| {
"Counter: " + ctx.count.to_str()
|"Reset", "Close"|
@owner:CH_DIALOG -> world;
}
Elements of this syntax:
"Counter: " + ctx.count.to_str()— the dialog message|"Reset", "Close"|— one row of buttons (displayed in the order written in source)@owner:CH_DIALOG— the target (owner) and channel (CH_DIALOG = -101) for receiving the dialog response-> world;— sends the dialog (fixed syntax)
Button order: LSL dialogs display buttons in reverse order, but HL automatically reverses the list at compile time. The order written in source is the order shown in the dialog.
Handling Dialog Responses with the listen Event
When a dialog button is pressed, a message arrives on the specified channel (CH_DIALOG). Use the #listen event to receive it:
main #listen :CH_DIALOG + owner |str msg, context ctx| {
switch msg {
"Reset" => {
ctx.count = 0;
"Counter reset" -> owner;
}
"Close" => "Closed" -> owner;
}
}
:CH_DIALOG— listens for messages on theCH_DIALOGchannel+ owner— accepts only messages from the owner (security measure)|str msg, context ctx|— receives the message and context
Full Code So Far
script main;
prim btn_plus, btn_minus, btn_menu;
const int CH_DIALOG = -101;
main #cache_linknum;
main #attachable;
context @main {
int count = 0;
}
main #init |context ctx| {
ctx.count = 0;
"Counter HUD Ready" -> owner;
}
btn_plus #touch_start @main |prim obj, context ctx| {
ctx.count += 1;
"Count: " + ctx.count.to_str() -> owner;
}
btn_minus #touch_start @main |prim obj, context ctx| {
ctx.count -= 1;
"Count: " + ctx.count.to_str() -> owner;
}
btn_menu #touch_start @main |prim obj, context ctx| {
"Counter: " + ctx.count.to_str()
|"Reset", "Close"|
@owner:CH_DIALOG -> world;
}
main #listen :CH_DIALOG + owner |str msg, context ctx| {
switch msg {
"Reset" => {
ctx.count = 0;
"Counter reset" -> owner;
}
"Close" => "Closed" -> owner;
}
}
Compile the script and add it to your HUD. Confirm that the +/- buttons change the count and that the Menu button opens a dialog.
Showing the Prim Description
Up to this point, the |prim obj| parameter on #touch_start has only been declared — never read. In this chapter we will use obj.name() and obj.desc() to fetch information about the touched prim and display values configured on the Second Life side.
Second Life Setup
Set the Description field of each button to any string you like. From the Second Life edit panel:
- Wear or rez the HUD.
- Open edit mode and select
btn_plus,btn_minus, andbtn_menuone by one. - Enter the following into the Description field on the General tab.
| Prim name | Description |
|---|---|
btn_plus | Increment counter |
btn_minus | Decrement counter |
btn_menu | Open menu |
obj.name() and obj.desc()
|prim obj| is the prim instance passed to the touch_start handler. obj.name() returns the identifier name and obj.desc() returns the Description:
btn_plus #touch_start @main |prim obj, context ctx| {
obj.name() + ": " + obj.desc() -> owner;
ctx.count += 1;
"Count: " + ctx.count.to_str() -> owner;
}
Touching the button now produces output like:
btn_plus: Increment counter
Count: 1
Full Code So Far
script main;
prim btn_plus, btn_minus, btn_menu;
const int CH_DIALOG = -101;
main #cache_linknum;
main #attachable;
context @main {
int count = 0;
}
main #init |context ctx| {
ctx.count = 0;
"Counter HUD Ready" -> owner;
}
btn_plus #touch_start @main |prim obj, context ctx| {
obj.name() + ": " + obj.desc() -> owner;
ctx.count += 1;
"Count: " + ctx.count.to_str() -> owner;
}
btn_minus #touch_start @main |prim obj, context ctx| {
obj.name() + ": " + obj.desc() -> owner;
ctx.count -= 1;
"Count: " + ctx.count.to_str() -> owner;
}
btn_menu #touch_start @main |prim obj, context ctx| {
obj.name() + ": " + obj.desc() -> owner;
"Counter: " + ctx.count.to_str()
|"Reset", "Close"|
@owner:CH_DIALOG -> world;
}
main #listen :CH_DIALOG + owner |str msg, context ctx| {
switch msg {
"Reset" => {
ctx.count = 0;
"Counter reset" -> owner;
}
"Close" => "Closed" -> owner;
}
}
Because the Description can be edited from Second Life without recompiling the LSL, this technique is also useful for swapping per-button labels or configuration values after deployment.
Color That Reflects State
Switch the color of the Menu button based on the counter value held in context. The set_color method changes a prim’s color dynamically.
The set_color Method
Set a prim’s color with #<prim-id>.set_color|<face>, <color>, <alpha>|.
| Argument | Type | Description |
|---|---|---|
| face | int | Face index. std::ALL_SIDES selects every face. |
| color | vec | <R, G, B> (each component in 0.0..1.0). |
| alpha | float | 0.0 (transparent) to 1.0 (opaque). |
std::ALL_SIDES is a constant from the standard library, so add import standard as std; at the top of the script.
Branching on the Counter Value
Switch to green for positive values, red for negative values, and white for zero. Use if / elif / else:
if 0 < ctx.count {
#btn_menu.set_color|std::ALL_SIDES, <0.0, 1.0, 0.0>, 1.0|; // green
} elif ctx.count < 0 {
#btn_menu.set_color|std::ALL_SIDES, <1.0, 0.0, 0.0>, 1.0|; // red
} else {
#btn_menu.set_color|std::ALL_SIDES, <1.0, 1.0, 1.0>, 1.0|; // white
}
Comparison operators: As a general coding convention this codebase uses only
</<=. Place the smaller value on the left side, e.g.0 < ctx.count.
Extracting a Procedure
The counter changes in four places: #init, btn_plus, btn_minus, and the Reset branch in #listen. Repeating the same if / elif / else block in each spot would be noisy, so extract the color-update logic into a procedure.
A procedure is defined with #<name> |<params>| { ... } and called with #<name>|<args>|;. By accepting the prim to color as a prim parameter, the same procedure can be reused for any button:
#update_color |prim target, int count| {
if 0 < count {
#target.set_color|std::ALL_SIDES, <0.0, 1.0, 0.0>, 1.0|;
} elif count < 0 {
#target.set_color|std::ALL_SIDES, <1.0, 0.0, 0.0>, 1.0|;
} else {
#target.set_color|std::ALL_SIDES, <1.0, 1.0, 1.0>, 1.0|;
}
}
Notes:
- A
primparameter such astargetis invoked the same way as a static prim identifier — with the#prefix:#target.set_color|...|;. - Call sites pass the target prim as a plain identifier (without
#):#update_color|btn_menu, ctx.count|;.
Full Code So Far
import standard as std;
script main;
prim btn_plus, btn_minus, btn_menu;
const int CH_DIALOG = -101;
main #cache_linknum;
main #attachable;
context @main {
int count = 0;
}
#update_color |prim target, int count| {
if 0 < count {
#target.set_color|std::ALL_SIDES, <0.0, 1.0, 0.0>, 1.0|;
} elif count < 0 {
#target.set_color|std::ALL_SIDES, <1.0, 0.0, 0.0>, 1.0|;
} else {
#target.set_color|std::ALL_SIDES, <1.0, 1.0, 1.0>, 1.0|;
}
}
main #init |context ctx| {
ctx.count = 0;
#update_color|btn_menu, ctx.count|;
"Counter HUD Ready" -> owner;
}
btn_plus #touch_start @main |prim obj, context ctx| {
obj.name() + ": " + obj.desc() -> owner;
ctx.count += 1;
#update_color|btn_menu, ctx.count|;
"Count: " + ctx.count.to_str() -> owner;
}
btn_minus #touch_start @main |prim obj, context ctx| {
obj.name() + ": " + obj.desc() -> owner;
ctx.count -= 1;
#update_color|btn_menu, ctx.count|;
"Count: " + ctx.count.to_str() -> owner;
}
btn_menu #touch_start @main |prim obj, context ctx| {
obj.name() + ": " + obj.desc() -> owner;
"Counter: " + ctx.count.to_str()
|"Reset", "Close"|
@owner:CH_DIALOG -> world;
}
main #listen :CH_DIALOG + owner |str msg, context ctx| {
switch msg {
"Reset" => {
ctx.count = 0;
#update_color|btn_menu, ctx.count|;
"Counter reset" -> owner;
}
"Close" => "Closed" -> owner;
}
}
Compile, install the HUD, and use the +/- buttons. The Menu button cycles through green ↔ white ↔ red — confirming that #cache_linknum is now actually doing the work that set_color requires.
Completion & Summary
The Counter HUD is complete. Here is a summary of the concepts covered so far.
Final Code
import standard as std;
script main;
prim btn_plus, btn_minus, btn_menu;
const int CH_DIALOG = -101;
main #cache_linknum;
main #attachable;
context @main {
int count = 0;
}
#update_color |prim target, int count| {
if 0 < count {
#target.set_color|std::ALL_SIDES, <0.0, 1.0, 0.0>, 1.0|;
} elif count < 0 {
#target.set_color|std::ALL_SIDES, <1.0, 0.0, 0.0>, 1.0|;
} else {
#target.set_color|std::ALL_SIDES, <1.0, 1.0, 1.0>, 1.0|;
}
}
main #init |context ctx| {
ctx.count = 0;
#update_color|btn_menu, ctx.count|;
"Counter HUD Ready" -> owner;
}
btn_plus #touch_start @main |prim obj, context ctx| {
obj.name() + ": " + obj.desc() -> owner;
ctx.count += 1;
#update_color|btn_menu, ctx.count|;
"Count: " + ctx.count.to_str() -> owner;
}
btn_minus #touch_start @main |prim obj, context ctx| {
obj.name() + ": " + obj.desc() -> owner;
ctx.count -= 1;
#update_color|btn_menu, ctx.count|;
"Count: " + ctx.count.to_str() -> owner;
}
btn_menu #touch_start @main |prim obj, context ctx| {
obj.name() + ": " + obj.desc() -> owner;
"Counter: " + ctx.count.to_str()
|"Reset", "Close"|
@owner:CH_DIALOG -> world;
}
main #listen :CH_DIALOG + owner |str msg, context ctx| {
switch msg {
"Reset" => {
ctx.count = 0;
#update_color|btn_menu, ctx.count|;
"Counter reset" -> owner;
}
"Close" => "Closed" -> owner;
}
}
Concepts Learned
| Concept | HL Syntax | Description |
|---|---|---|
| Script declaration | script main; | The unit of LSL output file |
| Import | import standard as std; | Loading the standard library |
| Constants | const int CH_DIALOG = -101; | Naming an intent so the literal isn’t duplicated |
| Prim definition | prim btn_plus; | Identifies a prim in the linkset |
| Properties | #cache_linknum, #attachable | Script behavior configuration |
| Events | #init, #touch_start, #listen | Responding to Second Life events |
| Message sending | "msg" -> owner; | Sending to various targets |
| State management | context @main { int count = 0; } | Variables shared across events |
| Dialogs | "msg" |"btn"| @owner:ch -> world; | Interactive menu display |
| Control flow | if / elif / else, switch msg { "val" => ...; } | Branching by condition or pattern |
| Procedures | #name |args| { ... }, #name|args|; | Reusable side-effect blocks |
| Prim methods | obj.name(), obj.desc(), #prim.set_color|...| | Reading prim info and updating appearance |
Next Steps
This tutorial covered the fundamentals of HL. To learn more, use the following resources:
- Language Reference — the complete specification for the type system, expressions, standard library, and more
- Playground — an in-browser editor to try HL code instantly
HL has many features not covered in this tutorial:
- Libraries (
.hlib) — bundle functions and constants into reusable modules - Procedures (
.hprc) — extract shared logic into separate files included viainclude - Message handlers (
.hmsg) — move link message handling into dedicated files - Typed lists (
ilist,slist, etc.) — type-safe list operations - map/fold expressions — transform and aggregate collections
- Tests (
.hltest) — write unit tests for your code
Using these features, you can build larger and more maintainable projects.