UEFI News and Commentary

Showing posts with label games. Show all posts
Showing posts with label games. Show all posts

Sunday, November 13, 2016

The UEFI Maze Game, Part 1

This UEFI Shell application features a very simple maze game that uses UEFI’s Graphics Output protocol to draw a random maze and direct a character from entrance to exit using a keyboard. It will be added to the SVN repository after the last article in this series is published.

This application features a few nifty touches, including converting bitmap (.bmp) files to HII, merging bitmaps using transparency and a nifty maze generation algorithm. This application uses both the standard C library, as well as UEFI-specific libraries.


This game doesn’t have any villains or time limits, yet. Originally, I planned to integrate the thermometer application previously discussed so that the main character got hotter and hotter and little ice cubs in the maze would cool him down. You can add villains in the maze, or the animation could move smoothly from cell to cell or there could be some sort of time limit.

The source is spread over two .C files. It also uses three bitmap files, which are included in the online source code. These bitmaps are: a rock, a player, and a solid green background.

The First Source File: Game.c
Figure 1: Global Variables in Game.c
Lines 1-13
These are the include files for the standard C library, basic UEFI services and, surprise(!) bitmaps. The MdePkg\Include\IndustryStandard contains constants, data structures and file formats from many popular industry standards, including PCI, ACPI and USB.
Lines 15-41
These are the key function declarations from the other source file, Bitmap.c. Rather than create a separate header file, they are just listed here.
Figure 2: Game State Data in Game.c


Next we move on the various global variables that maintain the game state.


Lines 46-47
DisplayImageStack is the global function that refreshes the screen from the internal buffer that holds the game’s bitmap image.
Lines 50-51, 64-65
These globals hold the width of the loaded background images and other images. These should be the same.
Lines 52-54
These point to the three bitmaps used in the game: the rock, the background and the player. Each is 50 x 50 pixels.
Lines 56-58
The game maintains a pixel-for-pixel copy of what is actually going to be displayed on the maze portion of the screen.  All of the player actions are updated here before they are copied to the screen. 
Lines 60-62
The game also maintains a cell-by-cell copy of what is in each part of the grid. The array mGrid contains a two dimensional array, mGridWidth cells wide by mGridHeight cells tall. Each element in the array points to one of the bitmaps: rock, background or player. The size of the array is calculated based on the screen resolution.
Lines 67-74
These mark the coordinates (within mGrid) of the character’s position, the entrance position and the exit position. Initially, the character’s position is at the entrance position. The game ends when the character reaches the exit position.
Figure 3 Game entry point in Game.c

Now that you are thoroughly bored with the global definitions, we finally reach the entry point of the game. 

Lines 590-596
This uses the standard C-style entry point. But there are no command-line options parsed.
Line 598
The srand() function is used to initialize the random number generator with a seed value. In this case, the seed value is derived from the system time. When debugging the maze generation algorithm, it was useful to set this to a fixed value so that the maze would be the same each time, facilitation easier debugging of issues.
Line 600
Reset the console so that the screen is empty.
Lines 602-606
Load all of the bitmap images and convert them into the format used by the UEFI graphics output functions. If there is an error, it means that not all of the images could be loaded or (less likely) the system cannot switch to graphics mode.
Line 608
Initialize the game data structures, including the maze.
Line 610-614
Display the maze for the first time.
Line 616
Enter the main game loop. This loop continues until the user indicates they are finished or they have reached the maze exit.
Line 619-621
Clear the screen and exit.

Figure 4 Setup bitmap images and Graphics Output Protocol in Game.c

The first step for the game is to load all of the bitmaps out of the files.

Lines 108-110
Find the instance of the UEFI Graphics Output protocol.
Lines 112-127
Load each of the three bitmaps from external files. Each of the files is formatted using the industry standard .bmp file format. 

Next Time
In the next installment, we'll look at exactly how to initialize UEFI's Graphics Output Protocol and load a bitmap.

Tuesday, April 01, 2014

Something About the Game I Made with a Thermometer In It

After making the simple app that uses composite images and transparency to change the appearance of an image based on user input (as described in this article), I decided to expand it a little into the beginnings of a simple game.  As it is now, it is simply a shell for a game, in which you can move a sprite around and change modes.

In this article, I will describe the thought process behind creating this as well as the code I wrote.  This article assumes you have read the referenced article above on composite images and transparency.

Source code for this project can be found here.

I knew I wanted this "game" to include the thermometer I had created, and have something affected by the changing temperature.  I decided I could have a little character run around the screen and his color would change with the temperature.  Because I was using the same thermometer as I did in my last project, I was able to use the same code.  The only change I made was a simple image re-size to make it a little smaller.

The next question was how I wanted the playable area to be laid out.  Would it be laid out in a grid, where each object occupied a square, or would it be free movement?  Since collision detection in a free movement environment is much trickier than a grid, I decided to lay it out in a grid.  This was a fairly easy thing to implement.

First, I created a struct to represent a square in the grid, which I called "Box."  It has it's X and Y coordinates, an array containing pointers to the adjacent Boxes in each of the four cardinal directions, and a boolean value indicating whether or not the box is occupied by an object.


The next step was to initialize the grid, which is simply an array of boxes.  So I started by clearing all the memory and initializing the first box's coordinates to (0, 0).
Now from here, I could have hard-coded all the values for every single box, but if I had decided to change the size of the grid, it would have been a big pain to rewrite, so I wrote a loop that initialized the coordinates and array of adjacent boxes for each box.  For each of the boxes, I first set it's Occupied value to false (since each box is initially empty).  I first then look in the East (right) direction to see if the box is on the right edge of the grid.  If it's not, the East adjacent box is initialized to the next box in the array, and if not, East is set to NULL.  Next, I check the North (up) direction to see if it is on the top edge of the grid.  Like the East check, I initialize the North box if it's not on the top edge, and set it to NULL if it is on the top edge. 

So along with initializing the array of boxes, we also need to initialize the coordinates, which we can do in the checks for the West edge.  We can do as we have done with North and East, and initialize the West value.  However, after that, we can begin initializing coordinates.  If the box is not on the West (left) edge of the grid, then it can base its X coordinate on the box to its left, and simply add the width of an image (which is the same as the width of a box).  Its Y coordinate is simply the same as the box to its left.  If it is a left-edge box, then its X coordinate is 0, and the Y coordinate can be gotten by adding the box height to the Y coordinate of the box above it.  Finally, we do the South check, just like the others were done, and finish initialization of the boxes.

Now I needed to deal with the images.  In this application, I had six images: the background image, the character sprite, an object sprite, and the three thermometer images.  I did the same thing as described in the articles referenced above, except with more images.  All the image setup was done in the same function, and the image buffers stored in global variables.  Details about the image setup can be seen in the previous articles or the source code (link provided above).  In the source code, I have included a miniature .FDF file that includes the additional lines I added which allow the appropriate images to be included.  In order to use this, copy the text from my .FDF file and paste it into the FILE statements section of Nt32Pkg.FDF.

The only thing I did differently with regards to image display was in ConvertBmpToGopBlt().  One of the conditions for images was unnecessarily strict, and would sometimes cause the function to falsely report an invalid bitmap image, so I removed it.  The line used to read:
if ((BmpHeader->Size != BmpImageSize) || 
      (BmpHeader->Size < BmpHeader->ImageOffset) ||
      (BmpHeader->Size - BmpHeader->ImageOffset !=  BmpHeader->PixelHeight * DataSizePerLine)) {
    return EFI_INVALID_PARAMETER;
  }
I removed the third condition so it now reads:
if ((BmpHeader->Size != BmpImageSize) || (BmpHeader->Size < BmpHeader->ImageOffset)) { return EFI_INVALID_PARAMETER; }
Which causes it to work properly.

Before we move onto the components of the actual game, we need to remember that in this game, we will be moving around a character as well as changing the temperature on the thermometer.  We could use different keys to change the temperature and move around, but I decided it would be best if the character were standing still while the temperature changed, so I made it so that the user could change which "mode" they were in, movement, or temperature change.  The mode is implemented using an enum, and stored in a global variable.

Up next is the actual character itself that the user can move around.  It contains a pointer to the buffer containing the original image, a pointer to the altered image (since we are going to change the image's color over the course of the game), its coordinates, and the box it is currently occupying.  We make the character a global variable, since there is only one.

Initializing the character struct is simple and can be done as soon as the images and grid are set up.  We first set the mode to be "MOVE" since we would like that to be the initial setting.  Image is set to point to the buffer referenced by the global variable.  The image is then copied into a new buffer, and ChangedImage points to that new buffer.  This way, ChangedImage can be altered without affecting the base original image.  Then, the coordinates and box are set to be the upper left-hand corner (just an arbitrary position).
After this, I decided to have some sort of additional object on the map along with the character.  So I created a basic Object struct.  It has an Image, ChangedImage, coordinates, and box it occupies.  This is actually basically the same as the Character struct, and in the future, I could go back and combine the two into one.  There is only one non-character object in this application, but multiple objects could simply be stored in a global array.

Initializing and setting up objects is similar to that of initializing the character.  In this program, I only included one object, but it would be simple enough to expand the function to deal with multiple objects.  Like the character image initialization, we set Image to be the globally accessible buffer, and ChangedImage to be a copy of the Image buffer.  The coordinates and grid position are set, and then we set the assigned box to be occupied.

Now we turn our attentions towards player actions.  The first and most basic action a player can take is to move.  Since we used a grid, the movement is easy to do.  Basically all it does is check the box adjacent to where the character currently is, and see if it is null or occupied.  If not, it changes the character's box and coordinates to that of its new location.

The next is its color change.  Now, this method does nothing incredibly special.  It simply multiplies the pixel colors by the percentage height difference of the thermometer.  It makes sure the colors never go above their maximum value, and are never negative.  The nice thing about this is that will not alter black pixels (which, in my implementation are interpreted as transparent), so pixels intended to be transparent will remain that way, whatever color changes might occur.  Notice that the transformations are based on the original image's color.  This way, colors at a given temperature are always the same. Another possible implementation of this is to have the blue color increase and red decrease as the temperature decreases, and vise versa as the temperature increases.  

Finally we get to the actual game loop itself.  It is just a do-while loop that waits until an end condition is reached (in this case, until the END key on the keyboard is pressed).  All it does is sit around and wait for the user to press a key on the keyboard.  If it's one of the arrow keys, and the game is in MOVE mode, it moves the character.  Otherwise, if it's in TEMP mode, it will change the temperature and change the player's color only if it's an up or down arrow key.  In this implementation, the mode is changed by pressing the PgUp key.  After that, it displays the images in their new positions.  Once the end condition has been met, the memory is freed, screen cleared, and we return success! In the images below, the image on the left shows the key scanning, and the right image shows the image display and cleanup.
   


And that's all she wrote!  

Tuesday, September 24, 2013

Zork on UEFI

Leave it to Matthew Garrett to find some extra time on his hands, and then decide to use it to port Zork (or rather, the Frotz Z-machine interpreter) to UEFI. He wrote about his experiences on his inimitable blog and, during the process, highlighted some of the on-going issues with writing applications for UEFI.

Matthew highlights the fact that the standard C library included in EDK2 only supports UEFI Shell applications, not UEFI applications running directly on UEFI or UEFI drivers. This makes porting code more difficult than necessary, since I've struggled to share code between UEFI drivers and applications, even going to the extreme of writing my "driver" as a UEFI application.

Of course, he just updated Frotz to be a UEFI shell application, and then launched it. This turns out to be trickier than it sounds, since the UEFI shell doesn't set the current working directory where he expected it.

As anyone who follows this blog knows, I am a great fan of stretching UEFI into new areas, like text adventures and utility C libraries. More on that in the next few weeks.