Alex Russell's Dos Game Programming in C for Beginners

Introduction . Chapter 1 . Chapter 2 . Chapter 2.1 . Chapter 3 . Chapter 4 . Chapter 5 . Chapter 6

Chapter 4

User Input

Games require fast, responsive input for enjoyable game play. Under DOS this requires a fair bit of knowledge of the underlying hardware and operating system. Most of this specialized knowledge is not applicable to any other game platform. Almost any other game programming platform (win32 and DirectX, consoles, etc.) will have built in support for fast game input. That said, here is what a DOS game requires.

Keyboard

The standard BIOS keyboard handler does not allow multiple key presses to be processed. If you only use the CTRL, SHIFT, and ALT keys you can get away with some simple BIOS tricks, but for more general multi-key support you have to write an int9 (the hardware interrupt used by the keyboard) ISR (Interrupt Service Routine) that completely replaces the normal BIOS keyboard handler. Our keyboard ISR will get the raw scan codes, and place them in a queue for later processing.

Mouse

The standard int33h mouse services are pretty good, but they require you to poll the mouse unless you install your own ISR to work in concert with int33h driver. Polling means that at intervals you ask the mouse driver what the mouse is up to. This isn't good as you could miss a mouse click or other action. Once again our ISR will place mouse events in a queue for later processing.

Joystick

The standard analog joystick is a very slow device to read under DOS. It requires waiting for the joystick to timeout. To prevent it from slowing a game down too much it is only read at regular intervals. The buttons can be read more quickly.

General Input Queue

We will take the input from all devices and place it in one general input queue. This simplifies i/o handling, and makes it easier to add different i/o devices. This is the way all modern operating systems handle i/o.

Keyboard INT9 ISR

This code allows us full control over the keyboard.

I use inline ASM code for much of the hardware access. The ASM code:

in al, 060h

puts the value of port 0x60 into al, and is equivalent to:
unsigned char a;

a=inportb(0x60);

mov al, 020h
out 020h, al

is equivalent to:

a=0x20;
outportb(0x20, a);

#define BYTE unsigned char
#define NUM_SCAN_QUE 256     // this MUST be 256, using BYTE roll-over for
                             // q code

// the interrupt keyword causes the compiler to save all the registers before the 
function is called, and restore them on exit. It also makes the function return via a 
IRET.

static void interrupt (far *oldkb)(void);   /* BIOS keyboard handler */

// Q code
BYTE gb_scan;
BYTE gb_scan_q[NUM_SCAN_QUE];
BYTE gb_scan_head;
BYTE gb_scan_tail;

/*
   invoked by the hardware keyboard interupt
   ques up the raw scan codes
    stuff raw scan codes into the array gb_scan_q[]
*/
/* ---------------------- get_scan() --------------------- April 17,1993 */
void interrupt get_scan(void)
{

   /* read the raw scan code from the keyboard */
   asm   cli

   asm   {

         in    al, 060h       /* read scan code */
         mov   gb_scan, al
         in    al, 061h       /* read keyboard status */
         mov   bl, al
         or    al, 080h
         out   061h, al       /* set bit 7 and write */
         mov   al, bl
         out   061h, al       /* write again, bit 7 clear */

         mov   al, 020h       /* reset PIC */
         out   020h, al

         /* end of re-set code */

         sti
         }

// save the raw scan code in a 256 byte buffer
   *(gb_scan_q+gb_scan_tail)=gb_scan;
   ++gb_scan_tail;
}

/*

save the old int9 ISR vector, and install our own
*/
/* ---------------------- init_keyboard() ---------------- April 17,1993 */
void init_keyboard(void)
{
   BYTE far *bios_key_state;

   /* save old BIOS key board handler */
   oldkb=getvect(9);

   // turn off num-lock via BIOS 
   bios_key_state=MK_FP(0x040, 0x017);
   *bios_key_state&=(~(32 | 64));     // toggle off caps lock and
                                      // num lock bits in the BIOS variable
   oldkb();      // call BIOS key handler to change keyboard lights

   gb_scan_head=0;
   gb_scan_tail=0;
   gb_scan=0;

   /* install our own handler */
   setvect(9, get_scan);

}

/* restore the bios keyboard handler */
/* ---------------------- deinit_keyboard() -------------- April 17,1993 */
void deinit_keyboard(void)
{
   setvect(9, oldkb);
}

As you can see this code is very DOS specific. We will need it for our demo game, but it isn't really useful for anyone thinking about a career in game programming these days. Most keys return two unique scan codes: a 'press' code, and a 'release' code. The `release' code is the `press' code with the high bit set. The second keypad return three scan codes, an 0xe0 followed by the normal scan codes. The pauses key is VERY weird - I'm not going to go into it here.

Joystick

The button status can be retrieved quickly with an inportb(), but to get the joystick values you send to a port, then poll the port until it goes to zero which means this is SLOW as you have to wait until the joystick times out. There are complicated methods using timer interrupts to avoid wasting this time, but I will present the simple 'wait for it to timeout' code.

/* note!!!! buttons are pressed when b0, b1 is zero (0)

   Also, the values returned by this routine will vary widely
   on machines that run at different speeds for joy_x and y

   reads joy1 only
   if joystick is not plugged in the joy_x and joy_y will both
   be 1023

   generally you read the joystick at fixed intervals because
   it is a slow process, and the buttons more frequently.

*/

short joy_x, joy_y;  // stores joystick position
BYTE b0, b1;      // button status flags


/* sets the global vars b1, and b0 */
/* ---------------------- read_joy_buttons() -------------- June 10,1993 */
void read_joy_buttons(void)
{
   asm {

               mov    dx,201h
               xor    ax, ax
               in     al,dx

               mov    bl, al
               and    bl, 010h
               mov    b0, bl

               and    al, 020h
               mov    b1, al
       }
}


/* sets the global vars joy_x, joy_y, b0, b1 - SLOW!!! */
/* this is slow compared to just checking the buttons.
   in many projects I only check the joystick once/(certain time)
   and the buttons more frequently.
*/
   
/* ---------------------- read_joy() --------------------- March 29,1993 */
void read_joy(void)
{
   asm {
               push   si

               xor    ax,ax
	       /* joystick port */
               mov    dx,201h
               mov    cx,ax
               mov    bx,ax
               mov    si, 1024   /* this number may have to be LARGER on
                                  FAST machines */

               cli    /* disable interupts so that our timing loop isn't
                         interupted */

	       /* begin digitize, prime joy port */
               out    dx,al
       }
jlp:           
   asm {
               dec    si
               jz     abt          /* timeout */
	       /* read port */
               in     al,dx
               test   al,1
               jz     nox
	       /* increment x counter if bit 0 high */
               inc    bx
       }
nox:           
   asm {
               test   al,2
               jz     noy
	       /* increment y counter if bit 1 high */
               inc    cx
       }
noy:           
   asm {
               test   al,3
	       /* keep going until both x and y done */
               jnz    jlp
       }
abt:           
   asm {
               sti    /* enable interupts */

	       /* store x and y coordinates, and button stats */

               mov    joy_y, cx
               mov    joy_x, bx

               mov    bl, al
               and    bl, 010h
               mov    b0, bl

               and    al, 020h
               mov    b1, al

               pop    si
       }

}

Mouse

For the mouse we will ask the normal int33h mouse driver to install a stub program that calls our real mouse handler. The stub is written in ASM.

;***************************************************************
;*                                                             *
;* File: cmousea.asm                                           *
;*                                                             *
;* Assembly language hook for CMOUSE library event handler     *
;* Assemble with /Ml switch                                    *
;*                                                             *
;***************************************************************
; real code for real men
; adjust for proper memory model
.MODEL SMALL,C

.CODE
      PUBLIC mouse_event_func,mouse_int

mouse_event_func DD ?


mouse_int PROC FAR
          PUSHF
          CALL CS:[mouse_event_func]
          RET
mouse_int ENDP

END

This ASM function is called by the int33h driver. Mouse_int() in turn calls what ever function mouse_event_func points to. Mouse_event_func is a pointer to a function - it is not itself a function.

This is the c code to use the mouse.

#define ESC 27

short mouse_x, mouse_y;
short mouse_present;
short mouse_hidden=0;
short button_stat=0;
unsigned short flags;

extern void far *far mouse_event_func;
void mouse_int(void);

typedef struct
	{
	unsigned int flags, x, y, button_flag;
	}
mouse_info_t;

#define MAX_MOUSE_EVENTS 10


#define MOUSE_MOVE   1
#define MOUSE_L_DN   2
#define MOUSE_L_UP   4
#define MOUSE_R_DN   8
#define MOUSE_R_UP   16

#define EVENT_MASK   31   /* the logical OR of the 5 above vars */

mouse_info_t mouse_info[MAX_MOUSE_EVENTS];
int head=0;
int tail=0;


/* the low level interrupt handler calls this */
/* ---------------------- mouse_handler() ----------------- April 1,1993 */
void far interrupt mouse_handler(void)
{

   /* save info returned by mouse device driver */
   asm   {
         mov   flags,   ax
         mov   mouse_x, cx
         mov   mouse_y, dx
         mov   button_stat, bx
         }


	// place the mouse information in a circular queue
	mouse_info[tail].x=mouse_x;
	mouse_info[tail].y=mouse_y;
	mouse_info[tail].button_flag=button_stat;
	mouse_info[tail].flags=flags;

	tail++;
	if ( tail == MAX_MOUSE_EVENTS )
		tail=0;
   if ( tail == head )
      {
      head++;
      if ( head == MAX_MOUSE_EVENTS )
         head=0;
      }

}


/*

   the assembler function mouse_int() calls
   mouse_event_func whenever the mouse moves, or a button
   is pressed, or released. mouse_event_func points to mouse_handler
   which ques up the mouse events, get_event can be used to read these
   events.

*/

/* is there a mouse, install int handlers */
/* ---------------------- init_mouse() -------------------- April 1,1993 */
short init_mouse(void)
{
   unsigned short c_seg, c_off;

   asm   {
         xor   ax, ax
         int   033h

         /* note BX holds number of buttons, but we don't care */
         mov   mouse_present, ax
         }

   if ( mouse_present )
      {
      /* install our own handler */
      mouse_event_func=mouse_handler; /* global func pointer */

      /* install mouse_int as mouse handler, which will call
         mouse_handler */

      c_seg=FP_SEG(mouse_int);
      c_off=FP_OFF(mouse_int);
      asm   {
            mov   ax, c_seg
            mov   es, ax
            mov   dx, c_off
            mov   ax, 0ch
            mov   cx, EVENT_MASK

            int 033h
            }

      /* set mouse x, y limits */
      asm   {
            mov   ax, 7
            mov   cx, 0
            mov   dx, 359

            int 033h

            mov   ax, 8
            mov   cx, 0
            mov   dx, 239

            int 033h


            /* set initial mouse_x, mouse_y */
            mov   ax, 3
            int 033h

            mov   mouse_x, cx
            mov   mouse_y, dx
            }   
      }

   return(mouse_present);
}

* ---------------------- deinit_mouse() ------------------ April 1,1993 */
void deinit_mouse(void)
{

   if ( mouse_present )
      {
      /* deinstall our mouse handler by making int 33 never call it */
      asm   {
            mov   ax, 0ch
            xor   cx, cx     /* mask == 0, handler never called */

            int 033h

            /* reset mouse driver */
            xor   ax, ax
            int   033h
            }
      }
}

See the source files joy.c, mouse.c, mousetub.asm, and key.c for mini programs that demonstrate using all this i/o code.

Event Queue

The next step is to combine all these different input streams into a single combined stream of input. Doing this makes the game program's code much simpler. This combined input stream is demonstrated in chap4.c Below is an excerpt from chap4.c.

// add an event to out generic input queue
/* ---------------------- add_input() ------------------- October 8,1998 */
void add_input(event_t *event)
{

	in[i_tail].type=event->type;
	in[i_tail].sub_type=event->sub_type;
	in[i_tail].x=event->x;
	in[i_tail].y=event->y;
	in[i_tail].data1=event->data1;
	in[i_tail].data2=event->data2;

	i_tail++;
	if ( i_tail == MAX_INPUT )
		i_tail=0;
   if ( i_tail == i_head )
      {
      i_head++;
      if ( i_head == MAX_INPUT )
         i_head=0;
      }

}

// see if there  any user generated input waiting for processing
int check_input(event_t *event)
{
	int is_event=0;
	static unsigned long next_joy_check=0;
	event_t new_event;
	int dx, dy;

	if ( get_tick() > next_joy_check )
		{
		// read the joy stick only at regular intervals
		read_joy();

		// this dx/dy thing is to prevent extra joy events caused
		// by normal joystick instability
		dx=abs(last_joy_x - joy_x);
		dy=abs(last_joy_y - joy_y);
		if ( dx > 10 || dy > 10 )
			{
			// add a joy event
			new_event.type=JOY;
			new_event.sub_type=J_MOVE;
			new_event.x=joy_x;
			new_event.y=joy_y;

			add_input(&new_event);
			last_joy_x=joy_x;
			last_joy_y=joy_y;
			}

		next_joy_check=get_tick();
		}

	read_joy_buttons();
	if ( last_b0 != b0 )
		{
		new_event.type=JOY;
		new_event.sub_type=J_BUTTON0;
		new_event.data1=!b0;

		add_input(&new_event);
		last_b0=b0;
		}
	if ( last_b1 != b1 )
		{
		new_event.type=JOY;
		new_event.sub_type=J_BUTTON1;
		new_event.data1=!b1;

		add_input(&new_event);
		last_b1=b1;
		}

	// place any pending mouse events in generic queue
	// could easily make mouse code place its events directly in
	// generic input queue
	while ( head != tail )
		{
		new_event.type=MOUSE;
		new_event.x=mouse_info[head].x;
		new_event.y=mouse_info[head].y;
		if ( mouse_info[head].flags & MOUSE_MOVE )
			new_event.sub_type=M_MOVE;
		else
			{
			if ( mouse_info[head].flags & MOUSE_L_DN )
				new_event.sub_type=M_L_BUT_DOWN;
			else
				{
				if ( mouse_info[head].flags & MOUSE_L_UP )
					new_event.sub_type=M_L_BUT_UP;
				else
					{
					if ( mouse_info[head].flags & MOUSE_R_DN )
						new_event.sub_type=M_R_BUT_DOWN;
					else
						{
						if ( mouse_info[head].flags & MOUSE_R_UP )
							new_event.sub_type=M_R_BUT_UP;
						}
					}
				}
			}

		add_input(&new_event);

		head++;
		if ( head == MAX_MOUSE_EVENTS )
			head=0;

		}

	// place any pending keyboard events in queue
	// in a real game you might want to do some processing to
	// the raw scan codes to convert them to ASCI or an other
	// more convenient format
	while ( gb_scan_head != gb_scan_tail )
		{
		new_event.type=KEY;
		new_event.data1=gb_scan_q[gb_scan_head];

		// 0xe0 indicates a key from the SECOND keypad, real code will
		// follow
		if ( new_event.data1 == 0xe0 )
			{
			gb_scan_head++;
			continue;
			}

		gb_scan_head++;

		if ( new_event.data1 & KEY_UP_MASK )
			new_event.sub_type=KEY_UP;
		else
			new_event.sub_type=KEY_DOWN;

		new_event.data1&=KEY_ALL_MASK;  // clear high bit

		// this is where you would convert the raw scan code to ascii
		// and do other high level processing if required
		// eg  new_event.data2=get_ascii(new_event.data1);

		add_input(&new_event);
		}

	// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	// check if there are any pending events, and return the oldest one

	if ( i_head != i_tail )
		{
		is_event=1;

		event->type=in[i_head].type;
		event->sub_type=in[i_head].sub_type;
		event->x=in[i_head].x;
		event->y=in[i_head].y;
		event->data1=in[i_head].data1;
		event->data2=in[i_head].data2;

		i_head++;
		if ( i_head == MAX_INPUT )
			i_head=0;
		}

	return is_event;
}


/* ---------------------- main() ------------------------ October 8,1998 */
void main(void)
{
	int done=0;
	event_t event;

	printf("Alex Russell dos graphic course\n");
	printf("Press ESC to exit\n");

	read_joy_buttons();
	last_b0=b0;
	last_b1=b1;

	read_joy();
	last_joy_x=joy_x;
	last_joy_y=joy_y;

	init_keyboard();
	init_mouse();

	while ( !done )
		{
		if ( check_input(&event) )
			{
			switch ( event.type )
				{
				case JOY:
					switch ( event.sub_type )
						{
						case J_MOVE:
							printf("JOY MOVE x %d y %d\n",
								event.x, event.y);
							break;

						case J_BUTTON0:
							printf("Button 0 is %s\n",
								event.data1 ? "UP" : "DOWN");
							break;

						case J_BUTTON1:
							printf("Button 1 is %s\n",
								event.data1 ? "UP" : "DOWN");
							break;
						}
					break;

				case MOUSE:
					switch ( event.sub_type )
						{
						case M_MOVE:
							printf("Mouse move x %d y %d\n",
								event.x, event.y);
							break;

						case M_R_BUT_DOWN:
							printf("MOUSE R Button DOWN\n");
							break;

						case M_R_BUT_UP:
							printf("MOUSE R Button UP\n");
							break;

						case M_L_BUT_DOWN:
							printf("MOUSE L Button DOWN\n");
							break;

						case M_L_BUT_UP:
							printf("MOUSE L Button UP\n");
							break;
						}
					break;

				case KEY:
					printf("KEY %s = 0x%0x %d\n",
						event.sub_type == KEY_UP ? "UP" : "DOWN",
						event.data1, event.data1);

					if ( event.data1 == 1 ) // scancode for ESC is 1
						done=1;
					break;
				}
			}

		}

	deinit_mouse();
	deinit_keyboard();

}

Chapter 4 Exercises

1. Why is reading the joystick slow.

2. What is the advantage of a single input queue containing input from all input devices?

3. What other types of events, other than mouse, keyboard, and joystick could go into an event queue?

4. Write a function to convert the scan codes for the normal a to z keys to ascii.

5. Many games do not require a fully analog joystick. Write a program that turns the joystick into a game pad. IE it only returns JOY_X_UP, JOY_X_DOWN, JOY_X_CENTRE, and the same codes for y. Assume the joystick is centred at the start of the program, and that 1/3 from centre is the threshold for movement.

6. Why is polled input not desirable, especially for the mouse.

Click here for chap4.zip (37 KB)

Introduction . Chapter 1 . Chapter 2 . Chapter 2.1 . Chapter 3 . Chapter 4 . Chapter 5 . Chapter 6

Copyright 1998 (c), Alex Russell, All rights reserved