2026-01-22 08:55:39
諏訪子
gamedev
c

【ゲーム】Xlibのみを使ったC言語でのスペースインベーダー(パート1)

ちょっとした実験をしてみましょう。
スペースインベーダーを作りますが、唯一許される依存関係はXlibだけです。
此の記事は4つのパートに分けました。
パート1では画面にグラフィックを描き、アニメーションさせ、プレイヤーを操作します。
パート2では敵とプレイヤーの弾の発射、フォントの読み込みと表示、オーディオの読み込みと再生を行います。
パート3ではスコア、ライフ、画面を横切るUFOを追加して、完全なゲームを完成させます。
そしてパート4ではタイトル画面、一時停止画面、ちょっとした視覚効果を追加します。
此の記事の目的は、出来るだけ余計な物を排除して、何処どこまで出来るかを示す事です。

パート4終了後に、完全なソースコードはMicrosoft GitHubとCodebergで公開予定です。

其れでは始めましょう!

Makefile

先ずはMakefileを用意します。
最近はdebug、develop、releaseの3つのモードで設定するのが好みです。
debugでは最適化なし+デバッグシンボル付きのビルド、
developでは最適化あり+デバッグシンボル付き、
releaseでは最適化あり+デバッグシンボルなしのビルドを作成します。

Linuxならおそらくgccgdb、BSD系ならclanglldbを使うでしょう。
あたしは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.xw.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つの小さな問題があります:

  1. ウィンドウが縦長すぎて、間に無駄なスペースが出来てゲームが簡単すぎる。
  2. ウィンドウタイトルが「broken」のまま。
  3. プレイヤーの位置がハードコードされている。

此れを修正します。
次回追加するスコア表示の為にエイリアンを少し下げます。

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()KeyPressMaskKeyReleaseMaskを有効にします。

  XSelectInput(w.display, w.xwindow,
      ExposureMask
    | KeyPressMask // 追加
    | KeyReleaseMask // 追加
  );

新しいヘッダも忘れずに。

#include <X11/keysym.h>

cleanup()関数の直前にplayerxplayervelocityを定義します。

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とあたし達だけです!

以上