Removing the Lives system from Wolf3D (Beta)

Tested On

A holdover from arcade games, Wolfenstein 3D is one of many games that utilize a "Lives" system in it's game - each time the player dies they lose a life, and when they have used up all of their lives, the game is over.

The choice of using Lives is a design choice, and of course everyone has a different vision. What if you don't want to use them in your game?

In this guide, we will go through the code and systematically remove the Lives system from the game. In it's place, we'll look at how to set it up so that the level always restarts when the player dies.

This code has been tested and compiles with AryanWolf3D's Wolf4SDL, and should work with DOS and classic Wolf4SDL. Your code might appear slightly different to what appears.

When it comes to editing features in the source code, it's not as simple as changing a single thing. For example, adding a new weapon requires multiple changes in multiple source files, as each file references different elements of the weapon. While this can take a little effort, the extra control it affords is important.

Lives are referenced a few times throughout the Wolf3D source code, and it's important when removing them, that we address every instance without breaking things.

First, we will set up our alternative sequences on death.

Open up WL_GAME and go to the GameLoop() function towards the bottom of WL_GAME. Within that function, we want to look at this code

            case ex_died:
                Died ();
                died = true;                    // don't "get psyched!"

                if (gamestate.lives > -1)
                    break;                          // more lives left

                VW_FadeOut ();
                if(screenHeight % 200 != 0)
                    VL_ClearScreen(0);

#ifdef _arch_dreamcast
                DC_StatusClearLCD();
#endif

                ClearMemory ();

                CheckHighScore (gamestate.score,gamestate.mapon+1);
#ifndef JAPAN
                strcpy(MainMenu[viewscores].string,STR_VS);
#endif
                MainMenu[viewscores].routine = CP_ViewScores;
                return;

This code triggers when the player dies. It runs the Died() function, and if the player is out of lives it will move to the highscore screen.

In this section, it calls these lines specifically

                if (gamestate.lives > -1)
                    break;                          // more lives left

These lines translate in English to "If the player still has lives, don't continue (To the highscores screen)". Since we'll be getting rid of lives, and setting it up so that the level always restarts (The player cannot completely "lose" the game), we want this code to always be true.

As such, we want to keep the "break;" and remove the IF condition (Checking how many lives the player has).

            case ex_died:
                Died ();
                died = true;                    // don't "get psyched!"

                // We removed a line here
                break;

                VW_FadeOut ();
                if(screenHeight % 200 != 0)
                    VL_ClearScreen(0);

#ifdef _arch_dreamcast
                DC_StatusClearLCD();
#endif

                ClearMemory ();

                CheckHighScore (gamestate.score,gamestate.mapon+1);
#ifndef JAPAN
                strcpy(MainMenu[viewscores].string,STR_VS);
#endif
                MainMenu[viewscores].routine = CP_ViewScores;
                return;

By removing the line with the IF condition, the "break;" will always happen, stopping the running of the function there. As such, none of the code between it and the next case (ex_victorious) will ever trigger. So we can remove that too. Once you've done that, your ex_died case should look like the following:

            case ex_died:
                Died ();
                died = true;                    // don't "get psyched!"

                // We removed a line here
                break;
                
                // We also removed everything else in this case up to the call to "return;", since the code will always stop at "break;"

Nice and neat.

With the changes in the last step, ex_died still calls on the Died() function, which has some crucial changes that will need to be made. The function is just above the GameLoop() function in WL_GAME, and has the following code at the end of it

    IN_UserInput(100);
    SD_WaitSoundDone ();
    ClearMemory();

    gamestate.lives--;

    if (gamestate.lives > -1)
    {
        gamestate.health = 100;
        gamestate.weapon = gamestate.bestweapon
            = gamestate.chosenweapon = wp_pistol;
        gamestate.ammo = STARTAMMO;
        gamestate.keys = 0;
        pwallstate = pwallpos = 0;
        gamestate.attackframe = gamestate.attackcount =
            gamestate.weaponframe = 0;

        if(viewsize != 21) // This code is part of Wolf4SDL, that allows for fullscreen with a toggle-able status bar. 
        {
            DrawKeys ();
            DrawWeapon ();
            DrawAmmo ();
            DrawHealth ();
            DrawFace ();
            DrawLives ();
        }
    }
}

This code makes two references to the Lives variable; it removes 1 life (Because the Player died), and then resets the Player's health and other variables for the start of the game, dependent upon if the Player still has lives.

Since we are removing Lives entirely and having the player always restart the level, we can remove and clean up some of the code.

    IN_UserInput(100);
    SD_WaitSoundDone ();
    ClearMemory();

    // We removed gamestate.lives--, so the Player doesn't lose a life

    // We also removed the IF statement, and the set of brackets surrounding the code. Now, it will run every time Died() is called.
    gamestate.health = 100;
    gamestate.weapon = gamestate.bestweapon
        = gamestate.chosenweapon = wp_pistol;
    gamestate.ammo = STARTAMMO;
    gamestate.keys = 0;
    pwallstate = pwallpos = 0;
    gamestate.attackframe = gamestate.attackcount =
        gamestate.weaponframe = 0;

    if(viewsize != 21)
    {
        DrawKeys ();
        DrawWeapon ();
        DrawAmmo ();
        DrawHealth ();
        DrawFace ();
        DrawLives ();
    }
}

Now, while the game still tracks and displays how many Lives the Player has, it will not matter if they die.

When a new game is started, the game sets multiple elements of the Player's information up. This includes Health, Ammo, and of course, Lives.

You'll find the NewGame() function within WL_MAIN.

/*
=====================
=
= NewGame
=
= Set up new game to start from the beginning
=
=====================
*/

void NewGame (int difficulty,int episode)
{
    memset (&gamestate,0,sizeof(gamestate));
    gamestate.difficulty = difficulty;
    gamestate.weapon = gamestate.bestweapon
            = gamestate.chosenweapon = wp_pistol;
    gamestate.health = 100;
    gamestate.ammo = STARTAMMO;
    gamestate.lives = 3;
    gamestate.nextextra = EXTRAPOINTS;
    gamestate.episode=episode;

    startgame = true;
}

In it, the entire Gamestate Structure is set to 0 by default, then individual variables are set. We'll want to remove the line setting Lives to 3.

    gamestate.health = 100;
    gamestate.ammo = STARTAMMO;
    // We removed this line.
    gamestate.nextextra = EXTRAPOINTS;
    gamestate.episode=episode;

    startgame = true;
}

A simple change, but by doing so, we ensure that the Player will spawn in a game without any extra lives.

We need to do one more change in this file before moving on. The game is designed so if something is wrong with a saved game file, the game will default to giving the player set weapons, ammo and of course, lives.
Find the LoadTheGame() function, and navigate to the bottom of it.

     IN_ClearKeysDown();
     IN_Ack();

     gamestate.score = 0;
     gamestate.lives = 1;
     gamestate.weapon =
       gamestate.chosenweapon =
       gamestate.bestweapon = wp_pistol;
     gamestate.ammo = 8;
    }

All we need to do is remove the call to lives.

     IN_ClearKeysDown();
     IN_Ack();

     gamestate.score = 0;
     // Removed this line
     gamestate.weapon =
       gamestate.chosenweapon =
       gamestate.bestweapon = wp_pistol;
     gamestate.ammo = 8;
    }

Done!

So far, we've removed the Lives that the Player starts with in a NewGame, and made it so that the player will always restart the current level on death.

While Lives don't matter anymore for the project, the Game still displays the current number, and still increases it in some situations (Such as collection the Extra Life item).

Fixing this all takes place in WL_AGENT. First, we'll make changes that prevent the player from being rewarded more lives, as they won't need them.

Find the GiveExtraMan() function, which should look something like as follows:

/*
===============
=
= GiveExtraMan
=
===============
*/

void GiveExtraMan (void)
{
    if (gamestate.lives<9)
        gamestate.lives++;
    DrawLives ();
    SD_PlaySound (BONUS1UPSND);
}

You can removed that whole function, as it will not be necessary.

Slightly further down, the GivePoints() function makes reference to GiveExtraMan()

/*
===============
=
= GivePoints
=
===============
*/

void GivePoints (int32_t points)
{
    gamestate.score += points;
    while (gamestate.score >= gamestate.nextextra)
    {
        gamestate.nextextra += EXTRAPOINTS;
        GiveExtraMan ();
    }
    DrawScore ();
}

This function is used to give the player an increase to their score, and also rewards an extra life when a score milestone is reached. We can remove these lines, as there's no need for the milestone reward.

/*
===============
=
= GivePoints
=
===============
*/

void GivePoints (int32_t points)
{
    gamestate.score += points;
    // We removed the check for NextExtra, which rewards an extra life every 40,000 points
    DrawScore ();
}

The collectible item that rewards some ammo, full health and an extra life still exists in the game. We'll want to find the GetBonus() function a bit further down WL_AGENT, and locate this particular case

        case    bo_fullheal:
            SD_PlaySound (BONUS1UPSND);
            HealSelf (99);
            GiveAmmo (25);
            GiveExtraMan ();
            gamestate.treasurecount++;
            break;

As you can see, it makes a reference to GiveExtraMan(), so all we have to do is remove it. Alternatively, you could have it give points to the player instead.

        case    bo_fullheal:
            SD_PlaySound (BONUS1UPSND);
            HealSelf (99);
            GiveAmmo (25);
            GivePoints (1000);            // We replaced the line here, but you could just remove it instead.
            gamestate.treasurecount++;
            break;

That is all the code that increases the player's lives.

While we've made changes that prevent Lives from being lost or gained, they currently still display on the status bar (Though always at 0). In this step of the guide, we'll remove the display. This will free up space on the status bar for other information that you might want to display.

Find the DrawLives() function within WL_AGENT

/*
===============
=
= DrawLives
=
===============
*/

void DrawLives (void)
{
    if(viewsize == 21 && ingame) return;
    LatchNumber (14,16,1,gamestate.lives);
}

Since we're not displaying Lives, we can just remove this whole function. Now that we've done that, the number of lives will not show up on the status bar, leaving the space blank. However, there are some times in the source code that DrawLives() is called upon. Since it has been removed, we'll need to also remove anything calling on it. If you don't it can cause errors with compiling.

In WL_GAME, there is a function called DrawPlayScreen(). This function makes calls to draw the whole status bar.

/*
===================
=
= DrawPlayScreen
=
===================
*/

void DrawPlayScreen (void)
{
    int    i,j,p,m;
    unsigned    temp;

    VW_FadeOut ();

    temp = bufferofs;

    CA_CacheGrChunk (STATUSBARPIC);

    for (i=0;i<3;i++)
    {
        bufferofs = screenloc[i];
        DrawPlayBorder ();
        VWB_DrawPic (0,200-STATUSLINES,STATUSBARPIC);
    }

    bufferofs = temp;

    UNCACHEGRCHUNK (STATUSBARPIC);

    DrawFace ();
    DrawHealth ();
    DrawLives ();
    DrawLevel ();
    DrawAmmo ();
    DrawKeys ();
    DrawWeapon ();
    DrawScore ();
}

We'll want to remove the call to DrawLives() here.

...

    UNCACHEGRCHUNK (STATUSBARPIC);

    DrawFace ();
    DrawHealth ();
    // Removed DrawLives
    DrawLevel ();
    DrawAmmo ();
    DrawKeys ();
    DrawWeapon ();
    DrawScore ();
}

If you are working with a version of the source that has Fullscreen capabilities built in (Wolf4SDL typically includes this), below DrawPlayScreen() you may find ShowActStatus(), which needs calls to Lives removed.

void ShowActStatus()
{
    // Draw status bar without borders
    byte *source = grsegs[STATUSBARPIC];
    int    picnum = STATUSBARPIC - STARTPICS;
    int width = pictable[picnum].width;
    int height = pictable[picnum].height;
    int destx = (screenWidth-scaleFactor*320)/2 + 9 * scaleFactor;
    int desty = screenHeight - (height - 4) * scaleFactor;
    VL_MemToScreenScaledCoord(source, width, height, 9, 4, destx, desty, width - 18, height - 7);

    ingame = false;
    DrawFace ();
    DrawHealth ();
    // Removed DrawLives
    DrawLevel ();
    DrawAmmo ();
    DrawKeys ();
    DrawWeapon ();
    DrawScore ();
    ingame = true;
}

Then, further down in WL_GAME, we'll need to revisit the Died() function. Right at the bottom, you can see DrawLives() is called, so you'll need to get rid of that.

    if (gamestate.lives > -1)
    {
        gamestate.health = 100;
        gamestate.weapon = gamestate.bestweapon
            = gamestate.chosenweapon = wp_pistol;
        gamestate.ammo = STARTAMMO;
        gamestate.keys = 0;
        gamestate.attackframe = gamestate.attackcount =
        gamestate.weaponframe = 0;

        DrawKeys ();
        DrawWeapon ();
        DrawAmmo ();
        DrawHealth ();
        DrawFace ();
        // Removed DrawLives
    }

}

Finally, in WL_MENU, there is a call to DrawLives() within CP_LoadGame(). We remove that, and that will be all instances of DrawLives() covered!

            fclose (file);

            DrawFace ();
            DrawHealth ();
            // Removed DrawLives
            DrawLevel ();
            DrawAmmo ();
            DrawKeys ();
            DrawWeapon ();
            DrawScore ();
            ContinueMusic (lastgamemusicoffset);
            return 1;
        }

The game calls on gamestate.lives in the menu code for when the game is quickly ended with the F7 key. We'll need to remove these instances.

Look in WL_MENU.C (Or WL_MENU.CPP if you're using the classic Wolf4SDL code) for CP_CheckQuick

        //
        // END GAME
        //
        case sc_F7:
            WindowH = 160;
#ifdef JAPAN
            if (GetYorN (7, 8, C_JAPQUITPIC))
#else
            if (Confirm (ENDGAMESTR))
#endif
            {
                playstate = ex_died;
                killerobj = NULL;
                pickquick = gamestate.lives = 0;
            }

The game sets gamestate.lives to 0 here, which is unnecessary. We'll tweak that line so only pickquick is set.

        //
        // END GAME
        //
        case sc_F7:
            WindowH = 160;
#ifdef JAPAN
            if (GetYorN (7, 8, C_JAPQUITPIC))
#else
            if (Confirm (ENDGAMESTR))
#endif
            {
                playstate = ex_died;
                killerobj = NULL;
                pickquick = 0;        // This is the edited line.
            }

We want to do the exact same change further down the file, within CP_EndGame

    DrawMainMenu();
    if(!res) return 0;

    pickquick = 0;            // This is the edited line.
    playstate = ex_died;
    killerobj = NULL;

    MainMenu[savegame].active = 0;
    MainMenu[viewscores].routine = CP_ViewScores;

Reaching this point, we've removed just about everything to do with lives! If you are using the DOS source code, you can also go into WL_DEF and remove the definitions referencing lives completely, reclaiming some memory for other features! In Wolf4SDL this is less necessary, but can be done to keep things "clean".

In the Gamestate Structure:

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

typedef struct
{
    short       difficulty;
    short       mapon;
    int32_t     oldscore,score,nextextra;
    // Removed lives

And under "WL_AGENT DEFINITIONS" we can remove the declarations for DrawLives() and GiveExtraMan(), since we've removed those functions completely.

void    DrawLevel (void);
// Removed these lines
// Removed these lines
void    DrawScore (void);
void    DrawWeapon (void);

 

Provided everything has been done properly, the game should compile successfully and be playable! If not, make sure you only changed the lines indicated in the steps, and that they were all correctly done.

If all is good, you should see something similar to the following

Lives Removed

Congratulations! You removed lives as a whole from your game, freeing up space and changing the way dying happens (In this case, each level is a checkpoint and you'll endlessly restart until you get through)

The steps above cover everything needed to remove lives from the game, and make the player simply start a level over again. However, you may want instead to make the game a bit more difficult, and have the player lose if they die.

To do that, we would need to change what we did just slightly. Back in WL_GAME, we'll revisit the ex_died case within GameLoop()

        case ex_died:
            Died ();
            died = true;            // don't "get psyched!"

            if (gamestate.lives > -1)
                break;                // more lives left

            VW_FadeOut ();

            ClearMemory ();

            CheckHighScore (gamestate.score,gamestate.mapon+1);

            #pragma warn -sus
            #ifndef JAPAN
            _fstrcpy(MainMenu[viewscores].string,STR_VS);
            #endif
            MainMenu[viewscores].routine = CP_ViewScores;
            #pragma warn +sus

            return;

Instead of our changes earlier, we'll want to remove the break instead. This way when the player dies, it will always go straight to the highscore screen.

        case ex_died:
            Died ();
            died = true;            // don't "get psyched!"

            // We removed these lines

            VW_FadeOut ();

            ClearMemory ();

            CheckHighScore (gamestate.score,gamestate.mapon+1);

            #pragma warn -sus
            #ifndef JAPAN
            _fstrcpy(MainMenu[viewscores].string,STR_VS);
            #endif
            MainMenu[viewscores].routine = CP_ViewScores;
            #pragma warn +sus

            return;

Then, back in Died(), instead of just removing the line declaring the IF statement, you'll want to remove everything within it as well. So now, the end of the function will look similar to the following

    //
    // fade to red
    //
    FinishPaletteShifts ();

    if(usedoublebuffering) VW_UpdateScreen();

    VL_BarScaledCoord (viewscreenx,viewscreeny,viewwidth,viewheight,4);

    IN_ClearKeysDown ();

    FizzleFade(screenBuffer,viewscreenx,viewscreeny,viewwidth,viewheight,70,false);

    IN_UserInput(100);
    SD_WaitSoundDone ();
    ClearMemory();

    // We removed all the rest of the lines in the function, before the bracket
}