現在のブログ
ゲーム開発ブログ (2025年~) Gamedev Blog (2025~)
レガシーブログ
テクノロジーブログ (2018~2024年) リリースノート (2023~2025年) MeatBSD (2024年)
【ゲーム】Xlibのみを使ったC言語でのスペースインベーダー(パート1)
ちょっとした実験をしてみましょう。
スペースインベーダーを作りますが、唯一許される依存関係はXlibだけです。
此の記事は4つのパートに分けました。
パート1では画面にグラフィックを描き、アニメーションさせ、プレイヤーを操作します。
パート2では敵とプレイヤーの弾の発射、フォントの読み込みと表示、オーディオの読み込みと再生を行います。
パート3ではスコア、ライフ、画面を横切るUFOを追加して、完全なゲームを完成させます。
そしてパート4ではタイトル画面、一時停止画面、ちょっとした視覚効果を追加します。
此の記事の目的は、出来るだけ余計な物を排除して、何処どこまで出来るかを示す事です。
パート4終了後に、完全なソースコードはMicrosoft GitHubとCodebergで公開予定です。
其れでは始めましょう!
Makefile
先ずはMakefileを用意します。
最近はdebug、develop、releaseの3つのモードで設定するのが好みです。
debugでは最適化なし+デバッグシンボル付きのビルド、
developでは最適化あり+デバッグシンボル付き、
releaseでは最適化あり+デバッグシンボルなしのビルドを作成します。
Linuxならおそらくgccとgdb、BSD系ならclangとlldbを使うでしょう。
あたしはFreeBSDを使っているので後者を使いますが、GNUツールでも問題なく動作します。
clangをgccに、lldbをgdbに置き換えるだけです。
注意: 下記のコードを其のままコピペしないで下さい。
makeは全ての行の先頭にタブ文字を要求しますが、コピペするとスペース2つになってしまい、makeが使えなくなります。
又、FreeBSD 15.0で必要なライブラリを記載しています。
最初はdebugビルドでコンパイルし、lddで必要なライブラリを確認してから追加する事をおすすめします。
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
ウィンドウのセットアップ
先ずは何もないウィンドウを作成します。
コードの説明は後でします。
#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;
}
試しに実行してみましょう。
$ 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)
あ、クラッシュしました!
わざとミスを入れて、何か失敗した時にどうなるか見せる為です。
...
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);
...
もう一度コンパイルしてみましょう。
よし、空のウィンドウが表示されました!
其れではコードを説明します。
#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;
此処では背景色をコンパイル時に決めるグローバルな値として定義しています。
又、ウィンドウに関する情報をまとめた構造体も用意しました。
必須ではありませんが、コードが大きくなると100倍楽になります。
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);
}
クリーンアップ用の関数です。
3行を1行にまとめる為の便利関数です。
int main(void) {
SpaceWindow w = {
.isrunning = 1,
.w = 600,
.h = 800,
.name = "スペースインベーダー"
};
ウィンドウのデフォルト値を定義しています。毎回全く同じ値です。
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);
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);
画面中央にウィンドウを作成します。
w.xとw.yを調整すれば好きな場所に開けますが、ゲームでは中央が一番です。
XSetWindowBackground()で背景色を黒に設定しています。
後で何度も修正するので、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);
ウィンドウを表示し、グラフィックスコンテキストを作成します。
GCは「garbage collector」ではなく「graphics context」の略です。
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;
}
無限ループを用意します。
ゲームエンジンを使った事がある人なら、此れが更新ループだとわかります。
ずっと更新し続けますが、終了条件で止めます。
最初はExposeイベントが来るのでウィンドウをクリアし、其の後は何もしません。
最後に終了時に安全に後始末します。
現状ではウィンドウを閉じてもクリーンアップされず、クラッシュ扱いになります。
其の為メモリリークが発生します。
今から修正します。
先ず新しいヘッダを追加します。
#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;
}
此れでウィンドウを閉じた時のデバッガのメッセージが
「Process ##### exited with status = 1」から
「Process ##### exited with status = 0」に変わります。
0は正常終了、1はエラー終了を意味します。
次に、X11ウィンドウはデフォルトで激しくちらつきます。
其れを防ぐ為、ピクスマップに描画する様にします。
...
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;
...
其れでは画面にグラフィックを描いてみましょう。
敵の1体を描きます。
先ずは簡単な矩形で説明します。
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;
緑のピクセルが表示されました!
何が起きているか説明します。
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;
最後にフレームを画面に転送します。
今度は敵全体を描いてみましょう。
後で沢山描く為に少し小さめにします。
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);
此れがエイリアンです。
但し此の方法は非常に面倒なので、XFillRectangle()の説明用にやっただけです。
もっと良い方法があります!
#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;
同じ結果になりました。
複数のエイリアンを描くにはどうすれば良いでしょうか?
関数にしてみましょう。
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);
}
}
結果:
残りのエイリアン、プレイヤー、バンカーも描いて仕上げます。
#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;
結果:
3つの小さな問題があります:
- ウィンドウが縦長すぎて、間に無駄なスペースが出来てゲームが簡単すぎる。
- ウィンドウタイトルが「broken」のまま。
- プレイヤーの位置がハードコードされている。
此れを修正します。
次回追加するスコア表示の為にエイリアンを少し下げます。
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);
...
結果:
ウィンドウをリサイズするとプレイヤーやバンカーが動くのは意図的です。
いよいよアニメーションに移ります。此処からは簡潔に進めます。
エイリアンの移動
エイリアンを左右に動かすには、ピクセル単位で移動させます。
先ずバンカーが1ピクセルずれていたので修正しました。
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 },
};
次にAliens構造体を作成します。
Cに慣れていない人向けに言うと、OOPのクラスの簡易版です。
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;
描画コードをdraw_frame()関数に切り出します。
w.をw->に変更し、draw/cleanup関数内の参照を削除して下さい。
ウィンドウがポインタになった為です。
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);
}
whileループは以下になります:
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;
}
}
}
後はエイリアンを左右に動かすだけです。
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);
usleep()を使うのでunistd.hをインクルードして下さい。
#include <unistd.h>
今回は動画です。
冒頭のラグは20年物のハードウェアでプログラミング&録画している為です。
実際のアニメーションは安定しています。
ウィンドウはもっと大きく横長にした方が良いと思います。
エイリアンとバンカーも其れに合わせて調整します。
又エイリアンの移動速度は速すぎるので遅くします。
...
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); // 変更
最後に、右端に到達したらエイリアンを下に移動させます。
X座標はもう出来たので、Y座標は簡単です!
今日は最後にプレイヤーの移動だけ残っています。
コードを見れば何をしているか直ぐに分かると思います。
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;
}
プレイヤーの移動
プレイヤーを動かす前に、XSelectInput()でKeyPressMaskとKeyReleaseMaskを有効にします。
XSelectInput(w.display, w.xwindow,
ExposureMask
| KeyPressMask // 追加
| KeyReleaseMask // 追加
);
新しいヘッダも忘れずに。
#include <X11/keysym.h>
cleanup()関数の直前にplayerxとplayervelocityを定義します。
int player_x = 175;
int player_velocity = 5;
スプライト作成の直後に追加:
int key_left_down = 0;
int key_right_down = 0;
player_velocityはプレイヤーの速度です。
感覚に合わせて調整して下さい。
低くすると遅く、高くすると速くなります。
プレイヤーの描画関数も調整します。
draw(w, player_x, player_y, 0xff00ff, SCALE, 11, 8, player);
此の関数を追加:
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;
}
キー押下を取得するだけです。
其の直後に:
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;
}
ExposeとConfigureNotifyのケースの直後に:
case KeyPress:
case KeyRelease:
handle_key_press(&w);
break;
draw_frame()関数をエイリアンロジックの外に移動します。
最後に、Qキーで終了出来る様にするのが好きです。
if (keysym == XK_q) w->isrunning = 0;
今日は此処までです。
もうすでにゲームが動いています。
OpenGLもVulkanもDirectXもなし。
npm installでギビバイト級のゴミを入れる必要もなし。
只Xlibとあたし達だけです!
以上