Timer mechanics

Tested On

A timer can be a useful tool in creating a sense of urgency in a game's story. Maybe there's a bomb that is about to go off, or the facility goes into lockdown. Either way, the player will have to forego any exploratory nature they normally have, and try to get out quickly.

For this guide we'll edit the source code to give the player a temporary damage buff to hitscan weapons (pistol, machinegun, chaingun), if they make a kill with the knife. You'll be shown:

  • How to create a timer that counts down.
  • How time is measured in Wolf3D (tics)
  • How to reset the timer.
  • How to have things happen while the timer is active.

First, you will need a variable that will hold the timer information. Since this will be linked to the player specifically, we put it in the Gamestate Structure of WL_DEF

//---------------
//
// gamestate structure
//
//---------------

typedef struct
{
    short       difficulty;
    short       mapon;
    int32_t     oldscore,score,nextextra;
    short       lives;
    short       health;
    short       ammo;
    short       keys;
    weapontype  bestweapon,weapon,chosenweapon;

    short       knifetimer;

By placing it in the gamestate structure, knifetimer will be set to 0 at the start of a new game (Which we want), and will be saved when the player saves the game (So if the player saves while they still have the bonus, the saved game will remember that).

Now that we have our knifetimer variable, we want it to activate if the player makes a kill with the knife.

Most things to do with enemies are programmed in WL_ACT2 and WL_STATE, which is where we'll look. Have a look at the KillActor() function in WL_STATE:

/*
===============
=
= KillActor
=
===============
*/

void KillActor (objtype *ob)
{
    int     tilex,tiley;

    tilex = ob->x >> TILESHIFT;         // drop item on center
    tiley = ob->y >> TILESHIFT;

    switch (ob->obclass)
    {
        case guardobj:
            GivePoints (100);
            NewState (ob,&s_grddie1);
            PlaceItemType (bo_clip2,tilex,tiley);
            break;

        case officerobj:

This function is what activates when an enemy is damaged enough that they lose all their health. Points and items are rewarded while the enemy goes into it's death animation.
At the bottom of the function, the killcount is increased and enemy properties are changed to reflect that they are dead.

Here, we'll want to set the killtimer to go for 10 seconds, but only if the player made the kill with the knife. The easiest way to do this will be by simply checking the player's weapon at the time of the kill. To do that, we'll add an if statement checking the player's current weapon.

        case deathobj:
            GivePoints (5000);
            NewState (ob,&s_deathdie1);
            PlaceItemType (bo_key1,tilex,tiley);
            break;
#endif
    }

    if (gamestate.weapon == wp_knife)
        gamestate.knifetimer = 700;

    gamestate.killcount++;
    ob->flags &= ~FL_SHOOTABLE;
    actorat[ob->tilex][ob->tiley] = NULL;
    ob->flags |= FL_NONMARK;
}

Here, when there is a kill, we are telling Wolf3D to check if the current weapon is a knife, and if it is, increase our knifetimer to a value of 700. Why 700? Well in Wolfenstein 3D, time is measured in tics, which represent 1/70 of a second. So, to do 10 seconds, we must multiply by 70.

Alternatively, if you find it easier to read, you could make one small visual change

    if (gamestate.currentweapon == wp_knife)
        gamestate.knifetimer = 10 * 70;

Here, we have used the format seconds * 70, which in the above example will still result in 350, but at a glance you can know how many seconds are assigned to knifetimer.

Now that we have our timer variable (knifetimer) and created the conditions for when the timer is set (our changes to KillActor()), we now need the game to start counting down.

The game will have to know almost immediately that the timer is active. For that, we'll go to the WL_PLAY file and go to the PlayLoop() function. This function runs all the time, and is the perfect place to run our timer. In fact, we can see a timer already in use here.

        //
        // MAKE FUNNY FACE IF BJ DOESN'T MOVE FOR AWHILE
        //
#ifdef SPEAR
        funnyticount += tics;
        if (funnyticount > 30l * 70)
        {
            funnyticount = 0;
            if(viewsize != 21)
                StatusDrawFace(BJWAITING1PIC + (US_RndT () & 1));
            facecount = 0;
        }
#endif

This code is specific to Spear of Destiny but it serves as a loose example. In this code, Spear of Destiny increases funnyticount constantly until it hits at least 301 seconds (represented by 301 * 70) then it's feature will trigger.

We will be doing something similar, except we'll be counting down. Just below that code you can add this code

if (gamestate.knifetimer > 0)
    gamestate.knifetimer -= tics;

We are simply telling the engine that if gamestate.knifetimer is above 0, count down by deducting tics from knifetimer. The moment our kniftimer variable hits 0 though, it will stop.

Now we have our timer get set when an enemy is killed, and the game knows to start counting down once it is set.

But for that to be useful, we have to tell the game what to do with it. In this case, we want the player's damage to be increased while it is positive.

Player attacks are handled in WL_AGENT, and we'll go to the bottom of the aptly-named GunAttack() function.

    //
    // hit something
    //
    dx = ABS(closest->tilex - player->tilex);
    dy = ABS(closest->tiley - player->tiley);
    dist = dx>dy ? dx:dy;
    if (dist<2)
        damage = US_RndT() / 4;
    else if (dist<4)
        damage = US_RndT() / 6;
    else
    {
        if ( (US_RndT() / 12) < dist)           // missed
            return;
        damage = US_RndT() / 6;
    }
    DamageActor (closest,damage);
}

This section of the function activates when an enemy is successfully hit, where it then measures the distance from the player to that enemy, and rolls a random number based on that distance (So enemies further away take less damage).

Our change will be simple - if the knifetimer is active (greater than 0), then damage with the pistol will be doubled.

We can see right at the bottom after the value for damage has been calculated, DamageActor() is activated, where the closest enemy will have damage removed from it's remaining health.

We'll change the value of damage just before this happens:

    //
    // hit something
    //
    dx = ABS(closest->tilex - player->tilex);
    dy = ABS(closest->tiley - player->tiley);
    dist = dx>dy ? dx:dy;
    if (dist<2)
        damage = US_RndT() / 4;
    else if (dist<4)
        damage = US_RndT() / 6;
    else
    {
        if ( (US_RndT() / 12) < dist)           // missed
            return;
        damage = US_RndT() / 6;
    }
    if (gamestate.knifetimer > 0)
        damage *= 2;
    DamageActor (closest,damage);
}

Reaching this point we now have all the functionality for our timer and damage bonus in the game - if you compile and run the game with these changes, you will now be able to take advantage of it.

However, there is nothing visual to let the player know when the buff is or isn't on. This is something we'll need to fix to make the gameplay experience a little better.

For this tutorial, we'll have the face on the statusbar change to show he is powered up. For this example, we'll re-use the grinning face that happens when the player collects a chaingun.

In WL_AGENT, we'll want to go to the DrawFace() function, and add a change for when the damage bonus is active.

    else if (gamestate.health)
    {
#ifdef SPEAR
        if (godmode)
            StatusDrawFace(GODMODEFACE1PIC+gamestate.faceframe);
        else
#endif
        if (gamestate.knifetimer > 0)
            StatusDrawFace(GOTGATLINGPIC);
        else
            StatusDrawFace(FACE1APIC+3*((100-gamestate.health)/16)+gamestate.faceframe);
    }

In this section of DrawFace(), the game checks a few things. First, it checks that the player has health. If they do, it then checks if the player has godmode (But only for Spear of Destiny), and if not then it'll display the normal statusbar faces dependent on health.

We have changed things around so that if the player has health, the engine will check if the player has the gamestate.knifetimer bonus before the regular face states. Godmode will still take priority if working in Spear of Destiny.

Finally, we will revisit KillActor() in WL_STATE, and update our code to refresh the face on the statusbar when the timer becomes active.

    if (gamestate.weapon == wp_knife)
    {
        gamestate.knifetimer = 350;
        DrawFace();
    }

    gamestate.killcount++;
    ob->flags &= ~FL_SHOOTABLE;
    actorat[ob->tilex][ob->tiley] = NULL;
    ob->flags |= FL_NONMARK;
}

Now, the player has a visual cue to let them know that they have the damage bonus!

Knife Kill

 

You now have a simple example of using a countdown timer to give players a limited bonus!

Timers are a cool tool that can be used for all sorts of things. You could implement a level with a time limit, parts of levels that activate when enough time has lapsed, and so on.

For further reading on timers in Wolf3D, you can check out MCS' guide on adding a timed bonus item in DOS at AReyeP.com, or how to add In-Game Messages to your game, which utilizes a timer to know how long to display the message.

You might find that the visual cue we created above was not enough to portray to the player what is happening. What else could you do to tell the player they have bonus damage?