現在のブログ
ゲーム開発ブログ (2025年~) Gamedev Blog (2025~)
レガシーブログ
テクノロジーブログ (2018~2024年) リリースノート (2023~2025年) MeatBSD (2024年)
【Game】Space Invaders in C Using Only Xlib (Part 1)
Let's do a little experiment.
We'll make Space Invaders, but the only allowed dependency is Xlib.
This article is divided into 4 parts.
In Part 1, we'll draw graphics on the screen, animate them, and control the player.
In Part 2, we'll handle enemy and player shots, load and display fonts, load and play audio.
In Part 3, we'll add score, lives, and a UFO crossing the screen to complete the full game.
And in Part 4, we'll add a title screen, pause screen, and some visual effects.
The purpose of this article is to show how far we can go by eliminating as much unnecessary stuff as possible.
After Part 4 is finished, the complete source code will be published on Microsoft GitHub and Codeberg.
Let's get started!
Makefile
First, prepare the Makefile.
Lately, I prefer setting up three modes: debug, develop, and release.
Debug: no optimization + debug symbols,
Develop: optimization + debug symbols,
Release: optimization + no debug symbols.
On Linux, you'll probably use gcc and gdb; on BSD systems, clang and lldb.
I'm using FreeBSD, so I use the latter, but it works fine with GNU tools too.
Just replace clang with gcc and lldb with gdb.
Note: Do not copy-paste the code below as-is.
Make requires a tab character at the beginning of every line, but copy-pasting turns it into 2 spaces, making make unusable.
Also, the libraries listed are for FreeBSD 15.0.
I recommend starting with a debug build, compiling, and using ldd to check required libraries before adding them.
NAME = spaceinvader
CFLAGS = -I/usr/include -I/usr/local/include -I/usr/local/include/freetype2
-L/usr/lib -L/usr/local/lib
LDFLAGS = -lc -lX11 -lXft
STATIC = -lsys -lxcb -lthr -lfontconfig -lfreetype -lXrender -lXau -lXdmcp
-lexpat -lintl -lbz2 -lpng16 -lbrotlidec -lz -lm -lbrotlicommon
all: debug runDebug
runDebug:
lldb -o "settings set target.x86-disassembly-flavor intel" -o run ${NAME}
run:
./${NAME}
debug:
clang -O0 -g ${CFLAGS} -o ${NAME} *.c ${LDFLAGS}
develop:
clang -O3 -g ${CFLAGS} -o ${NAME} *.c -static ${LDFLAGS} ${STATIC}
release:
clang -O3 ${CFLAGS} -o ${NAME} *.c -static ${LDFLAGS} ${STATIC}
strip ${NAME}
clean:
rm -rf spaceinvader
Window Setup
First, create an empty window.
Code explanations come later.
#include <X11/Xlib.h>
#include <X11/Xft/Xft.h>
#include <stdio.h>
#define BACKGROUND_D 0x232020
#define BACKGROUND_S "#232020"
typedef struct {
int x, y, w, h;
int isrunning;
const char *name;
Display *display;
Window xwindow;
int screen;
Drawable target;
GC gc;
Visual visual;
XftColor color;
Colormap colormap;
XEvent event;
} SpaceWindow;
void cleanup(SpaceWindow *w) {
if (w->gc) XFreeGC(w->display, w->gc);
if (w->xwindow) XDestroyWindow(w->display, w->xwindow);
if (w->display) XCloseDisplay(w->display);
}
int main(void) {
SpaceWindow w = {
.isrunning = 1,
.w = 600,
.h = 800,
.name = "スペースインベーダー",
.screen = 1
};
XGCValues values;
w.display = XOpenDisplay(NULL);
if (w.display == NULL) {
fprintf(stderr, "err: XOpenDisplay\n");
exit(1);
}
int dw = DisplayWidth(w.display, w.screen);
int dh = DisplayHeight(w.display, w.screen);
w.x = (dw - w.w) / 2;
w.y = (dh - w.h) / 2;
w.xwindow = XCreateSimpleWindow(w.display,
RootWindow(w.display, w.screen),
w.x, w.y, w.w, w.h, 1, 0xee4030, BACKGROUND_D);
if (!w.xwindow) {
fprintf(stderr, "err: XCreateSimpleWindow\n");
cleanup(&w);
exit(1);
}
XSetWindowBackground(w.display, w.xwindow, BACKGROUND_D);
XSelectInput(w.display, w.xwindow, ExposureMask);
XMapWindow(w.display, w.xwindow);
XFlush(w.display);
w.gc = XCreateGC(w.display, w.xwindow, 0, &values);
if (!w.gc) {
fprintf(stderr, "err: XCreateGC\n");
cleanup(&w);
exit(1);
}
w.visual = *DefaultVisual(w.display, w.screen);
while (w.isrunning) {
XNextEvent(w.display, &w.event);
switch (w.event.type) {
case Expose:
case ConfigureNotify:
XClearWindow(w.display, w.xwindow);
break;
default:
break;
}
}
cleanup(&w);
return 0;
}
Let's try running it.
$ make
clang -O0 -g -I/usr/include -I/usr/local/include -I/usr/local/include/freetype2 -L/usr/lib -L/usr/local/lib -o spaceinvader *.c -lc -lX11 -lXft
lldb -o "settings set target.x86-disassembly-flavor intel" -o run spaceinvader
(lldb) target create "spaceinvader"
Current executable set to '/home/suwako/dev/bored/spaceinvader/spaceinvader' (x86_64).
(lldb) settings set target.x86-disassembly-flavor intel
(lldb) run
Process 15761 launched: '/home/suwako/dev/bored/spaceinvader/spaceinvader' (x86_64)
Process 15761 stopped
* thread #1, name = 'spaceinvader', stop reason = signal SIGSEGV: address not mapped to object (fault address: 0x0)
frame #0: 0x0000000821f89e80 libc.so.7`memcpy + 48
libc.so.7`memcpy:
-> 0x821f89e80 <+48>: mov rdx, qword ptr [rsi]
0x821f89e83 <+51>: mov qword ptr [rdi], rdx
0x821f89e86 <+54>: mov rdx, qword ptr [rsi + 0x8]
0x821f89e8a <+58>: mov qword ptr [rdi + 0x8], rdx
(lldb)
Oh, it crashed!
I intentionally added a mistake to show what happens when something fails.
...
int main(void) {
SpaceWindow w = {
.isrunning = 1,
.w = 600,
.h = 800,
.name = "スペースインベーダー" // ここから .screenを削除
};
XGCValues values;
w.display = XOpenDisplay(NULL);
if (w.display == NULL) {
fprintf(stderr, "err: XOpenDisplay\n");
exit(1);
}
w.screen = DefaultScreen(w.display); // そして、そこに移動しよう
int dw = DisplayWidth(w.display, w.screen);
int dh = DisplayHeight(w.display, w.screen);
...
Let's compile again.
Good, an empty window appeared!
Now, let's explain the code.
#include <X11/Xlib.h>
#include <X11/Xft/Xft.h>
#include <stdio.h>
#define BACKGROUND_D 0x232020
#define BACKGROUND_S "#232020"
typedef struct {
int x, y, w, h;
int isrunning;
const char *name;
Display *display;
Window xwindow;
int screen;
Drawable target;
GC gc;
Visual visual;
XftColor color;
Colormap colormap;
XEvent event;
} SpaceWindow;
Here, the background color is defined as a global value set at compile time.
Also, we prepared a struct that bundles window-related info.
Not strictly necessary, but it makes the code 100x easier when it grows large.
void cleanup(SpaceWindow *w) {
if (w->gc) XFreeGC(w->display, w->gc);
if (w->xwindow) XDestroyWindow(w->display, w->xwindow);
if (w->display) XCloseDisplay(w->display);
}
A cleanup function.
A convenience function to combine 3 lines into 1.
int main(void) {
SpaceWindow w = {
.isrunning = 1,
.w = 600,
.h = 800,
.name = "スペースインベーダー"
};
Defining default window values.
They're always 100% the same every time.
XGCValues values;
w.display = XOpenDisplay(NULL);
if (w.display == NULL) {
fprintf(stderr, "err: XOpenDisplay\n");
exit(1);
}
w.screen = DefaultScreen(w.display);
Getting the display and screen.
int dw = DisplayWidth(w.display, w.screen);
int dh = DisplayHeight(w.display, w.screen);
w.x = (dw - w.w) / 2;
w.y = (dh - w.h) / 2;
w.xwindow = XCreateSimpleWindow(w.display,
RootWindow(w.display, w.screen),
w.x, w.y, w.w, w.h, 1, 0xee4030, BACKGROUND_D);
if (!w.xwindow) {
fprintf(stderr, "err: XCreateSimpleWindow\n");
cleanup(&w);
exit(1);
}
XSetWindowBackground(w.display, w.xwindow, BACKGROUND_D);
XSelectInput(w.display, w.xwindow, ExposureMask);
Creating the window in the center of the screen.
You can adjust w.x and w.y to open it anywhere, but for games, center is best.
XSetWindowBackground() sets the background to black.
Remember XSelectInput(w.display, w.xwindow, ExposureMask) because we'll tweak it many times later.
XMapWindow(w.display, w.xwindow);
XFlush(w.display);
w.gc = XCreateGC(w.display, w.xwindow, 0, &values);
if (!w.gc) {
fprintf(stderr, "err: XCreateGC\n");
cleanup(&w);
exit(1);
}
w.visual = *DefaultVisual(w.display, w.screen);
Showing the window and creating a graphics context (GC).
GC means "graphics context," not "garbage collector."
while (w.isrunning) {
XNextEvent(w.display, &w.event);
switch (w.event.type) {
case Expose:
case ConfigureNotify:
XClearWindow(w.display, w.xwindow);
break;
default:
break;
}
}
cleanup(&w);
return 0;
}
Setting up an infinite loop.
If you've used a game engine, you'll recognize this as the update loop.
It keeps updating forever but stops on an exit condition.
Initially, it handles Expose events to clear the window, then does nothing else.
Finally, it cleans up safely on exit.
Currently, closing the window doesn't clean up and treats it as a crash.
This causes memory leaks.
Let's fix that now.
First, add a new header.
#include <X11/Xlib.h>
#include <X11/Xft/Xft.h>
#include <X11/Xatom.h> // 追加
...
w.xwindow = XCreateSimpleWindow(w.display,
RootWindow(w.display, w.screen),
w.x, w.y, w.w, w.h, 1, 0xee4030, BACKGROUND_D);
if (!w.xwindow) {
fprintf(stderr, "err: XCreateSimpleWindow\n");
cleanup(&w);
exit(1);
}
// 追加
Atom wm_delete_window = XInternAtom(w.display, "WM_DELETE_WINDOW", False);
XSetWMProtocols(w.display, w.xwindow, &wm_delete_window, 1);
XSetWindowBackground(w.display, w.xwindow, BACKGROUND_D);
XSelectInput(w.display, w.xwindow, ExposureMask);
...
while (w.isrunning) {
XNextEvent(w.display, &w.event);
switch (w.event.type) {
case Expose:
case ConfigureNotify:
XClearWindow(w.display, w.xwindow);
break;
case ClientMessage: // 追加
if ((Atom)w.event.xclient.data.l[0] == wm_delete_window) {
w.isrunning = 0;
}
break;
default:
break;
}
}
cleanup(&w);
return 0;
}
Now, when closing the window, the debugger message changes from
"Process ##### exited with status = 1" to
"Process ##### exited with status = 0".
0 means normal exit, 1 means error exit.
Next, X11 windows flicker badly by default.
To prevent that, we'll draw to a pixmap instead.
...
typedef struct {
int x, y, w, h;
int isrunning;
const char *name;
Display *display;
Window xwindow;
int screen;
Drawable target;
GC gc;
Visual visual;
XftColor color;
Colormap colormap;
Pixmap backbuf; // 追加
XEvent event;
} SpaceWindow;
void cleanup(SpaceWindow *w) {
if (w->gc) XFreeGC(w->display, w->gc);
// 追加
if (w->backbuf) {
XFreePixmap(w->display, w->backbuf);
w->backbuf = None;
}
if (w->xwindow) XDestroyWindow(w->display, w->xwindow);
if (w->display) XCloseDisplay(w->display);
}
...
w.xwindow = XCreateSimpleWindow(w.display,
RootWindow(w.display, w.screen),
w.x, w.y, w.w, w.h, 1, 0xee4030, BACKGROUND_D);
if (!w.xwindow) {
fprintf(stderr, "err: XCreateSimpleWindow\n");
cleanup(&w);
exit(1);
}
// 追加
w.backbuf = XCreatePixmap(w.display, w.xwindow, w.w, w.h,
DefaultDepth(w.display, w.screen));
w.target = w.backbuf;
...
Now, let's draw some graphics on the screen.
We'll draw one enemy.
First, a simple explanation with a rectangle.
case Expose:
case ConfigureNotify:
XClearWindow(w.display, w.xwindow);
// 追加
if (w.backbuf == None) w.backbuf = w.xwindow;
w.target = w.backbuf;
XftDraw *backdraw = XftDrawCreate(w.display, w.backbuf,
DefaultVisual(w.display, DefaultScreen(w.display)),
DefaultColormap(w.display, DefaultScreen(w.display)));
if (!backdraw) {
cleanup(&w);
fprintf(stderr, "err: XftDrawCreate\n");
XFreePixmap(w.display, w.backbuf);
w.backbuf = None;
exit(1);
}
XSetForeground(w.display, w.gc, BACKGROUND_D);
XFillRectangle(w.display, w.backbuf, w.gc, 0, 0, w.w, w.h);
XSetForeground(w.display, w.gc, 0x00ff00);
XFillRectangle(w.display, w.backbuf, w.gc, 50, 50, 10, 10);
XCopyArea(w.display, w.backbuf, w.xwindow, w.gc, 0, 0, w.w, w.h, 0, 0);
XftDrawDestroy(backdraw);
XFlush(w.display);
break;
A green pixel appeared!
Let me explain what's happening.
if (w.backbuf == None) w.backbuf = w.xwindow;
w.target = w.backbuf;
XftDraw *backdraw = XftDrawCreate(w.display, w.backbuf,
DefaultVisual(w.display, DefaultScreen(w.display)),
DefaultColormap(w.display, DefaultScreen(w.display)));
if (!backdraw) {
cleanup(&w);
fprintf(stderr, "err: XftDrawCreate\n");
XFreePixmap(w.display, w.backbuf);
w.backbuf = None;
exit(1);
}
Backbuffer setup.
Not needed now, but we'll need it for animation, so set it up early.
XSetForeground(w.display, w.gc, BACKGROUND_D);
XFillRectangle(w.display, w.backbuf, w.gc, 0, 0, w.w, w.h);
XSetForeground(w.display, w.gc, 0x00ff00);
XFillRectangle(w.display, w.backbuf, w.gc, 50, 50, 10, 10);
This is where the actual drawing happens.
First draw the black background, then the green pixel.
If you don't draw the background first, the pixel gets hidden.
XCopyArea(w.display, w.backbuf, w.xwindow, w.gc, 0, 0, w.w, w.h, 0, 0);
XftDrawDestroy(backdraw);
XFlush(w.display);
break;
Finally, transfer the frame to the screen.
Now, let's draw the entire enemy.
Make it a bit smaller since we'll draw many later.
XSetForeground(w.display, w.gc, BACKGROUND_D);
XFillRectangle(w.display, w.backbuf, w.gc, 0, 0, w.w, w.h);
XSetForeground(w.display, w.gc, 0x00ff00);
// 1
XFillRectangle(w.display, w.backbuf, w.gc, 50, 50, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 62, 50, 2, 2);
// 2
XFillRectangle(w.display, w.backbuf, w.gc, 52, 52, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 60, 52, 2, 2);
// 3
for (int i = 50; i <= 62; i += 2)
XFillRectangle(w.display, w.backbuf, w.gc, i, 54, 2, 2);
// 4
XFillRectangle(w.display, w.backbuf, w.gc, 48, 56, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 50, 56, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 54, 56, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 56, 56, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 58, 56, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 62, 56, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 64, 56, 2, 2);
// 5
for (int i = 46; i <= 66; ++i)
XFillRectangle(w.display, w.backbuf, w.gc, i, 58, 2, 2);
// 6
XFillRectangle(w.display, w.backbuf, w.gc, 46, 60, 2, 2);
for (int i = 50; i <= 62; ++i)
XFillRectangle(w.display, w.backbuf, w.gc, i, 60, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 66, 60, 2, 2);
// 7
XFillRectangle(w.display, w.backbuf, w.gc, 46, 62, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 50, 62, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 62, 62, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 66, 62, 2, 2);
// 8
XFillRectangle(w.display, w.backbuf, w.gc, 52, 64, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 54, 64, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 58, 64, 2, 2);
XFillRectangle(w.display, w.backbuf, w.gc, 60, 64, 2, 2);
This is the alien.
But this method is very tedious, so I did it just to explain XFillRectangle().
There's a better way!
#define BACKGROUND_D 0x232020
#define BACKGROUND_S "#232020"
#define ALIEN_SCALE 2
static const int alien1[8][11] = {
{ 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0 },
{ 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 },
{ 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
{ 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1 },
{ 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1 },
{ 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
};
...
XSetForeground(w.display, w.gc, BACKGROUND_D);
XFillRectangle(w.display, w.backbuf, w.gc, 0, 0, w.w, w.h);
int alien1_x = 50;
int alien1_y = 46;
XSetForeground(w.display, w.gc, 0x00ff00);
for (int y = 0; y < 8; ++y) {
for (int x = 0; x < 11; ++x) {
if (alien1[y][x] == 0) continue;
int sx = alien1_x + x * ALIEN_SCALE;
int sy = alien1_y + y * ALIEN_SCALE;
XFillRectangle(w.display, w.backbuf, w.gc,
sx, sy, ALIEN_SCALE, ALIEN_SCALE);
}
}
XCopyArea(w.display, w.backbuf, w.xwindow, w.gc, 0, 0, w.w, w.h, 0, 0);
XftDrawDestroy(backdraw);
XFlush(w.display);
break;
Same result.
How to draw multiple aliens?
Let's make it a function.
void draw(SpaceWindow *w, int start_x, int start_y, unsigned int color) {
XSetForeground(w->display, w->gc, color);
for (int y = 0; y < 8; ++y) {
for (int x = 0; x < 11; ++x) {
if (alien1[y][x] == 0) continue;
int sx = start_x + x * ALIEN_SCALE;
int sy = start_y + y * ALIEN_SCALE;
XFillRectangle(w->display, w->backbuf, w->gc, sx, sy, ALIEN_SCALE, ALIEN_SCALE);
}
}
}
...
XSetForeground(w.display, w.gc, BACKGROUND_D);
XFillRectangle(w.display, w.backbuf, w.gc, 0, 0, w.w, w.h);
int sx = 50;
int sy = 46;
for (int y = sy; y < sy*2; y+=36) {
for (int x = sx; x < sx*7; x+=40) {
draw(&w, x, y, 0x00ff00);
}
}
Result:
Now, draw the remaining aliens, player, and bunkers to finish up.
#define BACKGROUND_D 0x232020
#define BACKGROUND_S "#232020"
#define SCALE 2 // 変更
static const int alien1[8][11] = {
{ 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0 },
{ 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 },
{ 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
{ 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1 },
{ 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1 },
{ 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
};
// 追加
static const int alien2[9][11] = {
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0 },
{ 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
{ 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0 },
{ 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
};
static const int alien3[8][11] = {
{ 0, 0, 0, 0, 1, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 1, 1, 1, 1, 0, 0, 0 },
{ 0, 0, 1, 1, 1, 1, 1, 1, 0, 0 },
{ 0, 1, 1, 0, 1, 1, 0, 1, 1, 0 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 0 },
{ 0, 0, 1, 0, 1, 1, 0, 1, 0, 0 },
{ 0, 1, 0, 0, 0, 0, 0, 0, 1, 0 },
{ 0, 0, 1, 0, 0, 0, 0, 1, 0, 0 },
};
static const int player[8][11] = {
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 },
};
static const int bunker[18][24] = {
{ 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 },
{ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 },
{ 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0 },
};
...
// 変更
void draw(SpaceWindow *w, int start_x, int start_y,
unsigned int color, int scale, int width, int height, const int obj[][11]) {
XSetForeground(w->display, w->gc, color);
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
if (obj[y][x] == 0) continue;
int sx = start_x + x * scale;
int sy = start_y + y * scale;
XFillRectangle(w->display, w->backbuf, w->gc, sx, sy, scale, scale);
}
}
}
// 追加
void draw_bunker(SpaceWindow *w, int start_x) {
XSetForeground(w->display, w->gc, 0xffff00);
for (int y = 0; y < 18; ++y) {
for (int x = 0; x < 24; ++x) {
if (bunker[y][x] == 0) continue;
int sx = start_x + x * 1;
int sy = 500 + y * 1;
XFillRectangle(w->display, w->backbuf, w->gc, sx, sy, 1, 1);
}
}
}
...
XSetForeground(w.display, w.gc, BACKGROUND_D);
XFillRectangle(w.display, w.backbuf, w.gc, 0, 0, w.w, w.h);
int start_x = 50;
for (int x = start_x; x < start_x*7; x+=40) {
draw(&w, x, 20, 0xff0000, SCALE, 11, 9, alien2);
draw(&w, x, 50, 0x00ff00, SCALE, 11, 8, alien1);
draw(&w, x, 80, 0x00ff00, SCALE, 11, 8, alien1);
draw(&w, x, 110, 0x00aaff, SCALE, 11, 8, alien3);
draw(&w, x, 140, 0x00aaff, SCALE, 11, 8, alien3);
draw_bunker(&w, x);
}
draw(&w, 175, 560, 0xff00ff, SCALE, 11, 8, player);
XCopyArea(w.display, w.backbuf, w.xwindow, w.gc, 0, 0, w.w, w.h, 0, 0);
XftDrawDestroy(backdraw);
XFlush(w.display);
break;
Result:
There are 3 small issues:
- The window is too tall, creating wasted space in between and making the game too easy.
- The window title is still "broken."
- The player's position is hardcoded.
Let's fix these.
Move the aliens down a bit for the score display we'll add next time.
void draw_bunker(SpaceWindow *w, int start_x, int start_y) { // 変更
XSetForeground(w->display, w->gc, 0xffff00);
for (int y = 0; y < 18; ++y) {
for (int x = 0; x < 24; ++x) {
if (bunker[y][x] == 0) continue;
int sx = start_x + x * 1;
int sy = start_y + y * 1; // 変更
XFillRectangle(w->display, w->backbuf, w->gc, sx, sy, 1, 1);
}
}
}
...
int main(void) {
SpaceWindow w = {
.isrunning = 1,
.w = 400,
.h = 400, // 変更
.name = "スペースインベーダー"
};
...
Atom wm_delete_window = XInternAtom(w.display, "WM_DELETE_WINDOW", False);
XSetWMProtocols(w.display, w.xwindow, &wm_delete_window, 1);
// 追加
XStoreName(w.display, w.xwindow, w.name);
Atom net_wm_name = XInternAtom(w.display, "_NET_WM_NAME", False);
XChangeProperty(w.display, w.xwindow, net_wm_name,
XInternAtom(w.display, "UTF8_STRING", False), 8,
PropModeReplace, (unsigned char *)w.name, strlen(w.name));
XClassHint *classHint = XAllocClassHint();
if (classHint) {
classHint->res_name = strdup("spaceinvader");
classHint->res_class = strdup("SpaceInvader");
XSetClassHint(w.display, w.xwindow, classHint);
XFree(classHint);
}
XSetWindowBackground(w.display, w.xwindow, BACKGROUND_D);
XSelectInput(w.display, w.xwindow, ExposureMask);
...
XSetForeground(w.display, w.gc, BACKGROUND_D);
XFillRectangle(w.display, w.backbuf, w.gc, 0, 0, w.w, w.h);
int start_x = 50;
// 追加
XWindowAttributes attr;
XGetWindowAttributes(w.display, w.xwindow, &attr);
int player_y = attr.height - 60;
int bunker_y = attr.height - 100;
// 変更
for (int x = start_x; x < start_x*7; x+=40) {
draw(&w, x, 50, 0xff0000, SCALE, 11, 9, alien2);
draw(&w, x, 80, 0x00ff00, SCALE, 11, 8, alien1);
draw(&w, x, 110, 0x00ff00, SCALE, 11, 8, alien1);
draw(&w, x, 140, 0x00aaff, SCALE, 11, 8, alien3);
draw(&w, x, 170, 0x00aaff, SCALE, 11, 8, alien3);
draw_bunker(&w, x, bunker_y);
}
draw(&w, 175, player_y, 0xff00ff, SCALE, 11, 8, player);
...
Result:
The player and bunkers moving when resizing the window is intentional.
Now, on to animation. From here, I'll keep it concise.
Alien Movement
To move aliens left and right, shift them pixel by pixel.
First, fixed the bunkers which were off by 1 pixel.
static const int bunker[18][24] = {
{ 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 },
{ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
{ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 },
{ 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0 },
};
Next, create an Aliens struct.
For those not familiar with C, this is like a simplified OOP class.
typedef struct {
int base_x;
int num_per_col;
int offset_x;
int direction;
int move_timer;
const int move_interval;
const int step_size;
} Aliens;
Extract the drawing code into a draw_frame() function.
Change w. to w-> and remove references in draw/cleanup functions.
The window is now a pointer.
void draw_frame(SpaceWindow *w, Aliens *alien) {
if (w->backbuf == None) w->backbuf = w->xwindow;
w->target = w->backbuf;
XftDraw *backdraw = XftDrawCreate(w->display, w->backbuf,
DefaultVisual(w->display, DefaultScreen(w->display)),
DefaultColormap(w->display, DefaultScreen(w->display)));
if (!backdraw) {
cleanup(w);
fprintf(stderr, "err: XftDrawCreate\n");
XFreePixmap(w->display, w->backbuf);
w->backbuf = None;
exit(1);
}
XSetForeground(w->display, w->gc, BACKGROUND_D);
XFillRectangle(w->display, w->backbuf, w->gc, 0, 0, w->w, w->h);
int start_x = 50;
XWindowAttributes attr;
XGetWindowAttributes(w->display, w->xwindow, &attr);
w->w = attr.width;
w->h = attr.height;
int player_y = w->h - 60;
int bunker_y = w->h - 100;
int row_spacing = 36;
for (int col = 0; col < alien->num_per_col; ++col) {
int x = alien->base_x + col * 40 + alien->offset_x;
draw(w, x, 50, 0xff0000, SCALE, 11, 9, alien2);
draw(w, x, 50 + row_spacing * 1, 0x00ff00, SCALE, 11, 8, alien1);
draw(w, x, 50 + row_spacing * 2, 0x00ff00, SCALE, 11, 8, alien1);
draw(w, x, 50 + row_spacing * 3, 0x00aaff, SCALE, 11, 8, alien3);
draw(w, x, 50 + row_spacing * 4, 0x00aaff, SCALE, 11, 8, alien3);
}
int bunker_spacing = 50;
draw_bunker(w, bunker_spacing, bunker_y);
draw_bunker(w, bunker_spacing * 2, bunker_y);
draw_bunker(w, bunker_spacing * 3, bunker_y);
draw_bunker(w, bunker_spacing * 4, bunker_y);
draw_bunker(w, bunker_spacing * 5, bunker_y);
draw_bunker(w, bunker_spacing * 6, bunker_y);
draw(w, 175, player_y, 0xff00ff, SCALE, 11, 8, player);
XCopyArea(w->display, w->backbuf, w->xwindow, w->gc, 0, 0, w->w, w->h, 0, 0);
XftDrawDestroy(backdraw);
XFlush(w->display);
}
The while loop becomes this:
Aliens alien = {
.base_x = 50,
.num_per_col = 7,
.offset_x = 0,
.direction = 1,
.move_timer = 0,
.move_interval = 15,
.step_size = 8
};
while (w.isrunning) {
while (XPending(w.display) > 0) {
XNextEvent(w.display, &w.event);
switch (w.event.type) {
case Expose:
case ConfigureNotify:
XClearWindow(w.display, w.xwindow);
draw_frame(&w, &alien);
break;
case ClientMessage:
if ((Atom)w.event.xclient.data.l[0] == wm_delete_window) {
w.isrunning = 0;
}
break;
default:
break;
}
}
}
All that's left is to move the aliens left and right.
alien.move_timer++;
if (alien.move_timer >= alien.move_interval) {
alien.move_timer = 0;
int next_offset = alien.offset_x + alien.direction * alien.step_size;
int group_width = alien.num_per_col * 40 + 11 * SCALE;
int left_edge = alien.base_x + next_offset;
int right_edge = left_edge + group_width;
if (right_edge >= w.w - 20 || left_edge <= 40) {
alien.direction = -alien.direction;
} else {
alien.offset_x = next_offset;
}
draw_frame(&w, &alien);
}
usleep(10000);
Include unistd.h because we use usleep().
#include <unistd.h>
This time it's a video.
The lag at the beginning is because I'm programming & recording on 20-year-old hardware.
The actual animation is stable.
The window should be larger and more horizontal.
Adjust aliens and bunkers accordingly.
Also, slow down the alien movement speed.
...
int bunker_spacing = 100; // 変更
draw_bunker(w, bunker_spacing, bunker_y);
draw_bunker(w, bunker_spacing * 2, bunker_y);
draw_bunker(w, bunker_spacing * 3, bunker_y);
...
SpaceWindow w = {
.isrunning = 1,
.w = 800, // 変更
.h = 600, // 変更
.name = "スペースインベーダー",
.screen = 1
};
...
Aliens alien = {
.base_x = 50,
.num_per_col = 14, // 変更
.offset_x = 0,
.direction = 1,
.move_timer = 0,
.move_interval = 15,
.step_size = 8
};
...
usleep(16666); // 変更
Finally, when reaching the right edge, move aliens down.
X coordinate is done, so Y is easy!
Today's last part is just player movement.
You can probably tell what it does just by looking at the code.
typedef struct {
int base_x;
int y_pos; // 追加
int num_per_col;
int offset_x;
int direction;
int move_timer;
const int move_interval;
const int step_size;
} Aliens;
...
for (int col = 0; col < alien->num_per_col; ++col) {
int x = alien->base_x + col * 40 + alien->offset_x;
int y = alien->y_pos;
draw(w, x, y, 0xff0000, SCALE, 11, 9, alien2);
draw(w, x, y + row_spacing * 1, 0x00ff00, SCALE, 11, 8, alien1);
draw(w, x, y + row_spacing * 2, 0x00ff00, SCALE, 11, 8, alien1);
draw(w, x, y + row_spacing * 3, 0x00aaff, SCALE, 11, 8, alien3);
draw(w, x, y + row_spacing * 4, 0x00aaff, SCALE, 11, 8, alien3);
}
...
Aliens alien = {
.base_x = 50,
.y_pos = 50, // 追加
.num_per_col = 14,
.offset_x = 0,
.direction = 1,
.move_timer = 0,
.move_interval = 15,
.step_size = 8
};
...
if (right_edge >= w.w - 20 || left_edge <= 40) {
alien.direction = -alien.direction;
alien.y_pos += 20;
} else {
alien.offset_x = next_offset;
}
Player Movement
Before moving the player, enable KeyPressMask and KeyReleaseMask with XSelectInput().
XSelectInput(w.display, w.xwindow,
ExposureMask
| KeyPressMask // 追加
| KeyReleaseMask // 追加
);
Don't forget the new header.
#include <X11/keysym.h>
Define playerx and playervelocity right before the cleanup() function.
int player_x = 175;
int player_velocity = 5;
Add right after sprite creation:
int key_left_down = 0;
int key_right_down = 0;
player_velocity is the player's speed.
Adjust to feel right.
Lower = slower, higher = faster.
Adjust the player drawing function too.
draw(w, player_x, player_y, 0xff00ff, SCALE, 11, 8, player);
Add this function:
void handle_key_press(SpaceWindow *w) {
if (w->event.type != KeyPress && w->event.type != KeyRelease) return;
KeySym keysym = XLookupKeysym(&w->event.xkey, 0);
int is_press = (w->event.type == KeyPress);
if (keysym == XK_Left) key_left_down = is_press;
else if (keysym == XK_Right) key_right_down = is_press;
if (player_x >= w->w - 80) player_x = w->w - 80;
else if (player_x <= 40) player_x = 40;
}
This just grabs key presses.
Right after that:
void update_player(SpaceWindow *w) {
int move = 0;
if (key_left_down) move -= player_velocity;
if (key_right_down) move += player_velocity;
player_x += move;
if (player_x < 40) player_x = 40;
if (player_x > w->w - 11*SCALE - 40) player_x = w->w - 11*SCALE - 40;
}
Right after the Expose and ConfigureNotify cases:
case KeyPress:
case KeyRelease:
handle_key_press(&w);
break;
Move the draw_frame() function outside the alien logic.
Finally, I like making Q key exit the game.
if (keysym == XK_q) w->isrunning = 0;
That's it for today.
The game is already running.
No OpenGL, Vulkan, or DirectX.
No need for npm install dumping gigabytes of garbage.
Just Xlib and us!
That's all