User:Dz/NPC Scripts
From OpenSimulator
Contents |
NPC Utility Scripts
These are useful utilites I have developed over time to help me use NPCs in OpenSimulator. Many of them are assembled from bits and pieces of code I have read/seen/fixed/admired. Some of the code snippets appear with permissions, Please do not remove the attributions where they can be found in the comments. The code has been shared, only asking simple consideration. Please respect the wishes of the original authors as I have attempted to do.
Remember, you will need to enable NPC functions, and may need to set the severity level of allowed osNPC function calls for these scripts to work.
If you have feedback on script errors, please post it to the page discussion User_talk:Dz/NPC_Scripts
NPC BotKiller
Sometimes..things go wrong...and you have a region full of wandering NPC's. Drop this code in a prim and touch it... It can take a while to remove them all.
PLEASE DON'T NUKE other peoples NPC's.
// OpenSimian BotKiller // Kills all the NPC's in the region.. Please use with discretion. // Iterate over a list of avatar keys, using them as an arguments to osNpcRemove // Add a delay to the timer if sim performance starts to drag during logouts // Feel free to use/distribute/modify to suit your needs // Prepared for transfer to MOSES grid - D Osborn 5.3.2013 integer who2kill = 0; integer howmany = 0; list avatars = []; default { state_entry() { llSetText("waiting ", <1.0, 0.0, 0.0>, 1.0); } touch_end(integer total_number) // should not change state in touch_start events.... { avatars = osGetAvatarList(); howmany = llGetListLength(avatars)/3; state KillThem; } changed(integer change) // Reset on region restart { if (change & CHANGED_REGION_RESTART) { llResetScript(); } } } state KillThem { state_entry() { llSetText("Processing ", <1.0, 0.0, 0.0>, 1.0); llSetTimerEvent(3.0); // remove 1 every 3 seconds to minimize performance impact } timer() { osNpcRemove(llList2Key(avatars,who2kill*3)); llSetText("Removed so far : " + (string) (who2kill + 1), <1.0, 0.0, 0.0>, 1.0); who2kill++; if(who2kill>=howmany) state default; llSetTimerEvent(3.0/ llGetRegionTimeDilation()); // Use timedilation to add to the delay if lagging } touch_end(integer interrupt) // abort by touching the object while it is processing { llResetScript(); } changed(integer change) { if (change & CHANGED_REGION_RESTART) { llResetScript(); } } }
NPC Router
This script re-directs any NPC that collides with the object to a random destination selected from an internal list.
Once the routers are placed, a text label display can be toggled by touching it. This makes it easy to collect locations
Routers should be placed to facilitate collisions with the avatar capsules.
The real trick of this router design is the rotate calculations. These prevents the NPCs from walking backwards when assigned new targets "behind" them.
// OpenSimian NPC router // D Osborn MOSES version 2013May06 // Assign random destination to a NPC that collides with volume detect object // Drop the script in a prim, resize and position the prim to facilitate collision with NPC av capsules // Modify the list of destination vectors to reflect your layout. // Router position text labels can be toggled by touching the prim. This also triggers the setpos() // This design imlements rotation calculations to prevent NPC avatars from walking backwards // Permissions and information about the rotation functions was here.. // http://wiki.secondlife.com/wiki/User:Pedro_Oval/Calculate_rotation_for_pointing_in_a_direction // Due credit is here ... Written by Pedro Oval, 2011-01-11 rotation PointAtHoriz2Rot(vector target) { return llRotBetween(<1., 0., 0.>, <target.x, target.y, 0.>); } rotation PointAt2Rot(vector target) { rotation r = PointAtHoriz2Rot(target); return llRotBetween(<1., 0., 0.>, target/r) * r; } // end of Pedros' rotation magic list DestinationList = [<70.0,70.0,30.0>,<97.0,100.0,37.0>,<70.0,190.0,33.0>]; // Modify DestinationList.. Keep the list of vectors small to minimize processing // Be VERY careful about assigning destinations outside of the region. // NPC's will move in a direct line, design your "paths" to be as free of obstacles as possible integer numDests = 0; integer showPos = 0; default { state_entry() { llVolumeDetect(TRUE); // Starts llVolumeDetect numDests = llGetListLength(DestinationList); } touch_start (integer numtouches) { while (numtouches) { if(showPos) { llSetText("", ZERO_VECTOR, 0.0); showPos=0; // Comment out the following 2 lines if you do NOT want your targets to "snap" to integer value locations vector newpos = llGetPos(); llSetPos(<llFloor(newpos.x),llFloor(newpos.y), llFloor(newpos.z)>); } else { showPos = 1; llSetText((string) llGetPos(), ZERO_VECTOR, 1.0); llSay(0,(string) llGetPos()); } numtouches--; } } collision_start(integer num) { integer i = 0; do { integer DestOffset = llFloor(llFrand(numDests)); vector NewDest = llList2Vector(DestinationList,DestOffset) ; osNpcSetRot(llDetectedKey(i), PointAt2Rot(NewDest - llGetPos())); osNpcMoveToTarget(llDetectedKey(i), NewDest, OS_NPC_NO_FLY); } while(num > ++i); } changed(integer change) { if (change & CHANGED_REGION_RESTART) { llResetScript(); } } }
NPC Creation Scripts
While NPC's provide a very low overhead way to populate regions, there is still overhead involved. Logging in and out, generating note cards with serialized appearances, and trying to figure out if they are at a destination while moving all consume server resources. Some simple planning can remove/reduce the impact of these operations during "normal" conditions.
Generate and check your NPC appearance WAY ahead of time. Sometimes you will find that you have to rebake your appearance AND remove and re-wear all your attachments before they will "clone" properly.
Log your NPC's in and out during times when there are few in the region. Allow time between each login.. NPCs wearing scripted attachments might require 5 seconds between each login/logout to minimize the impact of starting/stopping scripts.
Build error correction into your movement plans. If your NPC absolutely positively has to get to <X,Y,z>, You better have a alternative plan to sending that as a destination to a MOVETO function call once and hoping it arrives....
Animate instead of move. Take advantage of the fact that the siton function can sit an NPC on any prim in the region. Seated NPC's are easy to animate, and they aren't generating avatar capsule collisions.
While the simple generator can be fun to show off, it is also a useful tool to verify that the NPC appearance is what you expect before you place it in your region. Once you start collecting the note cards of different appearances, you will begin to understand the logistics of introducing variety to your sim through NPCs.
Basic Clone Generator
This script generates a clone of the avatar that touches it. The NPC will sit on the generator prim and say hello. Touching the generator again kills the NPC.
// drop this script in a small box. (0.25, 0.25, 0.15) // touch to create a NPC clone of your current appearance. // NPC will sit on the prim, then greet you. // Touch again to remove the NPC // This code is adapted from the wiki with the following modifications // State entry for llSitTarget added // touch start replaced by touch end... lsl wiki sez " state changes in touch start may trigger extra events // Used overloaded version of NPCCreate to illustrate use of OWNED and SENSE_AS_AGENT parameters // added restart on_rez to default and region reboot events to both states // Changed NPC Moveto to Sit on for initial NPC target. I find this useful for examining the NPC generated. // It also eliminates the issues of NPC's "never arriving" when moveto or movetotarget calls are used. key npc; default { state_entry() { llSitTarget(<0.30, 0.0, 0.35>, ZERO_ROTATION); } touch_end(integer number) { vector npcPos = llGetPos() + <1,0,0>; osAgentSaveAppearance(llDetectedKey(0), "appearance"); npc = osNpcCreate("Ima", "Clone", npcPos, "appearance", OS_NPC_NOT_OWNED | OS_NPC_SENSE_AS_AGENT ); state hasNPC; } changed(integer change) { if (change & CHANGED_REGION_RESTART) { llResetScript(); } } on_rez(integer start_param) { llResetScript(); } } state hasNPC { state_entry() { osNpcSit(npc, llGetKey(), OS_NPC_SIT_NOW); osNpcSay(npc, "Hi there! My name is " + llKey2Name(npc)); } touch_end(integer number) { osNpcSay(npc, "Goodbye!"); osNpcRemove(npc); npc = NULL_KEY; state default; } changed(integer change) { if (change & CHANGED_REGION_RESTART) { llResetScript(); } } }
Advanced Clone Generator
This script supports the deployment of large numbers of NPC bots. It maintains an internal library of appearance Notecards. Notecards are generated by touching the object and selecting the [Clone Me] button. The user will be required to provide a unique name to store an appearance.
Selecting the [Choose One] menu option will present the user with a menu to select the appearance notecard used to generate the NPC's.
Select [Create Bots] to generate the NPC's. They are created slowly to minimize impact on sim performance. Touching the generator before all NPC's are generated will stop the process.
NPC Animation Scripts
My experience with NPC animation is a combination of a very simple animation override with in-word scripted objects designed to detect collisions, proximity, or seated avatars can provide exceptional results. Limiting the AO tasks to movement animation types (Walk/Run/Fly) reduces the complexity and size of the script.
Using the inworld NPC Routers as targets, we can set up a network of paths between the NPC generators and the region destinations. For instance, in a city simulation, we could place a generator in a subway station, with routers at the intersections on the streets, to drive traffic around the city buildings. Routers at the building entrance can dirent NPC's inside to seating locations. Using scripted poseballs to provide places for the NPC's to interact, also allows non-NPC participation.
Basic NPC AO (Animation Override)
The intent is to provide an efficient way to give NPC's unique combinations of movement ONLY animations. These are simple scripts, easy to duplicate and modify the animations to use. Attach the AO to your HUD before you clone your appearance and generate the NPC. Touch the HUD to toggle it off and back on to make sure it is working. While designed for use with NPC's, these AO's will work just as well with other bot types or regular avatars.
This script monitors change events for a CHANGED_ANIMATION flag. This is vastly more efficient than parsing the list of current animations 3 or 4 times a second to see if you are still walking/standing/flying. There are other reasons NOT to try and use AOs ported from SL on NPC's. Most will fail to work at all, and some will actually animate the avatar that was cloned instead of the NPC.
An AO script is not much use with the animations to go with it. There are a number of good AO animation sets available in some of the public domain OAR and IAR files. I had the pleasure once to meet the creator of the animations contained in the Linda Kellie OAR files. The following scripts are adapted to those animation names so that you can drop them, and the animations (after you DL them) into a single prim and attach it to your HUD.
Rememeber, You must place the HUD on the ground to add animations, and you should take it back into inventory before wearing it to make sure the contents update properly.
// A basic OpenSimulator Walk and Stand animation override // All Modifications are Copyright 2010 by D Osborn. // This script is Licensed under the Creative Commons Attribution-Share Alike 3.0 License // For a copy of the license terms please see http://creativecommons.org/licenses/by-sa/3.0 // This work uses content from the Second Life® Wiki article llGetAnimation. (http://wiki.secondlife.com/wiki/LlGetAnimation) // Copyright © 2007-2009 Linden Research, Inc. Licensed under the Creative Commons Attribution-Share Alike 3.0 License // This AO is optimized for OpenSimulator and DOES NOT POLL the animation list multiple times a second // It relies instead on the CHANGED ANIMATION event. The timer is ONLY active when your avatar is swapping between stands. // It is NOT optimized code. Yes it could be smaller and probably faster. This is simple, and is intended to provide a working object // instead of a lasting tribute to anyones programming prowess. Feel free to Optimize and re-distribute to your hearts content. // To use: Place this script in an object that will be attached to your avatar // Place the animations in the same prim // Change the CUSTOMIZATION section to reflect the names of YOUR animations. // Attach to your avatar or a HUD position // To Reset Detach and re-attach the object or Edit the object and Reset the script // All of the overrides available via the traditional ZHAO can be controlled via this script. // The following Animation Types can be used by expanding the StartAnimation function to include the animation type // // [ Standing ] // [ Walking ] // [ Sitting ] // [ Sitting On Ground ] // [ Crouching ] // [ Crouch Walking ] // [ Landing ] // [ Standing Up ] // [ Falling ] // [ Flying Down ] // [ Flying Up ] // [ Flying ] // [ Flying Slow ] // [ Hovering ] // [ Jumping ] // [ Pre Jumping ] // [ Running ] // [ Turning Right ] // [ Turning Left ] // [ Floating ] // [ Swimming Forward ] // [ Swimming Up ] // [ Swimming Down ] // ************************** Customize YOUR AO by changing the names of the Animations, and the cycle time for your stands here. // Change the folling lines to reflect the animation names you want to use list StandNames = ["ao-sweetness-stand1", "ao-sweetness-stand2", "ao-sweetness-stand3", "ao-sweetness-stand4", "ao-sweetness-stand5"]; integer StandTime = 12; // change this number to the number of seconds between stands string WalkAnimation = "sweetness walk"; // Change this string to the name of the Walk animation you want to use string RunAnimation = "AO-Run-Female"; // Change this string to the name of the Run animation you want to use string SitAnimation = "sweetness-sit-1"; // Change this string to the name of the sit animation you want to use // For NPC AO, it is best to leave this blank. Expect SIT objects to provide the proper animation string CrouchAnimation = "AO-Crouch-Female"; // Change this string to the name of the crouch animation you want to use string FlyAnimation = "sweetness-fly-1"; // Change this string to the name of the fly animation you want to use string HoverAnimation = "sweetness-hover4"; // Change this string to the name of the hover animation you want to use string SoftLandAnimation = "AO-Softland1-Female"; // Change this string to the name of the softland animation you want to use integer LandingTime = 3; // Change this to reflect the length of the standing animation in seconds. string JumpAnimation = "AO-JumpFlip1-Female"; // Change this string to the name of the Jump animation you want to use // ******************************************************************************************************************************* // ***** Below there be dragons <wink> not really! ******* // You should not need to change anything below these lines // You are welcome to. If you break it, you get to keep all the parts! // key Owner; // the wearer's key string LastAnimation = ""; // last llGetAnimation value seen string LastAnimName = ""; string newAnimation = ""; float StandCount = 0.0; integer PowerStatus = 1; vector onColor = <42,255,42>; // all nice and green vector offColor = <128,128,128>; // and grey // User functions Initialize(key id) { if (id == NULL_KEY) // detaching { llSetTimerEvent(0.0); // stop the timer } else // attached, or reset while worn { llRequestPermissions(id, PERMISSION_TRIGGER_ANIMATION); Owner = id; StandCount = (float) llGetListLength(StandNames); } } OnOff() { vector color; if (PowerStatus == 0) { PowerStatus = 1; newAnimation = llGetAnimation(Owner); StartAnimation(); llOwnerSay("Over-ride active"); color = onColor; } else { PowerStatus = 0; llStopAnimation(LastAnimName); llOwnerSay("Over-ride off"); color = offColor; } llSetColor(color/255.0, ALL_SIDES); } StartAnimation() { if (LastAnimation != newAnimation) { if (newAnimation == "Walking") { llStopAnimation(LastAnimName); if(WalkAnimation != "") { LastAnimName = WalkAnimation; llStartAnimation(LastAnimName); } llSetTimerEvent(0); } if (newAnimation == "Running") { llStopAnimation(LastAnimName); if(RunAnimation != "") { LastAnimName = RunAnimation; llStartAnimation(LastAnimName); } llSetTimerEvent(0); } if (newAnimation == "Standing") { llStopAnimation(LastAnimName); if(StandCount > 0.0) { integer whichone = (integer)llFrand(StandCount);// pick a new stand LastAnimName = llList2String (StandNames,whichone); llStartAnimation(LastAnimName); if(StandCount > 1.0) llSetTimerEvent(StandTime); } else { llSetTimerEvent(0); } } if (newAnimation == "Sitting") { llStopAnimation(LastAnimName); if(SitAnimation != "") { LastAnimName = SitAnimation; llStartAnimation(LastAnimName); } llSetTimerEvent(0); } if (newAnimation == "Flying") { llStopAnimation(LastAnimName); if(FlyAnimation != "") { LastAnimName = FlyAnimation; llStartAnimation(LastAnimName); } llSetTimerEvent(0); } if (newAnimation == "Hovering") { llStopAnimation(LastAnimName); if(HoverAnimation != "") { LastAnimName = HoverAnimation; llStartAnimation(LastAnimName); } llSetTimerEvent(0); } if (newAnimation == "Soft Landing") { llStopAnimation(LastAnimName); if(SoftLandAnimation != "") { LastAnimName = SoftLandAnimation; llStartAnimation(LastAnimName); llSetTimerEvent(LandingTime); } llSetTimerEvent(0); } if (newAnimation == "Crouching") { llStopAnimation(LastAnimName); if(CrouchAnimation != "") { LastAnimName = CrouchAnimation; llStartAnimation(LastAnimName); } llSetTimerEvent(0); } if (newAnimation == "Jumping") { llStopAnimation(LastAnimName); if(JumpAnimation != "") { LastAnimName = JumpAnimation; llStartAnimation(LastAnimName); } llSetTimerEvent(0); } } } // Event handlers default { state_entry() { // script was reset while already attached if (llGetAttached() != 0) { Initialize(llGetOwner()); } } attach(key id) { Initialize(id); } run_time_permissions(integer perm) { if (perm & PERMISSION_TRIGGER_ANIMATION) { llOwnerSay("Over-ride active"); } } touch_start(integer whodunit) { OnOff(); } timer() { llStopAnimation(LastAnimName); integer whichone = (integer)llFrand(StandCount); // pick the new stand at random LastAnimName = llList2String (StandNames,whichone); llStartAnimation(LastAnimName); // llOwnerSay( "using " + LastAnimName); // uncomment this to see which stand gets trigger by the timer llSetTimerEvent(StandTime); } changed (integer change) { if (change & CHANGED_ANIMATION) { newAnimation = llGetAnimation(Owner); StartAnimation(); LastAnimation = newAnimation; // so we can check for changes // llOwnerSay("started " + newAnimation); // uncomment this to see the event types you can respond to // llOwnerSay( "using " + LastAnimName); // uncomment this to see which animations are being used } } }