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);
}
}

Monday, August 18, 2008

Back Home - Africa

We returned yesterday afternoon and it's back to work today.

It was another amazing experience. A ton of work and not much sleep, but it's always a great experience to serve others. We had the best team I have been on so far. I was on the team that was focused on the Academy. We taught classes during the day and ran an After School Program. There were a lot of teachers on the trip since it is summer here. I knew I would be teaching one computer class to each of the grades and didn't prepare beyond a few ideas and having downloaded some software I might need. I was just going to offer a few different topics and let the students decide which one they wanted the most. I could do any of the office tools, image editing (using Gimp), html and website creation or programming. Most of their current learning has been with Word so the office tools were out. They chose image editing so I helped them load Gimp.

All of the kids at the academy are orphans from the townships where Bridges Of Hope is working. The Bridges Of Hope model is away from state run orphanages (even the state sees orphanages as an unacceptable model) into a boarding school (the elite schools are boarding schools) where the students return to their communities when school is out. The townships are very dangerous and this may not be the best place for these students when school is out, but they still have the connection to their home and become role models for positive development. Turning what was a desperate situation for them into an opportunity to be a community leader.

They were pretty far behind in their computer skills so part of my goal was just to get them to click around on menus and try stuff. The first class was all about using the brush tools and learning to zoom in and out of a picture.

It looked like I was going to have a light load on Friday and I had fully planned to do some manual labor around the school. As I tried gather a few people the head of the school said the Computer teacher had failed to show and asked me to teach the classes. I spent another class on Gimp and showed them how to use selections to remove someone (me) from a picture and place me in another picture. This was a lot of fun and once they figured it out they were moving each other from picture to picture. I also showed them a few of the effects tools to change image colors and such. All and all it was a lot to pack in in an hour, but the students said they loved it and I did see them using Gimp outside of class which was nice.

The second week I had a few math classes with my wife. We taught Money math which included, saving, tithing, investing, and credit cards. We handed out monopoly money and asked them how they would each spend their first pay check. There were not many "savers", but after our class I'm sure most of them would have put away as much as possible and always fully pay a credit card bill. Some of the best lessons my father ever taught me. The only (nearly) guarnteed way to get rich is over a long time with a constant savings and a good interest rate.

I learned that the computer teacher had actuall quit and I got to teach four more classes on the second Friday. This time I knew the kids a lot better and had watched them all playing music from their thumb drives. I decided to teach them Audacity to make music. They really went crazy with this. It was hard because I tried to cram three weeks worth of classes into an hour, but by the end they were recording their own singing and creating rudimentary mixed tracks. They now have the software and enough knowledge to take it to the next level themselves.

For the afterschool program we helped put on a school play (High School Musical) and worked on it over the two weeks. They showed the play on our last night along with a talent show and it was a lot of fun. We also ran our "Choose To Wait" classes the second week. These are basically sex ed type classes where we talk about God's plan for sex and marriage. In an area that has 20-30% HIV/AIDS infection rates the best thing is to get young people to abstain from sex. Wear a condom if you are going to have sex, but don't expect that to fully protect you from HIV/AIDS or pregnancy. Even the condom box says that they are not 100% effective. I've taught this three times now in Africa and I went from being a partial skeptic to a full believer in the curriculum. It does an amazing job and I think that committing to abstain before my wife and I were married really helped us create a much deeper relationship. It is much different when you explain to kids why God wants us to only have sex inside of Marriage instead of just telling them to not have sex and giving no real reasons besides HIV/AIDS and pregnancy. Or just giving them a condom and falsely telling them they'll be safe as long as they use it.

The other teams spent a lot of time in the township called Sweet Home training leaders there on how to teach an Alcohol Abuse prevention and recovery program. They also taught first aid and classes on emotional wounds. The township we have adopedted is one of the poorest places I have ever been and it was exciting to see the foundation laid for a more positive future. It will take years and years, but I can see that there is potentional to change generations of neglect. The Life Wind model is all about community development with a focus away from relief. I have seen communities transformed by this process and I'm still excited to see more positive changes than what normally happens when we in the west simply throw money at problems and think our ideas will solve the needs that communities don't even recognize as needs. If you want to stop throwing money and time down the drain and truely make a change, Life Wind is an organization with a revolutionary model of community development.

Another team ran a VBS camp for the kids in the same township. I was there for part of two of the days and I would have to say that VBS is really just a bit of semi-controlled chaos.

Everyday was pretty much the same. Up at dawn, work all day, homework until 9pm, team meeting at 9pm then crash at midnight to do it all over again. We did get two days off. One was a trip to Robben Island to get acquinted with the CHE leaders from Sweet Home and the other was to a game drive where we saw the Big 5 animals from a jeep. It wasn't "in the wild", but it was a good time and a fun experience.

All in all it was a great first trip and the start of what looks like a long and fruitfull partnership. We relieved some of the staff at the academy for two weeks and help sow seeds of change in a very poor township.

Thursday, July 31, 2008

Two Week Break - Africa

I will not be posting for two weeks because I'm co-leading a missions/community development trip to South Africa. This is my third trip to Africa and my second as a leader. I originally wanted to make a difference in the HIV/AIDS pandemic and joined a trip sponsored by my church. Over the past few years it has become apparent that HIV/AIDS is really just a side-effect of extreme poverty. Last summer we searched for a new organization to work with and landed with Bridges Of Hope outside Cape Town. This will be our first team with this new partner ship.

We've since adopted Sweet Home, one of the poorest places I have ever been and a place with zero opportunity for residents. Bridges Of Hope has been working for the past five years in a neighboring community called Phillipe and the changes are miraculous. Their goal is to meet both the physical and spiritual needs of the community by raising up and listening to leaders in that community. Once the leaders have identified the needs we work on the top needs until that top need changes. It is a drastic change from the normal American solution where we come into a community and give them something they may or may not need, like a well, a sewage system, a road or a house. It is also different in that it is mostly development with relief when absolutely necessary. We help the community meet felt needs, not simply meet those needs for the community. This is done through training, micro-lending or just coming along side and helping to solve the biggest problems these communities face.

This is such a simple change, but has made a profound difference in the behaviour in both the community residents and we who are serving that community. It is an amazing process and I'm excited to see the changes in Sweet Home over the coming years.

If you would like to know more about the trip, we have a blog here. It will probably not be added to on our trip, but will be updated on our return.

We have also been looking at ways to bringing these principals to local communities where we server, including Santa Ana, Watts and Mexico.

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.

Wednesday, July 16, 2008

Second Life Physics Scripting - Pinball #13 - Flipper Rotation Issues

I've got the first flipper movement. It doesn't actually orient correctly or rotate around the correct axis, but it is movement and the rest is just some extra math.

I took the start rotation and end rotation ligning up the flipper where I wanted it, then running the script to see the rotation. This doesn't seem to work as the rotation is not the same as how I lined it up, but it's close and late at night and deserves to be a stopping point.


Here is the script in the flipper.

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 = <0.35240, 0.55950, 0.62163, 0.41994>;
rotation end_rot = <0.66946, 0.22398, 0.36898, 0.60458>;

default
{
state_entry()
{
//start_rot = llGetRot();
llOwnerSay("right rot = "+(string)llGetRot());
llSetRot(start_rot);
}

link_message(integer from, integer msg_id, string str, key id)
{
if (msg_id == MSG_RIGHT_ON_NUM)
{
llOwnerSay("right flipper on");
llSetRot(end_rot);
}
else if (msg_id == MSG_RIGHT_OFF_NUM)
{
llOwnerSay("right flipper off");
llSetRot(start_rot);
}
}
}


Here is the code in the main object that takes over the controls.

integer MSG_RIGHT_ON_NUM = 5115;
integer right_flipper = -1;

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

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;
llMessageLinked(right_flipper, MSG_SET_PARENT_ROT, (string)llGetRot(), NULL_KEY);

}
current_link_nr--;
}
}
}

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);
}
}

As you can see I'm using linked messages to tell the flipper to rotate. This is normal. Also, the control code is a separate script in the root object. Now my root object has 8 scripts. Blank Collision, Bumper, LinkNanny, LinkNanny-bumper, LinkNanny-sidewall, PayAndKeyboard, RootNanny, side-wall-bounce. LSL almost requires that you break up your code into different and more manageable scripts.

Tuesday, July 15, 2008

Simple Signpost

I created a simple signpost to advertise the blog. Here is a screenshot.


Here is the simple script that dumps the url into chat and allows them to click on it to view the page.


default
{
state_entry()
{
}

touch_start(integer total_number)
{
llSay(0, "For more information on this build, visit:");
llSay(0,"http://dev360.blogspot.com");
}
}

Monday, July 14, 2008

Second Life Physics Scripting - Pinball #12 - Keyboard Control

I was finally able to trap the keyboard and I wanted to post a code snipet because it should show it in a fairly simple way.
default
{
state_entry()
{
}

// this is called by llRequestPermission
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);
}
}

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.");

integer perm= llGetPermissions();

if (!perm&PERMISSION_TAKE_CONTROLS)
{
// get permission to take controls
// this was changed after the blog entry
// it needs to be called with detectedKey. During debugging
// you might still want to use llGetOwner to avoid others interupting.
llRequestPermissions(llDetectedKey(0), PERMISSION_TAKE_CONTROLS);
//llRequestPermissions(llGetOwner(), PERMISSION_TAKE_CONTROLS);
}
else
{
llSay(0,"We have permission to take control...trying");
// this was changed after the blog entry
// it needs to be called with detectedKey. During debugging
// you might still want to use llGetOwner to avoid others interupting.
llRequestPermissions(llDetectedKey(0), PERMISSION_TAKE_CONTROLS);
//llRequestPermissions(llGetOwner(), PERMISSION_TAKE_CONTROLS);
// the sl wiki says that you should always request permission
//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);
}
}
These are the two wiki entries I used to get this done.
http://wiki.secondlife.com/wiki/LlTakeControls
http://www.lslwiki.net/lslwiki/wakka.php?wakka=llTakeControls

One thing I don't like is that it only seams like you can get information on keys that are already mapped to movement. In this case a,w,s,d,e,c,arrow keys and page up/down. This seems very limiting, but I think I can make it work. Seems like an oversite that would have been easy to allow for early on in the development process and I have trouble understanding any reason for this other than a lack of vision in regards to in world controls.

Friday, July 11, 2008

Second Life Physics Scripting - Pinball #11 - More Collision Issues

I found another stuck ball on the top wall.



I also noticed that when I moved the glass sideways to get to the ball, the ball moved farther into the wall and was then sticking out about half way. This is even though the ball was not really near the glass since it was about halfway up the wall. Very strange behavior.

I also saw two cases where the ball was stuck on top of one of the bumpers wedged between the glass. I need to make the glass a little lower or the bumpers a little higher. I'll probably opt to make the bumpers higher as there will be problems with the curve where the glass meets not leaving enough room for the ball if I make the glass lower.

I also noticed that the scoreboard stopped functioning. I had to reset the scripts within the scoreboard to get it going again. This might have been cause by a mistaken "take" of the the pinball machine into my inventory then an extraction back out. It was stuck on the number from around that time and I didn't really check to see if it was still working. I'll keep an eye on this one.

I ran some tests on larger balls and smaller push vectors, but then the balls did not move well at all. I think I'm making a mistake and not taking the mass of the ball into account when pushing it away from the walls and bumpers. Need to look into that as well.


Made some good progress on the keyboard code and need to write up a post for that as well.

Fun.

Thursday, July 10, 2008

Second Life Physics Scripting - Pinball #10 - Collision Tweaking

I just checked in to see where the counter is and to make sure the system was still running. Nothing works better than time to test out a piece of code.

Problem, but I don't think it's a big one or one that can't be fixed fairly easily.



As you can see the ball is embedded into the wall of the top curve. The top curve is a hollow cylinder cut in half. But there is good news too.


The counter reached 31871 before there was a problem. I think I'll have to do two things to fix this. Slow the ball down a little bit more, remeber I said I had it a little larger than the ball size itself and that's probably why it was able to get inside the cylinder wall. To keep things simple, I might want to change the top wall to a flat surface anyway and make it look more curved with the final art.

There was one other observed problem. I left the test code in the scoreboard and the individual texture numbers so if someone clicks on them they increment. Just need to remove that test code.

I'm not sure if I've shown it with all the added bumpers yet? There were a bunch of small problems that were fixed. When I copied the bumpers a bunch of them were not working. I did a bunch of little tests and nothing seemed to work, then I noticed they were not running. I started to edit the scripts individually and turn them on, but then just selected them all and then selected 'Tools/Set Scripts to Running In Selection'. That was a pretty easy fix.

Also, the glass on top was a little too high/low at the curve and it was keeping the ball from getting into the upper sections. I moved them until they were properly aligned. It's all about using the alt and alt/ctrl keys to get a bunch of different perspectives when trying to position stuff.

When I was fixing the bumpers and had the glass off the top, the ball did leap off the table and I had to go searching for it way off in the distance. When I found it I just took it into my inventory, then I think I brought back out the wrong one since it had a really low minimum speed. When I showed it to someone the ball got to going way too slow so I edited it and increased the minimum.

Lots of tweaking.

Next I think I need to start working on the interactivity. I'm going to do some looking at mouse look and figure out how people take over the keyboard, then I'll be ready to add a bunch of interactive buttons attached to keys. At first I think it will just be a couple of flippers, but then I may go a little crazy with it.

Wednesday, July 9, 2008

Second Life Physics Scripting - Pinball #9 - Scoreboard Extension

I next extended the score board to have more digits. This was pretty simple, just copying the numbers and then copying and pasting parts of the scripts to add the extra digits. I first did this as a standalone scoreboard, then moved it onto the pinball machine.

Tuesday, July 8, 2008

Second Life Physics Scripting - Pinball #8 - Script Management

I was starting to do some more development on the walls and bumpers and started finding that managing the scripts in only four walls and three bumpers was going to be a problem, especially when I was planning on adding a bunch more of each in the next few iterations.

I went back and added a LinkNanny system described in the Tic-Tac-Toe Tutorial on the LSLWiki.

This was a bit of work and it took some tweaking to get it right. I ended up creating a different LinkNanny for the bumpers and a separate one for the walls. This allowed me to have separate sets of scripts for each. I was thinking it might be better to keep all this information in the RootNanny and do the distribution based on the prim name, but it was already setup this way so I'm going to just go with it.

My biggest problem with this is when you want to distribute out the latest scripts with "/1 update" it takes a long time. I've been editing directly on one of the objects until I get it right, then copying the script to the root object and the using "/1 update" to distribute them out. It takes too long to be useful in very small changes and my stuff is always so buggy that it takes a million small changes to get even the simpliest stuff working.

Monday, July 7, 2008

Second Life Physics Scripting - Pinball #7 - Linked Object Collision Problems

Linked objects caused some interesting problems with collisions for me. On the physics test from the previous post, I had the back wall which pushed the ball unlinked from the rest of the test. When I joined them together it caused the ball to stop at the bottom like it missed a collision and then would not go again until the ball bumped itself.

This wiki on collisions helped me understand the problem.

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

The problem being that the back wall became the root prim (the one selected last before the Tools/Link was selected). This means it was getting collision events for both the middle wall collision and the base of the system as the same event. Meaning that the collision never stopped so there was no need to send another collision event. I had to add a blank start_collision method to both the middle wall and the table prims. That fixed the problem.

Wednesday, July 2, 2008

Second Life Physics Scripting - Pinball #6 - Collision Tester


During this process I created a simple test of the ball bouncing through an object. It has a back wall script on the bottom wall which pushes the ball. I could move the middle wall closer and closer to test for the ball bouncing through at different speeds. I left it for a couple of days to test my theories and the speed change I made fixed the problems.

Here is the code for the back wall.


float maxspeed = 0.8;
default
{
collision_start(integer total_number)
{
//llOwnerSay("Collision start");
//llOwnerSay(llDetectedName(0) + " collided with me!");
if ((llDetectedName(0) == "ball") || (llDetectedName(0) == "ball2"))
{
// 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);
pos.y = pos2.y; // so we don't get any side to side movement
vector pos3 = pos2-pos;
//llOwnerSay("pos = "+(string)pos);
//llOwnerSay("pos2="+(string)pos2);
pos3.x = pos3.x*1.5;
pos3.z = pos3.z*1.5;
float mag = llVecMag(pos3);
if (mag>maxspeed)
{
float magdev = maxspeed / mag;
pos3 = pos3 * magdev;

//mag = llVecMag(pos3);
//llOwnerSay("------adjusted mag down to "+(string)mag);
}
//llOwnerSay("pos3 = "+(string)pos3);
//llOwnerSay("mag3 = "+(string)llVecMag(pos3));
integer ra = (integer) llFrand(1.0)-1;
//pos3.y = pos3.y*ra;
//pos3.y = pos3.y*8;
//llPushObject(llDetectedKey(0), <8,0,1>, <0,0,0>, FALSE);
llPushObject(llDetectedKey(0), pos3, <0,0,0>, FALSE);
}
//llOwnerSay("Collision done");
}
}


The code in the ball was the same as the pinball I showed previously. It really just jiggled once in a while.

Tuesday, July 1, 2008

Second LIfe Physics Scripting - Pinball #5 - Fix Sticky Ball

The problem with the automatic ball moving script was the granularity of the timer event. Once I fixed that (dropped it to 0.01) it when haywire like I expected it would because I was never letting it slow down. I changed it to have a minimum speed and back off the timer to ever half second. This is what I ended up with.
float movebump = 0.1;
float maxspeed = 0.42;
float minspeed = 0.02;
float speeddiff = 0.02;
// http://lslwiki.net/lslwiki/wakka.php?wakka=llApplyImpulse
default
{
state_entry()
{
llSetTimerEvent(0.5); // generate a timer event every 1 second
}
timer()
{
vector ra = <llfrand(movebump),llfrand(movebump),llfrand(movebump)>;
//llOwnerSay("ball moving "+(string)ra);
//llOwnerSay("ball moving mag "+(string)llVecMag(ra));
vector vel = llGetVel();
float velmag = llVecMag(vel);
if ( (velmag>0) && (velmag>maxspeed) )
{
//llOwnerSay("oldvelmag = "+(string)llVecMag(vel));
// need to slow the ball
float magdev = maxspeed / velmag;
vel = vel * magdev;

llApplyImpulse(llGetMass()*vel,FALSE);
//llOwnerSay("newvelmag = "+(string)llVecMag(vel));
}
else if ((velmag==0) (velmag<minspeed)) ra="<llFrand(movebump),llFrand(movebump),llFrand(movebump)>;
//llOwnerSay("ball moving "+(string)ra);
//llOwnerSay("ball vel 0 move bump = "+(string)llVecMag(ra));
// give the ball a little wiggle
llPushObject(llGetKey(), ra, <0,0,0>, FALSE);
//llOwnerSay("push mag = "+(string)llVecMag(ra));
}
}
}

Monday, June 30, 2008

Need Second Life volunteers for a quick photo

The Second Life pinball machine is coming along nicely. I'm over two weeks ahead on blog posts so you should be seeing slow, but significant progress over the coming weeks.

I'm getting to the point where I'm deciding on the art theme of the pinball machine and I've decided I would like to make it a "Second Life pinball machine". That means that I need to get some good in world photos that might work as the score board and table background. If you would be interested in being notified (and possibly immortalized on a pinball machine) when I would get everyone together, please send me an e-mail (wood@side8.com, SL:Wood Wheels) and I'll pick a time and a date for the shoot. Also if you have ideas for a place that might work well I'm open to suggestions. I'm thinking something on a hillside or pyramid shape will allow more people to fit in the sort of vertical space I'm needing.