イベント処理

このページでは、亀場で起こるいくつかの事象(イベント)をプログラム側で処理する方法について説明する。

1. 亀場でのイベントの取得

あなたのプログラムから、亀場の亀に、進め(FD())、右に回れ(RT())、左に回れ(LT())などの指令を送ると、亀は その指示に従って動いたり、絵を描いたりしてくれる。 また、Q_で始まる関数を使うと、周りの障害物などをチェックすることもできた。 けれども、突然、他の亀の撃った砲弾が当たったりした場合、どうやったらそのことをプログラムで「知る」 ことができるだろうか。

亀場で起こった事象(イベント)を、(もちろん事後的に)知るために、「コールバック関数」という仕掛けが 用意されている。 例えば、掃除ロボットのところで紹介した、壁に当たったら180°転回 して動き続けるロボットは、以下のようにプログラムすることもできる:

#include "turtle.h"

void run_into_wall(unsigned int time)      /* 壁に衝突したときに実行される関数の内容をここから記述 */
{
  int cnt ;
  for (cnt=0 ; cnt<60 ; cnt++) RT(3) ;     /* 60*3=180度転回する */
}                                          /* 関数の記述の終わり */

main()
{
  CON("localhost") ;
  BMODE() ;
  SET_RUN_INTO_WALL_HANDLER(run_into_wall) ;  /* 壁に衝突した際に実行される関数を登録しておく */
  PD() ;
  while (1) {
    FD(2) ;       /* ひたすら前進しつづける */
  }
}

main()関数の中で

SET_RUN_INTO_WALL_HANDLER(run_into_wall) ;

を呼び出しているが、これは「亀が壁に衝突した際に、run_into_wallという名前の関数を呼び出しなさい」という指示になる。 そして、その関数の実体は、main()関数より前に

関数定義のvoidは戻り値のないことを、unsigned intは「負号無し整数」を表す

void run_into_wall(unsigned int time) 
{
  int cnt ;
  for (cnt=0 ; cnt<60 ; cnt++) RT(3) ;
}

と定義されている。関数の中でRT(3)を60回反復しているから、亀は180度転回する。つまり、

壁に衝突する → run_into_wall関数が呼び出される → 180度転回する

という処理の流れが記述されているわけだ。run_into_wall関数の処理が完了すると、プログラムはまた元のFD()の繰り返しに復帰する。

詳しい方への補足:
亀場サーバーからの応答パケットの中に発生したイベントを表すコードが含まれており、 亀場関係の関数呼び出しの度に、そのコードに対応したコールバック関数が呼び出される ようになっています。 サーバー側から割り込みがかかるわけではありません。

RUN_INTO_何々 は、FD( )やBK( )を出した際に、移動経路の途中に障害物 があって、目的の移動先まで進めない場合に発生します。 その意味で、衝突はまだ発生していないので、RAN_INTOではなくて、RUN_INTOにしました。

FOUND_COINイベントは、周囲にコインの無い状態からコインがある状態に遷移した際の「エッジ」 を検出するようになっています。 一面がコインで敷き詰められたような状況では(「エッジ」が生じないので)イベントは発生しません。

このように、事象を処理するための関数は「イベントハンドラー」(event handler)と呼ばれる。 亀場では、以下のようなイベントについて、イベントハンドラーを設定することができる:

イベントハンドラーの設定 動作の説明
SET_HIT_BY_BULLET_HANDLER(関数名) 亀(自分)が被弾した際に呼び出される関数を登録する
SET_RUN_INTO_TURTLE_HANDLER(関数名) fdで前進しようとして、他の亀に衝突する場合に呼び出される関数を登録する
SET_RUN_INTO_DONUT_HANDLER(関数名) fdで前進しようとして、ドーナッツに衝突する際に呼び出される関数を登録
SET_RUN_INTO_STONE_HANDLER(関数名) fdで前進しようとして、石に衝突する際に呼び出される関数を登録
SET_RUN_INTO_WALL_HANDLER(関数名) fdで前進しようとして、壁に衝突する際に呼び出される関数を登録
SET_FOUND_COIN_HANDLER(関数名) 自分の周囲にコインが見つかった際に呼び出される関数を登録
SET_DETECTED_BY_FINDER_HANDLER(関数名) 他の亀のFINDERに自分が検出された際に呼び出される関数を登録
SET_DETECTED_BY_RADAR_HANDLER(関数名) 他の亀のRADARに自分が検出された際に呼び出される関数を登録
SET_GOT_MESSAGE_HANDLER(関数名) 同じチームメンバーからメッセージが発信された際に呼び出される関数を登録

2. イベントハンドラーの定義

イベントハンドラーは、通常の関数を定義するときと全く同様に記述すればよい。 ただし、亀場用のプログラミングにあたっては、簡単な決まり事がある。 つまり関数の定義は、以下のようでなければならない:

void 関数名(unsigned int time)
{

     イベント処理

}

関数の引数 time には、そのイベントが発生した時間が整数でセットされるので、必要ならば 関数の中のプログラムでその値を用いることができる。

ひとつのイベントハンドラーを複数のイベントで共用しても構わない。 例えば、壁に当たっても、亀に衝突しても、いずれも180度転回、ならば

#include "turtle.h"

void turn180(unsigned int time)         
{
  int cnt ;
  for (cnt=0 ; cnt<60 ; cnt++) RT(3) ;     
}

main()
{
  CON("localhost") ;
  BMODE() ;
  SET_RUN_INTO_TURTLE_HANDLER(turn180) ; /* 亀に衝突した際に実行される関数を登録 */
  SET_RUN_INTO_WALL_HANDLER(turn180) ;   /* 壁に衝突した際に実行される関数を登録 */
  PD() ;
  while (1) {
    FD(2) ;  
  }
}

といった具合だ。

イベント検出の一時的な停止

例えば、「亀に衝突したら、コインを落とす」という行動をプログラムしようとして

run_into_turtle(unsigned int time) {
    DROPCOIN() ;
    落とした後の行動・・・
}

というコードを書いたとしよう。一方、コインが見つかったときにはそれを拾うために、

found_coin(unsigned int time) {
    PICKCOIN() ;
}

というイベントハンドラーも登録されていたとする。そうすると、処理の流れは

亀に衝突 → run_into_turtle()関数の呼び出し → DROPCOIN()が実行される 
  → 亀場にコイン → found_coin()関数の呼び出し → PICKCOIN()が実行される

となって、結局、落としたばかりのコインをまたすぐ拾い上げることになってしまう。

指定できるイベントは以下のとおり。 詳しくは、ダウンロードしたファイルturtle.hの最初あたりの EVENT_何々の項目を参照のこと

EVENT_HIT_BY_BULLET
EVENT_RUN_INTO_TURTLE
EVENT_RUN_INTO_DONUT
EVENT_RUN_INTO_STONE
EVENT_RUN_INTO_WALL
EVENT_FOUND_COIN
EVENT_DETECTED_BY_FINDER
EVENT_DETECTED_BY_RADAR
EVENT_GOT_MESSAGE

そんな場合には、一連の動作が完了するまでの間、イベントの検出を禁止すればよい。 具体的には、TTL_DISABLE_EVENT(イベント名) とTTL_ENABLE_EVENT(イベント名) で 当該の箇所を挟みこんでおく:

run_into_turtle(unsigned int time) {
    TTL_DISABLE_EVENT(EVENT_FOUND_COIN) ;
    DROPCOIN() ;
    落とした後の行動・・・
    TTL_ENABLE_EVENT(EVENT_FOUND_COIN) ;
}

3. オブジェクト指向言語でのイベント処理

C言語でイベント発生を処理するには、上でみたように、(ポインターの機能を使った)関数呼び出しを用いるのが一般的だ。

一方、オブジェクト指向言語として知られるJavaで、上記のCのサンプルと同じ動作を行うプログラムを記述すると、以下のようになる。

public class runner extends Turtle {
    runner(String hostname) {
       super(hostname) ;
    }
    public void run_into_turtle(int time) {
       turn180() ;
    }
    public void run_into_wall(int time) {
       turn180() ;
    }
    public void turn180() {
       for (int cnt=0; cnt<60 ; cnt++) rt(3.0) ;
    }

    public static void main (String[] args) {
        runner r = new runner("localhost") ;
        r.bmode() ;
        r.pd() ;
        while(true) {
           r.fd(2.0) ;
        }
    }
} 

Java用のAPI

Turtleクラスには、run_into_turtle()やrun_into_wall()といったメソッドがあらかじめ定義されていて、 イベントが生じる都度、イベントの種類に対応したメソッドが呼び出されるように(Turtleクラスは)設計されている。 上記のプログラムのrunnerクラスは、Turtleクラスを継承(extends)し、これらのメソッドをオーバーライド(重ね書き)している。 すると、イベントが発生する度に、Turtleの「子」クラスであるrunnerクラスの run_into_turtle()やrun_into_wall() が実行されるという仕掛けだ。