Creating the Max Payne bullet effect in your Genesis3d game

By Gustav sirGustav Jansson [sir_gustav@hotmail.com]




Introduction

By the name bullet effect I don't mean the bullet-time effect that can be seen in the matrix(everything but the camera slows down, or everything but max slows down). I mean different particle effects when shooting at different materials. Example: There should be a small glass pieces when shooting at a window, but there should come wall pieces when shooting at the wall, but both are world geometry and both are handled the same by geWorld_Collision().
Even if this article describes how to create the bullet effect(or more exact how to get the material the bullet collided with), the same technique can also be used to play different sounds when the character walks on different materials, or anything else that would depend on the material on the floor.

Example of the bullet effect:

Click on each image to make them larger

Since I'm not basing my game on GTest, this article will not contain any shell specific functions(only shaddows to my own :), and before we start there are a few things that need to be cleared out..




Difficult words

Not really difficult, but they need a explenation anyway.




Assumptions

I will assume some things, they should all be listed here:




This article

Before we even start to look at the code we need to determine where the code of this article stops and where your code starts. I will only give you the function

int get_material_index(GE_Collision *col);
and a kick to get started on how to handle the material effect.




How to do it

No we know what to do, but how do we do it?




Modifying the Engine

Okay this isn't as hard as it sounds. Luckily someone already has done this, that someone is:

The new function can be found in xtra_g3d.h
GENESISAPI geBoolean GENESISCC getSingleTextureNameByTrace(geWorld* World, geVec3d *from, geVec3d *to, char* p_name);
Here is the source:
//code contributed by xing studios and with a wxb1 mod, modified by sirGustav
GENESISAPI geBoolean GENESISCC getSingleTextureNameByTrace(geWorld* World, geVec3d *from, geVec3d *to, char* p_name){
	int32			Node, Plane, i;
	geVec3d		Pos1, Pos2, Impact;
	GFX_Node		*GFXNodes;
	GFX_Face		*GFXFaces;
	GFX_TexInfo		*GFXTexInfos;		
	GFX_Texture		*GFXTextures;		
	Surf_SurfInfo		*Surf;
	int count = 0;
	
	GFXNodes = World->CurrentBSP->BSPData.GFXNodes;
	GFXFaces = World->CurrentBSP->BSPData.GFXFaces;
	GFXTexInfos = World->CurrentBSP->BSPData.GFXTexInfo;
	GFXTextures = World->CurrentBSP->BSPData.GFXTextures;
	
	//Pos1 = *ReferencePoint;
	//Pos2 = Pos1;
	//Pos2.Y -= 30000.0f;
	Pos1 = *from;
	Pos2 = *to;
	
	if(!Trace_WorldCollisionExact2((geWorld*)World, &Pos1, &Pos1, &Impact, &Node, &Plane, NULL))		
	{
		if (Trace_WorldCollisionExact2((geWorld*)World, &Pos1, &Pos2, &Impact, &Node, &Plane, NULL))
		{
			Surf = &(World)->CurrentBSP->SurfInfo[GFXNodes[Node].FirstFace];
			
			for (i=0; i< GFXNodes[Node].NumFaces; i++)
			{
				if (Surf_InSurfBoundingBox(Surf, &Impact, 20.0f))
				{
					int32 TexInfo = GFXFaces[GFXNodes[Node].FirstFace + i].TexInfo;
					int32 Texture = GFXTexInfos[TexInfo].Texture;
					strcpy(p_name, GFXTextures[Texture].Name);
					return GE_TRUE;
				}
				Surf++;
			}
		}
	}
	return GE_FALSE;
}
The source(the modified files) and all the necessary files to use this function with the modified Genesis3d 1.6 library will be should be found on the end of this document.




Writing the container

Using the previous function we can get the texturename based on the GE_Collision*. Now it's time for the container. This container is a hashtable, but could just as easily be a AVL or a Red-black binary search-tree. This container will store the texturename as the key and the materialnumber as a integer.
Once again you are lucky. I have written this one aswell. Here is the API:

HashTable* HT_CreateDefault();
HashTable* HT_Create(unsigned long size);
void HT_Destroy(HashTable* table);

#define HT_DESTROY(a)	{ HT_Destroy(a); a=0; }

void HT_Add(HashTable *table, int data, char* key);
int HT_Load(HashTable *table, char* fileName);

// returns the data as a simple return or as the pointer through the parameter
// it will return, read return, -1 if the texture wasn't found, and not change the parameter data
int HT_Find(HashTable *table, char* key, int* data);

void HT_Remove(HashTable *table, char* key);
void HT_Clear(HashTable *table);
No resizing of the hashtable will be performed in the current implementation, and the collisions are handled through a liked list. This container source is at the end of the document. The utility function HT_Load loads data from text file that looks like this(wt denotes a whitespace):
TEXTURE_NAME wt MATERIAL_INDEX wt TEXTURE_NAME wt MATERIAL_INDEX wt TEXTURE_NAME wt MATERIAL_INDEX




Writing the function

You are lucky. I have written this function aswell.

int getMaterial(GE_Collision* lCol){
	char texture[300];
	geVec3d from;
	geVec3d to;

	memset(texture, 0, 300);
	geVec3d_AddScaled(&(lCol->Impact), &(lCol->Plane.Normal), 5.0f, &from);
	geVec3d_AddScaled(&(lCol->Impact), &(lCol->Plane.Normal), -5.0f, &to);
	
	if( getSingleTextureNameByTrace(World, &from, &to, texture) ){
		int data;
		// the texture parameter now contains the texture name

		return HT_Find(materialStorage, texture, &data);
	}
	return MATERIAL_ERROR;
}
There you have it. The materialStorage is a global HashTable*(and is loaded at init), but the function could easily be modified to take this as parameter. If you want the other function that takes two vectors you can easily modify this one, but I think this function is more useful, since you probably want a more




How to continue

How should you continue now? That's a good question. I went through the whole Max Payne demo and shot a different object to see how they reacted, and defined a couple of materials (16 to be exact)

Here is an example on how it could be made:

int actOnBulletCollision(GE_Collision* lCol){
  int materialId;
  materialId = getMaterial(lCol);
  switch(materialId){
  case MATERIAL_STONE:
    // add smoke
    addGraySmoke(); // creates a spray
    break;
  case MATERIAL_ELECTRIC:
    // add spark
    addSpark(); // creates a sprite
    // add black smoke
    addBlackSmoke(1.0f); // create a spray after 1.0f second
    break;
  case MATERIAL_METAL:
    // add spark
    addBigSpark(); // creates a sprite
    break;
  case MATERIAL_ERROR: // getMaterial() returns MATERIAL_ERROR if an error occured
  default:
    return 0; // report to the caller that this function has failed
  }
  return 1;
}
It's pretty simple, after all. It's pretty simple to add effects. It's a matter of changinng the id of one(or more) texture(s) to a new material, and add it to the handling function. Of course you need to add the #define(or if you prefer const int) to materials.h (or wherever you keep them).




Improvements

The hashtable could be improved to handle resizing.

The getSingleTextureNameByTrace could return a null string(ie "") or return a GE_FALSE if the texture that was found is a world texture. Currently the function returns the texturename of the texture that is on the wall, which could be anything. The current solution to this is to apply all the textures to the level first. The sky texture(one of those textures that is used in the skybox) is applied to areas where there will be sky. Then bind that texture to a material id that means no effect(MATERIAL_NONE for an example) and exclude that material id from all material effect codes.




Downloads

Modified genesis3d 1.6 engine
The additional source code to the modification.
The hashtable source
Example materials.txt that contains the texturename and material id to the hashtable.Please note this is specific to my shell, It is likely that it won't work work in your shell, unless you have the same textures and the same material specification as I do(which I doubt).