22 April 2013
19 April 2013
31 March 2013
@wait
command is now @pause
.30 March 2013
times
parameter.The NPC puppeteer can be used to script (in the sense of playwriting) an NPC to move around and perform actions. The LSL script translates an easy-to-grasp semantics from a notecard and then makes the NPC act on them. This can be used, for example, to set-up "real" NPCs for games or simulations where you need a true NPC to move around and act out a "script".
Soundtrack: Oldtimer - Amiga Game Soundtrack.
The NPC is controlled by a script in the inventory of a primitive. The puppeteer NPC script requires the following items to be present in the same inventory as the script:
[WaS-K] NPC Puppeteer
- the script found in the Code section.appearance
- this can be generated with the appearance generator script.Script
- based on the Scriptwriting section (see that section for a working example).Stand
- this animation will be played by the NPC while it is idle (stand, wait, etc…)Walk
- this animation will be played by the NPC while it walks.This is how a puppeteer primitive should look like:
as you can observe, all the required components are placed inside a box. One may, of course, make the primitive transparent or turn it into an object that will blend-in with the rest of the environment.
The appearance generator script can be used to generate an appearance
notecard for the puppeteer. That script copies the appearance of the avatar touching the primitive and writes it to a notecard called appearance
which is then used by this script. The example shown in the video uses an agent called Alter Ego
, however if that agent would be wearing an animal costume, the puppeteer could be used to create very functional animals, as opposed to prim-only alternatives using the OpenSim wanderer script.
The script is supplied as a notecard named Script
and dumped in the inventory of the controlling primitive. It uses the following syntax:
Command | First Parameter | Second Parameter | Description |
---|---|---|---|
@spawn | name (string) | location (vector) | Rezzes an NPC with name at a location . |
@walk | destination (vector) | NaN | Makes the NPC walk to destination . |
@say | phrase (string) | NaN | Makes the NPC speak a phrase . |
@pause | animation cycles (integer) | NaN | Makes the NPC wait for a multiple of standing animation cycles . |
@wander | radius (integer) | cycles (integer) | Makes the NPC wander in radius , for cycles cycles. |
@stop | NaN | NaN | Halts the NPC script indefinitely. |
@delete | NaN | NaN | Removes the NPC. |
@animate | animation (string) | time (integer) | Makes the NPC trigger the animation animation for time seconds. |
@goto | label (string) | NaN | Jump to the label label in the script. |
@rotate | degrees | NaN | Rotate the NPC degrees around the Z axis. |
@sit | primitive name | NaN | Sit on a primitive with a given name. |
@stand | NaN | NaN | If sitting on a primitive, stand up. |
where every command has to be prefixed with the @
symbol. Any lines that do not start with @
will be ignored by the script and can be used either as comments or as jump labels for the @goto
command.
The following are examples for every command:
Command | Example | Effect |
---|---|---|
@spawn | @spawn=Alter Ego|<137.262848, 132.939011, 22.705553> | Will rez an NPC called Alter Ego at the coordinates <137.262848, 132.939011, 22.705553> . |
@walk | @walk=<138.980637, 137.669067, 22.77791> | Will make the NPC walk to the coordinates <138.980637, 137.669067, 22.77791> |
@say | @say=omg, walking is so tiresome… | Will make he NPC say on local chat omg, walking is so tiresome… |
@pause | @pause=2 | Will pause and stand for 2 animation cycles. |
@stop | @stop | Will halt all script execution leaving the NPC standing. |
@delete | @delete | Will delete the current rezzed NPC. |
@animate | @animate=Dance,10 | Will trigger the animation in the primitive's inventory named Dance for 10 seconds. |
@goto | @goto=LABEL | Will jump to the label LABEL and consume the jump. |
@rotate | @rotate=90 | Will rotate the NPC 90 degrees around the positive sense of the z axis. |
@sit | @sit=Chair | Will scan for an object named Chair and will make the NPC sit on it. |
@stand | @stand | Will make the NPC stand up if it is currently sitting on a primitive. |
Consider the following example Script
notecard:
@spawn=Alter Ego|<137.262848, 132.939011, 22.705553> @walk=<138.980637, 137.669067, 22.77791> @walk=<134.451523, 134.671173, 22.77791> @say=omg, walking is so tiresome... @walk=<134.451523, 130.5784, 22.77791> @pause=2 @walk=<142.783859, 130.871552, 22.77791> @wander=3|1 @walk=<137.5, 130.871552, 22.77791> @say=Uff, I'm done... @animate=Dance|10 @stop
This will make the NPC, in order:
<137.262848, 132.939011, 22.705553>
with the first name Alter
and last name Ego
.<138.980637, 137.669067, 22.77791>
.<134.451523, 134.671173, 22.77791>
.<134.451523, 130.5784, 22.77791>
.2
seconds.<142.783859, 130.871552, 22.77791>
.3
meter radius circle and perform a walk-wait
cycle once.<137.5, 130.871552, 22.77791>
.Dance
for 10
seconds.
A walk-wait
cycle means a transition from walking to waiting and back. The default behavior of the puppeteer is to repeat all the commands. To avoid that use @stop
or @delete
at the end of the script.
The @goto
command can be used to jump inside the script to a named label. For example:
REPEAT @walk=<138.980637, 137.669067, 22.77791> @walk=<134.451523, 134.671173, 22.77791> @say=omg, walking is so tiresome... @goto=REPEAT
Exceptionally, the @goto
and the @spawn
command are used linearly and removed from the queue once a jump has been performed. In other words, if we wanted to repeat the three previous commands, three times, one would write:
REPEAT @walk=<138.980637, 137.669067, 22.77791> @walk=<134.451523, 134.671173, 22.77791> @say=omg, walking is so tiresome... @goto=REPEAT @goto=REPEAT @goto=REPEAT
Consequently, it is not possible to write a loop that will never terminate. @goto
is only used to repeat certain parts of the script. However, if an endless repetition is desired, then the script can be written without the @stop
command which will make the script repeat itself indefinitely.
The @goto
command supports both forward and backward jumps. For example, in the snippet below:
REPEAT @walk=<138.980637, 137.669067, 22.77791> @walk=<134.451523, 134.671173, 22.77791> @say=omg, walking is so tiresome... @goto=REPEAT @goto=NEXT @say=i will never say this... NEXT @say=now i can sit around doing nothing @stop
the command @say=i will never say this…
will never get executed since after the @goto=REPEAT
command, the @goto=NEXT
command will jump over @say=i will never say this…
.
The script is based on automata, allowing for extension to be written that perform other actions (sit, fly, etc…). A rough automata, omitting some states, is illustrated below:
Every state defines an action for the NPC to perform. After the action is preformed, the script returns to the script
state which is responsible for interpreting the commands in the notecard. The script
state shuffles the commands by using a queue-based approach, where the first enqueued commands get dequeued off from the top of the queue and then enqueued at the end. This ensures that we can repeat the commands instead of deleting them.
Also, since the script uses a queue to store the commands, that queue can be easily wound and unwound in order to create continuations, letting the playwright repeat certain commands without having to write them again.
/////////////////////////////////////////////////////////////////////////// // Copyright (C) Wizardry and Steamworks 2013 - License: GNU GPLv3 // // Please see: http://www.gnu.org/licenses/gpl.html for legal details, // // rights of fair usage, the disclaimer and warranty conditions. // /////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////// // CONFIGURATION // ////////////////////////////////////////////////////////// // How much time, in seconds, does a standing animation // cycle take? float ANIMATION_CYCLE_TIME = 10; /////////////////////////////////////////////////////////////////////////// // INTERNALS // /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3 // /////////////////////////////////////////////////////////////////////////// vector wasCirclePoint(float radius) { float x = llPow(-1, 1 + (integer) llFrand(2)) * llFrand(radius*2); float y = llPow(-1, 1 + (integer) llFrand(2)) * llFrand(radius*2); if(llPow(x,2) + llPow(y,2) <= llPow(radius,2)) return <x, y, 0>; return wasCirclePoint(radius); } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3 // /////////////////////////////////////////////////////////////////////////// string wasKeyValueGet(string var, string kvp) { list dVars = llParseString2List(kvp, ["&"], []); do { list data = llParseString2List(llList2String(dVars, 0), ["="], []); string k = llList2String(data, 0); if(k != var) jump continue; return llList2String(data, 1); @continue; dVars = llDeleteSubList(dVars, 0, 0); } while(llGetListLength(dVars)); return ""; } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3 // /////////////////////////////////////////////////////////////////////////// string wasKeyValueSet(string var, string val, string kvp) { list dVars = llParseString2List(kvp, ["&"], []); if(llGetListLength(dVars) == 0) return var + "=" + val; list result = []; do { list data = llParseString2List(llList2String(dVars, 0), ["="], []); string k = llList2String(data, 0); if(k == "") jump continue; if(k == var && val == "") jump continue; if(k == var) { result += k + "=" + val; val = ""; jump continue; } string v = llList2String(data, 1); if(v == "") jump continue; result += k + "=" + v; @continue; dVars = llDeleteSubList(dVars, 0, 0); } while(llGetListLength(dVars)); if(val != "") result += var + "=" + val; return llDumpList2String(result, "&"); } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3 // /////////////////////////////////////////////////////////////////////////// string wasKeyValueDelete(string var, string kvp) { list dVars = llParseString2List(kvp, ["&"], []); list result = []; list added = []; do { list data = llParseString2List(llList2String(dVars, 0), ["="], []); string k = llList2String(data, 0); if(k == var) jump continue; string v = llList2String(data, 1); if(v == "") jump continue; if(llListFindList(added, (list)k) != -1) jump continue; result += k + "=" + v; added += k; @continue; dVars = llDeleteSubList(dVars, 0 ,0); } while(llGetListLength(dVars)); return llDumpList2String(result, "&"); } // Vector that will be filled by the script with // the initial starting position in region coordinates. vector iPos = ZERO_VECTOR; // Storage for destination position. vector dPos = ZERO_VECTOR; // Storage for the NPC script. list npcScript = []; // Storage for the next action. string npcAction = ""; string npcParams = ""; // Storage for talking on local chat. string npcSay = ""; default { state_entry() { osNpcRemove(wasKeyValueGet("key", llGetObjectDesc())); llSetTimerEvent(5); } timer() { npcScript = llParseString2List(osGetNotecard("Script"), ["\n"], []); if(llGetListLength(npcScript) == 0) { llSay(DEBUG_CHANNEL, "No script notecard found."); llSetTimerEvent(0); return; } llSetTimerEvent(0); state script; } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state script { state_entry() { @ignore; string next = llList2String(npcScript, 0); npcScript = llDeleteSubList(npcScript, 0, 0); npcScript += next; if(llGetSubString(next, 0, 0) != "@") jump ignore; list data = llParseString2List(next, ["="], []); npcAction = llToLower(llStringTrim(llList2String(data, 0), STRING_TRIM)); npcParams = llStringTrim(llList2String(data, 1), STRING_TRIM); llSetTimerEvent(1); } timer() { llSetTimerEvent(0); @commands; if(npcAction == "@spawn") { integer lastIdx = llGetListLength(npcScript)-1; npcScript = llDeleteSubList(npcScript, lastIdx, lastIdx); list spawnData = llParseString2List(npcParams, ["|"], []); llSetObjectDesc(wasKeyValueSet("name", llList2String(spawnData, 0), llGetObjectDesc())); list spawnDest = llParseString2List(llList2String(spawnData, 1), ["<", ",", ">"], []); iPos.x = llList2Float(spawnDest, 0); iPos.y = llList2Float(spawnDest, 1); iPos.z = llList2Float(spawnDest, 2); state spawn; } if(npcAction == "@goto") { integer lastIdx = llGetListLength(npcScript)-1; npcScript = llDeleteSubList(npcScript, lastIdx, lastIdx); // Wind commands till goto label. @wind; string next = llList2String(npcScript, 0); npcScript = llDeleteSubList(npcScript, 0, 0); npcScript += next; if(next != npcParams) jump wind; // Wind the label too. next = llList2String(npcScript, 0); npcScript = llDeleteSubList(npcScript, 0, 0); npcScript += next; // Get next command. list data = llParseString2List(next, ["="], []); npcAction = llToLower(llStringTrim(llList2String(data, 0), STRING_TRIM)); npcParams = llStringTrim(llList2String(data, 1), STRING_TRIM); // Reschedule. jump commands; } if(npcAction == "@walk") { list dest = llParseString2List(npcParams, ["<", ",", ">"], []); dPos.x = llList2Float(dest, 0); dPos.y = llList2Float(dest, 1); dPos.z = llList2Float(dest, 2); state walk; } if(npcAction == "@say") { npcSay = npcParams; state say; } if(npcAction == "@pause") { llSetObjectDesc(wasKeyValueSet("pause", npcParams, llGetObjectDesc())); state pause; } if(npcAction == "@wander") { list wanderData = llParseString2List(npcParams, ["|"], []); llSetObjectDesc(wasKeyValueSet("wd", llList2String(wanderData, 0), llGetObjectDesc())); llSetObjectDesc(wasKeyValueSet("wc", llList2String(wanderData, 1), llGetObjectDesc())); iPos = llGetPos(); state wander; } if(npcAction == "@rotate") { llSetObjectDesc(wasKeyValueSet("rot", npcParams, llGetObjectDesc())); state rotate; } if(npcAction == "@sit") { llSetObjectDesc(wasKeyValueSet("sit", npcParams, llGetObjectDesc())); state sit; } if(npcAction == "@stand") { state stand; } if(npcAction == "@stop") { state stop; } if(npcAction == "@delete") { state delete; } if(npcAction == "@animate") { list animateData = llParseString2List(npcParams, ["|"], []); llSetObjectDesc(wasKeyValueSet("an", llList2String(animateData, 0), llGetObjectDesc())); llSetObjectDesc(wasKeyValueSet("at", llList2String(animateData, 1), llGetObjectDesc())); state animate; } llSay(DEBUG_CHANNEL, "ERROR: Unrecognized script line: " + npcAction + "=" + npcParams); } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state rotate { state_entry() { osNpcSetRot(wasKeyValueGet("key", llGetObjectDesc()), llEuler2Rot(<0,0,(float)wasKeyValueGet("rot", llGetObjectDesc())> * DEG_TO_RAD)); llSetTimerEvent(2); } link_message(integer sender, integer num, string str, key id) { if(id != "@npc_say") return; osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str); } timer() { llSetObjectDesc(wasKeyValueDelete("rot", llGetObjectDesc())); state script; } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state sit { state_entry() { llSensorRepeat(wasKeyValueGet("sit", llGetObjectDesc()), "", PASSIVE|ACTIVE, TWO_PI, 96, 1); } sensor(integer num) { llSensorRemove(); osNpcSit(wasKeyValueGet("key", llGetObjectDesc()), llDetectedKey(0), OS_NPC_SIT_NOW); llSetTimerEvent(2); } timer() { llSetObjectDesc(wasKeyValueDelete("sit", llGetObjectDesc())); state script; } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state stand { state_entry() { osNpcStand(wasKeyValueGet("key", llGetObjectDesc())); state script; } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state animate { state_entry() { if(llGetInventoryType(wasKeyValueGet("an", llGetObjectDesc())) == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), wasKeyValueGet("an", llGetObjectDesc())); llSetTimerEvent((integer)wasKeyValueGet("at", llGetObjectDesc())); } link_message(integer sender, integer num, string str, key id) { if(id != "@npc_say") return; osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str); } timer() { if(llGetInventoryType(wasKeyValueGet("an", llGetObjectDesc())) == INVENTORY_ANIMATION) osNpcStopAnimation(wasKeyValueGet("key", llGetObjectDesc()), wasKeyValueGet("an", llGetObjectDesc())); llSetObjectDesc(wasKeyValueDelete("an", llGetObjectDesc())); llSetObjectDesc(wasKeyValueDelete("at", llGetObjectDesc())); state script; } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state spawn { state_entry() { list name = llParseString2List(wasKeyValueGet("name", llGetObjectDesc()), [" "], []); llSetObjectDesc(wasKeyValueSet("key", osNpcCreate(llList2String(name, 0), llList2String(name, 1), iPos, "appearance"), llGetObjectDesc())); osNpcLoadAppearance(wasKeyValueGet("key", llGetObjectDesc()), "appearance"); if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand"); llSetTimerEvent(10); } timer() { llSetTimerEvent(0); if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcStopAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand"); llSetObjectDesc(wasKeyValueDelete("name", llGetObjectDesc())); state script; } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state delete { state_entry() { osNpcRemove(llGetObjectDesc()); llResetScript(); } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state stop { state_entry() { if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand"); } link_message(integer sender, integer num, string str, key id) { if(id != "@npc_say") return; osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str); } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state walk { state_entry() { if(llGetInventoryType("Walk") == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Walk"); osNpcMoveToTarget(wasKeyValueGet("key", llGetObjectDesc()), dPos, OS_NPC_NO_FLY); llSetTimerEvent(1); } link_message(integer sender, integer num, string str, key id) { if(id != "@npc_say") return; osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str); } timer() { if (llVecDist(osNpcGetPos(wasKeyValueGet("key", llGetObjectDesc())), dPos) > 2) return; llSetTimerEvent(0); if(llGetInventoryType("Walk") == INVENTORY_ANIMATION) osNpcStopAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Walk"); state script; } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state say { state_entry() { osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), npcSay); npcSay = ""; state script; } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state wander { state_entry() { dPos = iPos + wasCirclePoint((integer)wasKeyValueGet("wd", llGetObjectDesc())); if(llGetInventoryType("Walk") == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Walk"); osNpcMoveToTarget(wasKeyValueGet("key", llGetObjectDesc()), dPos, OS_NPC_NO_FLY); llSetTimerEvent(0.1); } link_message(integer sender, integer num, string str, key id) { if(id != "@npc_say") return; osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str); } timer() { if (llVecDist(osNpcGetPos(wasKeyValueGet("key", llGetObjectDesc())), dPos) > 2) return; llSetTimerEvent(0); if(llGetInventoryType("Walk") == INVENTORY_ANIMATION) osNpcStopAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Walk"); if(wasKeyValueGet("wc", llGetObjectDesc()) == "0") { llSetObjectDesc(wasKeyValueDelete("wc", llGetObjectDesc())); llSetObjectDesc(wasKeyValueDelete("wd", llGetObjectDesc())); state script; } llSetObjectDesc(wasKeyValueSet("wc", (string)((integer)wasKeyValueGet("wc", llGetObjectDesc())-1), llGetObjectDesc())); state wait; } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state wait { state_entry() { if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand"); llSetTimerEvent(ANIMATION_CYCLE_TIME); } link_message(integer sender, integer num, string str, key id) { if(id != "@npc_say") return; osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str); } timer() { llSetTimerEvent(0); if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcStopAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand"); state wander; } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } } state pause { state_entry() { if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcPlayAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand"); llSetTimerEvent(ANIMATION_CYCLE_TIME * 1+llFrand((integer)wasKeyValueGet("pause", llGetObjectDesc())-1)); } link_message(integer sender, integer num, string str, key id) { if(id != "@npc_say") return; osNpcSay(wasKeyValueGet("key", llGetObjectDesc()), str); } timer() { llSetTimerEvent(0); if(llGetInventoryType("Stand") == INVENTORY_ANIMATION) osNpcStopAnimation(wasKeyValueGet("key", llGetObjectDesc()), "Stand"); llSetObjectDesc(wasKeyValueDelete("pause", llGetObjectDesc())); state script; } changed(integer change) { if(change & CHANGED_REGION_START) llResetScript(); } on_rez(integer num) { llResetScript(); } }