次のページ 前のページ 目次へ

13. クリア時間を計測させよう

前回までで一応遊べるようになりましたから、 ここからはより完成度をあげていく作業になります。 まずは手始めにゲームクリアまでの時間計測処理を組み込んでみましょう。

13.1 ザウルス内部での時間の扱い

一口に【時間】と言っても、 内部でどのように扱われているのか判らないと使いようがありません。 APIマニュアルのタイムマネージャの項を見てみると、 ザウルスにはRTCが内蔵されており、 その値をもとにして各地の時刻を計算したり、 人が見ることのできる日付文字列を作れるとのことです。 しかし、タイムマネージャでは年月日時分秒の値しか扱えません。

秒よりも細かい値はないかと調べてみると、 システム内部の時刻としてシステムタイマーカウントという値があり、 GetSystemTickCount()で読み取ることができるそうです。 この値は、システムが起動してから1/10秒単位でカウントアップされるもので、 年月日といった概念のない単なる ULONG型のシリアル値 となっています。

そうすると、日付を扱いたいところではタイムマネージャを、 処理中の時間待ちや実行制御などには、 システムタイマーカウント(SystemTick値)を用いるのが良さそうですね。

13.2 ゲーム時間の計測方法

定期的に発生するイベントとしては SystemTickイベント、FastTickイベントの2つがあります。 前者は特に設定せずとも送られてきますが、 しばらく画面を触ってないと 1/10 間隔だったものが 1/2 間隔にまで間引かれるという性質があります(省電力の為です)。 後者は使用前と後に所定の手続きを必要としますが ほぼ決められた間隔で発生します。

どちらを使うかは対象となる処理によって決まりますが、 プレイ時間の計測であれば1秒単位で十分ですから、 今回は面倒のない SystemTick イベントを利用することにします。 逆にビデオ再生や音声再生のような、時間に依存する処理の場合は、 FastTick イベントを使う方が良い事になりますね。

具体的な実装としては、時間計測して良いか否かのフラグを用意し、 FormEventFilter()FormOtherEvent() の中で、 SystemTickをカウントしていけば良いでしょう。 ただ先にも書いたように、 こうしたイベントは正確な時刻で発生するものではありませんから、 前回の時刻(SystemTick値)を覚えておいて 差分を求めて累積する必要があります。 では、次の項からは次のEXECワーク変数を用いて、 実際の処理を書いていきましょう。
  ULONG    prevtick;		/* 前回のTick値を保持 */
  ULONG    timecount;		/* プレイの累計時間 */
  BOOLEAN  tcntflg;		/* 計測許可フラグ */
  char     clrtime[6];		/* クリアした時間の文字列 */
※ところで、SystemTick は値がフル(=0xffffffff)になったところで、 また 0 に戻ります。プログラムを組む際は、 こうしたケースでも問題が起きない事を確認する事が大切です。:-)

13.3 カウントアップの処理

SystemTickイベントが来るごとにカウントアップする処理を考えてみます。 まず、tcntflg が 真の時のみ計測するように if文で判断します。 イベント発生時刻(Message->SystemTick.Count)は 引数 nowtick で受け取るとして、 前回から経過した時間は nowtick - g_ewk->prevtick になりますから、 これを前回までの累計 g_ewk->timecount に足せば今の累計が求まります (以下のif文の中の1行目が今話した事に相当します)。

2行目の処理は、 3項演算子( ? : )を使ってるのと 1の補数 ( ~ ) を取っているのが 見慣れない表記だと思いますが、これは万が一累計時間が g_ewk->timecount で表現できる最大値を超えた場合のリミット処理です。

3行目では、この次に呼ばれた時のために nowtick の値を g_ewk->prevtick に保存しています。

後は、この関数をSytemTickイベント処理から呼び出してあげれば良いですね。
void	TCNTUP( ULONG nowtick )
{
  ULONG  kekka;

  if ( g_ewk->tcntflg ) {
    kekka = g_ewk->timecount + nowtick - g_ewk->prevtick;
    g_ewk->timecount = (kekka < g_ewk->timecount) ? ~(ULONG)0 : kekka;
    g_ewk->prevtick = nowtick;
  }
}

13.4 計測の開始・継続・停止時の処理

処理内容を文章にすると、大体次のようになるでしょう。

【計測開始時】
  1. 前回時刻 g_ewk->prevtick を現在のシステム時刻に合わせる
  2. 累計時間 g_ewk->timecountを 0 にする
  3. 計測を許可 g_ewk->tcntflg を true にする
【計測停止時】
  1. 計測の停止 g_ewk->tcntflg を false にする
  2. 累計時間からクリア時間の文字列を作成
【計測継続時】
  1. 前回時刻 g_ewk->prevtick を現在のシステム時刻に合わせる
  2. 計測を許可 g_ewk->tcntflg を true にする
これをコード化したのが次の物です。 開始時と継続時と処理が重なるので同じ物を呼ぶようにしましたが、 やってる事は上に書いたものと同じです。 クリア時間文字列の生成は、 別にこのタイミングでやらなくても良いのですが、 面倒なのでここに入れときました。
void  TCLEAR( void )
{
  g_ewk->timecount = 0;
}

void  TSTART( void )
{
  TCLEAR();
  TCONT();
}

void  TCONT( void )
{
  g_ewk->prevtick = GetSystemTickCount();
  g_ewk->tcntflg = true;
}

void	TSTOP( void )
{
  int  wks,wkm;

    /* まずはタイムカウントの停止 */
  g_ewk->tcntflg = false;

    /* 次に、プレイ時間の文字列を作成 */
  wks = g_ewk->timecount / 10;
  if ( wks > 60*99+59 ) {
    wkm = 99;
    wks = 59;
  }else{
    wkm = wks / 60;
    wks = wks % 60;
  }
  sprintf( g_ewk->scinfo.score, "%02d:%02d", wkm, wks );
}

13.5 SystemTickイベント処理の追加

さて、SystemTick イベント処理の追加ですが、 とりあえず今回は FormOtherEvent() 内に入れてみます。 この中から、先程のカウントアップ処理 TCNTUP() を呼び出せばいいわけです。 TCNTUP() に渡す引数(時刻)は、 メッセージに入ってきている イベント発生時刻(Message->CommonMsg.MessageID)を使ってみました。

後は、カウントアップ処理を行った後、 画面上に経過時間の描画など行えばいいでしょう。 ここでは draw_timecount() という関数の中で描画させています。
void GameFormOtherEvent(APLFORMPTR AplForm, MESSAGE* Message)
{
  switch( Message->CommonMsg.MessageID ) {
    case SystemTickID:    /* 定期的(1/10s〜1/2s に発生するタイマイベント */
      TCNTUP( GetSystemTickCount() );
      draw_timecount();
      break;

    case AppliActivateID:  /* アクティブ状態の変化を捉える */
      if ( Message->AppliActivate.IsActivate ) {
        TCONT();
      }else{
        TSTOP();
      }
      break;
  }
}

void	draw_timecount( void )
{
  COLOR      fg,bg;
  POINT	     ps,pe;
  RECTANGLE  maskrect;
  char       wkbuf[20];

  sprintf( wkbuf, "%8d [s]", g_ewk->timecount / 10 );

  SetColorV( &fg, BLACK );
  SetColorV( &bg, YELLOW );
  ps.x = 234*4;
  pe.x = 320*4;
  ps.y = 18*4;
  pe.y = 29*4;
  maskrect.left	= ps.x;
  maskrect.top	= ps.y;
  maskrect.width  = pe.x - ps.x - 1;
  maskrect.height = pe.y - ps.y;
  DrawStr( glbAplForm->FormWnHandle,
    &ps, &fg, &bg, FBSET | QP40_CHR, wkbuf, 0, NULL, DS_NORMAL );
}
なお、上の例では、アクティブ状態が変化した場合に 自動的にタイマカウントを停止するようにしています。 これには、アクティブ状態変化通知イベントの AppliActivateイベント を用いています。

AppliActivateイベントでは、メッセージ中の Message->AppliActivate.IsActivate によってアクティブになったか、 非アクティブになったかが判別できます。 ここで、計測の停止処理 TSTOP() か、 計測の継続処理 TCONT() を呼ぶことで、 常駐ソフト(時計や電卓)に切り替えた場合に計測を停止できるわけです。

このゲームで必要かどうかはともあれ、 アクティブ状態の変化で処理を行う場合の参考にはなる事でしょう。:-)

13.6 フォーム初期表示の為の小細工

例えば、FormSetUp() の中で TSTART() または TCONT() を呼ぶように組んだとします。

でも FormSetUp() が呼ばれるのは、まだフォームが表示される前です。 早いとはいっても、牌が画面に表示されるまでに少し時間がかかりますから 0 秒で始まって欲しいのに、 プレイ開始時点で 2秒とか 3秒になってしまい不都合です。 そこで、初期化フラグ(not_init)を1つ用意し、 フォームの再描画処理の後に1度だけ任意の処理を実行できるようにしてみました。
void GameFormRedisp(APLFORMPTR AplForm, WINDOWHD WnHandle)
{
    /* デフォルトの再描画処理 */
  FormDefRedisp( AplForm, WnHandle );

    /* 今回付け足した細工部分 */
  if ( ! not_init ) {
    if ( cont_flg ) {
      TCONT();
    }else{
      TSTART();
    }
    not_init = true;
  }
}
フォームの再描画処理は、 何も指定しない場合は暗黙で FormDefRedisp() が呼ばれますが、 ユーザーが FormRedisp() 関数を作ってあげることで こうした細かい制御もできるようになります。

今回は関係ありませんが、複数のテキストエディタがある場合には、 現在アクティブなエディタ(カーソル描画が必要なエディタ)を 全コントロールの一番最後に描画しなければならないという決まりがありますが、 それには FormRedisp() 関数をユーザー定義する事で実現するわけです。

13.7 新規・やり直しボタンへの処理追加

当然の事ですが、新規ボタンを押した場合、 やり直しボタンを押した場合にもタイマーカウンタのクリアが必要です。 牌の並べ替え等の処理が終わった後、 画面の再描画をさせた後に TSTART() を呼んでやればいいでしょう。
  lm.CommonMsg.MessageID = RedispID;
  ControlCall( GameAreaUC, AplForm->Control, &lm );

  TSTART();  /* 新たに時間カウント開始 */
これで一通り、時間計測処理の完成です。


次のページ 前のページ 目次へ