0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

0
Last updated at Posted at 2026-01-22

ちょっとした実験をしてみましょう。
スペースインベーダーを作りますが、唯一許される依存関係は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は「(ガベージ・コレクタ)」ではなく「(グラフィックス・コンテキスト)」の略です。

  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>

今回は動画です。
#video/mp4

冒頭のラグは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()関数の直前にplayer_xplayer_velocityを定義します。

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;

#video/mp4

今日は此処までです。
もうすでにゲームが動いています。
OpenGLもVulkanもDirectXもなし。
npm installでギビバイト級のゴミを入れる必要もなし。
只Xlibとあたし達だけです!

以上

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?