Message Display and Input

Ah, this was a fun thing to implement. It is loosely based on Quake's on-screen messaging system, but I think it's a little more sophisticated. My goal was to make it good enough that I wouldn't need a separate console window, though it would be very easy to add one if I ever felt the desire to do so.

There is a linked list of messages. This linked list goes in chronological (and drawing) order - oldest messages go at the beginning (top) of the list. Here's the struct for a node in this list:

typedef struct msg_t
{
    char type;
    unsigned long dietime;
    char *text;
    struct msg_t *next;
} msg_t;

There are a couple different types of messages:

#define MSG_NORMAL      0
#define MSG_COMPUTER    1
#define MSG_ENEMY       2
#define MSG_DEATH       3
#define MSG_EXPENDABLE  4

The first four are just different colors. MSG_NORMAL is for messages typed by the player, MSG_COMPUTER is for messages from the computer, MSG_ENEMY is for messages received from other players, and MSG_DEATH is for obituaries.

MSG_EXPENDABLE is a little invention of my own though. When one of these messages is added to the message list, the add_msg() function first searches the list for any expendable messages that are already in there. If it finds any, it replaces the first one it finds with the new message. Otherwise it adds the message to the bottom of the list normally. This allows the game to use the message list for rapidly updating status indicators without filling up the buffer with messages.

Anyway, the other message property is dietime. This is simply the time at which the message is to be removed from the list. The game has a variable called ms_time that is incremented every millisecond, and that is used to keep time. So when a new message gets added to the list, its dietime is just set to ms_time + 6000 or something like that.

So. I have one function that is called by the program to add messages to the list:

void add_msg(char type, char *text, ...);

This function logs the message to disk (msg.log) and adds it to the message buffer. It assembles the given text parameters into a single string (using vsprintf()) and then calls add_string(). add_string() then looks at the width of the message in pixels, recursively calling itself with broken down strings until the string fits on the screen - this is how long messages can be wrapped. It's one of those "hey look, I used recursion" deals, but it was kind of a pain to get it to work exactly right. :)

In the main game loop I have a few maintenance functions that get called. process_messages() checks the dietime of each message and removes expired ones from the message list. draw_messages() is called towards the end of the drawing loop and simply traverses the linked list of messages, drawing them top-to-bottom going down the screen.

Now, all of this is about 400 lines of code (and blank spaces) in msg.c. I suppose I should talk about the input part of the console too.

The game handles three main types of keyboard input: Action keys that control player movement or firing, other keys in which the keypresses are important and holding down the key results in repeated events, and talking. Basically, I have a state variable that keeps track of whether the user is talking or not. The game calls either the normal input function or the console text input function depending on this variable. So if the normal input routine sees a 't' or a '/', it just goes into talking mode.

The talking input routine just keeps a text buffer and a counter that remembers how many characters into the buffer the cursor is. It then handles the backspace key to backspace, Esc to clear the text buffer and go right back to normal input mode, and Enter when the text is completed. Now, when you press enter, the text input function looks to see if there is a '/' at the beginning of the text buffer (which would indicate a command, rather than text communication with other players, or yourself possibly I guess.) :)

If the text starts with '/', it is broken down into a command string (the first word of the text) and argument strings for all of the subsequent words (delimiting by spaces). It builds an argument list using the argc and argv[] naming convention, which is easy to parse. I simply have an if/else block like this:

if (argc > 2) // ==> Two-argument commands
{
    if (!stricmp(argv[0], "loadvista"))
    {
        // process command using argv[1] and argv[2]
    }
    else
    // other commands would follow
}
else
if (argc > 1) // ==> Single-argument commands
{
    // same deal as above w/ the stricmp() and such
}
else          // ==> No-argument commands
{
    // you should get the picture by now
}

And there is one last thing I forgot to mention. I made a blinking cursor just to make the text input even nicer, and because every programmer should implement a blinking cursor at some point in their lives! Like any other event system in my game, it simply toggles a flag that says if the cursor is on or off by setting a variable called cursor_toggle_time to ms_time + 200, and constantly checking to see if that time has been passed. To control the visibility of the cursor, I use this simple (but unexpected, perhaps?) code:

if (cursoron)
{
    talk_buf[cursor] = '_';
    talk_buf[cursor + 1] = '\0';
}
else
    talk_buf[cursor] = '\0';

And I think that just about covers it! Please ask if you have any questions about anything I've talked about.