Saturday, November 26, 2011

Windows Console Game: Event Handling

The last post in this series was on writing to the console; this post I'd like to go over handling events within the Windows Console. There are two types of events we're interested in: Keyboard Events and Mouse Events. The rest of the type of events we're going to ignore (and MSDN actually advises this on a couple internally used event types). In order to read keyboard events and mouse events, we need to get a record of all events that have occurred to the console since the last time these events were retrieved. This can be done with the following console functions: GetNumberOfConsoleInputEventsReadConsoleInput.


In order to read the console's input buffer (the record of events that have happened from input, including user input), we have to know how many events there are in order to dynamically allocate memory to store those events. This is where GetNumberOfConsoleInputEvents comes. It takes the read handle and a DWORD pointer as its parameters, and places the number of events there are in the address pointed by the DWORD pointer.



/* Read console input buffer and return malloc'd INPUT_RECORD array */
DWORD getInput(INPUT_RECORD **eventBuffer)
{
  /* Variable for holding the number of current events, and a point to it */
  DWORD numEvents = 0;


  /* Variable for holding how many events were read */
  DWORD numEventsRead = 0;


  /* Put the number of console input events into numEvents */
  GetNumberOfConsoleInputEvents(rHnd, &numEvents);


  if (numEvents) /* if there's an event */
  {
    /* Allocate the correct amount of memory to store the events */
    *eventBuffer = malloc(sizeof(INPUT_RECORD) * numEvents);
    
    /* Place the stored events into the eventBuffer pointer */
    ReadConsoleInput(rHnd, *eventBuffer, numEvents, &numEventsRead);
  }


  /* Return the amount of events successfully read */
  return numEventsRead;
}


The above code is a function that places an array of INPUT_RECORD structures into the content of the pointer pointed by the parameter eventBuffer. If you're confused about the double asterisk **, then read the rest of the paragraph. If you already understand what this function is doing, skip to the next paragraph. The asterisk, known as the dereference operator, can be read as "the content pointed by". In order for this getInput function to place the INPUT_RECORD structs into the address pointed by a pointer, we have to pass a pointer to a pointer to the function -otherwise we'd only pass the value of a pointer to the function, which isn't what we want. Then, to access the the pointer we passed, we'd use one asterisk. To access the value pointed by the pointer that is pointed to by the parameter pointer, we must use a second *. See the below diagram for a visual representation. On the left, Pointer 1 is dereferenced giving you direct access to Pointer2. Pointer2 is dereferenced giving you direct access to the INPUT_RECORD. The right side of the diagram uses two dereferences to directly access INPUT_RECORD in a single stroke.




Now that we have a function for getting the INPUT_RECORD structs, we need to be able to loop through each structure and analyze what kind of record it is, and depending on what it does we can do whatever action we like.


while(1)
  {
    /* Get the input and number of events successfully obtained */
    numEventsRead = getInput(&eventBuffer);
    
    /* if more than 0 are read */
    if (numEventsRead)
    {
      /* loop through the amount of records */
      for (i = 0; i < numEventsRead; i++)
      {
        /* check each event */
        switch (eventBuffer[i].EventType)
        {
          /* if type of event is a KEY_EVENT */
          case KEY_EVENT:
            switch (eventBuffer[i].Event.KeyEvent.wVirtualKeyCode)
            {
              /* if escape key is pressed*/
              case VK_ESCAPE:
                return 0;
            }
        }
      }
    }
  }

The above code continuously loops until a user presses the escape key on their keyboard, which then makes the program close. This works by getting the INPUT_RECORD structs into the pointer eventBuffer (which points to type INPUT_RECORD) using our getInput function, and placing the return value thereof into numEventsRead. The loop then checks for a KEY_EVENT, and if found checks to see if the escape key was pressed. Similarly, you can check to see if a MOUSE_EVENT occurred in this very same way. If you understood what I've explained thus far in all of this series, you should be able to check for any keypress, with the help of the virtual keycode page, and then do something thereafter.


Now how about writing something onto the console when you click your left mouse button? The first thing you'd need is to be able to check for a MOUSE_EVENT record, and get the mouse's x and y coordinates from the MOUSE_EVENT structure. Here's an example of a case to do such a thing:


case MOUSE_EVENT:
  offsetx = eventBuffer[i].Event.MouseEvent.dwMousePosition.X;
  offsety = eventBuffer[i].Event.MouseEvent.dwMousePosition.Y;
  if (eventBuffer[i].Event.MouseEvent.dwButtonState == FROM_LEFT_1ST_BUTTON_PRESSED)
  {
    writeImageToBuffer(consoleBuffer, REDRECTANGLE.chars, REDRECTANGLE.colors, REDRECTANGLE.width, REDRECTANGLE.height, offsetx - 1, offsety - 1);
    write = YES;
  }

This indexes the INPUT_RECORD structs when a MOUSE_EVENT structure is found, and then accesses the data members X and Y, as detailed here, and places the values of the x and y position into two integers. The large constant FROM_LEFT_1ST_BUTTON_PRESSED is a Microsoft definition for a left-click. Now what is this writeImageToBuffer function? That doesn't seem to be a MSDN documented function... Well it's not! It's a simple function I wrote to write any image onto the screen buffer, given the correct parameters. The image it is writing is called REDRECTANGLE, which is actually a structure defined in a file I've written called redRectangle.h. The red rectangle is a square of 9 red characters, so to write this square onto where the user clicks, you must use offsetx - 1, and offsety -1, otherwise the top left of the red rectangle image will be placed on the left-click location, instead of the center.


Now lets check out redRectangle.h so you can see how I've set up the image of REDRECTANGLE!


/* redRectangle.h */


#ifndef FILEREDRECTANGLEH
#define FILEREDRECTANGLEH


/* A red rectangle! */


#define REDRECTANGLEW 3
#define REDRECTANGLEH 3


typedef struct
{
  int width;
  int height;
  int chars[REDRECTANGLEW * REDRECTANGLEH];
  int colors[REDRECTANGLEW * REDRECTANGLEH];
} _REDRECTANGLE;


_REDRECTANGLE REDRECTANGLE = 
{
  REDRECTANGLEW,
  REDRECTANGLEH,
  {
    219, 219, 219,
    219, 219, 219,
    219, 219, 219,
  },
  {
    4  , 4  , 4  ,
    4  , 4  , 4  ,
    4  , 4  , 4  ,
  }
};


#endif /* FILEREDRECTANGLEH */

This was the cleanest way I could think of for setting up an image that should be compatible with all versions of C. I'll start explaining from the top of the file. The two preprocessor directives ifndef and define are used to see if FILEREDRECTANGLEH is defined yet or not. If it is not yet defined, then the entire contents of the file will be included wherever an include of this file is placed. If the define FILEREDRECTANGLEH is already defined, it means that you've already included this file somewhere, and so the entire contents of the file will be skipped!


Screenshot of the final demonstration program, drawing on the window!


I have two defines for each image I write in this format, and they represent width and height as 3 in this image. These defines are critical for declaring a REDRECTANGLE, as the size needs to be defined at compile, since a template for the structure needs to be created for the program to run, and as such you need to know the size of the arrays within the REDRECTANGLE structure in order to properly place it in memory! However, the use of defines in this way makes it very simple to create other copies of images. For example, to create a new image called BLUETRIANGLE, you simply use find and replace (ctrl + h for a lot of programs) and replace all instances of REDRECTANGLE, with BLUERECTANGLE. You then would modify the color and character arrays to contain the correct data, and change the WIDTH/HEIGHT values for the defines accordingly. The structure called _REDRECTANGLE is typedef'd allowing declaration of variables by skipping writing the annoying legacy struct keyword. An instance of _REDRECTANGLE called REDRECTANGLE is then declared and initialized with the arrays containing the values for the ASCII characters, and color values.


Using this format in conjunction with the writeImageToBuffer function, you can pretty easily create new images and write them with limited amounts of code! Now lets check out a finished product: link (I didn't want to post the entire unwieldy thing here).


The last thing to mention is that if you update the screen with WriteConsoleOutputA every loop, you'll probably see little flickers as the screen is updated. This is simply a limitation of the function WriteConsoleOutputA. There are a few solutions to this. The easiest solution I've come up with is to just update screen less often, and I chose to do this by limiting the time in which the screen is updated to only when the consoleBuffer is changed. Another solution is to keep track of which portions of the screen you actually need to update, and call WriteConsoleOutputA and only write with the smallest portion of the screen as possible. Also supposedly if you update the screen with the same characters and colors multiple times it increases the chance of flickering lines, so this means you should try to avoid doing so. Lastly, you can multi-thread your program, and place just the call to WriteConsoleOutput within the additional thread. I do the first solution with a simple if statement:


  /* If write is 1, meaning the screen needs to be updated */
  if (write)
  {
    /* Write our character buffer (a single character currently) to the console buffer */
    WriteConsoleOutputA(wHnd, consoleBuffer, characterBufferSize, characterPosition, &consoleWriteArea);
    write = NO;
  }

This will only call the WriteConsoleOutputA function when write is not 0.


The entire final program demonstrates drawing a square image on the screen on left-click, and a red dot to the screen on right click. There is boundary checking on the click coordinates to prevent indexing outside of the screen!


You might by now have realized that the characters you're writing to the screen are oddly shaped rectangles. What about nice square characters? Square characters are essential for creating a nice game :( Well the next post in this series covers setting the console's font, font size, and even the color palette!


Series on creating a Windows Console game:

2 comments:

  1. Please make more guides! I cant wait longer :D

    ReplyDelete
  2. Next one is up!

    http://cecilsunkure.blogspot.com/2011/12/windows-console-game-set-custom-color.html

    ReplyDelete

Note: Only a member of this blog may post a comment.