高度なテクニック集

最終課題としてはこれまでに説明したプログラミングの方法の範囲内で作成すればよいが、より高度なプログラムに挑戦したい人のために、幾つか高度な内容を紹介しておく。現時点はすべてが理解できなくてもよい。

乱数

動きがワンパターンにならないように変化をつけるには乱数を使うとよい。乱数を発生させるには次の関数を使う。

int rand(void)
0〜RAND_MAXの範囲の疑似乱数を返す。使用するにはstdlib.hをインクルードすること。

rand関数は呼ぶたびに異なる乱数値を返す。引数はvoid、つまり不要。RAND_MAXは、OS XのCコンパイラでは0x7fffffff(=2,147,483,647、int型で表現できる最大値)と定義されている。疑似乱数とは、計算によって得られる、ほぼ乱数と見なすことのできる数値列である(計算なので純粋な乱数ではないが、普通に使う分には気にしなくてもよい程度にランダムということ)。計算式は一定なので、準備をせずにrand関数を呼ぶと同じ数列が得られてしまう。次のプログラムを複数回実行して確認してみよう。

/*****
    random.c
    乱数を10個発生させてみる
    M.Minakuchi
*****/
#include <stdio.h>
#include <stdlib.h>  // rand関数を使うために必要

int main() {
    int i;
    int random;

    for (i = 0; i < 10; i++) {
        random = rand();  // 乱数を得る
        printf("%d\n", random);  // 乱数を表示する
    }

    return 0;
}

プログラムを実行する度に異なる乱数が得られるようにするために、srand関数を使う。srand関数は乱数の計算に使う値(乱数の種、シードと呼ばれる)を指定する関数である。詳しく説明すると少々複雑なので、実行するタイミングによって異なる乱数を得るには次のようにして使うと覚えておこう。

/*****
    random.c
    乱数を10個発生させてみる(初期化あり)
    M.Minakuchi
*****/
#include <stdio.h>
#include <stdlib.h>  // rand関数を使うために必要
#include <time.h>  // time関数を使うために必要

int main() {
    int i;
    int random;
    
    srand(time(NULL));  // 乱数を現在時刻で初期化する

    for (i = 0; i < 10; i++) {
        random = rand();  // 乱数を得る
        printf("%d\n", random);  // 乱数を表示する
    }

    return 0;
}

気になる人向けの説明:time関数はGMT(グリニッジ標準時)の1970年1月1日0時0分0秒からの経過時間を秒単位で返す関数。引数には経過時間値を格納する変数のポインタを渡すが、NULLとしておけば無視される。実行するタイミングが異なる時刻であれば、time関数は異なる値を返すので、それを種にして計算する乱数は異なる値となる。ただし、上のプログラム例を何度か連続して実行してみれば分かるように、1秒くらいの差であれば最初の値が似通っているように見えるだろう。これはrand関数の計算方法が乱数としては不完全であるためである。なお、実用上は次の説明のようにして使えば問題はないし、適当にrand関数を空打ちして偏らないように工夫するのがよいだろう。

rand関数が返す値の範囲は幅広すぎて使いにくい。そこで、使いたい数字の範囲に変換する方法を考える。例えば、サイコロとして1〜6の乱数が欲しい場合、次のように考えるとよい。1.の、6で割った余りを使うのがポイント。

  1. 得られた乱数値を6で割った余りを計算する(0〜5の整数値となる)
  2. 1.で得られた値に1を足す(1〜6の整数値となる)

練習:上記のrandom.cを、サイコロを振った目(1〜6)を10回繰り返して表示するように修正せよ。

練習:さらに、6面体のサイコロではなく20面体のサイコロ(1〜20の目がある)を振った目を10回繰り返して表示するように修正せよ。

練習:さらに、20面体のサイコロを2個振った合計の目を10回繰り返して表示するように修正せよ。
ヒント:2つのサイコロがあるとして、それぞれに対して乱数を得る。

押したキーを区別する(HgGetChar関数)

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

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

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

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

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

例えば次のプログラムを実行すれば、押したキーの値がターミナルに表示される。プログラム中、' 'はスペース1文字を'(シングルクォーテーションマーク)で囲んでいる。'で1文字を囲むと、その文字コードの値を意味する。"(ダブルクォーテーションマーク)で囲む文字列とは異なることに注意。

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

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

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

    HgClose();
    return 0;
}

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

得られたキーの値を文字の値と比較することで処理を振り分けることができる。例えば次のプログラムは押したキーに応じて円、四角、×印を描き、スペースが押されたら終了する。

/*****
    keyFigure.c
    押したキーに応じて図形を描く
    M.Minakuchi
*****/
#include <stdio.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;  // スペースが押されたらループを抜ける
    }

    HgClose();
    return 0;
}

なお、ある変数と複数の値を比較したい場合はswitch-case文を使うのが便利である。

練習:このプログラムkeyFigure.cを、switch-case文を使って書き直してみよう。

マウスクリックを検出する(HgEvent関数)

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

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

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

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

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

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

    HgClose();
    return 0;
}

hgevent型はHandyGraphicで定義された変数型なので、他のCプログラムで一般的に使えるものではないことに注意。*や->はポインタや構造体の記法である。

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

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

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

    HgOpen(400, 400);
    HgSetEventMask(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の円を描く
    }

    HgClose();
    return 0;
}

練習1. ボタン

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

/*****
    mouse.c
    マウスがクリックされた場所に円を描く
    M.Minakuchi
*****/
#include <stdio.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);
    HgSetEventMask(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の円を描く
    }

    HgClose();
    return 0;
}

ヒント:長方形内となる範囲を考える。「条件分岐と繰り返し」の回の課題1.「領域判定」と同じ。

練習2. 折れ線

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

実行例はこちら

ヒント:前回クリックした位置を覚えておく変数を追加し、クリックするたびに値を更新する。

練習3. ○×

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

実行例はこちら

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

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

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

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

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

実行を停止させずに入力を受け付ける(HgEventNonBlocking関数)

HandyGraphicのHgEvent関数やHgGetChar関数はプログラム自体が入力を待って止まってしまうので、アクションゲームやシューティングゲームのような常に動き続けながら入力を受け付けるようなプログラムには使えない。プログラムを停止させずに、キーやマウスが押されたか検出するには、HgEventNonBlocking関数を使う(HandyGraphic説明書8.4節)。使い方はHgEvent関数と同じだが、HgSetEventMask関数で設定した種類のイベントが発生していない場合(ボタンが押されていない等)、NULLが返される。

次のプログラムは、円が動いている間でもマウスボタンを押すと座標が表示される。HgEvent関数の代わりにHgEventNonBlocking関数になっている点、変数eventの値がNULLの場合はマウスクリックされていないので処理を飛ばさなければならない点に注意。

/*****
      mouseNonBlocking.c
      マウスがクリックされた座標を表示する
      M.Minakuchi
*****/
#include 
#include 

#define WINDOWSIZE 400
#define RADIUS 50

int main() {
  hgevent *event;  // HandyGraphicのイベントを扱うための変数
  int x, y;  // クリックされた座標
  int cx = 0;  // 円の中心のx座標
  int vx = 5;  // 円の速度

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

  for (;;) {
    HgClear();
    HgCircle(cx, WINDOWSIZE / 2, RADIUS);
    event = HgEventNonBlocking();  // マウスクリックを検出する
    if (event != NULL) {  // マウスがクリックされた
        x = event->x;  // クリックされたx座標を取り出す
        y = event->y;  // クリックされたy座標を取り出す
        printf("pressed (%d, %d)\n", x, y);
    }
    cx += vx;
    if (cx <= 0 || WINDOWSIZE <= cx)  vx *= -1;
    HgSleep(0.05);
  }

  HgClose();
  return 0;
}

練習:上のプログラムを、マウスがで円の中をクリックしたら円を赤く塗って表示するように修正してみよう。
ヒント:円の中=円の中心とマウスカーソルの座標の距離が半径以下。平方根を計算しなくても、大小関係を比較すれば良いので両辺を2乗して比較すればよい。円の中か外かを表す変数を使うとプログラムがすっきり書ける(このような状態を表す変数をフラグと呼ぶ)。

実行例はこちら

なめらかにアニメーションさせる(ダブルバッファリング)

アニメーションの回で説明したようなプログラムだと表示がちらつく。円を一つ動かす程度ならさほどちらつかないが、大量に絵を描いて動かすとかなりちらつく。これは、絵を描く処理や、ウィンドウを消す処理に時間がかかるため、その過程が人間の目に見えてしまうためである。

アニメーションの原理で説明したように、表示の切り替えはできるだけ高速に行うことが望ましい。そこで、ダブルバッファリングと呼ばれる手法がコンピュータグラフィクスでは使われている。考え方としては、表示している画面とは別の裏画面を用意しておいて、次の絵を裏画面に描き、描き終わったら裏画面を表示している画面と置き換える。このようにすれば描画の過程は人間には見えない。HandyGraphicではこの裏画面をレイヤーという機能で実現している。

HandyGraphicでダブルバッファリングを使用するプログラム例を次に示す。

/*****
    boundDB.c
    横に跳ね返る円、ダブルバッファリング版
    M.Minakuchi
*****/
#include <stdio.h>
#include <handy.h>

#define WINDOWSIZE 400
#define RADIUS 50

int main() {
        int cx = 0;  // 円の中心座標
        int vx = 5;  // x方向の速度
        doubleLayer layers;  // ダブルバッファ用のデータ
        int windowID;  // ウィンドウの番号
        int layerID;  // レイヤーの番号

        windowID = HgOpen(WINDOWSIZE, WINDOWSIZE);
        layers = HgWAddDoubleLayer(windowID);  // ダブルバッファを作る

        for (;;) {
                layerID = HgLSwitch(&layers);  // 表示レイヤを入れ替える
                HgLClear(layerID);  // 描画用レイヤを消去する
                HgWCircle(layerID, cx, WINDOWSIZE / 2, RADIUS);  // 描画用レイヤに円を描く
                cx += vx;
                if (cx <= 0 || WINDOWSIZE <= cx) {
                        vx *= -1;
                }
                HgSleep(0.05);
        }

        // このプログラムは無限ループなので以下は不要だが念のため書いておく
        HgClose();
        return 0;
}

これまでのプログラムと異なる部分は太字の部分。描画する関数は、描くレイヤを指定するためにHgWCircleなど、関数名のHgの次にWが付いていることに注意。

詳しくはHandyGraphicの説明書11章、特に11.4章を参照。