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

11. 変数の実装とその扱い〜牌の描画まで

moreソフトを組む場合、マルチタスクという事もあり、 OUT, EXEC, FORM といった各状態に応じて必要最小限のメモリを確保するのが 他のアプリに優しい作り方です。 しかしその反面、 各関数に毎回 AplForm を引数で渡さないといけないなど 面倒な事も増えてしまいます。 ここでは、状態に応じてワークメモリを確保するというのを基本に、 グローバル変数を利用して楽に扱ってみましょう。

11.1 グローバル変数を使うか、各ワークを使うか?

【グローバル変数の利点・欠点】

C言語のグローバル変数として全ての変数を作っておけば、 それはプログラムの全域からアクセスできるものとなり、 OUT だ EXEC だ FORM だといった事は考えなくて済みます。 Cであれば各関数内でローカル変数も使えますから、 Add in BASIC のように変数の使いまわしに頭を悩ませる必要もありません。

特に、今回のように Add in からの移植の場合、 もともと使っている変数がグローバル変数だったわけですから、 このようにした方が変更個所が少なくてすむという利点もあります。

問題点は、先にも書いたように今現在必要のないメモリまで確保してしまい、 裏で動いている他のソフトや、常駐ソフトが満足に動けなくなる事です。 もっとも、OUT状態を持たないアプリであればあまり気にしなくても良いかも知れません。

【OUT,EXEC,FORMワークの利点・欠点】

利点は、その状態に応じて最小のメモリ使用量ですむ事。 使う側としてもその場その場に必要な変数とそうでない変数とを 意識して切り分けられるといったところでしょうか。

欠点は、それぞれの状態開始時に確保・初期化 (初期値が必要なら設定を行う処理)が必要で、 状態終了時にはメモリの解放を行わねばならない事。 例えばボタンが押された場合、 イベント関数に渡されるのは AplForm だけなので、 各ワークの参照が下記のように複雑になってしまう事です。 呼び出すサブルーチンにもこれらのポインタを渡さねばならず、 なかなか不便なものです。 このようにして、各ワークへのポインタを経由してアクセスしますので、 仮に g_ewk というポインタ経由でアクセスするとしたら、 EXECワーク内の変数 B は、g_ewk->B のように記述しなければならないのです。

【方針】

今回の場合は、OUT状態を持たず画面も1枚なので、 全てCのグローバル変数にしても構わない程度の物です。 ただ仮に今後拡張していく場合を考えるとワークの使い方も知っておいて損は無いので、 画面表示用配列などはEXECワークに入れる事にして、 初期値の必要な一部の物と1文字変数、 EXECワークへのポインタ等をグローバル変数にするという方針で作ってみます。

【注意事項】

C と BASIC の変数の扱いには1つ大きな違いがあります。 BASICでは、単純変数 A と、配列変数 A()、文字列変数 A$ が それぞれ別の物として同時に存在しますが、 C ではどれも名前だけで区別される為、 同時に A という名前を持つ物は存在できないのです。 こういう場合は名前を変えるか何か手を打たないといけません。

困った事に、今回のAdd in四川省でも幾つか同名の単純変数、 配列変数が存在していましたので、 利用状態を調べた上で、全体で必要な物だけをグローバル変数、 特定範囲のみで使う物をローカル変数にと分けた上で、 ローカル側の名前を変更する事にしました。 細かい事ですが、なかなか厄介なものです。

11.2 グローバル変数の使い方

Add in 四川省では *PINIT で P$, Q$ に牌データを読み込んでおき、 0 〜 35 の番号で個々の牌画像を扱いました。 moreソフトでも同様にシリアル番号で扱いたいので、 グローバル変数でUSHORT型の配列 pailist[] を用意し、 pailist[0] 〜 pailist[35] として使えるようにします (pailist[]には牌画像のリソースIDを格納します)。

ところで、C言語でグローバル変数を扱う場合、 ヘッダに宣言を書いておく必要があるのですが、 ヘッダファイル(****_C_usr.h)に初期値も含めて記述したのでは 各Cソースファイルに組み込まれてしまい具合が良くありません。 一例としては、次のようにグローバル変数の実体用ファイル(****glb.c)を作り、 実体はそこで定義・初期化しておき、 ヘッダファイルには宣言のみを書いておくのが良いでしょう。
[wst3_C_usr.h]

  /* ヘッダファイルの中は宣言のみ */

  extern  USHORT  pailist[36];

  extern  int	O;
  extern  int	P;
  extern  int	B;
  extern  int	C;
  extern  int	N;
  extern  char	O$;

  extern  int	I,J,K,L;
  extern  int	X,Y,V,W;
  extern  int	Z;

[wst3glb.c]

  /* wst3glb.c には変数の実体と、必要なら初期値を記述 */

  USHORT  pailist[36] = {
    pinzu1ID, pinzu2ID, pinzu3ID, pinzu4ID, pinzu5ID,
    pinzu6ID, pinzu7ID, pinzu8ID, pinzu9ID,
    souzu1ID, souzu2ID, souzu3ID, souzu4ID, souzu5ID,
    souzu6ID, souzu7ID, souzu8ID, souzu9ID,
    manzu1ID, manzu2ID, manzu3ID, manzu4ID, manzu5ID,
    manzu6ID, manzu7ID, manzu8ID, manzu9ID,
    tonID, nanID, syaID, peiID, hakuID, hatuID, chunID, hanaID, hisiID
  };

  int	O = 0;
  int	P = 1;
  int	B = 2;
  int	C = 3;
  int	N = -1;
  char	O$ = '\x0';

  char	J$[] = "順位  名前      記録    記録日時";
  char	K$[] = "                                    ";
  char	F$[] = "wst3hsco.TXT";

  int	I,J,K,L;
  int	X,Y,V,W;
  int	Z;

11.3 EXECワークの使い方

【EXECワークの構造定義】

EXECワーク(OUTワーク、FORMワークも同様)は、 ひとかたまりのメモリブロックとしてシステムから確保して利用します。 当然この中には1つだけではなく色々な値を入れて使うわけですから、 Cの構造体として定義しておくのが普通でしょう。 筆者は下記のように typedef を使って ExecWk型として定義しました。 このように構造体にしておくと、メモリを確保する際に必要な容量として sizeof(ExecWk) と記述でき、 これへのポインタも ExecWk * 型として表現できるようになり便利です。

とりあえず中に入れる変数の方は、 Add in四川省 の変数一覧から一部不要な物を取り払って C の書式に直して入れときましょうか (足りなければ後で追加すればいいだけです)。
[wst3_C_usr.h]

  typedef struct ExecWk_t {
    APLFORMPTR  AplForm;

    int    C[0x8F + 1];
    int    G[10][20];      /* 2次元配列(y,x)に変更 */
    int    LX[0x3 + 1];
    int    LY[0x3 + 1];
    int    VM[0x9 + 1];
    int    VS[0x9 + 1];
    int    VR[0x9 + 1];
    int    HM[0x13 + 1][0x1 + 1];
    int    HS[0x13 + 1][0x1 + 1];
    int    HR[0x13 + 1][0x1 + 1];
    int    VW[0x13 + 1];
    int    HW[0x9 + 1][0x1 + 1];
    int    LC;
    int    T1;
    int    T2;
    int    U1;
    int    U2;
    int    UT;
    int    UB;
    int    XW;
    int    YW;
    int    OK;
    int    SE;
    int    OE;
    int    BF;
    int    MD;
    int    S1X;
    int    S1Y;
    int    S2X;
    int    S2Y;
    int    P1;
    int    HF;
    int    SCH;
  } ExecWk;
【EXECワークの確保・初期化】

さて、ヘッダファイルでの構造定義はこれでいいとして、 実体となる領域を確保・初期化してあげないといけません。 EXECワークの場合は、PIMExecConstruct() で領域の確保、 PIMExecSetup() で値の初期化を行います。 これらの関数は、雛形が ****main.c に書かれていますので、 そこに次のような感じで処理を追加しましょう。 ここで使っている NewHeapPtr()という関数は、 ザウルスのメモリマネージャAPIの1つで、 指定したサイズのメモリブロックをシステムから確保するという物です。 確保に成功するとそこへのポインタ(NULL以外の値)を、 失敗するとNULL値を返す仕様になっていますので、 きちんと判定してあげましょう。

※Cでは実行時チェックは走らない為、判定せずに変なアドレスのままアクセスすれば大抵落ちます。

ここでは g_ewk というグローバル変数に今確保した領域へのポインタを入れ、 プログラム中のどこからでも簡単に参照できるようにしてみました。 これで ExecWk内のAplFormなら、g_ewk->AplForm と書けるようになったわけです。 g_ewk は構造体へのポインタで4byteしか使いませんから、 グローバル変数にしても全然問題ない大きさです。 これも先の pailist[] と同様、 ヘッダとグローバル変数の実体用ファイルに追加しておいてください。
[wst3_C_usr.h]

  extern  ExecWk  *g_ewk;

[wst3glb.c]

  ExecWk *g_ewk;

[wst3main.c]

BOOLEAN
PIMExecConstruct(PIMEXECWORK *PIMExecWork,MESSAGE *Message)
{
    /* EXECワークを確保しようとする、だめなら起動できない */
  if ( (g_ewk = PIMExecWork->PIMUserWork = NewHeapPtr( sizeof(ExecWk) )) == NULL ) {
    SetFailMessage( Message );
    return( false );
  }

    /* 確保できれば良し */
  return( true );
}

BOOLEAN
PIMExecSetup(PIMEXECWORK *PIMExecWork,MESSAGE *Message)
{
    /* ExecWk型の変数領域をヌルクリア */
  memset( g_ewk, 0, sizeof(ExecWk) );

    /* 乱数系列の初期化 */
  RANDOMIZE();

  return true;
}
【EXECワークの使用】

今説明したように、グローバル変数として ExecWk * 型の g_ewk を用意しましたので、 EXECワークを確保してから解放するまでの間は、 次のようにしてアクセスできます。 ちょっと頭に飾りが付いた程度で、さして面倒が無いとわかりますね。 なお当然の事ながら、EXECワークを確保する前や、解放した後にアクセスしてはいけません。
  g_ewk->AplForm
  g_ewk->G[3][5]
  g_ewk->FA
【EXECワークの解放】

自分で確保したメモリは自分で責任を持って破棄することが大切です。 EXECワークの場合、EXEC状態を終了(OUT状態への遷移)する時に、 PIMExecEnd()が呼ばれますので、この中で確保したメモリの解放を行います。 下記のソースでは、 OUT状態へ遷移しようとした際に自分自身を終了する処理も書いてあります。
void
PIMExecEnd(PIMEXECWORK *PIMExecWork,MESSAGE *Message)
{
    /* 確保済みの EXECWK を解放する */
  if ( g_ewk ) {
    DisposeHeapPtr( g_ewk );
    g_ewk = NULL;
  }

    /* アウト状態に移行するときにプロセス終了 */
  Message->CommonMsg.MessageID = AppliProcEndReqID;
  Message->AppliProcEndReq.AplFilter = AF_THIS_APPLI;
}

11.4 牌を画面一杯に描画

随分と前置きが長くなりましたが、ようやく画面用配列へのセットと牌描画処理です。 Add inの時は、一旦配列変数C()に牌番号を書き込んでおき、 シャッフルした上で画面用配列G()にコピーしていましたので、 こちらも同様にやってみましょう。 ところどころ書き換えていますが、おおむね元のBASICソースと同様です。
/*
	配列変数 C に牌画像を示す値(0〜35)を4つずつ入れる処理
*/
void	CINIT( void )
{
  int	j;

  for( j = 0; j < 18; j++ ) {
    g_ewk->C[j] = g_ewk->C[j+18] = g_ewk->C[j+36] = g_ewk->C[j+54] = j;
  }

  for( j = 18; j < 36; j++ ) {
    g_ewk->C[j + 54] = g_ewk->C[j + 72] = g_ewk->C[j + 90] = g_ewk->C[j + 108] = j;
  }
}

/*
	配列変数 C に入った内容をシャッフルする処理
	( 乱数で j,k の位置を求め入れ替える)
*/
void	SHUFFLE( void )
{
  int		i,j,k,l;

  for( i=O; i<=0x120; i++ ) {
    j = rand() % 144;		/* 0 〜 143 の乱数値を得る */
    k = rand() % 144;		/* (同上) */

    l		= g_ewk->C[j];
    g_ewk->C[j]	= g_ewk->C[k];
    g_ewk->C[k]	= l;
  }
}

/*
	配列変数C から画面用の配列変数Gにコピーする処理
*/
void	CGSET( void )
{
  int		x,y,k,i;

  for( y = 0; y < 10; y++ ) {
    for( x = 0; x < 20; x++ ) {
      g_ewk->G[y][x] = -1;
    }
  }

  k = 0;

  for( y = 1; y < 9; y++ ) {
    for ( x = 1; x < 19; x++ ) {
      g_ewk->G[y][x] = g_ewk->C[k++];
    }
  }

  for( y=1; y<=0x8; y++ ) {
    g_ewk->HW[y][0] = 0x3FE;
    g_ewk->HW[y][1] = 0x1FF;
  }

  for( i=1; i<=0x12; i++ ) {
    g_ewk->VW[i] = 0x1FE;
  }

  g_ewk->HW[0][0]	= 0;
  g_ewk->HW[0][1]	= 0;
  g_ewk->HW[9][0]	= 0;
  g_ewk->HW[9][1]	= 0;
  g_ewk->VW[0]	= 0;
  g_ewk->VW[0x13]	= 0;
}
ちょっと長かったですが、 要は CINIT() を呼び出して 配列変数 C[] に牌のシリアル番号を4つずつ入れ、 それを SHUFFLE() でランダムに並び替え、 CGSET() で C[] から G[][] にコピーしようというわけです。 後はこれらを、フォームの初期化などのタイミングで実行しておき、 ユーザーコントロールのコールバック関数から、 牌の再描画処理として呼び出す ALLDRAW() 関数の中身を作れば良いでしょう。
[例]  GameFormSetUp() 中に、次のように記述。

	CINIT();
	SHUFFLE();

	g_ewk->SCH	= 0;
	g_ewk->MD	= 0;
	Z		= 144;
	g_ewk->P1	= N;
	g_ewk->HF	= 0;

	CGSET();
実際には、ALLDRAW()は牌全体の描画ということにして、 牌1個用に one_draw() を作ってみます。 この中に選択時の反転処理も組み込んでしまえば、 画面用配列変数 G[][] の値によって、 いつでも選択状態を含めた画面が再現されるようになります。 描画関連を1個所に集められるのでスッキリもしますね。
/*
  全ての牌を G[] に従って描画する処理
  (なお、G[] は G[y][x] と2次元配列に変更した)
*/
void	ALLDRAW( void )
{
  int	I,J;

  /* 牌の描画 */
  for( I=1; I<=9; I++ ) {
    for( J=1; J<=8; J++ ) {
      if ( g_ewk->G[J][I] >= O ) {
        one_draw( I, J );
      }
      if ( g_ewk->G[9-J][19-I] >= O ) {
        one_draw( (19-I), (9-J) );
      }
    }
  }

}
#define	LEFTMGN		(8*4)
#define	TOPMGN		(40*4)
#define	PAI_X_SIZE	(17*4)
#define	PAI_Y_SIZE	(28*4)
#define	PAI_Y_MINI	(23*4)

/*
  牌を1つ描画する処理

  in x,y :	配列変数G[][]の添字としての x,y を渡すと、そこの牌を描画する。
		(つまり、x: 1〜18, y:1〜8 が有効という事)
		その際、bit8 を見て 1 ならば反転(セレクト)状態にする。
*/
int		one_draw( int x, int y )
{
  RESOURCE	myres;
  RECTANGLE	myrect;
  RECTANGLE	mask;
  POINT		Start,End;
  COLOR		fg;


  myrect.left = mask.left = LEFTMGN + (x-1) * PAI_X_SIZE;
  myrect.top  = mask.top  = TOPMGN  + (y-1) * PAI_Y_MINI;

  if ( g_ewk->G[y+1][x] >= 0 ) {
    mask.width  = PAI_X_SIZE - 1;
    mask.height = PAI_Y_MINI - 1;
  }else{
    mask.width  = PAI_X_SIZE - 1;
    mask.height = PAI_Y_SIZE - 1;
  }

  if ( g_ewk->G[y][x] >= 0 ) {
      /* 牌がある場合:牌をリソースより描画する */
    strcpy( myres.ResFile, "WST3IM00" );	/* リソースブロック */
    myres.ResID	= pailist[ g_ewk->G[y][x] & 0xff ];
    DrawImageRes( g_ewk->AplForm->FormWnHandle, &myrect, FRAME_MASK_ON,
       &myres, NORM_SCALE, &mask );

      /* 反転している場合の表示( bit8 をフラグに使用 ) */
    if ( g_ewk->G[y][x] & 0x100 ) {
      Start.x = End.x = mask.left;
      Start.y = End.y = mask.top;
      End.x += PAI_X_SIZE - 1;
      End.y += PAI_Y_MINI - 1;

      SetColorV( &fg, BLACK );

      BoxPaint( g_ewk->AplForm->FormWnHandle, &Start, &End, &fg, &fg, FSET, P_GRAY, NULL );
    }

  }else{
      /* 牌がない場合:背景色で塗りつぶす */
    SetColorRGB( &fg, 0, 102, 51 );
    SetColorInfo( &fg, 0 );

    Start.x	= mask.left;
    Start.y	= mask.top;
    End.x	= mask.left + mask.width;
    End.y	= mask.top + mask.height;

      /* 狭めた矩形領域の塗りつぶし */
    AreaClear( g_ewk->AplForm->FormWnHandle, &Start, &End, &fg, FSET, NULL );
  }

}
さあ、これでとりあえず起動するたびに並びの違う牌が画面一杯に表示されるようになりました。 何でしたら、何かボタンを押したところで再びシャッフルして表示し直すというのはどうでしょう? そんなに難しい物でもないので、色々と書き換えて試してみてください。


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