インタラクティブグラフィクス

今回はこれまでのプログラミングの基礎の復習を兼ねて、HandyGraphicを使ってキー入力やマウスクリックに対応して表示を変化させる方法を紹介しよう。関数の使い方はHandyGraphic独自のものとなるが、基本的な考え方は他のグラフィクスライブラリでもある程度は応用が効く。少々高度な内容も含まれるが、現時点はすべてが理解できなくてもよい。

HgGetChar 関数

これまでのHandyGraphicを使ったプログラムでは、プログラムが終了してしまわないようにHgGetChar関数を使ってきたが、この関数の本来の目的はどのキーが押されたか(正確には押して離されたか)を知るためにある。HandyGraphicユーザズガイド(説明書)の8.1節には次のように説明されている:

int HgGetChar(void)
返値: 0以上: 入力された文字、 -1: 異常

これまでこの関数の返値は無視していたが、この値を調べることでどのキーか分かるようになっている。

押されたキーがアルファベット、数字、記号などの表示可能な文字の場合は、その文字の文字コード(ASCIIコード)が返される。文字コードについては、例えばaならば10進数で97、16進数で0x61となる。ASCIIコード表は例えばこちらを参照。

0x61の0xは16進数であることを指定するための書き方である。

例えば次のプログラムを実行すれば、押したキーの値がターミナルに表示される。

/*****
    keycode.c
    押したキーの値を表示する
    M.Minakuchi
*****/
#include <stdio.h>
#include <stdlib.h>
#include <handy.h>

int main() {
    int key;  // 押したキーの値を覚えておく変数

    HgOpen(400, 400);
    for (;;) {
        key = HgGetChar();
        printf("key = %d\n", key);
        if (key == ' ')  break;  // スペースが押されたらループを抜ける
    }

    exit(0);
}

ダウンロードはこちら keycode.c

特定のキーが押されたかどうかを判定するには、文字コードの値を比較すればよい。文字コードは直接数値をプログラム中に書いてもよいし、C言語では1つの文字を'(シングルクォーテーションマーク)で囲むとその文字コードの値として扱われる。上のプログラムでは' 'と、スペースを'で囲むことで押されたキーがスペースかどうかを判定している。

複数の種類のキーを判定するには、if文を並べて書けばよい。例えば次のプログラムは押したキーに応じて円、四角、×印を描き、スペースが押されたら終了する。

/*****
    keyFigure.c
    押したキーに応じて図形を描く
    M.Minakuchi
*****/
#include <stdio.h>
#include <stdlib.h>
#include <handy.h>

int main() {
    int key;  // 押したキーの値を覚えておく変数

    HgOpen(400, 400);
    for (;;) {
        key = HgGetChar();
        HgClear();
        if (key == 'o') {
            HgCircle(200, 200, 150);
        }
        if (key == 's') {
            HgBox(100, 100, 200, 200);
        }
        if (key == 'x') {
            HgLine(100, 100, 300, 300);
            HgLine(300, 100, 100, 300);
        }
        if (key == ' ')  break;  // スペースが押されたらループを抜ける
    }

    exit(0);
}

このように、ある変数と複数の値を比較したい場合はswitch-case文を使うとよい。

HgEvent 関数

HgEvent関数はマウスクリックとキー入力の両方を扱うことができるが、ここではマウスクリックのみの取得方法を説明する。

次のプログラムは、ウィンドウ内でマウスクリックされたら座標を表示するプログラムである。(無限ループになっているので終了させるにはターミナルでControl+Cを押すこと。ウィンドウが閉じない場合はウィンドウをクリックしてやるとよいようである。)

/*****
    mouse.c
    マウスがクリックされた座標を表示する
    M.Minakuchi
*****/
#include <stdio.h>
#include <stdlib.h>
#include <handy.h>

int main() {
    hgevent *event;  // HandyGraphicのイベントを扱うための変数
    int x, y;  // クリックされた座標

    HgOpen(400, 400);
    HgEventMask(HG_MOUSE_DOWN);  // マウスクリックのみを検出するように設定

    for (;;) {
        event = HgEvent();  // マウスクリックを待つ。クリックされた情報はeventに格納される。
        x = event->x;  // クリックされたx座標を取り出す
        y = event->y;  // クリックされたy座標を取り出す
        printf("clicked (%d, %d)\n", x, y);
    }

    exit(0);
}

ダウンロードはこちらmouse.c

hgevent型はHandyGraphicで定義された変数型なので一般的に使えるものではないことに注意。*や->についてはポインタや構造体の記法であるが、基礎プログラミング演習II〜発展プログラミング演習Iで扱う予定。

このx, y座標を使って、クリックされた場所に円を描くならば次のようになる:

/*****
    mouse.c
    マウスがクリックされた場所に円を描く
    M.Minakuchi
*****/
#include <stdio.h>
#include <stdlib.h>
#include <handy.h>

int main() {
    hgevent *event;  // HandyGraphicのイベントを扱うための変数
    int x, y;  // クリックされた座標

    HgOpen(400, 400);
    HgEventMask(HG_MOUSE_DOWN);  // マウスクリックのみを検出するように設定

    for (;;) {
        event = HgEvent();  // マウスクリックを待つ。クリックされた情報はeventに格納される。
        x = event->x;  // クリックされたx座標を取り出す
        y = event->y;  // クリックされたy座標を取り出す
        printf("clicked (%d, %d)\n", x, y);
        HgCircle(x, y, 20);  // クリックされた位置に半径20の円を描く
    }

    exit(0);
}

マウスクリックの応用

練習1. ボタン

次のプログラムは右上に長方形を追加したものである。この長方形の中でマウスクリックされたらウィンドウを全部消して長方形を描き直し、長方形の外であったら円を描くように修正せよ。長方形の線上はどちらにしてもよい。

/*****
    mouse.c
    マウスがクリックされた場所に円を描く
    M.Minakuchi
*****/
#include <stdio.h>
#include <stdlib.h>
#include <handy.h>

#define BUTTON_X 300
#define BUTTON_Y 350
#define BUTTON_WIDTH 80
#define BUTTON_HEIGHT 30

int main() {
    hgevent *event;  // HandyGraphicのイベントを扱うための変数
    int x, y;  // クリックされた座標

    HgOpen(400, 400);
    HgEventMask(HG_MOUSE_DOWN);  // マウスクリックのみを検出するように設定

    HgBox(BUTTON_X, BUTTON_Y, BUTTON_WIDTH, BUTTON_HEIGHT);

    for (;;) {
        event = HgEvent();  // マウスクリックを待つ。クリックされた情報はeventに格納される。
        x = event->x;  // クリックされたx座標を取り出す
        y = event->y;  // クリックされたy座標を取り出す
        printf("clicked (%d, %d)\n", x, y);
        HgCircle(x, y, 20);  // クリックされた位置に半径20の円を描く
    }

    exit(0);
}

ヒント:長方形内となる範囲を考える。条件の回の資料を参照。

練習2. 折れ線

マウスクリックした位置に円を描く代わりに、直前にクリックした位置と線を結んで描くように修正せよ。(起動直後およびウィンドウ内を消した直後の最初のクリック時には線を引かないようにすることが望ましい)

実行例はこちら lines

ヒント:クリックした位置を覚えておく変数を追加する。

練習3. ○×

○×(3目並べ、tic-tac-toe)を作成せよ。

実行例はこちら oxo

全部を作るのは大変なので、次のように段階を分けて取り組むとよいだろう。

段階1(初級)
300x300の大きさのウィンドウに3x3のマス目になるように線を引き、クリックしたマス目に○が描かれるようにする。
(ヒント:クリックした座標(x, y)からマス目の番号(i, j)を計算し、マス目の番号(i, j)から描くべき○の中心座標を計算する。)

段階2(初級)
クリックしたマス目に、○と×が交互に描かれるようにする。
(ヒント:○か×かどちらの番か覚えておく変数を使う。)

段階3(中級)
クリックしたマス目に既に○か×が描かれていたら何もせず次のクリックを待つようにする。
(ヒント:マス目の状態を覚えておく配列を用意する。)

段階4(上級)
勝敗判定機能を追加する。
(ヒント:縦横斜めに○か×が揃っているか判定する。9回描いてもどちらも揃っていなければ引き分けと判定する。)

提出は必須とはしないが中級レベルまでできることを目指して欲しい。

HandyGraphicのイベント処理の制約

HandyGraphicのHgEvent関数やHgGetChar関数はプログラム自体が入力を待って止まってしまう。このため、アクションゲームやシューティングゲームのような常に動き続けながら入力を受け付けるようなプログラムを作成することはできない。

おまけ:乱数の使い方(7/1追加)

ゲームなどの不確定要素をプログラムで扱うためには、乱数を使うとよい。乱数を得るための基本的なプログラムは次のようになる:

/******
       random.c
       乱数のサンプルプログラム
       M.Minakuchi
******/
#include 
#include 
#include   // time関数を使うために必要

int main()
{
  int random;  // 乱数を格納する変数
  int i;

  srand(time(0));  // 現在時刻の値を使って乱数の初期化をする

  for (i = 0; i < 10; i++) {  // 10回、乱数を生成して表示する
    random = rand();  // 乱数を一つ拾う
    printf("%d\n", random);
  }

  exit(0);
}

srand関数は乱数を初期化する関数で、引数に初期値を与える。time関数は現在時刻の値を返す関数で、srand(time(0));とすることで、実行するごとに異なる乱数が得られるようにしている(試しに、この行をコメントアウトしてどうなるか確認するとよい。何度実行しても同じ乱数が得られてしまう)。

rand関数で得られる値はある計算式で求められ、正確には疑似乱数と呼ばれる。つまり純然たる乱数ではない。この乱数の計算の元となる値が同じだと同じ数列になってしまう。そのため、srand関数で初期値を設定し、さらに偶然の要素として現在時刻を使っている。

rand関数は整数型の乱数を返す関数である。呼び出すごとに新たな乱数が作られる。値の範囲は整数型で表現できる範囲の正の整数であるが、このままだと使いにくいので、通常は得たい乱数の値の範囲を決めて、その値で割った余りを使う。例えば0〜9の範囲の乱数を得たければ、10で割った余りと考えればよいので、次のようにする:

/******
       random.c
       乱数のサンプルプログラム
       M.Minakuchi
******/
#include 
#include 
#include   // time関数を使うために必要

int main()
{
  int random;  // 乱数を格納する変数
  int i;

  srand(time(0));  // 現在時刻の値を使って乱数の初期化をする

  for (i = 0; i < 10; i++) {  // 10回、乱数を生成して表示する
    random = rand() % 10;  // 0〜9の乱数を1つ作る
    printf("%d\n", random);
  }

  exit(0);
}

サイコロの目のように0から始まらない場合は1を足すなどして調整すればよい。

random = rand() % 6 + 1;  // 1〜6の乱数を1つ作る

本日の課題

最終課題の準備をしよう。まずどんなものを作るか構想を練るとよいでしょう。