Join us in Outworldz at www.outworldz.com:9000 or follow us:

How to make a fish swim in Second Life

In this section, you will learn how to animate multiple prims, smoothly and realistically.

fin movement

In this example, we are going to use 3 scripts total.  One of those scripts you have already loaded in the fins.   The next script will learn the movements we teach the whale.  We will save those motions in a notecard.   The third script will play back those movements.   The only script we will end up with needing is the the one that plays back the motion, and the notecard.   

This must be the last step you do when making a whale, or any other animal, robot,  or movable thingamabob.     You will not be able to unlink and re-link the whale after you have finished teaching the script to play back motion.   The scripts will remember each prim by number. Re-linking the prims in a different order  will lead to a real mess!  Trust me, I know.

Copy the following script into the root prim. Name it "Prim Learning script".  You will also need a notecard in your root prim. Name this notecard to "Movement". An empty note card is all you need.

Download

			
// Prim position root script
// Put this script in the root prim. Put the child script in all prims you wish to move.
// touch the script to start recording

// Reset - wipe out all recording.
// Name  - name a new recording
// Pause - insert a 1 second pause
// PlayBack - play back the current animation
// RemoveAll - removel all child scripts
// Record  - store a new set of child prim positions
//

The comments show you what the user Interface looks like:

First, you must set up the global variables:


integer debug = 0;

// notecard reading
integer iIndexLines;
integer i = 0;
integer move = 0;               // N movements rea from the notecard
string NOTECARD = "Movement";   // the notecard 
key kNoteCardLines;        // the key of the notecard
key kGetIndexLines;        // the key of the current line

//communications
integer linkchannel = 5001;     // for recording purposes
integer dialogchannel ;         // dialog boxes
integer playchannel = 50003;    // the playback channel
integer nPrims;                 // total number of prims 
integer PrimsCounter = 0;       // how many have checked in
integer timercounter = 0;       // how many seconds have gone by 
integer wantname;               // flag indicating we are waiting for a name to be chatted

// last heard prim params
vector primpos;
rotation primrot;

// the list of coords
list masterlist;		// the list of coordinates for all child prims
string curranimation; 	// the name of the current animation
list Menu;			  	// stoareg for the current menu
integer STRIDE = 6;		// the size of the data we store
integer Runtime;   		// flag that we are in running mode vs learning mode

There are several subroutines needed. The following two will strip any extra white space to the left or right of data read from a note card.

	

string strip( string str)
{
    return llStringTrim(str, STRING_TRIM);
}
string Getline(list Input, integer line)
{
    return strip(llList2String(Input, line));
}

DumpBack() will print the coordinates to chat. You must copy and past the chat into a notecard because Second Life cannot write a notecard.


DumpBack ()
{
    integer i;
    integer max = llGetListLength(masterlist);		// the number of recorded items
    integer flag = 0;
    for (i = 0; i < max; i+= STRIDE)
    {
        string aniname2 = llList2String(masterlist,i);
        curranimation = aniname2;
        
        integer primnum2 = llList2Integer(masterlist,i+1);
        vector  sprimpos2  = llList2Vector(masterlist,i+2);
        rotation sprimrot2 = llList2Rot(masterlist,i+3) ;
        llOwnerSay("|"+ aniname2 + "|" + (string) primnum2 + "|" + (string) sprimpos2 + "|" + 
(string) sprimrot2);
        flag++;
    }
    if (! flag)
        llOwnerSay("No recording!" );
}

PlayBack()  will animate the prim set when you click the Playback button

 



PlayBack (string name)
{
    integer i;
    integer max = llGetListLength(masterlist); // the number of recorded items

    for (i = 0; i < max; i+= STRIDE)		// look at each animation, they could be scattered
    {
        string aniname2 = llList2String(masterlist,i);
        if (aniname2 == name)
        {
            integer primnum = llList2Integer(masterlist,i+1);
            vector  sprimpos  = llList2Vector(masterlist,i+2);
            rotation sprimrot = llList2Rot(masterlist,i+3) ;
            string  msg = llList2String(masterlist,i+4);
            string UUID = llList2String(masterlist,i+5);
            if (llStringLength(msg) > 0)
                llSay(0,msg);
            if (llStringLength(UUID) > 0)
                llPlaySound(UUID,1.0);
            
            if (primnum < 0)
            {
                llSleep(-primnum);
            }  
            else
            {    
                sprimrot /=  llGetRot();       // Add in the local rot
                if (primnum !=0)
			llSetLinkPrimitiveParamsFast(primnum,[PRIM_POSITION,sprimpos,PRIM_ROTATION,sprimrot]);                 
		else
			llSetPrimitiveParamsFast([PRIM_POSITION,sprimpos,PRIM_ROTATION,sprimrot]);
            }
        }
    }
}

MakeMenu() builds the dialog ox menu system. Any named animations will be added to the menu so you can click them and preview them.

 


MakeMenu()
{
    list amenu = ["Reset","Record","PlayBack","Name","Dump","RemoveAll","Pause"];
    amenu += Menu;
    llDialog(llGetOwner(), "Pick a command",amenu,dialogchannel);
}

The default event is called when the script resets. Experts may notice that this script does not have an llResetScript() in it when it is rezzed. This allows you to use this script for playback and creation of notecards.  Instead, we re-establish the dialog channel listener, which is shut off when stored in inventory.

 


default
{
    on_rez(integer param)
    {   
        llListen(dialogchannel,"","","");
    }

state_entry is called when the script is first reset. We save the number of prims to compare with later, start the note card reader, and pick a channel to chat to the user on

 


    state_entry()
    {
        nPrims = llGetNumberOfPrims();
        llOwnerSay(" Total Prims = " + (string) nPrims);
        
        kNoteCardLines = llGetNumberOfNotecardLines(NOTECARD);
        kGetIndexLines = llGetNotecardLine(NOTECARD,0);
        dialogchannel = (integer) (llFrand(100) +600);
        llListen(dialogchannel,"","","");
        llMessageLinked(LINK_SET,0,"Reset","");
    } 

Each time the server reads a line from the notecard, it will fire the event 'dataserver'.

 


    // read notecard on bootup
    dataserver(key queryid, string data)
    {        
        if (queryid == kNoteCardLines)
        	iIndexLines = (integer) data;
        if (queryid == kGetIndexLines)
        {
            if (data != EOF)
            {

Each line is fetched with llGetNotecardLine()

 


                queryid = llGetNotecardLine(NOTECARD, i);

The line was delimited  in the note card  with the pipe "|" character. The llParseString2List() system call is used to make a list from the data. This makes it simple to pick each parameter out of the list with llList2String().  I also call the subroutine Getline() to strip out the white space and covert what is left into integers or floats.

 


                list lLine = (llParseString2List(data, ["|"], []));
                string junk = llList2String(lLine,0);
                string aniname = llList2String(lLine,1);
                integer Num = (integer)Getline(lLine,2);
                vector Pos = (vector) Getline(lLine,3);
                rotation Rot = (rotation) Getline(lLine,4);
                string msg = llList2String(lLine,5);
                string UUID = llList2String(lLine,6);
             

A blank line will have prim num of undef. If there is a linked prim num, there may be a sound to play or text to chat.

 

			
   
                if(Num>=0)
                {
                    if (llStringLength(msg) > 0)
                        llSay(0,msg);
                    if (llStringLength(UUID) > 0)
                    llPlaySound(UUID,1.0);
                    

I then add the items read from the notecard onto the play list.

 


                    masterlist += [aniname];
                    masterlist += [Num];
                    masterlist += [Pos];
                    masterlist += [Rot];
                    masterlist += [msg];
                    masterlist += [UUID];
                    Rot /= llGetRot();

The last part is to actually animate the prim that we have read. The new command llSetLinkPrimitiveParamsFast does the rotation and movement all at once (well almost), with no delays.

 


		    llSetLinkPrimitiveParamsFast(Num,[PRIM_POSITION,Pos,PRIM_ROTATION,Rot]);
                    move++;
                }
                i++;
                integer     InitPerCent = (integer) llRound(( (i+1) / (float) iIndexLines) * 100);      
                llSetText("Initialising... \n" + (string) InitPerCent + "%" , <1,1,1>, 1.0);
                if (InitPerCent == 100)
                    llSetText(""  , <1,1,1>, 1.0);
                kGetIndexLines = llGetNotecardLine(NOTECARD,i);
            }
            else
            {
                llOwnerSay("initialized with " + (string) move + " movements");
                llSetText(""  , <1,1,1>, 1.0);
                llMessageLinked(LINK_THIS, playchannel, "bird", "");
            }
        }    
    }
    

 If you touch the prim, we will bring up the dialog box menu :

 


    touch_start(integer total_number)
    {
        if (! Runtime && llDetectedKey(0) == llGetOwner()) 
        {
            MakeMenu();
        }
    }
    

When you touch the dialog box entry, or type text into chat, then the listen() event is fired. Each of these if statements will handle a different button.

 


    listen( integer channel, string name, key id, string message )
    {
        if (channel == dialogchannel)
        {
            if (message == "Reset")
            {
                masterlist = [];
                MakeMenu();
            }
            else if (message =="Pause")
            {
                masterlist += [curranimation];
                masterlist += [-1];			// pause 1 second
                masterlist += [<0,0,0>];		// null vector
                masterlist += [<0,0,-0,1>];		// null rotation
                masterlist += ["Pause"];
                masterlist += [""];
                MakeMenu();
            }
            else if (message == "RemoveAll")
            {
                llMessageLinked(LINK_SET,0,"Remove","");
                Runtime++;
            }
            else if (message == "Record")
            {
                PrimsCounter = 0;
                timercounter  = 0;
                llSetTimerEvent(1.0);
                llMessageLinked(LINK_SET,0,"Set","");
                MakeMenu();
            }
            else if (message == "Name")
            {
                llOwnerSay("Type the current animation name on channel /" + (string) dialogchannel);
                wantname++;
                MakeMenu();
            }
            else if (message == "PlayBack")
            {
                PlayBack(curranimation);
                MakeMenu();
            }
            else if (message == "Dump")
            {
                DumpBack();
                MakeMenu();
            }
            else if (wantname)
            {
                curranimation = message;
                MakeMenu();
                Menu += [message];
                llOwnerSay("Recording is ready for animation '" + curranimation + "'");
                llOwnerSay("Position all child  prims, then select the Menu item 'Record'. 
                When finished, click 'PlayBack' to play back the animation, or click 'Name' 
                to record a new animation,
                or click 'RemoveAll' to finish and shut down all child scripts");
                wantname = 0;
                PrimsCounter = 0;
                llMessageLinked(LINK_SET,0,"All","");
                timercounter  = 0;
                llSetTimerEvent(1.0);
                
            }
            else
            {
                if (debug)  llOwnerSay("Possible Animations:" + llDumpList2String(Menu,","));
                if (llListFindList(Menu,[message]) > -1)
                	PlayBack(message);
            }
                
        }
    }

Messages from other scripts are sent to the animator by llMessageLinked. If a message is sent on the 'playchannel', the script will animate the listed animation.   A simple script , such as a person sensor, collision detector, or other trigger can then play back any recorded animation.

The other possibility is that you are teaching the script new movements.   Messages that arrive on the 'linkchannel' are stored in the master animation list.

 



    link_message(integer sender_num, integer num, string message, key id) 
    {       
        if (num == playchannel)
        {
            if (debug) llOwnerSay("playback animation " + message);
            PlayBack(message);
        }
        
        else if (num == linkchannel)
        {
            PrimsCounter++;
            
            list my_list = llParseString2List(message,["|"],[""]);
            if (debug) llOwnerSay(llDumpList2String(my_list,","));

            string sprimpos  = llList2String(my_list,0);
            string sprimrot = llList2String(my_list,1);
            primpos = (vector) sprimpos;
            primrot = (rotation) sprimrot;
            if (llStringLength(curranimation) > 0)
            {
                masterlist += curranimation;
                masterlist += sender_num;
                masterlist += primpos;
                masterlist += primrot;
                masterlist += ""; // sounds
                masterlist += "";
                integer count = llGetListLength(masterlist) / STRIDE;
            }
        }
    }    
    
    

Clicking 'Record' will set off a flurry of Link Messages. This timer captures the number of expected messages and displays the total as they arrive.

 


    timer()
    {
        integer left = nPrims - PrimsCounter; // how many left to report in
        if (left)
            llOwnerSay((string) left + " remaining of " + (string) nPrims);
        else
        {
            llSetTimerEvent(0.0);
            llOwnerSay("Ready");
        }
            
        if (timercounter++ > 5)
        {
            timercounter = 0;
            PrimsCounter = 0;
            llOwnerSay("Giving up");
            llSetTimerEvent(0);
        }
    }
}

 

How to animate a multi-prim creature

Now that you have you script running,  click the whale. You should get a menu with several commands:

The menu buttons have the following operation:

Reset - Wipe out all recordings

Record - remember all child prim positions

PlayBack - playback all prim positions

Name - give a name to the current animation

Dump - print all animations to chat.  You will need to copy and paste this into the note card "Movement"

RemoveAll - deletes all scripts in child prims related to motion learning

Pause - insert a 1 second pause command into the note card

The first thing you must do is to click 'Reset'.  This step makes the script forget any values it may have read at start-up. This will wipe out any RAM-based  note card data.  The note card will still be inside our object and it will not be erased.   

Click the 'Name' button.  The system will respond with "Type the current animation name on channel /601" or some other random channel number.  Type in main chat the words '/60x up'.    This will name the first animation.

Now you can edit your whale and position the flukes, tail, and fins to the 'up' position.   Position all child prims, then select the Menu item 'Record'.  You can repeat this step to move our fins in small increments, or just move them once, insert a pause, and so on.

When finished, click 'PlayBack' to play back the animation.

The whale script needs two animations, 'up', and 'down'.   Click the Name button again, type in /60X down', and move the whale tail and prims for the tail down position.    Click Record.     You can play back the 'down' animation and the up animation by clicking the new buttons that appear in the menu.  If you make a mistake, just click Reset, and start over.

Once you are satisfied with the up and down positions, click 'Dump'.  A long list of our edits will be printed into chat.  You must copy this chat and paste it into the movement notecard.  You can reset the script and watch it load the notecard. each part of the fish will move and rotate into position.  This will happen slowly because of the very slow way that Second Life reads note cards.  Once it has loaded, you can click the whale, click 'Name' and name an animation ('up' or 'down'), then click Playback to watch it move.

Option:

There are two options for the notecard: chatted text, and play a sound.   Each line of the notecard consists of the following items:

junk | prim number | position | rotation | Chatted text | UUID or sound file name|

A sample looks like this:

[21:01] Object: |up|9|<-2.24379, -1.58608, -8.90106>|<-0.47732, -0.23147, 0.70642, 0.46857>|hello!|hello.wav
[21:01] Object: |up|10|<-2.46843, 1.23117, -8.86807>|<-0.13555, -0.43806, 0.53353, 0.71069>

You can make the whale speak and play sounds by adding the text or sound UUID to the notecard.

Chatted text:

To add chatted text, add a pipe and text like this:   |text||  to the end of any line.  When the script runs that line of animation, it will chat the word 'text' into main chat.  You can change the next script to chat this command into a private channel, and control objects, such as a bridge that can raise, or a boat that can toot when the whale passed by the boat. 

Sounds:

If you add a UUID of a sound file, or add the sound file to the root prim, then  you can play them on command. Just put them at the end of the chat text:  |toot toot|b5af85b9-8b85-43ca-842a-f36a64d648a2|  with a pipe at the end

When you are totally satisfied with the whale flippers, click 'RemoveAll' to finish and shut down all child scripts. You can also delete the Prim Learning script.

Next - > Part 5- teaching the fish to swim  - putting it all together

Back to the Best Free Tools in Second Life and OpenSim.