Coding.

Visual Engine Sharing

»Coding« aims at beginner level developers who have basic knowledge of their programming language of choice and are now at the first stages of creating a roguelike. The articles, although incorporating the subjective view and – in no means perfect – programming style of the author, point out some general issues which could seem to be difficult and try to show simple methods of solutions to these issues.

What You Should Know

In order to follow my explanations, you should know, at least in theory, how SDL works (i.e. that is uses so-called surfaces to represent bitmaps and window contents). I also expect you to know the very basics of programming, i.e. that you are aware of variables, constants, functions, loops and conditions.

The Quest

You have an idea, you have time, you already have coded the first couple of lines and you want your game to run on a wide range of systems. Your target audience consists of roguelikers and players of classical RPGs. Therefore you want ASCII output for the former and graphics for the latter. What should you do to achieve this goal?

One way was to maintain two different branches of your game, with different sources, completely parted from each other. One branch could use the curses library for console output and the other branch could use SDL for displaying graphics.

Another, probably better way was to let both versions share the same code base, write routines for both display modes and just create two different binaries at compile time.

A third way, and the way described in this article, was to only create one binary for both modes and decide at runtime which mode to use.

One Output Function – Two Display Modes

The goal of this article is to show that the use of an abstract output function instead of basic functions provided by a library makes the programmer’s life much easier. Using FreePascal, we will develop a basic output system which will be capable of showing single characters, strings and tiles.

The concept is very easy. By using the chr() function, we can access characters of the extended ASCII set, i.e. all chars with a code higher than 127. With 255 chars in total, we have 128 chars which can be used for other purposes than just displaying letters. We will use these characters to display graphical tiles in SDL mode. In console mode, chars higher than 127 will be »remapped« and shown in other colors, depending on our needs.

We will create two basic functions:

CharXY (intX, intY: Integer; chLetter: Char): Boolean;
TextXY (intX, intY: Integer; strText: String): Boolean;

Whenever we need to output a character or a graphical tile, we’ll use one of these two. TextXY() does nothing special, it just calls CharXY() so often as needed for displaying the complete string. The »magic« is done in CharXY() itself.

The Universal Function

Have a look at the following code:

FUNCTION CharXY (intX, intY: Integer; chLetter: Char): Boolean;
VAR
  TilesetRect, ScreenRect: SDL_RECT;
  intColor: Integer;
  chTransformedLetter: Char;
BEGIN
  // set the result of the function to true
  CharXY:=true;

  // check if a valid character has been given to the function
  IF (ord(chLetter)<32) OR (ord(chLetter)>254) THEN
    CharXY:=false;

  // if the character is valid, process it
  IF CharXY=true THEN
  BEGIN

    IF blUseSDL=true THEN
    BEGIN

      // ** SDL output **

      // get the correct character out of the tileset surface
      TilesetRect.x := (ord(zeichen)-32) * TILE_WIDTH;
      TilesetRect.y := 0;
      TilesetRect.w := TILE_WIDTH;
      TilesetRect.h := TILE_HEIGHT;

      // calculate the output position in the SDL window
      ScreenRect.x := intX * TILE_WIDTH;
      ScreenRect.y := intY * TILE_HEIGHT;
      ScreenRect.w := TILE_WIDTH;
      ScreenRect.h := TILE_HEIGHT;

      // copy the character to the screen
      SDL_BlitSurface(surTileset, @TilesetRect, surScreen, @ScreenRect);

    END
    ELSE
    BEGIN

      // ** console output **

      // default color for all chars < 127 (used for text)
      intColor:=white;

      // transform chars > 127 (these are the graphics in SDL
      // mode) into chars < 128 and change the display color
      IF ord(chLetter)>127 THEN
      BEGIN
        CASE ord(chLetter) OF
          // closed door
          128: BEGIN
                 chTransformedLetter:='+';
                 intColor:=darkgray;
               END;

          // opened door
          129: BEGIN
                 chTransformedLetter:='\';
                 intColor:=darkgray;
               END;

          // wall
          130: BEGIN
                 chTransformedLetter:='#';
                 intColor:=lightgray;
               END;
        END;
      END;

      // Use a VidUtil function to do the curses output
      TextOut(intX, intY, chTransformedLetter, intColor);

    END;
  END;
END;

To understand the function, you need to know the following facts:

• We use JEDI SDL for accessing SDL types and functions.
• The content of the SDL window is stored in the surface surScreen.
• Our tiles are saved in a bitmap stored in the surface surTileset.
• The constant TILE_WIDTH determines the width of a tile, e.g. 32.
• The constant TILE_HEIGHT determines the height of a tile, e.g. 32.
• FreePascal offers several color constants we can use in our game.
• For accessing curses, we use the Video unit.
• The TextOut function is provided by the VidUtil unit.
• We have a global variable blUseSDL to determine the output mode.

Now we’ll have a more detailed look on the algorithm. First of all, we declare some variables. The SDL_RECT data type is a simple record, containing four integer elements. The values of these elements are coordinates and the record is an easy way to pass the values to various SDL functions.

After declaring, we check if the character that has been passed to the function in chLetter has an ASCII code between 32 and 254. If the value was lower or higher, the function would exit and return false.

If the character is valid, blUseSDL is evaluated to determine if SDL or console output should be used.

The SDL Part

If SDL output is desired, the function now copies a rectangle from our tile bitmap in surTileset into the window surface surScreen. The copied area contains the character or tile with the ASCII value of chLetter.

The bitmap with our tileset consists of at least 127 characters, aligned from left to right, without spaces, sorted from the lowest to the highest – like pearls on a string. The first 95 tiles (number 32 to 127) contain letters, ciphers and special chars and are used for text output. Everything after tile 95 (beginning with ASCII value 128) is used for graphics:

An example bitmap with tiles

If you now want to draw a wall tile somewhere on the screen, you just need to know the ASCII number of the tile which depicts that wall. In the picture above and in the example source, this number is 130 (normally shown as character, but here »overridden« by a graphical tile). With this number you can simply call our function CharXY (15, 10, chr(130)); – note that we do nothing more than using FreePascals standard Chr() function to display a graphics on the screen. Rather easy, isn’t it?

A Virtual Console Window

The x- and y-coordinates used in the function call (here 15 and 10) refer to a matrix in which every cell contains one letter or tile at a time. It works like the 80×25-matrix of a console, but the actual size of every cell depends on the size of the tiles you wish to use (in fact, the cell size is the same as the size of your tiles). The total number of lines and rows in this »virtual console« depends on the width and height of your game’s main window.

If your window size was 640×480 and your tiles had a size of 32×32 (like in the example source), you would get a »resolution« of 20×15. If your window size was 800×600 and your tiles were 32×32, the available size was 25×18. And if your window size was 800×600, but your tiles had a size of 10×20 (as seen in LambdaRogue), the resulting space was 80×30.

The Console Part

Although displaying tiles that way is practical, the main advantage of the function is its ability to create console output as well. If UseSDL is false, the SDL part will be skipped. Instead, the branch after the first else will be executed.

Basically this part could look very easy, as it does not need to translate an ASCII code to a bitmap graphics. So it could just display chLetter. However, the output that is done here needs to have the same meaning as the output that would be done by the SDL part of the function – if ASCII code 130 stands for a closed door, both SDL and console output have to display a door. As there are many conventions in the roguelike genre how certain objects are symbolized, the standard character with that value can’t be used. So we have to do an intermediate step.

In this step, we use a case construct to evaluate the ASCII code of chLetter. Then, we use the variable chTranslatedLetter to store an alternative letter; in the example this is a +, easily recognized as closed door in nearly every roguelike. When the translating from the original character into the new character is done, we yet change the color of the following screen output, using one of FreePascals predefined color constants.

Finally, we call the TextOut function that accesses the console:

TextOut (intX, intY, chTransformedLetter, intColor); .

Displaying Strings

For the sake of completeness, the other function, TextXY(), shall be shown, too:

FUNCTION TextXY (intX, intY: Integer; strText: String): Boolean;
VAR
  i: Integer;
  chStr: Char;
BEGIN
  // set the result of the function to true
  TextXY:=true;

  // go through the string char by char
  FOR i:=1 TO length(strText) DO
  BEGIN

    // extract one char from the string
    chStr := strText[i];

    // display the character; if this makes any
    // problems, set the result of TextXY to false
    IF CharXY (intX+i, intY, chStr) = false THEN
      TextXY:=false;

  END;
END;

For displaying text, you will most likely use this function. Calling CharXY() directly will be mainly needed for dungeon output routines or functions that show »special effects«, like flying missiles or magic.

Remarks and Problems

Constrictions

If a developer decides to program his output in a way like shown here, he may feel constricted after a while, especially concerning the maximum number of available tiles. As the functions expect ASCII tiles (for showing texts) and graphical tiles (for showing dungeon features, items and creatures) to be in one bitmap file and as the maximum value the chr() function can handle is 254, he can’t use as many tiles as perhaps desired. Instead, he has to confine himself to a limited amount of tiles or (which would be the most logical reaction) adjust the functions to his needs.

It is, for example, possible to add a parameter intSubset that is used to switch between different parts of the bitmap. The calling function then has to care about the correct value of this parameter, and TilesetRect has to contain the correct part of surTileset, depending on intSubset.

The console part of the function also has to regard intSubset. This will of course complicate the, currently rather clear, case statement. In this regard we have to address another issue – the hard-coding of the different »translated chars« and their colors.

 

Hard-coded chars and colors

Doing so is not optimal. Instead, the chars and the colors should be read from a data file. This file would assign one ASCII value, e.g. our 130, to a character and a color. The function then would be much more flexible than at the moment. Such an assignment could be look like this:

[130]
char=#
color=lightgray

You might have recognized the format – it’s the one used by older Windows versions and (still) by several programs for .ini-files. Of course any other format, e.g. XML, will do just fine.

The file could be read into an array (textual color constants in the file had to be typecasted into integer). The data type of that array would be a record with two elements. The declaration in FreePascal would be:

TYPE ColoredChar = Record
  chChar: Char;
  intColor: Integer;
END;

VAR
  MyTranslatedChars: Array[128..255] of ColoredChar;

With such structures you wouldn’t need the case statement any longer. Instead, you would just write:

chTransformedLetter := MyTranslatedChars[ord(chLetter)].chChar;
intColor := MyTranslatedChars[ord(chLetter)].intColor;

This is way cleaner than to hard-code all possible characters and color combinations into a function whose only job is to bring these chars on the screen.

 

No OOP?

As you have seen, there exists a global variable blUseSDL. This might be criticized. This was mainly done to make the example code easier. The same goes for the lack of object oriented programming. It is rather easy to think of an object that represents the game window and to implement CharXY() and TextXY() as methods of this object. A valid call could then be for example

MyScreen.TextXY(2, 2, 'Status of '+TheAvatar.strName+':')

– not very complicated at all.

 

Two Modes – Different Screen Sizes

Finally, I want to address an issue related to window- and tile sizes. The basics were already covered above, but out of these basics a problem is born which can lead to strange results if not handled properly.

You probably want to use a standard resolution, e.g. 800×600, for the SDL mode of your game. If you now use the described functions to output text, chars or tiles, all will work fine until you accidently use a coordinate out of range.

This can happen very easily if the SDL window offers more rows or columns than the console mode. If you don’t think about this and e.g. use a CharXY(20,27,'T'), in a 80×25 console your game will crash with an access violation.

To avoid this, you have several options. You could just stop to use rows greater than 25, but in SDL mode this would cause a fairly large amount of unused space (perhaps this could be filled by a nice background texture).

You could also check intX / intY and automatically decrease these values if they wouldn’t fit the screen. This method is used by LambdaRogue, but it may be confusing.

The best way was to use tile sizes which translate perfectly between two modes. To have no overhead of rows and columns, you might have a look at the following overview:

Window Size Optimal Tile Size
640×480 8×19
800×600 10×24
1024×768 12×30

These sizes, of course, are very uncommon. Most available tilesets come in squared sizes and editing them would need as much work as creating a completely new tileset. However, if you don’t want to use graphics, but use a virtual (SDL) console, you only need tiles with letters, ciphers and special chars. Such tiles can easily be created with every graphics editor. Just open a new file in your desired size, select a font and adjust the size of the font to make all characters fit into the center of your file. Create all files you need and copy them into one single bitmap. This bitmap is your tileset.

Summary

This article described one way to combine SDL and console mode in one binary, without needing to recompile, to maintain different versions or the need to always select the correct output mode. In fact, the function was planned to be as easy as FreePascal’s console-only function WriteXY(). If implemented in a unit, it can be used again and again as the basis for the screen output of your roguelike.

However, there are also some traps and problems, and the article discussed the most important of these issues – together with possible resolutions.

I guess that there are already similar approaches taken by other developers, but I saw the need for a clear description of the basic principles for »newbies«. Hopefully reading this article was somewhat helpful.

Mario Donick

 

No comments have been added yet.

If you would like to comment on this article, use the form below. Your comment will be reviewed and activated as soon as possible.

Your name:


Your mail:


Your comment: