In this tutorial we will write a cross-platform, retro C program that accepts keyboard control of a text-based player character on screen.
Previously we discussed how some platforms have a conio.h
and some, such as Unix/Linux and MacOS don’t. This is because the header was developed for DOS/Windows, and others adopted the concept.
If we work out a solution for those platforms missing conio.h
can we develop truly cross-platform C programs?
Should we even try?
Our goal
All we want to do for now is create a C program where we can control some sort of character on screen using the keys W, A, S and D for up, left, down and right movements. We will want to delete the character, move it, then redraw it.
And we want this to be compilable for as many retro systems as possible.
Easy!
Introducing conio.h
conio.h
(Console Input Output) is a popular C header file that provides text input and output functions, but it is not part of the C standard. Despite it not being standard, cross-platform, or even the best solution, professors and teachers even now refuse to swap it out from their lectures, so people keep getting taught and coursework marked based on it.
While most people know it as a Borland/Turbo C (or even a Turbo Pascal) thing, apparently it first appeared in Lattice C, and it became a mainstay of DOS programming.
Most DOS or Windows C compilers therefore have the header, MacOS, UNIX and Linux do not.
Confusingly. due to the popularity on DOS/Windows, compilers such as cc65 do have a conio-compatible library.
The actual functions provided in conio.h vary from implementation to implementation, meaning just because it has a conio, doesn’t mean your identical code will still compile.
conio.h functions
conio.h
should include functions that match the following. As you can see, it is a very useful set of features for anyone building a C program around textual input and output.
kbhit | Check if a key was pressed |
cgets | Read a string |
cscanf | Read formatted string |
putch | Write a character |
cputs | Write a string |
cprintf | Formatted print string |
clrscr | Clear screen |
gotoxy | Put cursor at X,Y coordinate |
getch | Get char input |
A nice set of functions (and there are often more). If your system has the header file, all good right? Not entirely. Here is a selection from the CC65 conio implementation; Mostly the same, but slightly different.
cgetc clrscr cprintf cputc cputcxy cputs cputsxy cursor gotoxy kbhit textcolor
z88dk provides conio/DOS compatibility in the classic library:
cgets clrscr cprintf cputs getch cscanf putch gotoxy kbhit textcolor
Simple Keyboard Controlled Character Movement with conio.h



So can we at least create a cross-platform C program for those systems with conio?
Almost. Check this out
Simple Keyboard Controlled Character Movement with Conio
#include <stdio.h>
#include <conio.h>
// Global key variable
int key;
char x,y=10;
int main()
{
/* Clear Screen */
clrscr();
/* Hide cursor */
cursor(0);
/* Loop until Q is pressed */
while ((key = cgetc()) != 'Q')
{
// Delete the character
cputsxy(x,y,".");
// keys;
switch (key)
{
case 'w':
y--;
break;
case 'a':
x--;
break;
case 's':
y++;
break;
case 'd':
x++;
break;
default:
break;
}
cputsxy(x,y,"@");
}
cursor(1);
return(0);
}
Compile for Atari 800:
cl65 -o moveXL.xex move800.c -tatari
Compile for ZX Spectrum:
zcc +zx -vn -o move move.c -lndos -create-app -pragma-need=ansiterminal
It almost works, but the z80 implementation does not have the cursor
function, or the cputsxy
Instead we can use the following:
#include <stdio.h>
#include <conio.h>
#include <ctype.h>
// Global key variable
int key;
char x,y=10;
int main()
{
/* Clear Screen */
clrscr();
/* Loop until Q is pressed */
while ((key = toupper(cgetc())) != 'Q')
{
// Delete the character
gotoxy(x,y);
putch('.');
// keys;
switch (key)
{
case 'W':
case 'w':
y--;
break;
case 'A':
case 'a':
x--;
break;
case 'S':
case 's':
y++;
break;
case 'D':
case 'd':
x++;
break;
default:
break;
}
gotoxy(x,y);
putch('@');
}
//cursor(1);
return(0);
}
You will also notice some systems start up in uppercase or lowercase keyboard mode so some changes can be made to which keys were are detecting.
conio.h Alternative 1: ANSI Escape Codes
Going back to my very early days of the world of work, straight out of school aged 15, I spent my days sat at green on black terminal screens in the DEC VT range. Part of me still would like to get a working VT220 or similar today, though I know the CRT tends to pop after a while.
These guys were super expensive “dumb terminals” – they had only enough technology inside to connect to a remote computer (slow serial connections usually) and handle input and output.
To make this system work across various vender implementations, and then color Bulletin Boards and more, required a standard for putting stuff on these screens, the ANSI standard.
Not all systems adopted this standard, unfortunately, so you get situations where the Commodore family for example use PETSCII and their own, different escape codes.
[2J | Clears the window/terminal |
x;yH | Move the cursor to x, y coordinate |
?25l | Hide the cursor |
?25h | Show the cursor. |
[cm | Set text colour (30 to 37) and background (40 to 47):0. Black [30m 1. Red [31m |
Using these codes is as simple as printing them out to the terminal with an escape character before it to show that the code following should be interpreted as a control sequence rather than output. The terminal software then interprets the code to the best of its ability (eg. a monochrome CRT won’t be able to display color).

This is fine for putting things on the screen, but we will still need a way to get input from the user, and preferably a general-purpose one that is not limited to one hardware implementation.
One workaround (but a messy one) is to use system calls for the target operating system you are working on, for example in Linux/Unix/MacOS you can use the following function to fill in the missing getch()
getch() for MacOS/Linux/Unix
On Unix-like systems we can turn echo on/off using the echo
/-echo
parameter to stty
We can also set that we want raw
input rather than have the terminal interpret the keypresses. Unfortunately this does mean it won’t respond to ctrl-c
to quit out.
/* Get key input */
/* requires stdlib.h */
char getch()
{
char c;
system("stty raw -echo");
c=getchar();
system("stty -raw echo");
return c;
}

conio.h Alternative 2: Curses and Ncurses
Ncurses, or New Curses, is a free, cross-platform library for creating text mode graphical interfaces. It is still being maintained and forms the basis for many tools people rely on daily.
Curses, named for Cursor Optimization, was developed by Ken Arnold at Berkley for BSD. Ncurses (pronounced “enn-curses”) expanded on the subsequent work by Pavel Curtis on pcurses, and has since spread it to many systems and languages, including Python, Ruby, PHP, and JavaScript.
Simple Keyboard Controlled Character Movement with Ncurses
#include <stdio.h>
#include <ncurses.h>
/* Clear screen and set up Curses */
void clrscr()
{
/* Initialise the screen */
initscr();
erase();
noecho();
raw();
move(0, 0);
/* Cursor off */
curs_set(0);
refresh();
}
/* Move cursor to coordinate */
void gotoxy(unsigned int x, unsigned int y)
{
move(y,x);
}
/* Put string at coordinate */
void cputsxy(unsigned int x, unsigned int y, char outString[255])
{
mvprintw(y,x, outString);
}
// Global key variable
int key;
char x,y=10;
int main()
{
/* Clear Screen */
clrscr();
/* Loop until Q is pressed */
while ((key = getch()) != 'Q')
{
/* Delete the character */
cputsxy(x,y,".");
/* Handle keys */
switch (key)
{
case 'w':
y--;
break;
case 'a':
x--;
break;
case 's':
y++;
break;
case 'd':
x++;
break;
default:
break;
}
/* Print our character */
cputsxy(x,y,"@");
/* Refresh the terminal */
refresh();
}
/* Turn echo and cursor back on */
echo();
curs_set(1);
/* End program */
return(0);
}
To use ncurses library functions, you have to include ncurses.h in your program and link using the parameter -lncurses
cc -o prog prog.c -lncurses
Unfortunately, from what I have found, while modern desktops are covered, and the Amiga, there is no ncurses
for 8-bit machines. Please let me know if there is one!
What should we do?
My personal approach will be to write game logic separate from graphics rendering. I would also like to split out controls (keyboard, mouse, joystick?)

As any Amiga owner back in the 1980s will tell you (or perhaps even more convincing, Amstrad owners), directly porting games from one system to another tends to give you the lowest-common experience. Amstrad owners often got to play Spectrum conversions based on what the Spectrum was capable of, Amiga owners to begin just got Atari ST ports with very little in the way of upgrade. Even on the Amiga and ST, when more powerful graphics were possible on the 1200 and STE, game companies targeted the base systems.
Imagine if the C64 was only ever given Commodore PET or Vic 20 games?

Back in the day we used to type BASIC programs into our home computers from magazine listings. Invariably there would be a section telling you how to change the listing so it would work on your computer versus the code originally printed.
If we split our code and have the common parts common but customize for the target machine where possible, we can have the best native experience on each system. We won’t have to restrict the Amiga graphics to the Apple ][ color palette 😉
As mentioned previously, each system also needs individual handling in terms of emulator and the file formats those emulators accept, and then optionally how to get those files onto the physical hardware.
All of this is a long way to warn you of forthcoming segues, rabbit holes, and diversions, as we build games for every one of my collected retro systems, and create retro-style games for modern systems. Where code can be the same we will do that, where it needs to be different we will examine what the differences are.