Text manipulation in Quake I: The basics

It’s fair to say that QuakeC does not have strong support for strings. Essentially all strings in QuakeC are static and pre-compiled, with a few built-in functions for constructing a string from a float or a vector at runtime. Although strings are very helpful for communicating messages to the player, the strings can only be concatenated through functions like bprint or centerprint. There is no way to edit a string dynamically.

However, there is another way to print text to the player, by using the SVC messages directly. This lets us control the message character by character – but trying to do that in QuakeC poses its own challenges because the language has no arrays. In this post we will skirt round those challenges by creating a very simple example to learn how the SVC messages are used. Future posts in this series will set up a system to manage and manipulate text in a new way.

Our toy example for today will be to create a “centerprint health meter”. We will print one dot character for every point of health our player has remaining. This obviously impractical example is chosen because:

a) It’s as simple a concept as you can get, but also

b) Because it’s something you basically can’t do with the centerprint function.

To explain point b) a little more: centerprint doesn’t work like the console printing functions, where you can call it multiple times to build up a message from smaller strings. Each time you call it, the previous centerprint message is wiped clean and replaced with your new one. You can define centerprint as:

void(string s1,string s2,string s3,string s4,string s5,string s6,string s7,string s8) centerprint

and then the 8 strings you call it with will be concatenated together, but that’s still a long way from printing 100 individual dots for a full health player.

Start from a clean copy of the QuakeC source code and FTEQCC as your compiler, and open up defs.qc. Look for the SVC_ constants and add the following line:


Now open client.qc and find the PlayerPostThink function. We’re going to add all of our code to the end of this function, so that the message gets sent every frame. Remember that in real code spamming messages every frame should be avoided, because it overloads the network. Add this code:

local float i;
msg_entity = self;
for(i = 0; i < self.health; i = i + 1)
WriteByte (MSG_ONE, '.');
WriteByte (MSG_ONE, 0);

The important thing to learn here is the WriteByte function. Quake has a system of messages which update players about various events. Most of the time these messages are generated for us by the engine, but we can use the WriteByte function to write them manually. Let’s look at a simple example of a manual message from an existing quake function, W_FireAxe:

WriteCoord (MSG_BROADCAST, org_x);
WriteCoord (MSG_BROADCAST, org_y);
WriteCoord (MSG_BROADCAST, org_z);

Each message starts with a “Header byte” which announces which type of message we are sending. In this case SVC_TEMPENTITY tells us the message is creating a temporary entity (the engine name for built-in particle effects). The structure of the rest of the message depends on the message type. For SVC_TEMPENTITY messages the next thing in the message is another byte, which tells the engine which kind of built-in particle effect to use – today it is TE_GUNSHOT. Finally the message needs the coordinates that the particle should be drawn at, and these are sent one coordinate at a time, x then y then z. The message is a fixed length, so the engine knows we have finished.

So what is the structure of a SVC_CENTERPRINT? Well, it starts with the header byte, followed by a string. A string is encoded as a series of one or more non-zero bytes, with a zero byte at the end to tell the recipient that the message has finished. This means a centerprint message can vary in length. A simple way to create a centerprint message is like this:

WriteString (MSG_ONE, "Welcome to QUAKE");

The WriteString function does a lot of work for us. It takes each character in the string in order, writes the byte for that character into the message, and then writes a zero byte to end the message. If that sounds a little daunting, check out this sidebar on how bytes and strings relate:

Strings & Bytes

Strings in Quake are a series of bytes behind-the-scenes. Each character takes up exactly one byte – a number between 0 and 255. The letters and numbers in quake have the same numeric values as the ASCII encoding, so ‘A’ is 65, ‘B’ is 66 etc. Strings always end with a zero byte. If you look at the conchars graphic, you can see all of the characters available to you. The red coloured letters are exactly 128 characters further on from the usual white characters, so to draw a red ‘A’ you would want to send a byte with value 65+128=193. You’ll also see things like the volume sliders in there. If you send a byte with the corresponding value you can draw these as well.


The characters of quake

The trick behind our example bit of code is sending the bytes that make up the string manually. In the loop

WriteByte (MSG_ONE, '.');

Each time round we send one more dot character byte. It’s important to point out at this point that we’re using a FTEQCC feature here which isn’t in all QuakeC compilers. Notice that the dot character is enclosed in single, not double quotes. It is not compiled as a string – instead it is converted to the ascii number for the character inside the quotes (46 for a dot). It’s the easier-to-read version of:

WriteByte (MSG_ONE, 46);

The final line of code adds the zero byte to the end of the message. At this point I think it’s fair to warn you that sending messages incorrectly can crash games, so it’s vital not to forget to send the zero byte, but equally vital to not send it until you’ve finished sending text.

Having spoken about the final line of the added code, we just need to go back and talk about the first one! It looks like this:

msg_entity = self;

To explain this bit, we also need to look at the first parameter to WriteByte, which we have skipped over until now. This parameter controls who the message is sent to, and there are four options(but we won’t deal with the last one):

float MSG_BROADCAST = 0;
float MSG_ONE = 1;
float MSG_ALL = 2;
float MSG_INIT = 3;

MSG_BROADCAST and MSG_ALL both send the message to all clients that are connected. The difference between the two is that MSG_BROADCAST is unreliable – if there isn’t enough bandwidth or the packet gets dropped the engine won’t resend it, which is fine for cosmetic things like particles. MSG_ALL packets get resent until they get through. MSG_ONE confines the message to one player (which is what we want for centerprint) – but which one? As you may have guessed, the answer is that msg_entity is set to determine which player gets a MSG_ONE message.

Phew! So that’s basically how messages work in quake, the important take-home message is that you can surround a bunch of WriteByte calls with
msg_entity = self;

WriteByte (MSG_ONE, 0);
and the bytes we send in-between will be written as characters. Now it’s homework time, a tradition started by Coffee in his “ai cafe” all those years ago. Four homework tasks of increasing difficulty on the code we just added:

1) Modify the code so that it prints ‘[‘ at the start of the health meter and ‘]’ at the end.
2) Currently if you have over 40 health the dots don’t all fit on the screen. Take out the [ and ], but modify the loop so that a newline is inserted every 32 dots. The byte value to send for a new line is 10.
3) Modify the code further so that dots for health above 100 (megahealth bonus) are printed in red, instead of white.
4) The rows of dots do not line up regularly. Add padding spaces so that they look neat.

Solutions can be found in the next part.


2 thoughts on “Text manipulation in Quake I: The basics

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )


Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.