V7/15/98
©1998-99 Pangea Software
All Rights Reserved
The Nanosaur game engine uses QuickDraw 3D as its core 3D geometry processing engine, therefore, the bulk of the Nanosaur code deals with generating the terrain and doing skinned animation on the dinosaur models. A complete understanding of QuickDraw 3D is necessary to use this engine and will not be covered in this documentation.
The Nanosaur code is well documented and the functions should be fairly clear. I don’t use cryptic variable or function naming conventions, and I generally put functions in C files with logical names. For example, all of the code pertaining to the terrain generation resides in the files Terrain.c & Terrain2.c. Likewise, all of the code dealing with the T-Rex enemy is in the file Enemy_Rex.c.
In the CodeWarrior project, the C files are grouped according to their functionality. The following outlines the meanings of the various groups:
-
System
This group contains all of the C files dealing with basic system operation. Everything from the boot code to random number generation to the master linked list is contained in these files. -
Terrain
This group contains all of the files responsible for generating the 3D terrain. -
Player
The 3 files in here deal with the player. All of the code to control the player to managing the weapons is in this group. -
Enemies
All of the enemies in the game have their own C file and have been grouped here. -
Items
Everything from powerups to time portals to the lava rocks are in this group. -
Skeleton
This group contains the most complex C files in the entire engine. These files handle the animation and rendering of all of the dinosaurs in the game. -
QD3D
Most of the core QuickDraw 3D code is kept in this group of C files. Everything from loading a 3DMF file to setting up a Draw Context is here. -
Screens
Each of the files in this group handles one of the screens (such as title screen, high scores screen, main menu, and even the infobar) in the game.
This document is not going to explain every single function in every single C file. Most of the functions are self-explanatory either by the name of the function or the comments in the code. Instead, this document will try to explain the fundamental workings of the key components of the Nanosaur engine. Any specific questions can easily be answered by sending an email to brian@pangeasoft.net.
To generate the data for the Nanosaur engine, several custom tools must be used:
-
BioOreo Pro
This is the keyframe animation tool which is used to create the animation scripts for all of the skinned models. In Nanosaur, the main character plus all of the enemy dinosaurs were animated with this tool. -
OreoTerrain
This tool is tile-based terrain editing tool which is used to create the Nanosaur landscapes and is also used to place all of the objects (enemies, powerups, etc.) on the terrain. -
3DMF Optimizer
All models to be used in the game or by the other tools first need to be optimized using this tool. This tool takes 3DMF geometry files and optimizes them and saves them in a simple 3DMF format that the other tools and the game can work with easily. -
3DMF Linker
Nanosaur uses large 3DMF files which contain all of the 3D models used in the game rather than creating a separate file for each object. 3DMF Linker is used to take your original 3DMF models for each object and merge them all into a single large file which the Nanosaur engine can reference into easily. -
OreoSprite
This tool is not used extensivly by the Nanosaur engine. It is only used to create sprites for the game’s status bar (the score, weapon icons, etc.). -
3DMF Mapper
This tool is not required by the Nanosaur engine, but it does make certain tasks easier. Most 3D modeling applications have lousy texture mapping features. 3DMF Mapper is a great texture mapping tool for doing organic models such as dinosaurs and other animals.
Instructions for using these tools are not included in this document. Each tool comes with its own set of documentation which explains how to use the tool and the file format(s) which it outputs.
You have also been provided the source code to BioOreo Pro and OreoTerrain since these tools are specific for Nanosaur and you may wish to add features or change the file formats of these tools for your specific application.
Once you have licensed the Nanosaur engine from Pangea Software, you are free to do whatever you want with it except sub-license it to anyone else or start giving it away to people. You may modify the code as much as you like and may use it in as many applications as you like — all royalty free!
Remember, however, that only the code and tools have been licensed to you. The artwork to Nanosaur is the property of the artist who drew it and is therefore not part of the licensing agreement. You should not use any artwork or audio from Nanosaur in your own applications since the original owner of the art or sound may sic his lawyers on you.
The first thing you need to learn about this game engine is that the entire thing essentially runs off of a gigantic linked list of “objects.” Don’t worry, this isn’t some mystical object like you think of in C++. These objects are more like “real” objects in that each is simply a big data structure which contains all of the data for one item in the game. Each dinosaur is an object, each rock, each bullet, each shadow is an object.
An object is simply a node in a linked list. The code often refers to objects as nodes and vice versa. The structure which defines an object is called ObjNode (object node) and its definition as seen in Structs.h is as follows:
struct ObjNode
{
struct ObjNode *PrevNode; // ptr to previous node in linked list
struct ObjNode *NextNode; // ptr to next node in linked list
struct ObjNode *ChainNode;
struct ObjNode *ChainHead; // a chain's head (link back to 1st obj in chain)
short Slot; // sort value
Byte Genre; // obj genre
Byte Type; // obj type
Byte Group; // obj group
void (*MoveCall)(struct ObjNode *); // ptr to obj's move routine
TQ3Point3D Coord; // coord of object
TQ3Point3D OldCoord; // coord @ previous frame
TQ3Vector3D Delta; // delta velocity of object
TQ3Vector3D Rot; // rotation of object
TQ3Vector3D RotDelta; // rotation delta
TQ3Vector3D Scale; // scale of object
float Speed; // speed: sqrt(dx2 * dz2)
float Accel; // current acceleration value
TQ3Vector2D TerrainAccel; // force added by terrain slopes
TQ3Point2D TargetOff; // target offsets
TQ3Point3D AltCoord; // alternate misc usage coordinate
unsigned long CType; // collision type bits
unsigned long CBits; // collision attribute bits
Byte Kind; // kind
signed char Mode; // mode
signed char Flag[6];
long Special[6];
float SpecialF[6];
float Health; // health 0..1
float Damage; // damage
unsigned long StatusBits; // various status bits
struct ObjNode *ShadowNode; // ptr to node's shadow (if any)
struct ObjNode *PlatformNode; // ptr to obj which it on top of.
Struct ObjNode *CarriedObj; // ptr to obj being carried/pickedup
Byte NumCollisionBoxes;
CollisionBoxType*CollisionBoxes; // Ptr to array of collision rectangles
// box offsets (only used by simple objects with 1 collision box)
short LeftOff,RightOff,FrontOff,BackOff,TopOff,BottomOff;
TriangleCollisionList *CollisionTriangles; // ptr to triangle collision data
short StreamingEffect; // streaming effect (-1 = none)
TQ3Matrix4x4 BaseTransformMatrix; // matrix which contains all of the
// transforms for the object
// as a whole
TQ3TransformObject BaseTransformObject; // extra LEGAL object ref to
// BaseTransformMatrix (other legal
// ref is kept in BaseGroup)
TQ3Object BaseGroup; // group containing all geometry, etc.
// for this object (for drawing)
float Radius; // radius use for object culling calculation
SkeletonObjDataType *Skeleton; // pointer to skeleton record data
TerrainItemEntryType *TerrainItemPtr; // if item was from terrain,
// then this pts to entry in array
};
typedef struct ObjNode ObjNode;
The ObjNode structure is pretty massive, but it contains everything you’d ever need to know about any object in the entire game. I’m not going to go into the gritty details of every single record in the ObjNode structure since the comments in the code explain most of it, but the fundamental ones will be covered in this document.
The most important records in the ObjNode structure are the following:
-
PrevNode
Points to the previous node in the linked list. -
NextNode
Points to the next node in the linked list. -
Slot
Determines the position to insert the node in the linked list. This linked list is ordered from smallest to largest Slot value.
All of the other records are used by specific parts of the game. Only these three basic records are used to manage the objects in the linked list.
The basic principle behind the Nanosaur engine is to create an ObjNode for each game item as it is needed and then get rid of the node when the item is not needed anymore. Generally, new ObjNode’s are created as the player runs along the terrain and a new enemy or item comes into range. As other enemies and items go out of range, their ObjNode’s are deleted. So, at any given time, the Object linked list contains all of the currently active items in the game. When the player fires a bullet, a new ObjNode is created for the bullet and when the bullet explodes its ObjNode is deleted.
There are several different “Genres” of ObjNodes for performing different tasks.
-
SKELETON_GENRE
This ObjNode will contain information describing an animated character based on a skeleton file created with BioOreo Pro. -
DISPLAY_GROUP_GENRE
Simple geometry models are described by the data in this ObjNode. -
EVENT_GENRE
Even generes don’t actually contain any geometry like the other two. These objects simply describe events such as timers or “particle emitters.” For example, a special Event object generates the random lava balls that appear in the lava fields. Each lava ball generated by the event object is actually a display group genre object.
There are specific functions for creating Skeleton Objects or Display Group Objects, but they all in turn call one core ObjNode initialization function MakeNewObject and take a pointer to an “object definition” data structure as input. The NewObjectDefinitionType contains some default information about the object you want to create. This data is then passed to one of the Object creation routines to actually setup the ObjNode for the new object.
typedef struct
{
Byte genre,group,type,animNum;
TQ3Point3D coord;
unsigned long flags;
short slot;
void (*moveCall)(ObjNode *);
float rot,scale;
}NewObjectDefinitionType;
Not all fields are used by each genre of Object, but here’s what they all mean:
-
genre
Obviously defines the genre of Object we are about to create. Only needs to be set if calling MakeNewObject directly. -
group
For Display Group objects this refers to the 3DMF group containing the model we want to use. -
type
Refers to the specific model inside the 3DMF group which we want to use for this object. -
animNum
Used by Skeleton objects to determine which animation sequence the object should be using upon creation. -
coord
Defines the 3D coordinate of the object in the game. -
flags
Contains bits which define special features of the object. -
slot
Where to put this new Object in the linked list -
moveCall
This is a pointer to the Object’s “move” routine. This function will automatically be called by the MoveObjects function each frame of the game. -
rot
The initial y-axis rotation of a Skeleton or Display Group Object. -
scale
The initial scale of a Skeleton or Display Group Object.
As mentioned above, there are different functions for creating the different genres of objects:
-
MakeNewDisplayGroupObject
This function is used to create a new Display Group Object. For example, the following code is used to create a new “spore pod” in Nanosaur:NewObjectDefinitionType gNewObjectDefinition; ObjNode *newObj; gNewObjectDefinition.group = LEVEL0_MGroupNum_Pod; gNewObjectDefinition.type = LEVEL0_MObjType_Pod; gNewObjectDefinition.coord.x = x; gNewObjectDefinition.coord.y = GetTerrainHeightAtCoord_Planar(x,z); gNewObjectDefinition.coord.z = z; gNewObjectDefinition.flags = 0; gNewObjectDefinition.slot = 100; gNewObjectDefinition.moveCall = MoveSporePod; gNewObjectDefinition.rot = 0; gNewObjectDefinition.scale = .5; newObj = MakeNewDisplayGroupObject(&gNewObjectDefinition);
As you can see, the required fields of the NewObjectDefinitionType structure are filled out and then passed to MakeNewDisplayGroupObject. MakeNewDisplayGroupObject automatically sets the genre field and takes care of doing everything else needed to initialize the Object.
-
MakeNewSkeletonObject
This function is used to create a new Skeleton Object. The code to create a new skeleton object looks very similar to the above code with a few exceptions. Here’s the code to put the Nanosaur on the game’s Main Menu:gNewObjectDefinition.type = SKELETON_TYPE_DEINON; gNewObjectDefinition.animNum = 1; gNewObjectDefinition.scale = .8; gNewObjectDefinition.coord.x = -350; gNewObjectDefinition.coord.y = 00; gNewObjectDefinition.coord.z = 630; gNewObjectDefinition.slot = 10; gNewObjectDefinition.flags = STATUS_BIT_HIGHFILTER|STATUS_BIT_DONTCULL; gNewObjectDefinition.moveCall = nil; gNewObjectDefinition.rot = 0 newObj = MakeNewSkeletonObject(&gNewObjectDefinition);
This code tells the game to create the new Skeleton Object and start it playing animation #1. It also sets a few of the status bits in the flags field. These status bits tell the game engine to use the best texture filtering and not to bother cull-testing the model when rendering it.
This is all that is needed to put an animating character on the screen. Once the ObjNode has been created and put into the linked list like this, everything else is automatically handed by the game engine and you don’t need to worry about a thing.
Once you have created a new ObjNode as in above, all processing is handled pretty much automatically. In your game loop you only need to call two functions: MoveObjects and DrawObjects. When these functions are called, the ObjNode linked list is traversed and each object is processed.
The MoveObjects function will automatically call the “MoveCall” for each Object. Remember the MoveCall was set in the definition structure you filled out when you created the Object. Skeleton Objects also have their animation automatically updated here. You don’t have to do a thing — it’s all automatic!
Your move function can do whatever it likes to the input ObjNode. The one thing to know is that you must call UpdateObjectTransforms at the end of the function to update the Object’s transform matrices if you have moved, scaled, or rotated the object. To move the Object you just fiddle with the Coord field of the ObjNode (ie. myObj->Coord.x = 100). Or, to rotate it you would do something like myObj->Rot.z += 1.2. These actions alone don’t actually do anything. You must call UpdateObjectTransforms to update the transform matrices to these new orientation values.
When you want to remove an Object from the linked list (say the enemy gets blown up or walks too far away) all you need to do is call DeleteObject which takes care of disposing of all memory allocated by the ObjNode and removes the ObjNode from the linked list. It is very important that you never try to access fields of an ObjNode which you have just Deleted since the memory may now be in use by something else and modifying it could cause a crash.
Later in this document I will give more details how the actual Skeleton animation code works, but for now there are some things to know about Skeleton Objects in general.
Skeleton Objects automatically take care of updating the skeleton’s animation from frame to frame. You use the following functions to change the Skeleton’s animation:
-
SetSkeletonAnim
Use this function to set the animation number of a Skeleton Object. -
MorphToSkeletonAnim
Causes the animation to morph from the current frame to the 1st frame of the indicated animation. You can set the speed of the morph.
Also, keep in mind that Skeleton Objects do use up a lot of memory in addition to just the ObjNode structure. Notice that many of the fields in the ObjNode structure are there for Skeleton Objects. When a new Skeleton Object is created, a bunch of other memory is allocated to the ObjNode for these fields.
There are many additional functions you can use to manage ObjNodes, and most of them are found in Objects.c and Objects2.c. Most of these functions are self explanatory, therefore, their functionality is not covered here.
Normally, creating a new View object in QuickDraw 3D is a very tedious and annoying process to endure. The QD3D_Support.c file contains a bunch of functions I have written to assist with working with QD3D.
To create a new Draw Context / View Object with the Nanosaur engine, you can do something like the following:
QD3DSetupInputType viewDef;
QD3D_NewViewDef(&viewDef, gCoverWindow);
viewDef.camera.hither = 10;
viewDef.camera.yon = 500;
viewDef.camera.fov = 1.0;
viewDef.lights.fogHither = .3;
QD3D_SetupWindow(&viewDef, &gGameViewInfoPtr);
The function QD3D_NewViewDef is called to initialize a QD3DSetupInputType structure to its default values. The QD3DSetupInputType structure is a large data structure which contains all of the settings which you wish to base your new View from. In the code above, we are modifying some of the default values that were set in QD3D_NewViewDef. We are changing some of the camera’s values (hither, yon, fov) and one of the light values (fogHither). You can look in QD3DSupport.h for the description of the full data structure. You are free to modify any of the default values after QD3D_NewViewDef has been called.
Once you have filled out your QD3DSetupInputType structure, you simply pass it to QD3D_SetupWindow. QD3D_SetupWindow automatically creates a new View object based on the information in the input data structure. No work on your part is required — it is all done automatically!
QD3D_SetupWindow does just return you a reference to a View object. Instead it fills out another data structure: QD3DsetupOutputType. This new structure looks like this:
typedef struct
{
Boolean isActive;
TQ3ViewObject viewObject;
TQ3ShaderObject shaderObject;
TQ3ShaderObject nullShaderObject;
TQ3StyleObject interpolationStyle;
TQ3StyleObject backfacingStyle;
TQ3StyleObject fillStyle;
TQ3CameraObject cameraObject;
TQ3GroupObject lightGroup;
TQ3DrawContextObject drawContext;
WindowPtr window;
Rect paneClip;
TQ3Point3D currentCameraCoords;
TQ3Point3D currentCameraLookAt;
float hither,yon;
}QD3DSetupOutputType;
This structure contains everything you’ll ever need to know about the new View that was created. Here you have access to the camera, the lights, the draw Context, etc. Since this structure contains everything important about the View, this is the structure that you will usually be passing to most of the rendering functions. For example, you pass this data to DrawObjects when you are about to draw the objects in the linked list.
In QD3DSupport.c you will see that there are a lot of other utility functions which do everything from creating new lights, to updating the camera position, to creating texture shaders. All of these functions are fairly self-explanatory but if something does not make sense, find out where it is being called in the Nanosaur code to get a better idea how it should be used.
There is also a file called QD3DGeometry.c which contains a bunch of useful geometry related functions. Here there are functions for calculating bounding boxes, exploding geometries into small triangles, and animating the UV coordinates on models. All of these functions are used in Nanosaur, so once again, look thru the code to learn how they are used.
The terrain in Nanosaur is actually a 2D tiled map like you would find in any old scrolling game from years past. The thing that makes it 3D is that there is a “heightmap” layer which is used to provide height information for each corner of each tile in the map. So, we are essentially taking a 2D map and extruding it up to make it 3D.
The OreoTerrain documentation explains how to create the terrain files and use the editor. This documentation will explain the general functionality of the terrain engine inside Nanosaur. It will explain how the OreoTerrain data files are actually processed by this engine.
The tiles that make up a terrain map in Nanosaur are 32x32 pixels in size. For speed and efficiency, however, the Nanosaur engine also uses what are called “SuperTiles.” A SuperTile is nothing more than a 5x5 matrix of tiles. Each SuperTile is a single piece of geometry which is built on-the-fly as the player moves along the terrain. The texture for the SuperTile is also built on-the-fly by simply drawing the textures for the 25 tiles into a buffer and then assigning that to the SuperTile’s geometry.
Since each tile is 32 pixels wide and each SuperTile is 5 tiles. That would make each SuperTile’s texture 160 pixels in dimension. Since 3D accelerators only work with textures which are powers of 2, the Nanosaur terrain engine actually shrinks the 160x160 SuperTile texture down to 128x128 pixels. The 2MB version of the game actually shrinks the textures down to 64x64.
As the player moves along the terrain, new SuperTiles are created and old ones are removed. The Nanosaur engine knows which SuperTiles are not visible by the camera and culls them from the scene before rendering.
The terrain.h header file contains various values for adjusting the terrain parameters. For example, TERRAIN_POLYGON_SIZE determines the size of a single tile in world units. Even though a tile may be 32 pixels in size, you can make the geometry be any size you want. Nanosaur defaults to 140 units, but you can set this to whatever works for your application.
The SUPERTILE_SIZE value determines the actual size of a SuperTile in terms of tiles. Keep in mind that changing this value or the SUPERTILE_TEXMAP_SIZE value will mean that you need to rewrite the texture shrinking function since it’s custom hard-coded for these values. In general, you should not need to change most of the values in this header file.
The SUPERTILE_ACTIVE_RANGE value is of particular interest. This value is normally set to 3. It indicates how many SuperTiles to the left, right, front, and back of the camera to make the visible scene. If you increase your camera’s yon value, you will need to increast the SUPERTILE_ACTIVE_RANGE value so that more SuperTiles will be visible off in the distance. With the value of 3, there are 6x6 (or 36) active SuperTiles at any given time. Note that the more SuperTiles that are active, the more VRAM you will need to hold their 36 textures.
Another very important value is HEIGHT_EXTRUDE_FACTOR. This determines how much to extrude the vertices of the terrain. Each vertex has a height value of 0 to 255 which it gets from the OreoTerrain data file. This height value is then multiplied by the HEIGHT_EXTRUDE_FACTOR to get the actual y-coordinate of the vertex. Therefore, the larger a number you assign to HEIGHT_EXTRUDE_FACTOR, the taller and steeper the terrain will appear.
At boot, you need to call the function InitTerrainManager to initialize the terrain manager. This will allocate memory for the terrain SuperTiles and do other buffer allocation.
Next, you will need to load the terrain data files. The code to do this should look something like this:
FSMakeFSSpec(gDataSpec.vRefNum, gDataSpec.parID, "\p:terrain:level1.trt", &spec)
LoadTerrainTileset(&spec);
FSMakeFSSpec(gDataSpec.vRefNum, gDataSpec.parID, "\p:terrain:level1.ter", &spec)
LoadTerrain(&spec);
Note that the LoadTerrain function also scans for the player’s starting coordinate which is one of the map “items” in the data. Once the data files are loaded you need to “prime” the terrain by calling PrimeInitialTerrain. This will initialize all of the SuperTiles which need to be visible when the first frame of animation is rendered. At this point calling DrawTerrain is all you need to do to draw the terrain.
When the player moves, the function DoMyTerrainUpdate needs to be called. This will tell the terrain manager to scroll the terrain, create new SuperTiles, and delete old SuperTiles. It also takes care of initializing any new items or enemies which may have scrolled into range as well.
The best way to understand how these functions work is to look at the Nanosaur code which calls them. It’s probably not important that you understand exactly how each of the functions works. The code is commented well, but the terrain engine is fairly complex and unless you have a really good reason for doing so, it is not advisable to mess with it.
Perhaps the most often called terrain manager function is GetTerrainHeightAtCoord_Planar. This call returns a y-coordinate based on the input x and z coodinates. Given any x and z coordinate in the world, this function will return the y coordinate (the height) of the terrain at that point.
This function is extremely accurate when the point is on top of an active SuperTile. If the point is outside of range where no SuperTile is currently active, then the function returns a “rough” result based on the OreoTerrain data file’s height map information. The function GetTerrainHeightAtCoord_Quick is used to get this value (GetTerrainHeightAtCoord_Planar automatically calls this if the point is out of range).
You will notice that every moving object in the game from the player to the bullets to the enemies calls GetTerrainHeightAtCoord_Planar after each move to update its y-coordinate or to see if it has hit the “floor.”
As mentioned above, new rows and columns of SuperTiles are created as the player moves along the ground. In addition to new SuperTiles, any terrain items which exist on those SuperTiles are also initialized at this time. However, as SuperTiles are deleted off of the trailing edge of the scroll “window”, items are not deleted. Items are generally deleted when they are significantly out of range. The function TrackTerrainItem() is used to determine if an ObjNode is out of the default range. If so, then the object’s move handling function will delete it.
The OreoTerrain documentation explains how terrain items are identified by item numbers with other sub-parameters. As the Nanosaur Terrain manager scans for new items to initialize as you scroll along the terrain, it uses a big jump table to handle the items. In Terrain2.c, there is a long jump table called gTerrainItemAddRoutines[]. When the Terrain manager encounters an item, it looks up the item’s initialization or “Add” function in this table and then calls it. The Add function then takes care of making the new ObjNode for that terrain item. You will notice that the Nanosaur code is very consistent in that all terrain item initialization routines start with “Add...” (i.e. AddGasVent, AddBush, AddEnemy_Stego, etc.). Most of these functions look very similar: they create a new ObjNode and set custom information about the object.
This section explains how the collision detection in the game engine works. There are basically 3 different types of collision:
- Object bounding box collision
- Object triangle collision
- Tile collision
The file Collision.c contains all of the core collision detection routines, but much of the custom handling is done in MyGuy.c and Enemy.c,
Object collision is the collision between two ObjNode’s. There are several fields in the ObjNode structure which are important to the collision detecting process:
-
CType
“Collision Type” for use in classifying an object type. -
CBits
“Collision Bits” specifies collision options. -
NumCollisionBoxes
the number of collision boxes assigned to this ObjNode. -
CollisionBoxes
an array of CollisionBoxType’s which contains the world-space collision box coordiantes. -
CollisionTriangles
pointer to collison triangle information for doing triangle-accurate collision detection.
The CType field determines the collision classification of an object. When a collision function is called a CType value is passed in which indicates what objects to perform collision on. An object is only tested if one or more of its CType bits match the input CType’s bits. In other words, if an object is an enemy, then it will have the CTYPE_ENEMY bit set. If you want to see if a bullet has hit an enemy then you pass the CTYPE_ENEMY bit to the collision function. If you also want to check if the bullet has hit the player, you also pass the CTYPE_PLAYER (CTYPE_ENEMY | CTYPE_PLAY).ER
All of the CType values are defined in Structs.h:
enum {
CTYPE_PLAYER = 1, // Me
CTYPE_ENEMY = (1<<1), // Enemy
CTYPE_MYBULLET = (1<<2), // Player's bullet
CTYPE_BONUS = (1<<3), // Bonus item
CTYPE_TRIGGER = (1<<5), // Trigger
CTYPE_SKELETON = (1<<6), // Skeleton
CTYPE_MISC = (1<<7), // Misc
CTYPE_BLOCKSHADOW = (1<<8), // Shadows go over it
CTYPE_HURTIFTOUCH = (1<<9), // Hurt if touched
CTYPE_PORTAL = (1<<10), // time portal
CTYPE_BGROUND2 = (1<<11), // Terrain BGround 2 path tiles
CTYPE_PICKUP = (1<<12), // Pickup
CTYPE_CRYSTAL = (1<<13), // Crystal
CTYPE_HURTME = (1<<14), // Hurt Me
CTYPE_HURTENEMY = (1<<15), // Hurt Enemy
CTYPE_BGROUND = (1<<16), // Terrain BGround path tiles
CTYPE_PLAYERTRIGGERONLY = (1<<17) // combined with _TRIGGER, this..
// ..trigger is only triggerable by player
};
You will notice that when most ObjNode’s are created in an item’s Add function, the CType and CBits are set to tell the collision system what kind of object it is and how collision should be performed on it. For example, when a Time Portal terrain item is initialized, the following code sets this collision type and bits:
newObj->CType = CTYPE_PORTAL;
newObj->CBits = CBITS_TOUCHABLE;
Every ObjNode which you plan on colliding with needs a collision box. The collision box is a bounding box which closely approximates the bounds of an object. If an object is moving, this bounding box will need to be updated on every frame. Nanosaur allows for an ObjNode to have multiple collision boxes, but in most cases only 1 box is needed (as is such in the Nanosaur code).
To easily set an ObjNode’s collision box, simply call the following function:
SetObjectCollisionBounds(theObjNode, top, bottom, left, right, front, back)
The top, bottom, left, right, front, and back values are the offsets from the object’s origin for its bounding box. The Time Portal’s collision box is defined as:
SetObjectCollisionBounds(newObj,500,0,-60,60,60,-60);
Once you have set an ObjNode’s Ctype, CBits, and collision box, you’ve done all you need to do to setup its collision information for doing simple bounding box collision detection. Just remember that if the object moves, you need to be sure that the bounding box gets updated. To do this you should call CalcObjectBoxFromNode() which will automatically recalculate the bounding box’s coordinates based on the ObjNode’s new coordinate and the old top, bottom, etc., offsets that you fed it at init time.
Note, that in most cases you will call a function called UpdateObject at the end of its “move” function. UpdateObject will automatically call CalcObjectBoxFromNode for you so you don’t really need to think about it.
Occasionally, you may want to be able to collide more accurately with an ObjNode, and when this happens you need to calculate Collision Triangles for the object. Objects with this kind of collision still have bounding boxes, but you do not need to specify them explicity like we did before. Instead, the function CreateCollisionTrianglesForObject() takes care of everything for us. It scans the object and creates a list of triangles for later collision detection and also builds the collison box for internal use.
Collision Triangles can only be assigned to non-moving objects! Do not try to do this on something that moves because it just won’t work.
In Nanosaur, the gray boulders are an example of an object with collision triangles. The boulder’s collision information is setup with the following code:
newObj->CType = CTYPE_MISC;
newObj->CBits = CBITS_ALLSOLID;
CreateCollisionTrianglesForObject(newObj);
That’s all that needs to be done to make the collision detection on this object work on a polygon-accurate level. You can run around and jump on boulders in a very accurate collision scheme.
Tile collisions are somewhat complex when explained in detail, so I will try to explain just the important points here since it is unlikely that you will ever need to modify this part of the Nanosaur engine.
In the Nanosaur engine, the terrain maps have 3 layers: the texture layer, height layer, and the path layer. The path layer contains collision tiles which are used to determine where solid areas of the map are. See the OreoTerrain documentation for more information.
The collision manager will automatically take care of detecting collisions with tiles, but suffice to say that the function GetTileCollisionBitsAtRowCol() is called to get the collision information about the tiles which the ObjNode in question is colliding against.
Most generic Objects can call a single collision function to automatically handle all collision detection: HandleCollisions(). This function takes an ObjNode and a CType as input values, but also relies on some global values for doing collision detection:
-
gCoord
Contains the current global coordinate of the ObjNode. This function does not use the Coord field in the ObjNode structure! -
gDelta
Contains the motion delta values that the ObjNode just now moved. This function does not use the Delta field in the ObjNode structure!
The HandleCollisions function takes care of performing collisions against all ObjNodes with matching Ctypes, and the collision information is stored in the array gCollisionList[]. You can use gNumCollisions and gCollisionList to manually find out information about what kinds of collisions just occurred, but it is more likely that you will usually let the collision manager handle these events on its own.
In the case that your object is not generic and you need to handle collisions in a specific way, you will want to call the function CollisionDetect. All this functions does is build the gCollisionList, but unlike HandleCollisions, nothing is done about these collisions. Both the player and enemy manager code have specialized collision handling logic, so they only call CollisionDetect and then parse through the data in gCollisionList manually. The player’s collision handling function is called DoPlayerCollisionDetect, and the enemy’s collision function is DoEnemyCollisionDetect.
It is best to examine the existing Nanosaur code to learn more about how these functions are called and used. The important thing to remember is that most of the time, all collisions are handled automatically and it is not important that you understand everything about how the code works.
Triggers are a specific type of object which does something when the player touches it. PowerUps, Exploding Crystals, and Lava Stepping Stones are examples of Triggers — they all respond to being touched by the player.
The player’s collision detection handler calls the Trigger collision handler function called HandleTrigger(). This function determines if the collision requires action by the Trigger Object and if so, it calls the Trigger’s “DoTrig_” function.
The Skeleton Manager is really the core part about the Nanosaur engine which makes it unique. This is the section of code which handles all of the character animation for the dinosaurs in Nanosaur. The skeleton data is created with BioOreo Pro, and the documentation for that tool explains the workings of the Skeleton in more detail than is covered here. In this document, I will cover the specifics of the function calls you need to use to work with Skeleton models in your application.
The first thing you need to do in order to use Skeletons in your application is to load the .skeleton file(s) that you are going to use. A single Skeleton can be used to simultaneously animate an infinite number of Skeleton objects on the screen. So, to load the T-Rex Skeleton, we do the following:
LoadASkeleton(SKELETON_TYPE_REX);
This function takes a “SKELETON_TYPE” as input. These values are enumerated in SkeletonObj.h:
enum
{
SKELETON_TYPE_PTERA,
SKELETON_TYPE_REX,
SKELETON_TYPE_STEGO,
SKELETON_TYPE_DEINON,
SKELETON_TYPE_TRICER,
SKELETON_TYPE_SPITTER
};
You can easily add or remove Skeleton Types from this list, just be sure to always update the MAX_SKELETON_TYPES value to indicate the correct quantity of Types you have.
LoadASkeleton() calls a function named LoadSkeletonFile(). This function has a big switch statement which handles the loading of the specific skeleton file indicated by the Type you have passed in. LoadSkeletonFile() then takes care of actually loading in the data for the Skeleton and also loads the Skeleton’s 3DMF model file. Basically, calling LoadASkeleton will do all memory allocations and geometry parsing that needs to be done. You really don’t have to worry about much — it’s all automatic.
When a Skeleton’s 3DMF file is loaded via LoadASkeleton(), the geometry in the file is “decomposed” into data which the Nanosaur engine can use to do the animation. Bascially, this decomposition simply creates lists of vertices and normals for the model while removing all duplicates. When the engine animates a skeleton, it uses this decomposed data to construct the actual TriMesh which is eventually displayed on the screen.
Remember that the Skeleton animation in Nanosaur is done by deforming the original 3DMF geometry of the dinosaur. We keep an original copy of the geometry in our decomposed lists, deform the geometry and store the result into a temporary TriMesh, and then submit the TriMesh in immediate mode to render it. Sounds relatively straight-forward, but believe me, it is a very complex process and luckily for you, you don’t need to worry about how it works.
During decomposition, the following functions are used to determine if two points or vectors are identical or “close enough”:
PointsAreCloseEnough()
VectorsAreCloseEnough()
Notice how one of these functions appears:
Boolean VectorsAreCloseEnough(TQ3Vector3D *v1, TQ3Vector3D *v2)
{
if (fabs(v1->x - v2->x) < 0.02f)
if (fabs(v1->y - v2->y) < 0.02f)
if (fabs(v1->z - v2->z) < 0.02f)
return(true);
return(false);
}
There is an identical function in BioOreo Pro which does the same thing. The “tolerance” values used in both the Nanosaur engine and BioOreo Pro must match exactly or bad things will happen.
In the above function, the tolerance values are set to 0.02. In the PointsAreCloseEnough function, the tolerance is 0.001. The tolerance values in BioOreo Pro are identical. If you change them in one app, you must change them in the other.
The third genre of Objects in the Nanosaur engine is the SKELETON_GENRE. Just like the other genre (DISPLAY_GROUP_GENRE and EVENT_GENRE), there is a special function call used to create a new Skeleton Object:
MakeNewSkeletonObject()
Just as with the other genres, this function takes a NewObjectDefinitionType structure as input. You fill out the usual data plus a few additional fields. The following code is used to create the player’s Skeleton Object in Nanosaur:
gNewObjectDefinition.type = SKELETON_TYPE_DEINON;
gNewObjectDefinition.animNum = PLAYER_ANIM_FALLING;
gNewObjectDefinition.coord.x = gMyStartX;
gNewObjectDefinition.coord.y = y + 400;
gNewObjectDefinition.coord.z = gMyStartZ;
gNewObjectDefinition.slot = PLAYER_SLOT;
gNewObjectDefinition.flags = STATUS_BIT_DONTCULL|STATUS_BIT_HIDDEN;
gNewObjectDefinition.moveCall = MoveMe;
gNewObjectDefinition.rot = (float)gMyStartAim * (PI2/8);
gNewObjectDefinition.scale = MY_SCALE;
gPlayerObj = MakeNewSkeletonObject(&gNewObjectDefinition);
Most of this should look pretty familiar from earlier in this document. The main difference is the use of the animNum field. A .skeleton file contains not only the skeleton geometry information, but also the animation information for all of the skeleton’s animation sequences. When you create a new Skeleton Object you specify which animation to start with by filling in the animNum field.
Everything else behaves just as it does for a DISPLAY_GROUP_GENRE Object, but as we’ll see later, the Object’s Move function needs to do a little more work to keep the Skeleton Object updated.
In the ObjNode structure, there is a field called Skeleton. When you create a Skeleton Object, this field will point to a large block of data which contains all of the additional information needed to describe a Skeleton Object:
typedef struct
{
Byte AnimNum; // animation #
Boolean IsMorphing; // flag set when morphing
float MorphSpeed; // speed of morphing (1.0 = normal)
float MorphPercent; // % morph from kf1 to kf2 (0.0 - 1.0)
JointKeyframeType JointCurrentPosition[MAX_JOINTS]; // for each joint,
// holds current interpolated keyframe values
JointKeyframeType MorphStart[MAX_JOINTS]; // morph start & end keyframes
// for each joint
JointKeyframeType MorphEnd[MAX_JOINTS];
float CurrentAnimTime;// current time index for animation
float LoopBackTime; // time to loop or zigzag back to
float MaxAnimTime; // duration of current anim
float AnimSpeed; // time factor for speed of executing
// current anim (1.0 = normal time)
Byte AnimEventIndex; // current index into anim event list
Byte AnimDirection; // forward or backward
Byte EndMode; // what to do when reach end of anim
Boolean AnimHasStopped; // flag gets set when anim has reached
// end of sequence
// (looping anims don't set this!)
TQ3Matrix4x4 jointTransformMatrix[MAX_JOINTS]; // holds matrix xform
// for each joint
SkeletonDefType *skeletonDefinition; // point to skeleton's shared data
TQ3TriMeshData localTriMeshes[MAX_DECOMPOSED_TRIMESHES]; // the triMeshes
// to submit for this ObjNode
}SkeletonObjDataType;
This structure holds information ranging from the status of the current animation to the TriMeshes used to render the model. The structure is large and complicated, but the commenting in the code is fairly clear about how everything operates. For the most part, you should never have to worry about anything in this structure that is not documented below.
A looping animation will continue to play and will automatically be updated each time through the game’s main loop. The MoveObjects() function calls UpdateSkeletonAnimation() which does the deformation on the skeleton’s geometry for the current frame. It handles all of the interpolation of the keyframes to get the exact state of each vertex in the mesh. To change from one animation to another or to morph to a different animation, there are two functions which can be called inside the Skeleton’s Move function:
SetSkeletonAnim()
MorphToSkeletonAnim()
Use SetSkeletonAnim() to immediately switch to a different animation sequence. The following code shows how the player starts the death sequence in Nanosaur:
SetSkeletonAnim(theNode->Skeleton, PLAYER_ANIM_DEATH);
This will cause the animation to start at time = 0 of the given animation sequence. This function also resets the animation playback speed to 1.0 (normal) by setting the AnimSpeed field in the ObjNode’s SkeletonObjDataType structure.
By default, animations and morphs always play at “normal” speed. Normal speed simply means the speed that the animation appears in BioOreo Pro. You can speed up and slow down any animation simply by changing the AnimSpeed field. A value of 2.0 would make the animation run 2x faster, and a value of 0.5 would make it run half as fast. In Nanosaur, I tweak the animation playback speed for the running animation depending on how fast the player is moving. The faster the player moves, the faster I play the animation so he doesn’t appear to moon-walk.
A morph causes the current frame of animation to morph to the first frame of animation in the target animation before continuing to play the target animation. It’s really the same as a “SetAnim” except that it doesn’t immediately start the next animation. Instead, it morphs to the animation before playing it.
In Nanosaur, when the player stops running, he morphs back to the stand position. This makes the transition much smoother than a SetAnim:
MorphToSkeletonAnim(theNode->Skeleton,PLAYER_ANIM_STAND,3.0);
This looks just like SetSkeletonAnim() except that there is a third parameter which is the morph speed parameter. The bigger the number, the faster the morph will be. There’s no real meaning to this number, just use whatever gives you the desired visual effect. In general, I use numbers between 2 and 6 for most of the morphing in Nanosaur.
Very often you may need to know the world-space coordinate of a Skeleton’s head, foot, or whatever. For example, in Nanosaur I need to know the coordinate and orientation of the player’s gun so I know where to start the bullets and which direction to fire them in.
Getting this information is very easy because there are a few utility functions in the Nanosaur engine for extracting the coordinates and matrix associated with any of the Skeleton’s joints:
FindCoordOfJoint()
FindCoordOnJoint()
FindJointFullMatrix()
This function will return the current world-space coordinates of the joint in question.
Nanosaur calls this function for a kick animation that never made it into the final game. I needed to know the coordinates of the player’s foot so that I could see if he had successfully kicked an enemy:
FindCoordOfJoint(theNode, MY_FOOT_JOINT_NUM, &footCoord);
You need to know the joint # of the limb you are finding. This is done by looking at the Animation Timeline window in BioOreo Pro. The footCoord value will contain the world-space coordinates of this joint upon return.
This function is similar to FindCoordOfJoint() except that it returns the coordinates of a point relative to the joint. In other words, if I’m shooing a bullet out of the tip of my gun, I need the coordinates of the tip of the gun, not the origin of the gun. FindCoordOfJoint() only returns me the coordinates of the origin of a joint, but FindCoordOnJoint() gives me the coordinates of any point relative to the origin.
So, the code for finding the coordinate of the tip of the player’s gun looks like this:
static TQ3Point3D gGunTipOffset = {0,0,-65};
FindCoordOnJoint(theNode, MYGUY_LIMB_GUN, &gGunTipOffset, &gunTipCoord);
I know by looking at my model in Form*Z that the tip of the gun is -65 units down the z-axis from the origin. Therefore, I set the gGunTipOffset to (0,0,-65). When I pass this to FindCoordOnJoint(), it returns the world- space coordinates of this offset.
The other two functions return coordinates, but not any orientation information. I still need to know what direction my gun’s tip is facing so I can know which direction to shoot my bullets. Actually, to be fair, I don’t actually use this function to get the bullet’s trajectory in Nanosaur. Since the gun doesn’t really move much relative to the player, I cheat and just assume its pointing straight and I use the player’s y-axis rotation information.
But... if I were to do this correctly, I would do the following:
FindJointFullMatrix(playerObj, MYGUY_LIMB_GUN, &jointMat);
This would return to me the full 4x4 transformation matrix for that joint.
So, hopefully, you have at least a rough understanding of how the Nanosaur engine works. Most of the code is fairly easy to understand and is well commented. My advice for working with this code is to start with the Nanosaur engine as a shell and gradually take things out and add your own things in. Going slow will avoid any catastrophic events.
Should have run into problems or have questions that are not answered in this document, please don’t hesitate to contact me (pangea@bga.com).