Light Racer 3D - Days 8-9 - Core Gameplay Elements

Light Racer 3D is an exciting project because every day of development adds substantial improvements to the game. The last 2 days have been no exception. Make sure to watch the video at the end of this post. It shows how totally playable the game is. If I were to stop right now, the game would be at least as good as a 3D counterpart to the original Light Racer. As it stands right now, I've got a list of about 20 things I'd like to do for this game to take it from cool to amazing. Because of memory allocation sensitivity on Android, I had to employ a few tricks to deal with the dynamic geometry of the trails. I also ran into a problem getting the explosions properly billboarded.

Day 8 - What was done:

Got text working
synced up game start with OGL init
Serialized ModelData3D into .s3d files - added utilities for developers
Finished all trail rendering including the "falling" trail

Day 9 - What was done:

Got billboard-hacked explosion in (had problems getting bb to work correctly)
Get score text working efficiently
Get level text working
Change touch input to be left-right zone
Added camera turn rate

Summary

OBJ file parsing was taking several seconds per file, especially on the racer model. This is because of string parsing, which was hard to do without allocating lots of strings. As you may or may not know, Android is very slow with many memory allocations so reducing those is key to a quick app. I wrote a little tool that parses the obj file and serializes the result to another file. I then added a little utility that reads that serialized file back into the same result. The game now just uses that serialized file and in my tests, it's been 7-8 times as fast. I know I can make something faster but this will do for now.

The trail geometry required a little hack that made it quick to work with. Each segment of trail is held in a class called a TrailSegment. The TrailSegment is simply a list of coordinates to make the path and some state information about the segment, such as, if it is active and how high it's currently standing. Since trail segments are modified, I added the direct buffers to them so that they can hold their own geometry. They have a flag indicating if their geometry needs to be updated, and when the trails are drawn for a player, if that flag is true, the geometry is updated before drawing happens. The last bit of trail, which is from the last turn to the current location of the player, reuses the same direct buffers for all players. The geometry is simple and always uses the same indices, so I simply update the 4 vertices for every draw for every player. It's quick and easy.

I spent about a day trying to get billboarding perfect for the explosion. If you aren't familiar with what billboarding is, imagine a piece of paper lying on the ground. If it's billboarded, no matter where you move to or what angle you look at, it's always facing you. That would be creepy in real life but for games, it's really nice because you can do things like put in a texture-animated explosion, like LR3D has. I'm still bad at linear algebra so I had problems getting the billboard to work correctly, but at the end of the day I figured out a quick hack that would make it work well enough. I'm happy with the hack because, while it's not perfect, it's much less CPU intensive than actual billboarding. My hack is simply to track the rotation and angle of the camera and counter-rotate the explosion quad to match. If the camera is at 30 degrees, the explosion sits at 60. If the camera rotates left, the explosion rotates right. This makes the explosion look fairly convincing from the majority of angles.

There is point sprite function in GL11, but I'm doing the whole game in GL10 to ensure compatibility amongst all devices.

If you're wondering how the nice camera rotation works, here's a bit of code for you:

switch (player.curDirection) {
case EAST: {
targetZRot = 90;
break;
}
case WEST: {
targetZRot = 270;
break;
}
case NORTH: {
targetZRot = 0;
break;
}
case SOUTH: {
targetZRot = 180;
break;
}
}
if (cameraZRotation != targetZRot) {
// rotate shortest way always
long targetMovementAmount = (renderFrameDelta * CAMERA_TURN_RATE) + zRotCarryOver;
int rotAmount = (int) targetMovementAmount / 1000;
zRotCarryOver = (int) (targetMovementAmount % 1000);
//Log.d(TAG, "currentZRot=" + cameraZRotation + ", targetZRot=" + targetZRot + ", amount=" + amount);
if (targetZRot < cameraZRotation) {
if (cameraZRotation - targetZRot > 180) {
cameraZRotation -= 360;
cameraZRotation += rotAmount;
} else {
cameraZRotation -= rotAmount;
if (cameraZRotation < targetZRot) {
cameraZRotation = targetZRot;
}
}
} else {
if (targetZRot - cameraZRotation > 180) {
cameraZRotation += 360;
cameraZRotation -= rotAmount;
} else {
cameraZRotation += rotAmount;
if (cameraZRotation > targetZRot) {
cameraZRotation = targetZRot;
}
}
}
}
if (cameraZRotation != 0) {
Matrix.rotateM(cameraMatrix, 0, cameraZRotation, 0, 0, 1);
}

And finally, here's a video of me playing the game on my G1.

2 Comments

Post a comment here or discuss this and other topics in the forums

Billboard algorithm

Billboarding Technique.

Inputs:
CameraPos
BillboardTransformationMatrix
WorldYVector (aka (0.0, 1.0, 0.0)) - Can be hard coded.

Outputs:
BillboardTransformMatrix, now facing towards the camera.

Algorithm:
Vector newZ = CamPosition - BillboardTransformMatrix.GetTranslation().
Normalize this vector. This is your billboard's forward axis.
You need to generate a new X and Y axis for the billboard, so take
the WorldY.CrossProduct(ZAxis) and that will give you your new X-Axis. If WorldY and Z are normalized, you don't need to normalize X.
Now, you can't use the WorldY vector as the Y-Axis because it will produce a wierd graphical effect when the camera is not orthogonal (read "perpendicular") ( to the WorldY axis. (WorldY will become acute to the z-axis when camera is raised above 90 degrees, obtuse if camera is lowered below 90 degrees, since it is a fixed axis).
So, generate a new YAxis from ZAxis.Cross(XAxis), and normalize.

Finally,
BillboardTransformMatrix.SetForward(ZAxis)
BillboardTransformMatrix.SetRight(XAxis)
BillboardTransformMatrix.SetUp(YAxis)

If I recall right, OpenGL's cross product produces orthogonal vectors in counter-clockwise order, so the cross products should work as specified. After this operation, you can scale, translate, and rotate as normal (although I only recommend rotating around the Z-Axis, as any other axis will break the billboard's facing towards the camera).

Summary,
Vector ZAxis = CamPos - BTM.Position;
ZAxis.Normalize();
Vector XAxis = WorldY.Cross(ZAxis);
Vector YAxis = ZAxis.Cross(XAxis);
//XAxis.Normalize(); YAxis.Normalize(); // Optional sanity check.
BTM.SetMatrix(XAxis, YAxis, ZAxis, BTM.Translate);

Thanks, Nick!

Just to clarify for people who aren't aware - Nick's post applies to Y being "up." If you use Z as your up, just swap Y/Z in this example.

I actually ended up billboarding a different way:

BB verts are oriented to point down the Y axis (Z is up in my world)
float dir = atan2(camY - posY, camX - posX);
glRotatef(toDegrees(dir), 0, 0, 1);

I'm doing that because it seems more efficient than using two cross products, though I've never tested to see which uses less CPU.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.