⛩️

HandyGraphicsでキャラ操作

2022/06/23に公開

やってみよう

HandyGraphicsでキャラクターを操作するシステムを考えてみましょう。
今回はWASDで上下左右に移動する機能を実装してみようと思います。

まずは、基本の型を用意する。

#include <handy.h>
#include <stdio.h>

#define WINDOW_W (600)
#define WINDOW_H (600)

int main(void) {
  HgOpen(WINDOW_W, WINDOW_H);

  HgClose();
  return 0;
}

イベントの処理

イベントと聞くと、催し事みたいなものを想像しますよね。しかしプログラミングの世界においてイベントとという言葉はそのような楽しそうな意味はありません。イベントという言葉は「事象」という意味があります。イベント・ホライゾンという言葉を知っている人もいるのではないでしょうか。あれって日本語だと「事象の地平線」って言いますよね。そのイベントです。つまりイベントとは単なる事象です。
単なる事象と言われてもピンとこない人が大半かもしれません。そのような人々をさらに混乱させるために言うと、イベントは単なるデータです。また、HandyGraphicsの公式ガイドでは以下のように説明されています。

ウィンドウ上をマウスでクリックしたり、キーボードから何かを入力した場合にその情報を取得するこ とができます。このようにプログラム外部からやってくる情報をイベントと呼びます。

つまり、情報のことを「イベント」と呼んでいるだけなのです。そして、この情報はデータなのです。データであるからにはプログラムで処理できるんです。さあ、処理したくなってきたでしょう??

冗談はさておき、実際にイベントを処理してみましょう。イベントを処理するには、まずイベントを取得しなければなりません。しかし、多くの方は、イベントを取得することは初めての体験ではないのです。

HgGetChar();

これに見覚えはないでしょうか?(え、ないって?)
そうです。プログラムの最後の方にいつも登場するあいつです。今まではこいつを「プログラムが勝手に終了しないようにするストッパー」として使っていました。しかし、こいつの本来の役割は、「キー入力を得ること」なのです。だって関数名を見てください。「Charを得る」ですよ!!
もっと詳しく言うと、この関数は、
「キー入力があるまで待ち、キーが入力されたらキーに対応するcharを返す」
という動作をします。ということは以下のようなプログラムを書くことだってできるのです。

//(省略)

char c = ' ';
for (;;) {
  HgClear();
  HgText(100, 100, "%c", c);
  c = HgGetChar();
}

//(省略)

このプログラムがどのような動作をするか、予想がつくでしょうか。
結論から言うと、「キーを押すと、押したキーの文字が描画される」という動作をします。
ここまででイベントの取得に関する基本はおしまいです。そうです。これだけなんです。

さてここまでの話からイベントの取得方法をまとめると、

  • 取得したイベントの情報を保存するための変数を用意する
  • イベントを取得する関数を呼ぶ
  • 関数の戻り値を変数に代入する

となります。しかし注意が必要な点もあります。うまい話ばかりじゃないよ!
注意が必要なのは

  • イベント情報のデータの型
    • さっきはcharだったが、いつもそうとは限らない
  • イベント取得の関数が、イベントが来るまで待ってしまう
    • プログラムを中断させたくないときは使えない

という点です。しかしご安心を、解決策はあります。(なきゃこんな記事書いてない)

イベントの型

ここからは構造体の話が出てきます。構造体ってなんやって方は教科書読むなり、調べるなりしてください。
HandyGraphicsではイベント情報は常にhgeventという構造体で扱います。だって、さっきみたいなcharしか扱えなかったら面倒だもんね。hgeventにはいくつかのメンバが用意されています。メンバっていうのは構造体がそれぞれ持っている変数みたいなものです。厳密な説明ではないよ!
じゃあ実際にどんなメンバを持っているのって話ですが、

  • イベントの種類
  • マウスイベントの座標
  • イベントが発生したウィンドウのID
  • キーイベントのchar

などなどです。(もうちょっと他にもあったりする)

このように、イベントの情報を一つの構造体にまとめてしまうことで何かと楽になるんです。
アマゾンでゲーミングチェアを注文したらパーツが別々に届いた!なんて嫌ですよね。段ボールにまとめて入って届くのが普通だと思います。構造体を使うのも、受け渡しが楽になるからなんですね。

ここまで、ダラダラと話してきましたが実際にイベントの情報を受け取ってみましょう。やることは今までと似ています。次のプログラムを見て、今までと何が違うのか探してみてください。(サイゼの間違い探しよりかは簡単なはず)

// 今までの書き方
char c;
c = HgGetChar();

// 新しい書き方
hgevent *e;
e = HgEvent();

まんま見た通りです。

  • charhgeventに変わりました
  • HgGetChar()HgEvent()に変わりました

これだけです。と言いたいところですが厄介な点もあります。
まず、*eです。こいつです。こいつなんやねんって思ってるんじゃないでしょうか。
*がつくとその変数はポインタ型を格納するようになります。ポインタの話は、本筋から大きく外れちゃいそうなのでここでは省きます。気になる人は教科書「やさしいC言語」かネットで調べてみてください。
次に、HgSetEventMask(HG_KEY_EVENT_MASK);というコードが必要になります。
これは受け取るイベントの種類を指定します。これを書かないとキー入力を受け付けません。
よって、イベントの構造体を受け取る書き方は次のようになります。

HgSetEventMask(HG_KEY_EVENT_MASK);
hgevent *e;
e = HgEvent();

イベントの待機

今まで使ってきた関数、例えばHgGetChar()はイベントが発生するまで次の処理が行われません。しかし、更新し続けるアニメーションを実装したい場合とっても不便です。次のようなプログラムがあったとしましょう

int x = 0; y = 100;
HgSetEventMask(HG_KEY_EVENT_MASK);
hgevent *e;
for(;;) {
  HgClear();
  HgCircle(x, y, 25);
  e = HgEvent(); // <---ここで止まっちゃう!!
  x++;
}

一度HgEvent()で止まったプログラムは次の入力が発生するまでプログラムを止めてしまいます。これでは滑らかなアニメーションとか到底無理に思えますよね。しかし、こんなときに使える方法があるんです。
以下のソースコードを見て、何が変わったか探してみてください。

int x = 0; y = 100;
HgSetEventMask(HG_KEY_EVENT_MASK);
hgevent *e;
for(;;) {
  HgClear();
  HgCircle(x, y, 25);
  e = HgEventNonBlocking();
  x++;
}

見つけられましたか?

e = HgEventNonBlocking();

この部分が変わっています。この関数を使うと処理を止めずにイベントを取得することができます。
しかし、問題は「何もイベントが発生しなかった場合」です。何もイベントが発生しなかったとき変数eはどうなるんでしょうか。イベントが発生すれば変数eにはイベントのデータに対するポインタが代入されます。しかし、イベントが発生していないときはどうすればいいでしょうか。先ほどまでのケースでは「ただイベントを待つ」ということをしました。ですが、今回はそうはいきません。待つわけにはいかないのです。
このようなときプログラムはある解決策を取ります。それは「何もないことを表すポインタ」を代入するのです。一般的に「何もないことを表すポインタ」は「NULL(ヌル)」と呼ばれます。この言葉聞いたことがあるんじゃないでしょうか?

イベントが発生しなかったとき、変数eにはNULLが代入されます。しかし、NULLが入っているときはイベントが発生しなかったときですから、処理は行ないたくないのです。これはどうやって実装すればいいでしょうか。
実はそこまで難しい話でもありません。以下のように書けばいいのです。

HgSetEventMask(HG_KEY_EVENT_MASK);
hgevent *e;
e = HgEventNonBlocking();
if(e != NULL) { // e が NULL でないなら
  // 何か処理する
}

さて、ここまでの内容をまとめてみましょう。まず、先ほど直面した問題というのが、

  • 詳細なイベント情報を扱いたい
  • イベントの取得でプログラムを止めたくない

でした。そして、解決方法というのが

  • hgeventという構造体を使う
  • HgEvent()の代わりにHgEventNonBlocking()を使う

でした。ここまでついてこれてるでしょうか。

いざ、イベントの処理実装

ここからは、今までの内容を利用してイベントを処理してみたいと思います。

int x = 0, y = 0;
int vx  = 0;
HgSetEventMask(HG_KEY_EVENT_MASK);
hgevent *e;
for(;;) {
  e = HgEventNonBlocking();
  if (e != NULL) {
    // イベントを処理する
  }
  
  x += vx;

  HgClear();
  HgCircle(x, y, 100);
}

これが基本の型になります。次にイベント処理を考えてみましょう。
結構前ですが、イベントには種類があるという話をしました。イベントを処理するにあたって重要なことの一つが、「イベントの種類によって場合分け」することです。キー入力のイベントを処理するときは以下の場合分けが大事になってきます。

  • キーが押されたイベント(HG_KEY_DOWN)
  • キーが離されたイベント(HG_KEY_UP)

プログラムで書くとこうなります。

if (e->type == HG_KEY_DOWN) {
  // キーが押された時の処理
} else if (e->type == HG_KEY_UP) {
  // キーが離された時の処理
}

押されたキーの判定

次に重要なのが「どのキーが押されたか判定する」ことです。押されたキーの判定にはe->chを使います。
プログラムで書くと次のようになります。

if (e->type == HG_KEY_DOWN) {
  if (e-> == 'a') {
    // a が押された時の処理
  }  
}

以上の内容を利用してプログラムを書いてみましょう。

int x = 0, y = 100;
int vx  = 0;
HgSetEventMask(HG_KEY_EVENT_MASK);
hgevent *e;
for(;;) {
  e = HgEventNonBlocking();
  if (e != NULL) {
    // イベントを処理する
    if (e->type == HG_KEY_DOWN) {
      // キーが押された時の処理
      if (e->ch == 'a') {
        // aが押されたら
	vx = 1;
      }
    } else if (e->type == HG_KEY_UP) {
      // キーが離された時の処理
      if (e->ch == 'a') {
        // aが離されたら
	vx = 0;
      }
    }
  }
  
  x += vx;

  HgClear();
  HgCircle(x, y, 100);
}

これはaを押しているあいだ円が動くプログラムです。そして、この仕組みを応用すれば複雑なキー入力に対応することができます。全て解説しているととても長くなってしまうので省きます(眠いし)。また、どうしても条件分岐が増えてプログラムが読みにくくなってしまいます。これの解決策としては

  • イベントの処理専門の関数をつくる
  • switch分を使う

などがあります。実際にこれらを使ってみた例を下に置いておきます。興味のある方は是非ご覧あれ。

control.c
#include <handy.h>
#include <stdio.h>

#define WINDOW_W (600)
#define WINDOW_H (600)

typedef struct {
  int left;
  int right;
  int up;
  int down;
} Contoller;

void handleInput(hgevent *e, Contoller *ctrl) {
  if (e == NULL) {  // eventが発生しなかったら何もしない
    return;
  }
  if (e->type == HG_KEY_DOWN) { // キーが押されたら
    switch (e->ch) {
      case 'w':
        ctrl->up = 1;
        break;
      case 's':
        ctrl->down = 1;
        break;
      case 'a':
        ctrl->left = 1;
        break;
      case 'd':
        ctrl->right = 1;
        break;
    }
  } else if (e->type == HG_KEY_UP) { // キーが離されたら
    switch (e->ch) {
      case 'w':
        ctrl->up = 0;
        break;
      case 's':
        ctrl->down = 0;
        break;
      case 'a':
        ctrl->left = 0;
        break;
      case 'd':
        ctrl->right = 0;
        break;
    }
  }
}

int main(void) {
  int x = WINDOW_W / 2, y = WINDOW_H / 2;

  HgOpen(WINDOW_W, WINDOW_H);
  HgSetEventMask(HG_KEY_EVENT_MASK);
  hgevent *e;
  Contoller ctrl;
  for (;;) {
    HgClear();

    e = HgEventNonBlocking();
    handleInput(e, &ctrl);

    if (ctrl.left) {
      x -= 1;
    }
    if (ctrl.right) {
      x += 1;
    }
    if (ctrl.up) {
      y += 1;
    }
    if (ctrl.down) {
      y -= 1;
    }

    HgSetFont(HG_GB, 36);
    HgText(x, y, "押忍!");
    HgSleep(1.0 / 60);
  }

  HgClose();
  return 0;
}

Discussion