In-game Messages

Tested On

In many games, players will receive helpful in-game messages informing them of their actions, most commonly in respect to the things they collect.

Quake's In-game Messages

These messages can be helpful in informing the user exactly what that mystery box they just picked up it, or that a door is locked and needs a particular key to open.

For this guide, we'll look at implementing in-game messages into a Wolf3D project, for either DOS or Wolf4SDL! By the end of it, you will learn:

  • Defining global functions in your code
  • The use of a countdown timer
  • Printing text elements onto the screen

And you will of course also have the In-game Messages feature for use in your project!

This guide is adapted from WSJ's tutorial on DHW, which can be found in it's original form here.

If you are using DDWolf, In-Game Messages are already added in! Feel free to read to get a basic understanding of how they work, or skip straight to Creating an Ingame Message in DDWolf

In most cases, the variables you use will be of an int, byte or other similar type and will hold a number value. In this case though, we require something that can store an entire message. This is where a char variable comes in.

Go to the Gamestate Structure in WL_DEF, and add the following line at the bottom of it:

    int            episode,secretcount,treasurecount,killcount,
                secrettotal,treasuretotal,killtotal;
    long        TimeCount;
    long        killx,killy;
    boolean        victoryflag;        // set during victory animations
    char         message[40];
} gametype;

What is a char? It is a "character variable", and as it's name implies is used to hold and recall a character. If you were to implement code like

char examplechar = 'a';

Then call on examplechar, it would return the text 'a'.

We need more than one letter in our message, though. So we make message an array with 40 characters (Numbered 0-39, as the code considers 0 a value). When the code sees the number inside the square brackets, it reads that as there being 40 values/characters that can be stored within the variable.

Now that we have our variable in gamestate.message, we need to tell the game what to do with it.

First, at the top of WL_DRAW you'll need a new variable with which to store a timer for our messages, so they don't display indefinitely.

If you need more of an explanation of the use of timers in Wolf3D, check out the Timer Mechanics guide, which takes you through the steps of adding a timed bonus in more detail.

In the GLOBAL VARIABLES section at the top of the file, add these lines:

unsigned screenloc[3]= {PAGE1START,PAGE2START,PAGE3START};
#endif
unsigned freelatch = FREESTART;

long     lasttimecount;
long     frameon;
int      messagetime=0;

unsigned    wallheight[MAXVIEWWIDTH];

We create messagetime, and set it an initial value of 0. Now go to the bottom of WL_DRAW, where we'll want to place these two new functions:

/*
========================
=
= GetMessage
=
= gets ingame messages
=
========================
*/

void GetMessage (char *lastmessage)
{
    messagetime = 140; // time for message to display
    strcpy(gamestate.message, lastmessage);
}   

/*
========================
=
= DrawMessage
=
= displays ingame messages
=
========================
*/

void DrawMessage (void)
{
    messagetime-=tics;
    fontnumber = 0;
    SETFONTCOLOR(0xf,0x0); // set the color

    PrintX=8;
    PrintY=2; // position the message

    US_Print(gamestate.message); // print message
    if (messagetime <= 0)
        DrawPlayBorderSides ();
}

These functions contain everything needed for In-game Messages to work.

GetMessage() is the function that we will call to tell the game what message we want displayed. It will take whatever value we assign to lastmessage, and copy it into our gamestate.message variable we created earlier. It also sets our timer to 140 tics (2 seconds).

Then, DrawMessage() handles the heavy-lifting. It specifies what font to use when printing a message (Wolf3D by default comes with two fonts - one small and one large), and what colour the font is, then displays the actual message. When the function is active, it also starts deducting tics from our timer, turning it into a countdown.

After that, it selects the coordinates for where the text will start from. For this guide, we'll be having it placed just off from the top left corner of the screen. As the very corner coordinate is (0,0), we specify and y coordinates of (8,2) in the PrintX and PrintY variables.
Depending on your game, those coordinates can be altered to place the text anywhere on the screen you want!

Finally, USPrint() is called to print the value of gamestate.message to the screen.

Of course, creating a function to draw the message is meaningless unless we also call the function so it triggers during the game.

We will want to move back up in WL_DRAW until we find the ThreeDRefresh() function. Inside of it, we can find calls to functions that draw elements like the Player's weapon (Which is always drawn on top).

//
// draw all the scaled images
//
    DrawScaleds();            // draw scaled stuff
    DrawPlayerWeapon ();    // draw player's hands 

Inside of there, we will make our call to DrawMessage().

//
// draw all the scaled images
//
    DrawScaleds();            // draw scaled stuff
    DrawPlayerWeapon ();    // draw player's hands 

    if (messagetime > 0)
        DrawMessage();

ThreeDRefresh() is involved in the raycasting and visual aspects of the game and is practically always running, so we can rest assured knowing that DrawMessages() will be seen. It is a function that should be altered with great care though, being a crucial element of the game.

We have DrawMessage() display on the condition that our messagetime timer is active. So, messages will display for two seconds, then stop displaying.

You will find if you try to compile the code right now, you will get an error and it will fail.

This is because the code we write activates in the order it occurs. So ThreeDRefresh() looks for DrawMessage() when it is called, checking the code earlier in the file for it's existence. Our functions are at the bottom, which means the game won't be able to find them.

The solution is to simply declare the name of our functions before ThreeDRefresh(), with lines like the following:

void   GetMessage(char *lastmessage);
void   DrawMessage (void);

If we put those at the top of WL_DRAW in it's GLOBAL CONSTANTS section, then the code will compile because ThreeDRefresh() now knows they exist.

This won't be enough for our needs, though. As you'll see in the next step, we will need flexibility to use GetMessage() across multiple files of the project. For that, we will need to define these functions in a way that every file knows of it's existence.

You will notice at the top of a lot of files like WL_DRAW and WL_AGENT is this line:

#include "WL_DEF.H"

This tells the engine to "include" all the information from WL_DEF when running the code within the file. So, if a variable is defined in WL_DEF, it will also be defined within the file that uses the above line of code.

So, instead of defining our functions at the top of WL_DRAW (If you added the functions to GLOBAL CONSTANTS just before, delete them), we'll define them within WL_DEF! You will find a section dedicated to WL_DRAW DEFINITIONS, and that's where we'll want to insert our lines.

void    CalcTics (void);
void    FixOfs (void);
void    ThreeDRefresh (void);
void  FarScalePost (void);

void   GetMessage(char *lastmessage);
void   DrawMessage (void);

/*
=============================================================================

                         WL_STATE DEFINITIONS

Now, any of the files in the project that call to include WL_DEF will have access to GetMessage() and DrawMessage()! As such your project should now compile, as when ThreeDRefresh() reaches DrawMessage(), it can confirm it's existence within WL_DEF.

Now we have the functions defined and the game knows to draw them, it's time to try using them!

WSJ's GetMessage() routine is very simple to use.

GetMessage("This is a message");

We'll create a message that shows up when collecting a First Aid Kit.

For the First Aid Kit, we'll visit WL_AGENT and go to the GetBonus() function, that tells the game what to reward the player with when an item is collected.
Inside of it, we'll locate the snippet for the item:

    case bo_firstaid:
        if (gamestate.health == 100)
            return;

        SD_PlaySound(HEALTH2SND);
        HealSelf(25);
        break;

This code triggers when a player collects a First Aid Kit, rewarding the player with health and playing a sound. When they do, we also want the game to display a message telling them what they picked up. We'll insert a use of GetMessage()

    case bo_firstaid:
        if (gamestate.health == 100)
            return;

        SD_PlaySound(HEALTH2SND);
        HealSelf(25);
        GetMessage("Collected a First Aid Kit.");
        break;

Now, the game will take the message you specify between the quotes in GetMessage(), and it will be drawn by DrawMessage()! It should look similar to the following in your game.

First Aid Kit

 

If you are using the DDWolf source port, you will find that the GetMessage() function is set up slightly different.

void GetMessage(char *lastmessage, int color);

This version of GetMessage() allows you to set a color for the message being displayed (Stored in the variable messagecolor). Feel free to look at both our message functions in WL_DRAW and see how it differs!

The colors on offer are those of the "Wolf3D Palette", which looks as follows:

Wolf3D Palette

To reference these colors for your messages, you will want to use the format '0x__', replacing the underscores with the number of the color (from 0-63).

By default, messagecolor is assigned a value of '0x35', so it is the 36th color reading left-to-right, top-to-bottom.

If you were adding a message for collecting the First Aid Kit as in the previous step, it would be written instead as follows:

    case bo_firstaid:
        if (gamestate.health == 100)
            return;

        SD_PlaySound(HEALTH2SND);
        HealSelf(25);
        GetMessage("Collected a first aid kit.", 0x1);
        break;

This would print the message, and use the second tile in the palette (Remember, the first tile is considered '0', so using '0x1' would be the second color, not the first!), making the message text blue!

With all that done, you can now use in-game messages in your own project!

  • Think about what else in-game messages could be used for! For example, what about creating unique death messages when a particular enemy kills you?
  • Maybe you want the messages printed elsewhere on the screen? Try changing the values in DrawMessage() to print them at other coordinates!
  • If you're using DDWolf, take some time to look at what it does differently and maybe make some changes of your own.