Showing posts with label second life. Show all posts
Showing posts with label second life. Show all posts

Wednesday, September 1, 2010

Second Life Game Tile Testing


I've been messing around with game tile systems again using second life. Looking at the rudiments of a table top game like checkers, chess or even something as complex as monopoly. Trying to figure out how to develop something that doesn't use physics, but uses a tile based movement system.

I started by building a blank table top and adding a piece to it. Right now, the piece simply moves from corner to corner when someone clicks on the table. Under the hood there is a lot more going on than you might first think.



This is the code in the init script in the table.

default
{
state_entry()
{

//list bb = llGetBoundingBox(llGetKey()); // get my bounding box
vector sz = llList2Vector(llGetPrimitiveParams([PRIM_SIZE]),0);
llMessageLinked(LINK_ALL_OTHERS,1,(string)sz,NULL_KEY);
}

touch_start(integer total_number)
{
//llSay(0, "Touched.");
llSay(1024,"move");
}
}

You might as why did I comment out llGetBoundingBox. The reason is it works great when the table is by itself, but once the piece is on the table, the piece's location is included in the bounding box. This makes the box larger than expected.

So, the script get's the actual prim size from the table top and passes it to all the other linked objects (in this case the "disc" using a linked message. The identifier of the message is the number "1" which in this case means "table size", and the size (vector) is passed as a string.


You can also see that when the table is touched, it says "move" on channel 1024 which you can guess that the "disc" is listening for. Why not use llMessageLinked for both communications? We'll I should, but the "move" message was added early on and I started using linked messages later and have to get back to change to use llMessageLinked.


This is the code for the disc

vector mysize;
vector mysizediv2;
vector tablesize;
vector min;
vector max;
integer posMinOrMax;

default
{
state_entry()
{
//list bb = llGetBoundingBox(llGetLinkKey(1)); // get the bounding box of the table
//max = llList2Vector(bb, 1); // max corner
//min = llList2Vector(bb, 0); // min corner
//min = >min.x,min.y,max.z<;

//llWhisper(0,(string)min+" "+(string)max);

posMinOrMax = 0;

llListen(1024,"",llGetLinkKey(1),"move");
}


listen( integer channel, string name, key id, string message )
{
if (channel==1024)
{
if (posMinOrMax==0)
{
llSetPos(max);
//llWhisper( 0, "max" );
}
else
{
llSetPos(min);
//llWhisper( 0, "min" );
}
posMinOrMax = 1 - posMinOrMax;

}
}

link_message(integer sender_num, integer num, string str, key id)
{
if (num==1)
{
mysize = llList2Vector(llGetPrimitiveParams([PRIM_SIZE]),0);
mysizediv2 = mysize / 2;
mysizediv2.z = 0.0;

tablesize = (vector) str;
llWhisper(0,"got size = "+(string)tablesize);
vector sz = tablesize / 2;
min = >-sz.x,-sz.y,sz.z*2< + mysizediv2;
max = >sz.x,sz.y,sz.z*2< - mysizediv2;

}
}
}

You can see that early on I calculated the min and max positions based on the bounding box which I have already said is wrong. On the listen channel when I hear "move" I toggle the position between the min and max. That should be pretty straight forward.


The interesting stuff is in the link_message function. I first get the size of the "disc" using the llGetPrimitiveParams call. All positioning is done from the center of the object, so if I use the absolute min and max sizes of the table then the disc will hang off the edge. I divide the size by two because that is how far I have to inset the position from the table's min and max.

I then convert the table size vector from the string that was passed on the link message back to a vector and calculate the min and max positions using the inset of half the disc size. The min adds half the disc size and the max subtracts. The other thing is that the table's origin is at the center, and positioning the disc has to take that into account. So the min is the table position (center) subtracting half the size of the table. The max is the table position (center) and adding half the table size.

Wednesday, August 25, 2010


I had a discussion with another developer about the moonphase virtual sculpture. They mentioned a technique that might improve bandwidth usage by using llSetTextureAnim instead of setting the offset of the texture directly. The idea being that llSetTextureAnim would be running on the actual client which doesn't require any bandwidth, while llOffsetTexture is run on the server and all the clients viewing that object need to receive a message that the object change.

So I did some testing with llSetTextureAnim and I couldn't get the same results. When you use ROTATE in llSetTextureAnim then you can't specify the texture offset, only which portion of the texture you want to use. It seems like it is used to have a tiled texture where each section of the tile can be used as an individual tile. In my case, I used a half transparent texture and am rotating that around the sphere. Using llSetTextureAnim I don't see a way to get the same effect. I'm a little slow most of the time so I may be missing something. Here is a screen shot of using
default
{ state_entry()
{
llSetTextureAnim(ANIM_ON | LOOP | SMOOTH | ROTATE,ALL_SIDES, 1,1, 0, TWO_PI, TWO_PI/360);
}
}



I also tried using a non-ROTATE texture animation which only cause the texture to turn on and off when the "tile" reached the non-transparent section.

I really like the idea of reducing bandwidth by having the texture changes run on each client, but don't see a way to get this to work for this sculpture. llSetTextureAnim is definitely a good tool for the tool belt and I'll have to consider some sort of sculpture using a tiled texture. That would mean that each client would see something different on their machine so there couldn't be a discussion about the "current" look of the sculpture between two people, but I'm not sure that's a real problem.

Wednesday, August 11, 2010

lsl - Giving a notecard


I was wading back into the Second Life waters after volunteering to help build an art gallery for a group at work. I was also working on a couple of sculptures.

One of the things I wanted to add was to have my object give a notecard to someone who clicked on it.

This was simple.
First. Create a new notecard in your inventory.
Drag it into the content of the object you want to give that notecard.
Let's say it's called "Light Hand Sculpture - notecard"

Add this script and it will give the notecard to someone who clicks on your object.

default
{
state_entry()
{
}
touch_start(integer total_number)
{
llGiveInventory(llDetectedKey(0), "Light Hand Sculpture - notecard");

}
}


Here is the sculpture I was working on. It is on a private sim so I can't give you a link to see it. Sorry.


"Light Hand"

Tuesday, August 10, 2010

Traded land

The land I was using had all the land around it purchased by one person who was trying to create a cohesive sim. I was the last hold out and not because I was being stubborn, I just haven't logged on for a long time because I've been focused on XNA. I agreed to trade the land for something comparable. It turned out this was pretty simple for her to find and I traded my land.

New location:

Monday, August 17, 2009

Class preparation

I've been putting all my extra time into a class I will be teaching at Chapman University next semester. We'll be using XNA and C# to build an open source game engine. I have the project all setup, the training and managment of subversion is ready and I have a pretty good outline and a good direction for the engine. I've done a few games using XNA, but for the tool side needed to figure out the WinForms integration and found these microsoft sites on WinForms/XNA to be very helpful.

I'm a blender fan, but I really need to train people on 3dsMax. I purchased an edu copy a year ago (still way to expensive) and I'm finally forcing myself to use it. I've been going through the tutorials and think it will be a good way to learn max. I also need to get through all the skeleton animation stuff so I know how to do it in Max. I've done it with Blender, but for this class I really need to use the tools that we have on the computers in class.

It starts in two weeks. I have a ton of material so far, but there is still a lot to go over.

I also went back and looked at the pinball game in second life and I have been itching to do some more work on it. I finally think I know how to fix the bumper mechanic and add a little skill to the game. Too much going on at this point, but hopefully the idea can slowly rise to the top of the queue and I can do some more work in SL since I enjoy that environment.

Monday, August 25, 2008

Second Life Physics Scripting #28 - disconnecting users

I arrived at the game to find that it would not let me start because it thought someone else was still playing.

This is the code that wasn't allowing me to join.


touch_start(integer total_number)
{
if (avatar_attached)
{
llSay(0, "Currently allows only one player at a time. Sorry.");
return;
}

But the real problem was in the avatar_attached flag not being reset. The user must have logged out without hitting the release keys button so I never go the disconnect event. There doesn't seem to be anyway to drop those connections so I need to at least pay attention to their distance from the table.

This is the new timer code.

timer()
{
....

// see if we still have control
if (avatar_attached)
{
if( !(llGetPermissions() & PERMISSION_TAKE_CONTROLS) )
{
auto_tick_count = 0;
avatar_attached = FALSE;
}
else
{
// check the distance to the avatar...
key curkey = llGetPermissionsKey();

list temp;
vector pos;
vector pos2;

temp = llGetObjectDetails(curkey,[OBJECT_POS]);
pos = llList2Vector(temp,0);

pos2 = llGetRootPosition();

float dist = llVecDist(pos,pos2);
llOwnerSay("dist = "+(string)dist);
if (dist>15)
{
llSay(0,"Player too far away - resetting");
auto_tick_count = 0;
avatar_attached = FALSE;
llRequestPermissions(avatar_key, 0);
avatar_key = NULL_KEY;
}
}
}
}

I get the key of the avatar that is currently attached. Then I check that avatar's distance. If they are too far away, I reset the avatar attached key. There was still one more problem if they walked back into the area then they could still send control events so I added some extra code to runtime permissions to save the last player that attached.


run_time_permissions(integer perm)
{
// permissions dialog answered
if (perm & PERMISSION_TAKE_CONTROLS)
{
// we got a yes
// take up and down controls
llTakeControls(CONTROL_ROT_LEFT | CONTROL_ROT_RIGHT | CONTROL_UP | CONTROL_DOWN, TRUE, FALSE);
// remove any balls on the table
llSay(1296,"byebye");
in_play = FALSE;

avatar_attached = TRUE;
avatar_key = llGetPermissionsKey();
RestartGame();
}
}


And then to the control code to double check the current key with the key sending the control event.


control(key id, integer held, integer change)
{
if (id!=avatar_key)
{
list temp = llGetObjectDetails(id,[OBJECT_NAME]);
string nm = llList2String(temp,0);

llSay(0,nm+", you have been disconected, press the Release Keys button and touch the pinball machine again.");
return;
}

I wanted to send them the message directly and probably can with an IM, but for now it was just easier to send it with their name attached to identify that the message was to them.

Friday, August 22, 2008

Second Life Physics Scripting #27 - New Artwork

I finally found a screen shot I liked for the base table, then used my own avatar for the upper scoreboard. Not that my avatar is anything special, it's about as plain as you can get on the day you are SL born except for the customer t-shirt. My thought is that the simple avatar looks even more second life so will hopefully add to the effect? Probably just looks cheesy.

I also finally got really tired of the default plywood texture and made a red to yellow gradient for the bumpers and used a neon blue for the side walls. It may be overkill and too simplified looking, but I think it is better than it was. I need to do a better layout with the bumpers, get some sound and particle effects on the bounces and add some bigger scoring elements. It will probably never be 'done'.

Thursday, August 21, 2008

Second Life Physics Scripting - Pinball #26 - The First Player

I put in some logging to see if anyone was playing the actual game. I had my first player (Grimley Graves) and asked him if the game was actually working. He owns a haunted house down the street which is how he found the game. Sure enough, the game didn't work at all. He offered to help me test it and I found the problem.

The problem was in the keyboard code from post #12. This is what I had.


llRequestPermissions(llGetOwner(), PERMISSION_TAKE_CONTROLS);


It turns out that the first parameter is the person who's keys you want to take over. Sure enough since I'm the owner and was neaby when he touched the table, I received the dialog asking if I wanted to play. Oops.

This is the correct code.


llRequestPermissions(llDetectedKey(0), PERMISSION_TAKE_CONTROLS);


The llDetectedKey(0) is the key for the first avatar that touched the table. Once I made this change everything worked great. I went back and changed post #12 to reflect this difference so someone doesn't have this same problem later.

Wednesday, August 20, 2008

Second Life Physics Scripting - Pinball #25 - A Bit Of Art

I played around with the art a little today, just using the standard sunset images that come with every second life inventory. I'm planning on making some images of a bunch of people and trying to use those.

I added some code to see if anyone actually plays the game. This just saves each name to a list and I listen for a specific chat and print out the list if the machine hears me.


list player_list;

integer isNameOnList( string name )
{
integer len = llGetListLength( player_list );
integer i;
for( i = 0; i < len; i++ )
{
if( llList2String(player_list, i) == name )
{
return TRUE;
}
}
return FALSE;
}


state_entry()
{
...
listen_handle = llListen( 0, "", llGetOwner(), "" );
...
}
touch_start(integer total_number)
{
...
string detected_name = llDetectedName( 0 );
//if( isNameOnList( detected_name ) == FALSE )
if( detected_name != "Wood Wheels" )
{
player_list += detected_name;
}
}
listen(integer channel, string name, key id, string message)
{
...
else if ((channel==0) && (llToLower(message)=="players"))
{
llSay( 0, "Player List:" );
integer len = llGetListLength( player_list );
integer i;
for( i = 0; i < len; i++ )
{
llSay( 0, llList2String(player_list, i) );
}
llSay( 0, "Total = " + (string)len );

llSay(0," High Score - "+highscore_name+" - "+(string) highscore);
}
}

It has been a few days since that time and no one has played it. I did attract some sort of interest tough because the previously empty land above me has added an advertising post. I wouldn't normally mind, but since it had some sort of porn ad I placed a brick wall in front of it.



I need to do some work on the art and layout and then add some more game play elements, like scoring better than 1 point per bumper bounce.

Tuesday, August 19, 2008

Second Life Game Scripting - Pinball #24 - Game Play

I finally made it to the point where I could add what I call "Full Circle Game Play". This is where the player is given a limited opportunity, score is kept and the game restarts when they are out of resources.

In this case I decided to place this code in the PayAndKeyboard script I use to keep track of the keyboard and will add the pay to later if people start to play.


integer balls_remain;

string curscore_name;
integer curscore;
integer scoreboard = -1;

string highscore_name = "Wood Wheels";
integer highscore = 27;

First the variables that are used and a startup high score. Since this score will be reset each time I recompile the script I've decided to edit this entry by hand when I make changes so then I can at least keep a longer term high score. I wonder who will be the first to beat my high score? I'll post their name here.

Then I added a restart game method in the PayAndKeyboard script because this will be called from two places. When they run out of balls and when they first start the game.

RestartGame()
{
llSay(0,"Game Restarting");

llSay(0,"Current High Score - "+highscore_name+" - "+(string)highscore);

balls_remain = 5;
llSay(0, "You have "+(string)balls_remain+" balls remaining");

curscore = 0;
llMessageLinked(scoreboard,MSG_SET_NUM,(string)curscore,NULL_KEY);
}

That link message was part of the original scoreboard and I'm just resetting the score to 0 for the game.

From the keyboard code, the run_time_permissions is called after we are given permission to listen to the keys.

run_time_permissions(integer perm)
{
// permissions dialog answered
if (perm & PERMISSION_TAKE_CONTROLS)
{
// we got a yes
// take up and down controls
llTakeControls(CONTROL_ROT_LEFT | CONTROL_ROT_RIGHT | CONTROL_UP | CONTROL_DOWN, TRUE, FALSE);

// remove any balls on the table
llSay(1296,"byebye");
in_play = FALSE;

avatar_attached = TRUE;

RestartGame();
}
}

In this code we first tell any ball currently on the table to go away. This is the same message sent by the back wall and the ball is listening for this.

We also call RestartGame to kick off the actual game play. We set the avatar_attached flag after the "byebye" because this has logic issues when this script is actually listening for this exact message.

There is one other place where we can restart a new game. This is where the

if ((channel== 1296) && (llToLower(message) == "byebye"))
{
if (avatar_attached)
{
balls_remain= balls_remain - 1;
llSay(0,"Oops, drain. "+(string)balls_remain+" balls remain");
if (balls_remain<=0)
{
if (curscore>highscore)
{
llSay(0,"HIGH SCORE!!!!!");
highscore = curscore;
highscore_name = curscore_name;
}
RestartGame();
}

llSay(0, "Press 'Page Up' to start another ball.");
}
else
{
llSay(0, "Press 'Page Up' to start another ball.");
}

in_play = FALSE;
}

This is the code that decrements the number of balls remaining, checks for high score and restarts the game if the number of balls is 0. This is why I reset the avatar_attached flag after sending "byebye". Note there I saved the player name curscore_name when the user first touched the table.


touch_start(integer total_number)
{
if (avatar_attached)
{
llSay(0, "Currently allows only one player at a time. Sorry.");
return;
}

....

string detected_name = llDetectedName( 0 );
curscore_name = detected_name;
}

Most touch_start samples iterate through the total number of touches. In this case it is only a one player game so I only use 0 as the player name since I'm going to ignore all others onece the avater is attached to the table and playing.

Oh, I almost forgot. I had to add one more message to the score board so it sends the current score to the root prim whenever the score changes. I then save that in the curscore. I thought about sending a message to the scoreboard to have it send the score back, but I was worried about the asynchronous nature of the communication and thought the control code would be much more complicated than simply sending it on each change.

This is the listener in the PayAndKeyboard script.

integer MSG_TOTAL = 4116; // current score from the scoreboard

link_message(integer from, integer msg_id, string str, key id)
{
if (msg_id == MSG_TOTAL)
{
//llOwnerSay("scoreboard says: "+str);
curscore = (integer) str;
}
}


This is the full link_message function in the scoreboard. The scoreboard is listening for message from the bumpers when they tell it to increment the score. And the root script sends the 0 score when the game starts (see above). Yes, that means that the PayAndKeyboard will set the curscore to 0, then call the scoreboard which will send that same score back to the root again. It's just one extra message and probably not a big deal.

Notice that this function now sends MSG_TOTAL to LINK_ROOT whenever it receives a message to change the score. I could probably have had this as a single call outside the if/else statement, but I'm always worried about some rogue message setting off the message or some future change not needing it and leaving it in that I just added the same line to both sections of the if/else.

link_message(integer from, integer msg_id, string str, key id)
{
//llOwnerSay("scoreboard received "+(string)msg_id);
if (msg_id == MSG_SET_NUM)
{
curval = (integer)str;
//llOwnerSay("setting num to "+(string)curval);
doSendNumbers();

// send the score to the root
llMessageLinked(LINK_ROOT,MSG_TOTAL,(string)curval,NULL_KEY);
}
else if (msg_id == MSG_INC_NUM)
{
curval = curval + 1;
//llOwnerSay("inc num to "+(string)curval);
doSendNumbers();

// send the score to the root
llMessageLinked(LINK_ROOT,MSG_TOTAL,(string)curval,NULL_KEY);
}
}

Wednesday, July 30, 2008

Second Life Physics Scripting - Pinball #23 - llRez and llDie

The next feature is to remove the ball when it hits the back wall and then to have it respawn when the user presses the page up key.

The first problem is that the ball has to remove itself. There doesn't seem to be a way to have a script remove another object.

I first set out to have the ball remove itself when it had a collision with the back wall. The problem with this is that the back wall is linked and the collision name had "pinball 1.4" which may change over time and that this collision only occured once when the ball was first placed on the table since it is all one linked set.

The next solution is to send a message to the ball when the back wall is hit and I did this with the chat system llSay.


if (llDetectedName(0) == "ball")
{
...
//llPushObject(llDetectedKey(0), pos3, <0,0,0>, FALSE);

llSay(1296,"byebye");

}

This was as simple as removing the push and adding the llSay. The next step is to have the ball listen for the message.

integer listen_handle;

default
{
state_entry()
{
listen_handle = llListen( 1296, "backwall", NULL_KEY, "" );
}
listen(integer channel, string name, key id, string message)
{

//llOwnerSay("ball listen, heard = "+name);
if ((channel==1296) && (llToLower(message) == "balldied"))
{
//llOwnerSay("Ball died!");
llDie();
}
}

}

The ball is listening for the object backwall to say "byebye" on channel 1296. At first I thought this would be too slow, but it seems very quick and the ball goes away before bouncing back onto the table.

Make sure you keep a copy of the ball in your inventory because when it dies you don't want to have to recreate the scripts over and over.

The next step is the spawning of the ball (llRez) when the user presses the page up key. I did this in the PayAndKeyboard script in root table object. This is the same script that handles the key presses to move the puck flipper.


RezBall()
{
if (!in_play)
{
in_play = TRUE;

// find the size of the this table
vector sz = llList2Vector(llGetPrimitiveParams([PRIM_SIZE]),0);

vector pos = llGetPos();
//llOwnerSay("pos = "+(string)pos);
vector up = llVecNorm(pos * llGetLocalRot());
up = up * ((sz.z/2) + 0.21); // add the size of the ball
//llOwnerSay("up = "+(string)up);

pos = pos + up;
//llOwnerSay("rez pos = "+(string) pos);

llRezObject("ball",pos,<0,0,0>,ZERO_ROTATION,0);
}
}

I wrote a separate function for the rez ball. This still has problems as I think it rezes the ball too low on the table, but it has a neat effect in that the ball sort of oozes out of the table. It needs looking into at some point.

Next is the event to rez the ball.

control(key id, integer held, integer change)
{
... puck flipper code

// page up key
if ((held&CONTROL_UP) && (change&CONTROL_UP))
{
if (!in_play)
{
llSay(0," here we go!!!");
}
RezBall();
}
}


Then we have to have a way to reset the in_play flag so the user can press the "page up" key again and have the ball rez. This is as simple as adding the listen method the same as we used for the ball. Both the ball and this PayAndKeyboard script will both hear the same message from the backwall.

state_entry()
{

listen_handle = llListen( 1296, "backwall", NULL_KEY, "" );
llSetTimerEvent(30);

... other initialization code
}

listen(integer channel, string name, key id, string message)
{
if ((channel==1296) && (llToLower(message) == "balldied"))
{
llSay(0,"Oops, drain. Press 'Page Up' to start another ball.");
in_play = FALSE;
llSetTimerEvent(30);
}
}

As you can see I also added some timer events. This is so the table is actually doing something when people first walk up to it. A teaser to get people interested. 30 seconds after every drain, then timer goes off. If the ball isn't already in play (because someone pushed page up), then a new ball is spawned.

timer()
{
if (!in_play)
{
llSay(0,"Autoplay starting");
RezBall();
}
llSetTimerEvent(0);
}

Tuesday, July 29, 2008

Second Life Physics Scripting - Pinball #22 - llRezObject ball

I'm beginning to think I can use a different blog entry title and may change to Second Life Game Scripting on the next one since I'm no longer really dealing with physics and have moved onto building the game elements?

The next step is to spawn the ball (llRezObject) when the game starts, and to remove the ball when it hits the back wall. I decided to make the first attempt fairly simple and just rez the ball in the center of the table when the up key is pressed. This seemed pretty straight forward and didn't seem to take very long. Reading up on how to do it took most of the time. Here is the code that I used to rez the ball from inventory.


control(key id, integer held, integer change)
{
... code for the flipper controls ...
if ((held&CONTROL_UP) && (change&CONTROL_UP))
{
if (!in_play)
{
in_play = TRUE;

llSay(0," here we go!!!");

// find the size of the this table
vector sz = llList2Vector(llGetPrimitiveParams([PRIM_SIZE]),0);

vector pos = llGetPos();
llOwnerSay("pos = "+(string)pos);
vector up = llVecNorm(pos * llGetLocalRot());
up = up * ((sz.z/2) + 0.21); // add the size of the ball
llOwnerSay("up = "+(string)up);

pos = pos + up;
llOwnerSay("rez pos = "+(string) pos);

llRezObject("ball",pos,<0,0,0>,ZERO_ROTATION,0);
}
}
}


Adding the ball to inventory was pretty simple. Select the root object and drag the ball from my inventory into the root object's contents. The contents are the same place all the scripts are stored. The first time I did this it took me a while to figure out that you could also store objects in the contents, not just scripts.

Next up I have to change the collision script on the back wall to derez the ball instead of bouncing it back. Then I'll have to send a message to the root prim so I can reset the in_play flag so the person can rez another ball. From there it's a matter of giving each player a set number of balls and keeping track of a high score. Getting closer to a full circle game....

Monday, July 28, 2008

Second Life Physics Scripting - Pinball #21 - Puck Flipper Collisions

Blackjack! No, pinball! I think we have a game! At least the mechanic of a game. This is the first time I've felt like this could actually be a little bit fun to play and I feel like I'm geting closer to something playable. Very cool.

Here is the puck flipper collision script.


collision_start(integer total_number)
{
if (llDetectedName(0) == "ball")
{
// positions are in global coordinates

// find the position of the collision...
vector colpos = llDetectedPos(0);
//llOwnerSay("colpos = "+(string)colpos);

// find the location of the puck prim
vector locpos = llGetPos();
//llOwnerSay("locpos = "+(string)locpos);

// subtract two to get local coordinates on the puck
colpos = locpos - colpos;
//llOwnerSay("colpos = "+(string)colpos);

// divide by the global rotation so we are back
// in the original build orientation
rotation locrot = llGetRot();
colpos = colpos / locrot;
//llOwnerSay("colpos after rot = "+(string)colpos);

// find the size of the wedge
vector sz = llList2Vector(llGetPrimitiveParams([PRIM_SIZE]),0);
//llOwnerSay("puck size = "+(string)sz);
// this size is pre-rotations. We are interested in the Y value

// find the width of the puck
float puckwid = sz.y/4;

// the start of the puck, origin is center so divide by 2
float puckstart = (curval * puckwid) - (sz.y/2);
float puckend = puckstart + puckwid + puck_overlap;
puckstart = puckstart - puck_overlap;

if ((colpos.y>=puckstart) && (colpos.y<=puckend))
{
//llOwnerSay("PUCK IN CURCOL "+(string) curval);

// the dir we what to pushin
vector pushdir = <1,0,0>;
pushdir = pushdir * locrot;
//llOwnerSay("push dir = "+(string)pushdir);

llPushObject(llDetectedKey(0), pushdir, <0,0,0>, FALSE);
}
else
{
// shoot it towards the back wall because
// if it is going slow and they get the puck
// under the ball it seems like it should shoot away
// the dir we what to pushin
vector pushdir = <-1,0,0>;
pushdir = pushdir * locrot;
//llOwnerSay("push dir = "+(string)pushdir);

llPushObject(llDetectedKey(0), pushdir, <0,0,0>, FALSE);
}
}
}


Since the last version I added the checks to see which portion of the puck flipper the ball is actually hitting. Just a simple division by the number of pucks in the texture and then a check to see if it is in the currently active portion of the puck.

After playing with it for a while I added the extra push towards the back wall if it isn't in the puck area. It seemed like I was able to move the puck under the ball a lot and it felt like it should shoot away, but there is really no easy way to tell where the ball is every move of the puck, so I decided to shoot it towards the back wall.

Up next, a ball launcher and a ball killer when it hits the back wall. From there, a game start and game end condition, then maybe some extras besides simple bumpers. Somewhere in there I'll have to move to some real art from the plywood. Very cool.

Friday, July 25, 2008

Second Life Physics Scripting - Pinball #20 - Complete Puck Flipper System

Is this ever going to end? 20 posts already and there still seems like a ton of game code to add. This next step was all about trying to figure out where the ball crossed what I'm now calling the "puck flipper". I just typed it in once and it stuck, but now that I look at it I don't think it's the best name since it seems like the brain really wants to swap the f and p. Maybe it's just my brain? Oh well, it's in the code now.

Removing the flipper and hooking up the new puck flipper to the machine was a pretty simple exercise and only took a few minutes.


Anyway, it was pretty easy to setup the collision system, but actually a little hard to figure out where it crosses when you think about it in larger terms. Look at this first step.


collision_start(integer total_number)
{
if (llDetectedName(0) == "ball")
{
// positions are in global coordinates

// find the position of the collision...
vector colpos = llDetectedPos(0);
//llOwnerSay("colpos = "+(string)colpos);

// find the location of the puck prim
vector locpos = llGetPos();
//llOwnerSay("locpos = "+(string)locpos);

// subtract two to get local coordinates on the puck
colpos = locpos - colpos;
}
}

This is pretty straight forward. Take the positions of the two elements and subtract their positions. This will give you the coordinates local to the puck. If you divide the y size by 4, this gives you the size of each puck positions. Multiply by the current puck and position and add a little bit in each direction and you can then compare that to the Y collision position.

Easy you say. Until you start to rotate the pinball machine around. I've built it aligned along the X axis, but if you rotate it 90 degrees into the Y axis then all the calculations will no longer work! You have to take this rotation into account. This next piece shows that code.


collision_start(integer total_number)
{
if (llDetectedName(0) == "ball")
{
// find the position of the collision...
vector colpos = llDetectedPos(0);
//llOwnerSay("colpos = "+(string)colpos);

// find the location of the puck prim
vector locpos = llGetPos();
//llOwnerSay("locpos = "+(string)locpos);

// subtract two to get local coordinates on the puck
colpos = locpos - colpos;
llOwnerSay("colpos = "+(string)colpos);

// divide by the global rotation so we are back
// in the original build orientation
rotation locrot = llGetRot();
colpos = colpos / locrot;
llOwnerSay("colpos after rot = "+(string)colpos);
}
}

Don't think I'm some sort of genuis and this just fell out of my brain. I spent at least an hour tweaking to get this right. For the longest time I had what I originally wrote colpos = colpos * locrot;. After I got my view port inside the pinball machine and dropped the ball at both edges a bunch of times I realized the Y values were giving opposite results when the table was rotated. I figured that using division of the rotation might swap the signs and sure enough it did and now I can move on.

I really like the puck mechanic so far and think it will be really effective. I think it will be even better if I used it with more than 4 positions. Given a bunch of positions it might even look just like a pong puck and allow for some really cool game mechanics. I've not seen anyone use this yet, so I'm excited to see this in action.

Thursday, July 24, 2008

Second Life Physics Scripting - Pinball #19 - Puck Flipper Mechanic

Since the flipper mechanic just wasn't going to work I've decided for a bar at the bottom of the table where you can move what area is currently active. Sort of like pong, but more primitive. I'm hoping that people will identify with it as a puck because it's attached to a pinball machine. I wanted to use a changing texture on a single primitive to increase the speed changes since there aren't the same delays for texture changes as there are for position and rotation. This is some quick temporary art I came up with as the texture.



The image is split into four positions that will bounce the ball if it hits in the current position. I offset a little into the other colors to make it a little easier and allow for you to hit things that were right on the borders of the colors.

I'm sliding this around on a cube face much like I did the number texture. There was nothing fancy when figuring out the offsets. Hit and miss and tweaking the u/v numbers on the texture editing. I did only apply this texture to a single side of a rectangular cube. You do that by toggling the "select texture" radio button on the editor. Here is the script that changes which color is currently active.


// texture offsets
// h = 0;
// v =
// 0.375 (red=1)
// 0.125 (blue=2)
// -0.125 (green = 3)
// -0.375 (yellow = 4)
integer curval = 1;
doOffset()
{
//llOwnerSay("offset ="+(string)ftmp);
if (curval<0)
{
curval = 3;
}
else if (curval>3)
{
curval = 0;
}
float foffset = 0.375;
if (curval == 1)
{
foffset = 0.125;
}
else if (curval == 2)
{
foffset = -0.125;
}
else if (curval == 3)
{
foffset = -0.375;
}
llOffsetTexture(0.0,foffset,ALL_SIDES);
}

default
{
state_entry()
{
doOffset();
}

touch_start(integer total_number)
{
curval++;
doOffset();
}
}


Wednesday, July 23, 2008

Second LIfe Physics Scripting - Pinball #18 - Integrated Flipper

Okay, after all that work on the flipper I tried to integrate it anyway just to see if the mechanice was salvagable. It was a complete bust. The collisions did not occur properly while the rotation was in progress and overall system did not behave at all like a pinball flipper. I tried to add the llPushObject code to the flipper when it was on only, but then trying to trap the ball with the flipper had all kinds of issues. Overall, it was a complete waste of time.


Just for fun, here is the complete flipper_rotate_listener script.


integer MSG_RIGHT_ON_NUM = 5115;
integer MSG_RIGHT_OFF_NUM = 5116;
integer MSG_LEFT_ON_NUM = 5117;
integer MSG_LEFT_OFF_NUM = 5118;

rotation start_rot;
vector start_pos;
integer orig_axis_flag = FALSE;
vector orig_axis;

integer on=FALSE;
integer cur_rotation=0;
integer max_rotation = -60;
integer rotation_tick = -20;
float timer_gap = 0.1;

float bumpspeed_waiting = 0.2;
float bumpspeed_on = 0.62;
float bumpspeed_off = 0.01;

default
{
state_entry()
{
start_rot = llGetLocalRot();
start_pos = llGetLocalPos();

state waiting;
}
}

state waiting
{
state_entry()
{
}

link_message(integer from, integer msg_id, string str, key id)
{
if (msg_id == MSG_RIGHT_ON_NUM)
{
llOwnerSay("flipper rotate on");
state rotate_on;

}
else if (msg_id == MSG_RIGHT_OFF_NUM)
{
state rotate_off;
}
}

} // state waiting

state rotate_on
{
state_entry()
{
llSetTimerEvent(timer_gap);
}

timer()
{
if (((rotation_tick>0) && (cur_rotation>max_rotation)) ||
((rotation_tick>0) && (cur_rotation>max_rotation)))

{
cur_rotation = cur_rotation + rotation_tick;

// what is currently pointing up in the z direction
// was originally in the -y direction.
if (!orig_axis_flag)
{
orig_axis_flag = TRUE;
orig_axis = llVecNorm(<0,-1,0> * llGetLocalRot());
}

// a rotation around the parents z normal
//rotation z_45 = make_quaternion(parent_norm,rotation_tick*DEG_TO_RAD);
rotation z_rot = llAxisAngle2Rot(orig_axis,rotation_tick*DEG_TO_RAD);

// we need to find the axis which is the heal of the wedge
// the original orientation of the flipper wedge is <0,0,1>
// i use -1 to point at the heal of the wedge
vector norm = <0,0,-1>;
// rotate that norm so it points towards the point of the wedge
norm = llVecNorm(norm * llGetLocalRot());
// find the size of the wedge
vector sz = llList2Vector(llGetPrimitiveParams([PRIM_SIZE]),0);
// get the center location of the wedge
vector flipper_pos = llGetLocalPos();
// find the offset distance from the center
vector offset = norm * (sz.z / 2);
// add that offset to the flipper_pos to get the axis
vector rot_axis = flipper_pos + offset;

// calculate the new rotation by adding the z axis rotation
// z_rot is the old name it is really z_plusang
rotation new_rot = llGetLocalRot()*z_rot;
// http://wiki.secondlife.com/wiki/Rotation
// set the new local rotation
llSetLocalRot(new_rot);

// lsl wiki solution for non center rotation
vector new_pos = rot_axis + ((flipper_pos - rot_axis) * z_rot);
llSetPos(new_pos);
}
else
{
// stop the timer and wait for the off event
llSetTimerEvent(0);
}
} // timer

collision_start(integer total_number)
{
//llOwnerSay("Collision start");
//llOwnerSay(llDetectedName(0) + " collided with me!");
if (llDetectedName(0) == "ball")
{
// need to find the direction from the backwall to the ball
vector pos = llGetPos();
list a = llGetObjectDetails(llDetectedKey(0), ([OBJECT_POS]));
vector pos2 = llList2Vector(a,0);
//vector pos3 = pos-pos2;
vector pos3 = llVecNorm(pos2-pos);
llOwnerSay("bump pos3 = "+(string)pos3);
//if (pos3.z>0) // it's not behind us
//{
//pos3.y = pos3.y*2;
//integer ra = (integer) llFrand(1.0)-1;

pos3 = pos3 * bumpspeed_on;
llOwnerSay("bump mag = "+(string)llVecMag(pos3));

//float mag = llVecMag(pos3);
//if (mag>maxspeed)
//{
// float magdev = maxspeed / mag;
// pos3 = pos3 * magdev;
//
// mag = llVecMag(pos3);
//}
//llOwnerSay("sidewall = "+(string)pos3);

llPushObject(llDetectedKey(0), pos3, <0,0,0>, FALSE);
//}

}
}

link_message(integer from, integer msg_id, string str, key id)
{

if (msg_id == MSG_RIGHT_OFF_NUM)
{
llSetTimerEvent(0);
state rotate_off;
}
}
} // state rotate_on

state rotate_off
{
state_entry()
{
// already in local coords since child prim
llSetPos(start_pos);
llSetLocalRot(start_rot);
cur_rotation = 0;
state waiting;
}
link_message(integer from, integer msg_id, string str, key id)
{

if (msg_id == MSG_RIGHT_ON_NUM)
{
state rotate_on;
}
}
} // state rotate_off


And the pay and keyboard script in the pinball parent.


integer right_flipper = -1;
integer left_flipper = -1;

integer MSG_RIGHT_ON_NUM = 5115;
integer MSG_RIGHT_OFF_NUM = 5116;
integer MSG_LEFT_ON_NUM = 5117;
integer MSG_LEFT_OFF_NUM = 5118;

default
{
state_entry()
{
integer current_link_nr = llGetNumberOfPrims();
// Check if it's more than one
if (1 < current_link_nr)
{
// avatars sitting on us get added at the end, so subtract...
while (llGetAgentSize(llGetLinkKey(current_link_nr)))
--current_link_nr;

while(current_link_nr>0)
{
if (llGetLinkName(current_link_nr)=="right_flipper")
{
llOwnerSay("found right_flipper: "+(string)current_link_nr);
right_flipper = current_link_nr;
}
else if (llGetLinkName(current_link_nr)=="left_flipper")
{
llOwnerSay("found right_flipper: "+(string)current_link_nr);
left_flipper = current_link_nr;
}
current_link_nr--;
}
}
}

run_time_permissions(integer perm)
{
// permissions dialog answered
if (perm & PERMISSION_TAKE_CONTROLS)
{
// we got a yes
// take left, right, up and down controls
llTakeControls(CONTROL_ROT_LEFT | CONTROL_ROT_RIGHT | CONTROL_UP | CONTROL_DOWN, TRUE, FALSE);
}
}

touch_start(integer total_number)
{
llSay(0, "Would you like to play? I'll need to take over your keyboard.");
llSay(0, "This doesn't work yet, it's still under construction.");
llSay(0, "The only key that works is 'd' and even then, the flipper is off the actual table, getting closer and there is only one of them...");
llSay(0, "To get your keys back simply click on the Release Keys button just above the Fly button");

integer perm= llGetPermissions();

if (!perm&PERMISSION_TAKE_CONTROLS)
{
llRequestPermissions(llGetOwner(), PERMISSION_TAKE_CONTROLS);
// get permission to take controls
}
else
{
llSay(0,"We have permission to take control...trying");
llRequestPermissions(llGetOwner(), PERMISSION_TAKE_CONTROLS);
//llTakeControls(CONTROL_LEFT| CONTROL_RIGHT | CONTROL_UP | CONTROL_DOWN,
// TRUE, TRUE);
}
}
control(key id, integer held, integer change)
{
//llSay(0, "control id="+(string)id);
//llSay(0, " held="+(string)held+" change="+(string)change);
//llSay(0, " ROT_RIGHT"+(string)CONTROL_ROT_RIGHT);
if ((held&CONTROL_ROT_RIGHT) && (change&CONTROL_ROT_RIGHT))
{
//llSay(0," right flipper on");
llMessageLinked(right_flipper, MSG_RIGHT_ON_NUM, "", NULL_KEY);

}
else if (((held&CONTROL_ROT_RIGHT)==0) && (change&CONTROL_ROT_RIGHT))
{
//llSay(0," right flipper off");
llMessageLinked(right_flipper, MSG_RIGHT_OFF_NUM, "", NULL_KEY);
}

}
}

Tuesday, July 22, 2008

Second Life Physics Scripting - Pinball #17 - Flipper Rotations Working

Rotations, what a nightmare. A nightmare of my own making, but I finally got the problem solved and it looks like I mostly had confused myself, but after lots of reading and a thousand tests I finally got the flipper to rotate on the test table.



Here is the final code for the timer routine that rotates the flipper in x number of steps.


timer()
{
if (((rotation_tick>0) && (cur_rotation<max_rotation)) ||
((rotation_tick<0) && (cur_rotation>max_rotation)))

{
cur_rotation = cur_rotation + rotation_tick;

// what is currently pointing up in the z direction
// was originally in the -y direction.
if (!orig_axis_flag)
{
orig_axis_flag = TRUE;
orig_axis = llVecNorm(<0,-1,0> * llGetLocalRot());
}

// a rotation around the parents z normal
//rotation z_45 = make_quaternion(parent_norm,rotation_tick*DEG_TO_RAD);
rotation z_45 = llAxisAngle2Rot(orig_axis,rotation_tick*DEG_TO_RAD);

// we need to find the axis which is the heal of the wedge
// the original orientation of the flipper wedge is <0,0,1>
// i use -1 to point at the heal of the wedge
vector norm = <0,0,-1>;
// rotate that norm so it points towards the point of the wedge
norm = llVecNorm(norm * llGetLocalRot());
// find the size of the wedge
vector sz = llList2Vector(llGetPrimitiveParams([PRIM_SIZE]),0);
// get the center location of the wedge
vector flipper_pos = llGetLocalPos();
// find the offset distance from the center
vector offset = norm * (sz.z / 2);
// add that offset to the flipper_pos to get the axis
vector rot_axis = flipper_pos + offset;

// calculate the new rotation by adding the z axis rotation
// z_45 is the old name it is really z_plusang
rotation new_rot = llGetLocalRot()*z_45;
// http://wiki.secondlife.com/wiki/Rotation
// set the new local rotation
llSetLocalRot(new_rot);

// lsl wiki solution for non center rotation
vector new_pos = rot_axis + ((flipper_pos - rot_axis) * z_45);
llSetPos(new_pos);
}
else
{
// stop the timer and wait for the off event
llSetTimerEvent(0);
}
} // timer

It turns out that the basic problem I was having was that I didn't think there was a llSetLocalRot because I already knew there wasn't a llSetLocalPos. So that final call to llSetLocalRot was llSetRot for the longest time and this added a little extra y rotation which caused the flipper to lean in the wrong direction. This was a ton of time to actually find this problem, but rotation can be like that and this is really just another bug in a long development process.

The crazy part is that I continued to work on this even though I knew the mechanic was not going to work. The flipper just takes too long to rotate each step and I don't want a flipper that only has two angles of bounce. Very annoying that there is a lot of work there and the api/engine doesn't support the mechanic I want to use, but then this is the way in software development. You have to make your designs fit the platforms you use and hope people make good platforms. SL is good, but there are a number of changes I would like to see made, but I don't think that is any different than any other api/platform.

I'd rather not see them do the kitchen sink stuff that was done with Java and basically ruined a very nice small system. Then again, they ignored the client side and just gave it up to flash which was a huge mistake that can't really be addressed at this point because now they made the foot print way too big to be a client side platform. I still love Java, I just think Schwartz badly mismanaged it just like he has mismanaged Sun. Sorry for the short rant.

So I think I'm going to use textures to do flippers flat near the back of the machine. We'll see how that turns out next.

Monday, July 21, 2008

Second Life Physics Scripting - Pinball #16 - Flipper rotations using Quaternions

I messed around a little bit with the flipper when "test" table was tilted and got a little frustrated. I thought to myself, this would be a lot easier if this system had Quaternions instead of just a simple llEuler2Rot function. I went to look for a solution and sure enough, this is exactly what the Linden's did originally. The lslwiki even has a special page for it.

http://www.lslwiki.net/lslwiki/wakka.php?wakka=quaternions

Quaternions may sound scary, but they are really very simple. You create a unit vector (a vector of length=1) and the quaternion is a rotation around that vector. Pretty simple for a scary sounding word.

Oh, and after some more digging and using the make_quaternion function listed on the lslwiki, I found the note that this is exactly the same as the llAxisAngle2Rot function included in Second Life. Exactly what I was looking for and named a lot less scary that quaternion.

Friday, July 18, 2008

Second Life Physics Scripting - Pinball #15 - Flipper rotation and llSetPos problems

Spent a ton of time trying to get the flipper to rotate off axis.

At one point the llSetPos stopped working and I went back and created a test system to try and figure it out.



I kept calling llSetPos and nothing would move at all. This had to do with the requirement of calling llSetPos if you want to rotate off axis. Being my own worst enemy, I had done a test and changed some calls from llGetLocalPos to llGetPos. While there is no llSetLocalPos, the change from llGetLocalPos was what caused the problem. Since the flipper is a linked prim, the coordinates must be changed in local coordinates otherwise the world coordinates were off the map and nothing moved.

Then I also found the problem with the off axis rotation. Here is the final piece of code, with all of my debugs and junk code just so you can see how many interation tests went into it. This is from the link_message method outlined in the previous post.


// a rotation of 45 degrees around the z-axis
rotation z_45 = llEuler2Rot( <0, 0, -45 * DEG_TO_RAD> );
// the tilt of the table
rotation y_352 = llEuler2Rot( <0, 352 * DEG_TO_RAD, 0>);

// the original orientation of the flipper wedges is <0,0,1>
// i use -1 to point at the heal of the wedge
vector norm = <0,0,-1>;
// rotate that norm so it points towards the point of the wedge
norm = llVecNorm(norm * llGetLocalRot());
//norm = llVecNorm(norm * llGetRot());

// find the size of the wedge
vector sz = llList2Vector(llGetPrimitiveParams([PRIM_SIZE]),0);
llOwnerSay("size = "+(string)sz);
vector flipper_pos = llGetLocalPos();
vector offset = norm * (sz.z / 2);
vector rot_axis = flipper_pos + offset;
//vector rot_axis = start_pos + offset;

llOwnerSay("flipper pos="+(string) flipper_pos);
llOwnerSay("rot_axis ="+(string) rot_axis);
llMessageLinked(dbg_marker, 1,(string)rot_axis, NULL_KEY);
//llOwnerSay("distance from center to heal = "+(string)offset);
llOwnerSay("get rot = "+(string)llGetRot());
llOwnerSay("get local rot = "+(string)llGetLocalRot());
rotation new_rot = llGetLocalRot() * z_45; // compute global rotation
//new_rot = new_rot * norm;
// http://wiki.secondlife.com/wiki/Rotation
// find the rotated norm

vector rotated_offset = offset * new_rot;
llOwnerSay("new rot = "+(string)new_rot);
llSetRot(new_rot);

// ll wiki method
//vector rotatedOffset = offset * z_45; // rotate the offset to get the motion caused by the rotations
//vector newPos = llGetPos() + (offset - rotatedOffset) * llGetRot(); // move the prim position by the rotated offset amount
//rotation newRot = rot6X * llGetRot();

//llOwnerSay(" cur pos = "+(string)llGetLocalPos());
// lsl wiki solution
vector new_pos = rot_axis + ((flipper_pos - rot_axis) * z_45);
//vector new_pos = rot_axis + ((flipper_pos - rot_axis) * new_rot);
//vector new_pos = rot_axis + (offset * new_rot);
//vector new_pos = llGetLocalPos() - (offset * llGetLocalRot());
//vector newPos = llGetLocalPos() + (offset - rotatedOffset) * llGetRot()
//new_pos.x = new_pos.x + 2;
llOwnerSay(" new pos = "+(string) new_pos);
llSetPos(new_pos);
//llOwnerSay(" after = "+(string)llGetLocalPos());


After all that, the problem ended up being this one line.


vector new_pos = rot_axis + ((flipper_pos - rot_axis) * z_45);
//vector new_pos = rot_axis + ((flipper_pos - rot_axis) * new_rot);

As you can see I was multiplying the offset by the new_rot which includes some of the initial rotations of the flipper. When all I really had to do is rotate it by the z_45 which is the change in rotation. This doesn't include the slight rotation of the table, but I'll be adding that as well soon. For now I'm going to do some more work on this test block first.

This still doesn't work all that well and I don't think it will behave like a pinball machine because the inbetween stages will not hit the ball properly. I'm going to make this a lot more complicated and fold it into a timer method and try rotating in increments. Wish me luck.

Thursday, July 17, 2008

Second Life Physics Scripting - Pinball #14 - Flipper Rotation Off Axis Issue

I've spent way too much time trying to get the flipper to rotate properly. I'm not even sure if it will work properly when I do get it to rotate, but now I'm on a mission to get this working.

Basically there are two rotation pages.
http://www.lslwiki.net/lslwiki/wakka.php?wakka=rotation
http://wiki.secondlife.com/wiki/Rotation

The rotation from the center of the flipper works great, the off axis rotation is even harder. I think I spent a lot of time on one flipper that was rotated differently than I thought so I started over with a new flipper and the same code.

Basically, I need to calculate the base/heal of the flipper. I do this by looking at the initial orientation of the tip of the flipper and it was straight up in the z direction. I take the negative z vector and multiply it by the starting rotation which points me in the direction of the heal and convert it to a unit vector. I then multiply that unit vector by half of the z size which should give the the starting location of the heal offset from the center of the flipper. Tricky. That will be the point of rotation and that is what I've struggled with so far.

Both wiki's say it should be fairly straight forward, but I keep getting a rotational axis that is missing the mark.

Here is the current (not working code with a ton of test code). The test code is not offset so I can find it more easily.

link_message(integer from, integer msg_id, string str, key id)
{
if (msg_id == MSG_RIGHT_ON_NUM)
{
//llOwnerSay("right flipper on");
//llSetLocalRot(end_rot);
// a rotation of 45 degrees around the x-axis
rotation x_45 = llEuler2Rot( <0, 0, -45 * DEG_TO_RAD> );


// the original orientation of the flipper wedges is <0,0,1>
// i use -1 to point at the heal of the wedge
vector norm = <0,0,-1>;
// rotate that norm so it points towards the point of the wedge
norm = llVecNorm(norm * llGetLocalRot());

// find the size of the wedge
vector sz = llList2Vector(llGetPrimitiveParams([PRIM_SIZE]),0);
llOwnerSay("size = "+(string)sz);
vector offset = norm * (sz.z / 2);
vector rot_axis = start_pos + offset;
llMessageLinked(dbg_marker, 1,(string)rot_axis, NULL_KEY);
llOwnerSay("distance from center to heal = "+(string)offset);
llOwnerSay("get rot = "+(string)llGetRot());
rotation new_rot = llGetLocalRot() * x_45; // compute global rotation
//new_rot = new_rot * norm;
// http://wiki.secondlife.com/wiki/Rotation
// find the rotated norm
vector rotated_offset = offset * new_rot;
llOwnerSay("new rot = "+(string)new_rot);
llSetLocalRot(new_rot);

llOwnerSay(" cur pos = "+(string)llGetLocalPos());
vector offsettimesrot = (-offset*new_rot);
llOwnerSay("offset times rot ="+(string)offsettimesrot);
vector new_pos = rot_axis + (offset * new_rot);
//vector new_pos = llGetLocalPos() - (offset * llGetLocalRot());
//vector newPos = llGetPos() + (offset - rotatedOffset) * llGetRot()
llOwnerSay(" new pos = "+(string) new_pos);
llSetPos(new_pos);

}
else if (msg_id == MSG_RIGHT_OFF_NUM)
{
//llOwnerSay("right flipper off");
llSetLocalRot(start_rot);

// already in local coords since child prim
llSetPos(start_pos);
}
}

At this point I'm having trouble visualizing the actual origin that is being rotated, so I've created a thin black cylinder called "dbg_marker" and I'm going to position that what I calculate to be the origin of the heal.


As usual I can find the marker by traversing the link list.


test_find_marker()
{
integer current_link_nr = llGetNumberOfPrims();
// Check if it's more than one
if (1 < current_link_nr)
{
// avatars sitting on us get added at the end, so subtract...
while (llGetAgentSize(llGetLinkKey(current_link_nr)))
--current_link_nr;

while(current_link_nr>0)
{
if (llGetLinkName(current_link_nr)=="dbg_marker")
{
llOwnerSay("found dbg_marker: "+(string)current_link_nr);
dbg_marker = current_link_nr;
}
current_link_nr--;
}
}
}

The code to actually move the marker is yet another link message and some code in the marker. There really isn't a lot of api code to move another object to a specific location, you have to send a message to the object and have it move itself. Sort of a pain, but it forces you to separate out your logic which is a good thing.

Here is the code placed in the dbg_marker to move it to the location.

default
{
state_entry()
{

}

link_message(integer from, integer msg_id, string str, key id)
{
if (msg_id == 1)
{
vector vpos = (vector)str;
llOwnerSay("dbg_marker position"+(string)vpos);

llSetPos(vpos);
}
}
}


Then this final screen shot which shows that the location I calculated with all that crazy math was actually precisely on target.


It is obviously very very close to working, I just have a transformation out of order or something... Hmmm.... Welcome to my world.