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
Keyboard
Mouse
Joystick
General Input Queue
Keyboard INT9 ISR
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:
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
Mouse
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.
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
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
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.
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.
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.
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.
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.
This code allows us full control over the keyboard.
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);
}
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
}
}
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
#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
}
}
}
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();
}