Monday, May 18, 2009

Imagine Cup

Where has all my time gone?

My work got really busy and I've been mentoring a team creating a game for the Imagine Cup. Never expected it, but all those trips to Africa doing community development have been helpful in building a game that is designed to help work on the United Nations Millennium Development Goals.

The team is in the final week (two days) of development and everything is coming along. I think this is one of the most interesting projects I have worked on in years and really follows closely the work in Africa I (my wife and a lot of good friends) have been involved with over the past five years. When we started the team we were looking at a different contest and the Imagine Cup came along after that contest was delayed. Very cool to see the fruits of many years of labor multiplying into completely new avenues of opportunity.

The game is called Project 2015. The Project 2015 website is currently a month and a half old and used for the first submission. I expect it to be updated in the next week. Stay tuned.

Friday, April 10, 2009

Testing some shading options

As I've been testing lately, you can purchase all the source code for this sample for $5
(http://store.payloadz.com/go?id=254377).

I’ve been testing some shading options and working on the 3D aspects of XNA that I have been needing to learn. One of the problems is that I have 3DStudioMax, but it is only on a machine I don’t use as much. I’ve been contemplating moving the license, but that has issues. I’ve used Blender in the past and have enough knowledge to be dangerous to myself, but went back to it as I have not been able to use Max when I need it. So, this was as much a Blender session as an XNA lighting session.

I’m using Reimer Grootjan’s book XNA 2.0 Game Programming Recipesand found it very helpful in the shading and rendering aspects. I know it’s for XNA 2.0 and I’m using XNA 3.0, but the stuff I’m looking at has not changed (as is most of the book) so it is still highly relevant to learning XNA 3.0.

The section I used was on directional lighting on page 514. It shows how to get the BasicEffect to work with a directional light.

My code looked like this when I was done.

effect.LightingEnabled = true;
effect.DirectionalLight0.Direction = light0Pos;
effect.DirectionalLight0.DiffuseColor = Color.White.ToVector3();
effect.DirectionalLight0.Enabled = true;
effect.PreferPerPixelLighting = false;
effect.DirectionalLight0.SpecularColor = Color.White.ToVector3();
effect.SpecularPower = specPower;
With, specPower and light0Pos set at the top of the class definition.


Vector3 light0Pos = new Vector3(-0.5f, -1.0f, 0.0f);
float specPower = 64;
After weeks of procrastination and thinking I was going to have to create a custom shader it was nice to find out that adding a directional light was very simple. One of the games I was going to do requires a spotlight and I do think that will require a custom shader so it's off the list until I get some extra time.

So, first up with Blender. I’m trying to display a gemstone. I created a isosphere then scaled and stretched it to look like a gem stone. Then I added a red texture. You can see that I set the Col (Color), Spe (Specular Color) and the Mir (Mirror Color) for the gem.


This was fine, except that the gem came out all smooth.


I know from experience that this is caused by the rendering system using vertex normals to draw the individual triangles of the gem as smoothly as possible. This is almost always the case. But for a gemstone, you want it to have facets. I spent a ton of time trying to figure out how to get blender to remove the vertex normals and only use surface normals.

This ended up being easy inside of blender, but took a while to figure out how to get this information into the direct X file. To do this in blender, you select the object in edit mode, then click the Set Smooth button to use vertex normals for rendering and Set Solid to use surface normals for rendering.


Easy and you can see the two rendered versions here.


That’s all well and good in Blender rendering, but XNA kept displaying the gem as a smooth surface. After tons of fiddling, I noticed one of the options on the Blender DirectX exporter was “no smooth”. The tool tip says exactly what I wanted to hear “Every vertex has the face normal, no smoothing”.



Once I exported with that setting everything worked.



Getting back to Blender, I found it really hard to learn, and hard to relearn, but for the price (Free) it really is an amazing tool. I have a book on it that I've barely touched called Introducing Character Animation with Blender by Tony Mullen. I've had it for a few years and only touched the surface on it and every time I pick it up I'm always impressed. It really does look like a great book. As I write this he has a new version due out soon and another book on Blender that was just released. See below.

My real problem with Blender came in the classes I was teaching on Torque last year and the fact that we could never get the bone systems to work properly. It seemed that 3DSMax was the only real way to do modeling for Torque. Maybe that has changed?

I did find this great reference image on the Blender hotkeys (http://en.wikibooks.org/wiki/File:BlenderHotkeysObjectMode.png. If you are going to use Blender, you are going to want to learn as many as possible. It really is a fast fast tool once you get to know the UI. Mostly I only use Space Bar, B (box select), A (select/deselect all), S (scale), R (rotate X,Y,Z).




As I've been testing lately, you can purchase all the source code for this sample for $5.
(http://store.payloadz.com/go?id=254377).

Wednesday, April 1, 2009

Balance Air Game

This is the best one so far. Didn't have a lot of time again so I went 2D and wished I had enough time to add a little sound (like a hair dryer sound) and do some more complex goals (like a bucket), but I think it's probably the most fun so far. It's not super pretty, but good luck, it's really really hard.


The game is called Balance Air. You have a hair dryer at the bottom of the screen and you need to balance the ball on air and force it into the goal. You move the blower left or right to change the direction of the ball. There are 7 levels which are basically just different locations for the goal. The score for each goal is dropping over time.

As I did last time you can download a free executable of the game. It is available here: http://www.woodsgoods.com/gawxna/02balanceair.

The complete source and Visual Studio project is available for $5 at http://store.payloadz.com/go?id=246205 .

Enjoy and please give me some feedback.

Wednesday, March 25, 2009

Skeet Slider game

I didn't even think I was going to be able to get a game done this week. I was spending a bunch of time on HLSL (the XNA shader language) and not getting where I needed to get. I decided I still needed to get a game done this week and I have yet to do a 2D game using XNA so I decided (yesterday) that would be the best way to complete the project in the time left.

The game is called Skeet Slider. Not my best by far, but much better than the past few weeks while I've been learning the XNA framework. You slide a puck across the bottom of the screen trying to avoid bouncing blocks. It's all about timing. I wanted to add a feature to change the velocity of the puck, but I'm out of time. Here is a screen shot of the game on level 4, the highest level. I spent a little time at the end to add better art for the game so it is cleaner that what I've written in XNA so far.



I also messed with the publish system in XNA Game Studio 3.0 and created an installer you can use to run the game. It is available here: http://www.woodsgoods.com/gawxna/01skeetslider.

I have also been considering offering the source code to some of these games to try and cover some of my time. It's a minimal $5 to purchase the C# source code to the Skeet Slider XNA Game. Please tell me if you think this is reasonable or not? I could really use some feedback.

Wednesday, March 18, 2009

SpinMag game

I used the same rotating magnet from last week and added a different scoring mechanic. I also added a levels using png images to place items on the map. There are four levels, but the game is really tough. Currently it only has one life. I wanted to add more, but ran out of time. I again spent a bunch of time reading about texturing and lighting and have not made the progress I had hoped. I'm considering a game that uses spot lights as a mechanic next week.

This is a screen shot of the game.

This is the map image. Each pixel represents the location of one of the 3d spheres.

Here is the code that reads the map image and converts it to 3d objects.

protected void LoadMap(String mapname)
{
// the map is just an image file with pixels of various colors
// red = barrier
// black = open space
// cyan = closed space
// green = starting point
Texture2D map1 = content.Load<Texture2D>(mapname);

mapwid = map1.Width;
maphei = map1.Height;
int numpix = mapwid*maphei;

if ((map == null) (map.Length < numpix))
{
map = new byte[numpix];
}

// want this map to fit over the circle, so calculate the size of each ball
// scale of the ball
mapballsz = (int)(maxballdist*2) / mapwid;
mapxstart = -(mapwid / 2-1) * mapballsz;
mapystart = -(maphei / 2-1) * mapballsz;

/*
* the sample I used to write this method
* ms-help://MS.VSCC.v90/MS.VSIPCC.v90/MS.XNAGS30.1033/XNA/GetData``1_B8CC1053 */

Rectangle sourceRectangle = new Rectangle(0, 0, mapwid, maphei);

Color[] retrievedColor = new Color[numpix];


map1.GetData<Color>(
0,
sourceRectangle,
retrievedColor,
0,
numpix);

goalcount = 0;
int i;
for (i = 0; i < numpix; i++)
{
if (retrievedColor[i] == Color.Cyan)
{
map[i] = MAP_CLOSED;
}
else if (retrievedColor[i] == Color.Red)
{
map[i] = MAP_BAR;
}
else if (retrievedColor[i] == Color.Blue)
{
map[i] = MAP_GOAL;
goalcount++;
}
else
{
map[i] = MAP_OPEN;
}
}
}

Wednesday, March 11, 2009

SpinDart

The games are getting closer, but still have a ways to go on getting my art pipeline down in a week. That's where I spent a bunch of time this week trying to get some models out of 3ds Max which I'm new to. I got the models out, then left them on a different machine so this screen shot is untextured models.

It isn't what I started out to make and I may make a different game with this same mechanic next week. Locking the z axis and making this a 2d game helped the time constraints some, but I would like to be able to get that third axis in a week long game cycle.

The game is a "gravity" style game which is one of my favorites. Impossible to control which means the mechanic doesn't really work, but I'm not 100% sure it is impossible to control. Given a shared high score system I think I would see some different approaches to making points, but alas I haven't written the shared high score system yet...




Here is a code snipet that does the collision checking. I didn't use the XNA system which I looked into a little and decided it was easier to write this simplified code.

private void checkCollision()
{
// check the position of the ball with that of the spinner
Vector3 vspin; // = Vector3.Backward; // pointed up
Matrix m = Matrix.CreateRotationY(spinnerRot);
vspin = Vector3.Transform(Vector3.Forward, m);

// I know the spinner is 7.2 units before scaling (looking in milkshape)
vspin = vspin * 7.42f * scaleXY;

// now check the position of the ball in z space
if ((ballPos.Length()>vspin.Length()+ballradius) && (distToOriginLineXZ(ballPos, vspin) < ballradius))
{
// collision

// wwh score or death?
score = score + 1;
}

// check if we are inside the center of the spinner
// spinner sphere radius = 2
if (ballPos.Length() < 2 * scaleXY + ballradius)
{
// wwh score or death?
mode = STATE_OVER;
if (score > highscore)
score = highscore;
//score = score + 1;
}
}
private float distToOriginLineXZ(Vector3 pt, Vector3 oline)
{
// used this reference
// http://local.wasp.uwa.edu.au/~pbourke/geometry/pointline/
// p1 = 0.0,0.0
// p2 = oline.x,oline.z
// p3 = pt.x,pt.z

// oline is a line from the origin
Vector3 vi = Vector3.Zero;
float u = ((pt.X - 0.0f) * (oline.X - 0.0f) + (pt.Z - 0.0f) * (oline.Z - 0.0f)) / (oline.Length()*oline.Length());
vi.X = u * (oline.X);
vi.Z = u * (oline.Z);

pt = pt - vi;
return pt.Length();
}

Wednesday, March 4, 2009

Sphere Pong

This week's mechanic I was working on didn't really work, but it was a fun idea. I created a 3d pong game where you move the puck around a sphere and try and keep the ball in the middle. The problem is there is no reference point. I need some sort of skysphere or spherical markings or maybe also a tail on the ball so you can more easily tell direction. Ran out of time, but may come back and revisit this one someday.

I'm still learning XNA and the graphics look crummy, but for me right now this is more about learning the api than working on making a good looking game. Forward progress at any rate.
For a code sample a lot of the time was spent in positioning the puck around the sphere. I only finished the x,z plane as it didn't work for that, so no reason to add the third dimension. Here is the code to position the puck around the sphere.
// move the paddle using the upVector (true) or the cross product of the origin and up (false)
// direction is a + or - 1 for up, down, left or right
private void movePaddle(bool usingUpVector, float dir)
{

if (!usingUpVector)
{
// need to take the cross between the up and the origin
Vector3 vp = paddlePos;
vp.Normalize();
// vector change
Vector3 vc = Vector3.Cross(vp, paddleUp) * dir;
vc.Normalize();
vc = vc * paddleSpeed;

// find the new location, will be outside the sphere
vp = paddlePos + vc;

// fix up the distance ot the sphere
vp.Normalize();
vp = vp * radius;

paddlePos = vp;

// the up vector should not have changed, right?

// move the camera behind the puck
fixupCamera();
}
else
{
// for this case, take the cross product
// move in the direction of the up vector (+ or -)
// position against the spehere
// use the cross product to calculate the new up vector
}
}

Wednesday, February 25, 2009

Bounce game

This week I was working on learning and using the physics engine JigLibX. Painful to say the least. I need to be spending my time learning XNA since this is only the third week. No documentation on JigLib and I spent a lot of time trying to figure out the connections between it and the XNA rendering system. I don't have a good understanding of the rendering system in the first place and mix that with a lack of understanding of JigLibX and it was a surprise that I actually got a game done.



In this game the ball is bouncing at a regular rate. The idea is to move the little green dot around and tilt the tiles so the bouncing ball ends up landing on the green platform with the least number of bounces. The ball started in the bottom right corner. One of the biggest time sinks was the fact that there is no hidden surface removal (as you can see in the top right corner platform) and I spent a bunch of time trying to figure that out and didn't get it done. Had to move on. For me the jury is still out on JigLibX, but it seemed like it had a lot of potential, just needed more samples (that included hidden surface removal) and documentation.

If you want the source, send me a message and I'll consider sending it to you.

Wednesday, February 18, 2009

First Full Circle XNA Game

After going through the tutorial last week I decided my best bet was to create a full circle game. That's a game with a main menu, the game, and a game over screen that moves back to the main menu. This is a shot of that main menu (very simple).


There also needs to be some sort of scoring or timing sytem. This will typically end up being the template for the rest of the games from this point forward. I used the art work from the tutorial and created a grid. Then added on model that could move up, down, left and right (only 2d) in the grid. One of the models is spinning and the idea is to move the red model on top of that model with as few moves as possible (looping around the other side is a key mechanic). In the screen shot, pressing the right key will move on top of the spinning ship as clicking the left key will take three moves. Get it?

For a piece of sample code, this is the row column stuff and grid keyboard movement I have written a million times. Maybe I can come back here next time and just cut and paste.

private int rowFromPos(int p)
{
return p / tilerows;
}
private int colFromPos(int p)
{
return p % tilecols;
}
private int posFromRowCol(int row, int col)
{
return (row * tilecols) + col;
}

private void positionXYFromIndex(int idx, ref Vector3 vSet)
{
vSet.X = fLeftX + colFromPos(idx)*gapX;
vSet.Y = fTopY + rowFromPos(idx)*gapY;
vSet.Z = 0.0f;
}

if ((newState.IsKeyDown(Keys.Right)) && (!oldState.IsKeyDown(Keys.Right)))
{
int row = rowFromPos(curSel);
int col = colFromPos(curSel);

col++;
if (col >= tilecols)
{
col = 0;
}

curSel = posFromRowCol(row, col);
positionXYFromIndex(curSel, ref selPosition);
moves++;
score--;
}
else if (oldState.IsKeyDown(Keys.Right))
{
}

if ((newState.IsKeyDown(Keys.Left)) && (!oldState.IsKeyDown(Keys.Left)))
{
int row = rowFromPos(curSel);
int col = colFromPos(curSel);

col--;
if (col < 0)
{
col = tilecols - 1;
}

curSel = posFromRowCol(row, col);
positionXYFromIndex(curSel, ref selPosition);
moves++;
score--;
}
else if (oldState.IsKeyDown(Keys.Left))
{
}

if ((newState.IsKeyDown(Keys.Up)) && (!oldState.IsKeyDown(Keys.Up)))
{
int row = rowFromPos(curSel);
int col = colFromPos(curSel);

row++;
if (row >= tilerows)
{
row = 0;
}

curSel = posFromRowCol(row, col);
positionXYFromIndex(curSel, ref selPosition);
moves++;
score--;
}
else if (oldState.IsKeyDown(Keys.Up))
{
}

if ((newState.IsKeyDown(Keys.Down)) && (!oldState.IsKeyDown(Keys.Down)))
{
int row = rowFromPos(curSel);
int col = colFromPos(curSel);

row--;
if (row < 0)
{
row = tilerows - 1;
}

curSel = posFromRowCol(row, col);
positionXYFromIndex(curSel, ref selPosition);
moves++;
score--;
}
else if (oldState.IsKeyDown(Keys.Down))
{
}

Sunday, February 1, 2009

XNA learning started

I am mentoring a group of students at Chapman in a game development contest to build a game around the U.N. Millennium goals. I'm having a ton of fun with it.(http://imaginecup.com/Competition/mycompetitionportal.aspx?competitionId=21).

That said, I don't have any code duties on that so to learn XNA I've been back to trying to write a game a week, this time using XNA instead of Java (http://www.woodsgoods.com/gaw). I don't think the quality will be there, but I'll try and post some screenshots and lessons learned moving forward.

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.