Main  |  Maps  |  Programming | Links |  Files |  Demos |  Forums

This tut lets your mod have BREAKABLE GLASS! Sweet, eh? OK, this is actually fairly easy. I wanna thank Wang for helping me with the code and Kalifa for the glass models. Whoops, I can't forget Mastaba for the help on the random code. You need to grab the media pack for this tut and optionally my sample map. Note: Mappers need to first, make it a func_door(anything will do) then press "n" and change the func_door to func_breakable, or optionally edit the "entities.def" file and add in func_breakable, so you can just select it from the normal drop down list in Q3Radiant. They also need to set the life of the glass by pressing "n" while selecting the entity and in the key box type in "health" and in the value box type in whatever NUMBER of life it should have. If you dont it defaults to 5 life. Your mapper needs to add the ifx.shader into their baseq3/scripts directory as well as their mymod/scripts folder. You will want he/she to edit the shaderlist.txt in their baseq3/scripts directory and add at the VERY bottom
ifx
Your mapper needs to have done that so upon compile it does the lightmap and everything correctly. And guess what? I actually commented this tutorial pretty well.

OK, first lets add our function for when Quake3 recognizes the glass. Add this to the bottom of g_misc.c:

/*QUAKED func_breakable (1 0 0) (-16 -16 -16) (16 16 16)
 Explodes glass
 */
 void SP_func_breakable( gentity_t *ent ) {
  int health;
  
  // Make it appear as the brush
  trap_SetBrushModel( ent, ent->model );
  // Lets give it 5 health if the mapper did not set its health
  G_SpawnInt( "health", "0", &health );
  if( health <= 0 )
   health = 5;
 
  ent->health = health;
  // Let it take damage
  ent->takedamage = qtrue;
  // Let it know it is a breakable object
  ent->s.eType = ET_BREAKABLE;
  // If the mapper gave it a model, use it
  if ( ent->model2 ) {
      ent->s.modelindex2 = G_ModelIndex( ent->model2 );
  }
  // Link all ^this^ info into the ent
  trap_LinkEntity (ent);
 }

Below that code lets add the function that G_Damage calls to see if we can break it yet:

/*
 =================
 G_BreakGlass
 =================
 */
 void G_BreakGlass(gentity_t *ent, vec3_t point, int mod) {
     gentity_t   *tent;
 	vec3_t      size;
     vec3_t      center;
 	qboolean    splashdmg;
 	
 	// Get the center of the glass
     VectorSubtract(ent->r.maxs, ent->r.mins, size);
     VectorScale(size, 0.5, size);
     VectorAdd(ent->r.mins, size, center);
 
 	// If the glass has no more life, BREAK IT
 	if( ent->health <= 0 ) {
 	G_FreeEntity( ent );
     // Tell the program based on the gun if it has no splash dmg, no reason to ad ones with
 	// splash dmg as qtrue as is that is the default
 	switch( mod ) {
 		case MOD_GAUNTLET:
 		splashdmg = qfalse;
 		break;
 		case MOD_SHOTGUN:
 		splashdmg = qfalse;
 		break;
 		case MOD_MACHINEGUN:
 		splashdmg = qfalse;
 		break;
 		case MOD_RAILGUN:
 		splashdmg = qfalse;
 		break;
 		case MOD_LIGHTNING:
 		splashdmg = qfalse;
 		break;
 		default:
 		splashdmg = qtrue;
 		break;
 	}
 	// Call the function to show the glass shards in cgame
 	// center can be changed to point which will spawn the
 	// where the killing bullet hit but wont work with Splash Damage weapons
 	// so I just use the center of the glass
 	switch( splashdmg ){
 	case qtrue:
     tent = G_TempEntity( center, EV_BREAK_GLASS );
 	break;
 	case qfalse:
     tent = G_TempEntity( point, EV_BREAK_GLASS );
 	break;
 	}
 	tent->s.eventParm = 0;
 	}
 }
  

Now lets let G_BreakGlass be called from g_combat.c. Open up g_local.h and find:

//
 // g_misc.c
 //
 void TeleportPlayer( gentity_t *player, vec3_t origin, vec3_t angles ); 

Below it place:

void G_BreakGlass( gentity_t *ent, vec3_t point, int mod );

Thats all we got to do with g_misc.c, now lets go to g_spawn.c and find this:

void SP_func_timer (gentity_t *self);

And below it add:

void SP_func_breakable (gentity_t *ent);

Now find:

	{"func_timer", SP_func_timer},			// rename trigger_timer?

Below it place:

	{"func_breakable", SP_func_breakable},

Thats it with g_spawn.c. In case you want to know, that last line we added tells it to call our SP_func_breakable function when it finds a func_breakable in the map. Open up g_combat.c and find:

	// shootable doors / buttons don't actually have any health
 	if ( targ->s.eType == ET_MOVER ) {
 		if ( targ->use && targ->moverState == MOVER_POS1 ) {
 			targ->use( targ, inflictor, attacker );
 		}
 		return;
 	}

Under it add:

	// If we shot a breakable item subtract the damage from its health and try to break it
 	if ( targ->s.eType == ET_BREAKABLE ) {
         targ->health -= damage;
 		G_BreakGlass( targ, point, mod );
 		return;
 	}

OK, we are done with g_combat.c, now we gotta get the bots to shoot the glass! So open up ai_dmq3.c and find:

	//if it is a door
 	if (!strcmp(classname, "func_door")) {
 		if (trap_AAS_FloatForBSPEpairKey(ent, "health", &health)) {
 			//if health the door must be shot to open
 			if (health) return ent;
 		}
 	}

Below it add:

	//if it is some glass
 	if (!strcmp(classname, "func_breakable")) {
 		return ent;
 	}

Now the last edit for this file, go find:

		else if (!strcmp(classname, "func_door")) {
 			//shoot at the shootable door
 			trap_AAS_ValueForBSPEpairKey(ent, "model", model, sizeof(model));
 			modelindex = atoi(model+1);
 			//if the model is not loaded
 			if (!modelindex) return;
 			VectorClear(angles);
 			BotModelMinsMaxs(modelindex, ET_MOVER, mins, maxs);
 			//door origin
 			VectorAdd(mins, maxs, origin);
 			VectorScale(origin, 0.5, origin);
 			//
 			VectorSubtract(origin, bs->eye, movedir);
 			vectoangles(movedir, moveresult->ideal_viewangles);
 			moveresult->flags |= MOVERESULT_MOVEMENTVIEW;
 			moveresult->flags |= MOVERESULT_MOVEMENTWEAPON;
 			//select the machinegun and shoot
 			trap_EA_SelectWeapon(bs->client, WEAPONINDEX_MACHINEGUN);
 			if (bs->cur_ps.weapon == WEAPONINDEX_MACHINEGUN) {
 				trap_EA_Attack(bs->client);
 			}
 			return;
 		}

Agin, directly below it add:

		else if (!strcmp(classname, "func_breakable")) {
 			//shoot at the shootable door
 			trap_AAS_ValueForBSPEpairKey(ent, "model", model, sizeof(model));
 			modelindex = atoi(model+1);
 			//if the model is not loaded
 			if (!modelindex) return;
 			VectorClear(angles);
 			BotModelMinsMaxs(modelindex, ET_BREAKABLE, mins, maxs);
 			//door origin
 			VectorAdd(mins, maxs, origin);
 			VectorScale(origin, 0.5, origin);
 			//
 			VectorSubtract(origin, bs->eye, movedir);
 			vectoangles(movedir, moveresult->ideal_viewangles);
 			moveresult->flags |= MOVERESULT_MOVEMENTVIEW;
 			moveresult->flags |= MOVERESULT_MOVEMENTWEAPON;
 			//select the machinegun and shoot
 			trap_EA_SelectWeapon(bs->client, WEAPONINDEX_MACHINEGUN);
 			if (bs->cur_ps.weapon == WEAPONINDEX_MACHINEGUN) {
 				trap_EA_Attack(bs->client);
 			}
 			return;
 		}

Thats all for ai_dmq3.c! What that does is first is says if there is a func_breakable, it MUST be shot to open/unblock its path/whatever. Then in BotAIBlocked it looks for func_breakable and if its there, the bot pulls out his boomstick and shoots it. Now we are nearly done with the game part of this. So lets keep on rolling. All we got left is to open up bg_public.h and add some stuff. So open it up and find:

typedef enum {
 	ET_GENERAL,
 	ET_PLAYER,
 	ET_ITEM,
 	ET_MISSILE,
 	ET_MOVER,

Add below it:

	ET_BREAKABLE,

One last thing! Find this:

	EV_GIB_PLAYER,			// gib a previously living player

And below it add:

	EV_BREAK_GLASS,

Now, thats it for the game module! So go to cg_local.h and find:

	qhandle_t	gibLeg;
 	qhandle_t	gibSkull;
 	qhandle_t	gibBrain;

Add below it:

	qhandle_t	glass01;
 	qhandle_t	glass02;
 	qhandle_t	glass03;

Now find:

void CG_GibPlayer( vec3_t playerOrigin );

And add below it:

void CG_BreakGlass( vec3_t playerOrigin );

Thats it for cg_local.h, lets open up cg_main.c and register the models. Find:

	cgs.media.gibLeg = trap_R_RegisterModel( "models/gibs/leg.md3" );
 	cgs.media.gibSkull = trap_R_RegisterModel( "models/gibs/skull.md3" );
 	cgs.media.gibBrain = trap_R_RegisterModel( "models/gibs/brain.md3" );

Like always, below it add:

	cgs.media.glass01 = trap_R_RegisterModel( "models/breakables/glass01.md3" );
 	cgs.media.glass02 = trap_R_RegisterModel( "models/breakables/glass02.md3" );
 	cgs.media.glass03 = trap_R_RegisterModel( "models/breakables/glass03.md3" );

Go open up cg_event.c and find:

	case EV_GIB_PLAYER:
 		DEBUGNAME("EV_GIB_PLAYER");
 		trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.gibSound );
 		CG_GibPlayer( cent->lerpOrigin );
 		break;

Below it add:

	case EV_BREAK_GLASS:
 		DEBUGNAME("EV_BREAK_GLASS");
 		// Change cgs.media.gibSound to whatever sound you want it to use
 		// I think the gib sound sounds pretty good
 		trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.gibSound );
 		CG_BreakGlass( cent->lerpOrigin );
 		break;

Now open up cg_effects.c, we need to add the functions that produce the gibs, so at the bottom of the file add:

/*
 ==================
 CG_LaunchGlass
 ==================
 */
 void CG_LaunchGlass( vec3_t origin, vec3_t velocity, qhandle_t hModel ) {
 	localEntity_t	*le;
 	refEntity_t		*re;
 
 	le = CG_AllocLocalEntity();
 	re = &le->refEntity;
 
 	le->leType = LE_FRAGMENT;
 	le->startTime = cg.time;
 	le->endTime = le->startTime + 30000 + random() * 3000;
 
 	VectorCopy( origin, re->origin );
 	AxisCopy( axisDefault, re->axis );
 	re->hModel = hModel;
 
 	le->pos.trType = TR_GRAVITY;
 	VectorCopy( origin, le->pos.trBase );
 	VectorCopy( velocity, le->pos.trDelta );
 	le->pos.trTime = cg.time;
 
 	le->bounceFactor = 0.3;
 
 	le->leFlags = LEF_TUMBLE;
 	le->leBounceSoundType = LEBS_BRASS;
 	le->leMarkType = LEMT_NONE;
 }
 
 /*
 ===================
 CG_BreakGlass
 
 Generated a bunch of glass shards launching out from the glass location
 ===================
 */
 #define	GLASS_VELOCITY	175
 #define	GLASS_JUMP		125
 void CG_BreakGlass( vec3_t playerOrigin ) {
 	vec3_t	origin, velocity;
     int     value;
 	// How many shards to generate
 	int     count = 50;
 	// The array of possible numbers
 	int     states[] = {1,2,3};
 	// Get the size of the array
     int     numstates = sizeof(states)/sizeof(states[0]);
 
 	// Countdown "count" so this will subtract 1 from the "count"
 	// X many times. X being the "count" value
 	while ( count-- ) {
 	// Generate the random number every count so every shard is a
 	// of the three. If this is placed above it only gets a random
 	// number every time a piece of glass is broken.
 	value = states[rand()%numstates];
 	VectorCopy( playerOrigin, origin );
 	velocity[0] = crandom()*GLASS_VELOCITY;
 	velocity[1] = crandom()*GLASS_VELOCITY;
 	velocity[2] = GLASS_JUMP + crandom()*GLASS_VELOCITY;
 	switch (value) {
 	case 1:
 	// If our random number was 1, generate the 1st shard piece
     CG_LaunchGlass( origin, velocity, cgs.media.glass01 );
     break;
 	case 2:
 	CG_LaunchGlass( origin, velocity, cgs.media.glass02 );
     break;
 	case 3:
 	CG_LaunchGlass( origin, velocity, cgs.media.glass03 );
 	break;
 	}
 	}
 }
 

Now the the VERY last step. We gotta let it know how to draw this object. In cg_ents.c, find:

	case ET_MOVER:
 		CG_Mover( cent );
 		break;

And below it place:

	case ET_BREAKABLE:
 		CG_Mover( cent );
 		break;

Yes, the ET_BREAKABLE is suppost to call CG_Mover. All that function does is tell the engine how to render it. And CG_Mover is the exact way we want to render our glass. Compile ALL 3 projects and enjoy.

I spend my time programming and writing these tutorials, so please feel free to use this code in your mod as long as www.inolen.com and the author receive credit.

Design Copyright inolen 2000