Post tutorial RSS Doom Source Code Tutorial 12

Modding The Monsters in Doom Part 5 - The Arachnotron. In this special article we will demonstrate how multi-mode monster attacks may be achieved in Doom.

Posted by on - Intermediate Client Side Coding

Doom Source Code Tutorial 12
Game: Doom2, but may also apply to Doom.
Level: Intermediate.
Objectives: to demonstrate how multi-mode monster attacks may be achieved in Doom.
Resources required: VC++ 2008 Express Edition; source code for Doom (tested with Doomsday ver 1.9.0-beta6.9).
Introduction:
Many monsters in doom have 2 modes of attack either with missiles when the enemy is at some distance away from the monster or a melee attack when the adversary is physically very close. In this article we will demonstrate that it is possible to create several modes of attacks for these monsters based on certain criteria. In this particular tutorial we will show how to make the baby spider switch to shooting imp's fire balls instead of plasma and to spray lead using guns as well as continuously launching a volley of high speed rockets. In the process,as we said will show how you can control these attacks when certain conditions are met ,that you the modder can choose to set for this interaction to take place. We are using the arachnotron as an example but you can certainly apply these ideas to almost any other monster in Doom or Doom2. Please note that this mod is based on jdoom, but I am sure that our friends who prefer other ports should have enough information to know their way around the code base as we have covered enough ground by now.
Procedure:
1. To begin your mod, open the defs file objects.ded in doomsday and browse through the relevant attack states for the arachnotron:

State {
ID = "BSPI_ATK1";
Sprite = "BSPI";
Frame = 0;
Flags = fullbright;
Tics = 20;
Action = "A_FaceTarget";
Next state = "BSPI_ATK2";
}

State {
ID = "BSPI_ATK2";
Sprite = "BSPI";
Frame = 6;
Flags = fullbright;
Tics = 4;
Action = "A_BspiAttack";
Next state = "BSPI_ATK3";
}

State {
ID = "BSPI_ATK3";
Sprite = "BSPI";
Frame = 7;
Flags = fullbright;
Tics = 4;
Next state = "BSPI_ATK4";
}

State {
ID = "BSPI_ATK4";
Sprite = "BSPI";
Frame = 7;
Flags = fullbright;
Tics = 1;
Action = "A_SpidRefire";
Next state = "BSPI_ATK2";
}

2. You can observe that the second state calls the action function A_BspiAttack that carries out the actual attack, let's look inside this one for more clues, find it in the file p_enemy.c in jdoom:

void C_DECL A_BspiAttack(mobj_t *actor)
{
if (!actor->target)
return;
A_FaceTarget(actor);
// Launch a missile.
P_SpawnMissile(MT_ARACHPLAZ, actor, actor->target);
}

3. This simply calls the P_SpawnMissile () function which launches one plasma shot at a specified speed towards the target. I will not go into details here of how this function works as it involves calling a host of other functions that include collision detection code amongst other things which is beyond the scope of this tutorial. For now just take it that this function when called is capable of launching any missile type and it can identify the type from the Map Thing (MT_.....) or object name that you use as a parameter in the appropriate argument which is in this case MT_ARACHPLAZ . So if we want to launch a different missile like say an imp's fire ball at the target, all we have to do is just change this parameter from MT_ARACHPLAZ to MT_TROOPSHOT . Very simple! Let's do it then and make the Arachnotron shoot imp's fire balls instead of plasma shots. However,first we need some criteria to be met before this monster decides to switch to this second weapon. There are a host of conditions we can choose from and your imaginations and ingenuity plays a major part here. Of course you can still emulate others' ideas, if you choose to. Let's opt for a simple one as always, as these are meant to be a noob level tutorials anyway. We will base our switch on the distance between the monster and its target. If the enemy is too close it will shoot normal plasma missiles , if on the other hand it moves farther away from this monster to some pre-determined distance the Arachnotron will switch to shooting imp's fire balls .
4. We need some means of calculating the distance between the monster and its target. Fortunately we do not need to write a new function as there is already a built-in one that ships with jdoom (ver 1.9.0beta6.9) that should do the job for us and at no cost . Our modified function should now look like this:

void C_DECL A_BspiAttack(mobj_t *actor)
{
//declare a variable for distance between monster and target
float distance ;
// use built-in function to estimate this distance,actor is the monster,
//pos is short for position.
distance = P_ApproxDistance(actor->target->pos[VX] - actor->pos[VX],
actor->target->pos[VY] - actor->pos[VY]);
if (!actor->target)
return;
A_FaceTarget(actor);
// I found that 768 as distance threshold suitable, you can change it!
if (distance < 768)
//launch normal plasma shot if target is too close
P_SpawnMissile(MT_ARACHPLAZ, actor, actor->target);
else
//otherwise launch an imp fire ball instead if target is too far
P_SpawnMissile(MT_TROOPSHOT, actor, actor->target);
}

5. Before you build your mod, quickly look back at the last attack state above, you'll see that it calls the re-fire function A_SpidRefire which is also used for its bigger cousin the Spider Brain Boss, so everything should work out fine and we should not need to do any more changes .Let's do it.
6. Copy and paste the above code block to replace the existing code in A_BspiAttack and build your mod. Now to test your work choose a level that has this monster as the star of the show and with plenty of open space .I've tested this mod a number of times on level 19 ,Doom2 , using ultra violence skill level. As soon as you jump to this level with idclev cheat ,there should be a single arachnotron on your left ready to do battle. Move back and forth from this monster while dodging its shots , you ‘ll find that it changes missiles from plasma to fire balls and back depending on how far you get away from it.
7. Let's now make the arachnotron shoot bullets just like its bigger relative. We may choose any of the existing configurations, the zombie,the sergeant or the chain gunner; OK I hear you shout "heavy weapons dude". All right then we will just copy the weapon code for that guy, here is the complete code:

void C_DECL A_BspiAttack(mobj_t *actor)
{
//declare a variable for distance between monster and target
float distance ;
int angle, bangle, damage;
float slope;
// use built-in function to estimate distance
distance = P_ApproxDistance(actor->target->pos[VX] - actor->pos[VX],
actor->target->pos[VY] - actor->pos[VY]);
if (!actor->target)
return;
// I found this distance threshold suitable, u can change it!
if (distance < 768)
{
//shoot lead, copy the code for the chain gunner.
S_StartSound(SFX_SHOTGN, actor);
A_FaceTarget(actor);bangle = actor->angle;
slope = P_AimLineAttack(actor, bangle, MISSILERANGE);
angle = bangle + ((P_Random() - P_Random()) << 20);
damage = ((P_Random() % 5) + 1) * 3;
P_LineAttack(actor, angle, MISSILERANGE, slope, damage);
}
else
//otherwise launch the normal plasma ball instead if target is too far
P_SpawnMissile(MT_ARACHPLAZ, actor, actor->target);
}
Now the arachnotrion is smarter; it will spray you with lead at close range and when you move further back to get out of range, it decides that it is best to roast you with deadly plasma missiles. Very clever monster!
8. We are feeling more ambitious today , how about making the arachnotron fire rockets at you? No, it is not a tall order and it can be done.However, we have to be extra careful here for several reasons:
a.Giving a rocket launcher to this monster with its full devastating power will make it rival the main boss ,the cyberdemon.
b.There can be too many arachnotron on some maps and the the mayhem may be just too much to say the least.
c.The code responsible for calculating the radius damage (collateral damage) is quite involved and may slow down the game on some computers if too many rockets are fired in one go.
So as a compromise solution we agree to give this monster a rocket launcher, but a cut down version. We will reduce the speed of the rockets, the rate of firing as compared to plasma balls and most important , remove the secondary damage function altogether.
9. Before we go ahead with our modification, we need to elaborate further on what we have stated in c above. Most probably, any player of this game knows that there are 2 components to rocket damage, the main one resulting from the direct impact of the missile, and that is the one listed in the Rocket Thing's data and has a value of 20 as shown in red below:

Thing {
ID = "ROCKET";
DoomEd number = -1;
Spawn state = "ROCKET";
See state = "NULL";
Pain state = "NULL";
Melee state = "NULL";
Missile state = "NULL";
Death state = "EXPLODE0";
Xdeath state = "NULL";
Raise state = "NULL";
See sound = "rlaunc";
Attack sound = "None";
Pain sound = "None";
Death sound = "barexp";
Active sound = "None";
Reaction time = 8;
Spawn health = 1000;
Speed = 20; ← we want to reduce the speed
Radius = 11;
Height = 8;
Mass = 100;
Damage = 20; ← normal direct damage, reduce this one too.
Flags = noblockmap | missile | dropoff | nogravity | brightexplode;
Flags2 = noteleport;
}

and there is the other deadly, almost hidden secondary or radius of damage effect ,lurking inside the missile "EXPLODE" State as an action function, as shown below:

State {
ID = "EXPLODE1";
Sprite = "MISL";
Frame = 1;
Flags = fullbright;
Tics = 8;
Action = "A_Explode"; ← we need to disable this one!
Next state = "EXPLODE2";
}

State {
ID = "EXPLODE2";
Sprite = "MISL";
Frame = 2;
Flags = fullbright;
Tics = 6;
Next state = "EXPLODE3";
}

State {
ID = "EXPLODE3";
Sprite = "MISL";
Frame = 3;
Flags = fullbright;
Tics = 4;
Next state = "NULL";
}
It is therefore necessary to take care of this one too in order to keep the game balanced and let the the game run at full speed.
10. One more thing ;do not forget that you, the player ,certainly do not want to use the same wimpy rockets given to the monster; the only problem is that the game still uses the same states and code to shoot the rockets used by both of you!
11. So we need to solve several issues simultaneously: modify the properties of the rockets for the monster and make them weaker , remove radius of damage effect and most importantly let the player use the same rocket launcher at full power at anytime and as normal.
The solution is to modify these values dynamically from within the weapons code as shown in the following code blocks. First make changes to the monster attack function:

void C_DECL A_BspiAttack(mobj_t *actor)
{
//declare a variable for distance between monster and target
float distance ;
int angle, bangle, damage;
float slope;
// use built-in function to estimate distance
distance = P_ApproxDistance(actor->target->pos[VX] - actor->pos[VX],
actor->target->pos[VY] - actor->pos[VY]);
if(!actor->target)
return;
// I found this distance threshold suitable, u can change it!
if (distance < 768)
{
//first ensure that tics are reset back to normal before you fire lead
STATES[S_BSPI_ATK2].tics = 4;
STATES[S_BSPI_ATK3].tics = 4;
//shoot lead
S_StartSound(SFX_SHOTGN, actor);
A_FaceTarget(actor);
bangle = actor->angle;
slope = P_AimLineAttack(actor, bangle, MISSILERANGE);
angle = bangle + ((P_Random() - P_Random()) << 20);
damage = ((P_Random() % 5) + 1) * 3;
P_LineAttack(actor, angle, MISSILERANGE, slope, damage);
}
else
//shoot rockets
{
// reduce direct damage rate and speed of rockets
MOBJINFO[MT_ROCKET].damage = 1;
MOBJINFO[MT_ROCKET].speed = 10;
// kill secondary or collateral damage, set it to NULL
STATES[S_EXPLODE1].action = NULL;
// increase time between each shot for slower refire
STATES[S_BSPI_ATK2].tics = 10;
STATES[S_BSPI_ATK3].tics = 10;
//now go ahead and launch weakened rockets as target is too far
P_SpawnMissile(MT_ROCKET, actor, actor->target);
}
}

12. Now that rockets are weak for everyone including the player , we need to restore speed and damage rate back to normal when the rocket launcher is being used by the player. Add the following code to A_FireMissile() in p_pspr.c as follows:

void C_DECL A_FireMissile(player_t *player, pspdef_t *psp)
{
P_ShotAmmo(player);
player->update |= PSF_AMMO;
if (IS_CLIENT)
return;
//reset values back to full power and speed
MOBJINFO[MT_ROCKET].damage = 20;
MOBJINFO[MT_ROCKET].speed = 20;
STATES[S_EXPLODE1].action = A_Explode;
P_SpawnMissile(MT_ROCKET, player->plr->mo, NULL);
}

The rocket launcher used by the player should now fire normal rockets.
Note that we have to reset the tics, speed ,damage and the action function back to the previous states for both the player and the monster each time, immediately before firing. Simple but effective.In future articles we will explore the application of some very useful built-in functions in common use by the game to manipulate these State changes dynamically; these include P_SetMobjState() and P_SetPsprite() .

Conclusion:
It is possible to create and simultaneously run several versions of the same weapon in the game by dynamically modifying the attributes and functions used in the weapons' code.
One final word: this mod was written recently by me; I believe that it still needs extensive beta testing .I've played this mod at least 10 times using mainly level 19 in doom2 with no problems at all. Should it crash or behave erratically when you try it on your own, then please let me know. I need your valuable comments and feedback to improve these tutorials. Please drop me a line on my email.
Next article is coming soon, you don't want to miss it!
Adam.
adam.delyon@gmail.com
Return to tutorials page

Post a comment
Sign in or join with:

Only registered members can share their thoughts. So come on! Join the community today (totally free - or sign in with your social account on the right) and join in the conversation.