第7回:イベント処理(2) 選択肢の表示・決定



このシナリオを流して読んでみると、途中で文の繋がりが分からなくなったと思います。

実はこのシナリオ、図7-1に示す流れで進めることを想定して書かれています。
ここで、枠の中の数字はページ番号を表しています。
6ページ目で選択肢による分岐を行い、
「気にせず連れて帰る」を選択した場合は7へ跳んで8で終了し、
「やっぱり初日屋まで送っていく」を選択した場合には9へ跳んで11で終了します。

図7-1:シナリオの流れ

このシステムでは、シナリオの流れで2つのフェーズに分けることにします。
選択肢を表示している選択フェーズと、それ以外の通常フェーズです。

システム上でこれを実装するために、
通常フェーズと選択フェーズを区別する列挙型TPhaseを定義します。
type節の下の方、TMainFormのクラス定義の後に書きます。
TPhase型変数phaseをグローバルで宣言し、
phaseがnormalの時は通常フェーズ、choiceの時は選択フェーズとします。
その他、選択した後に跳ぶページ番号を格納する配列変数SkipToと、
選択している選択肢番号を示す変数ChoiceNumを宣言します。

            
type
  TMainForm = class(TForm)
  (中略)
  public
    { Public 宣言 }
  end;
            
            

  //通常フェーズと選択フェーズ
  TPhase = (normal, choice);
            
            
var
  MainForm: TMainForm;

implementation

const
  (中略)
var
  (中略)
  CurTrack : Integer; //現在演奏中のCDのトラック
            
            
  phase : TPhase; //フェーズ
  SkipTo : array[0..3] of Integer; //選択肢による跳び先
  ChoiceNum : Integer; //選択している選択肢番号
            

初期値をFormCreateで設定します(リスト12)。
配列SkipToには、未定義値として-1を代入します。

(リスト12)
            
procedure TMainForm.FormCreate(Sender: TObject);
            
            
var
   i : Integer;
            
            
begin
   //変数の初期値を代入
   CurBack := 1;
   CurChara := 1;
   SetChara := False;
   TextPoint := 0;
            
            
   phase := normal;
   for i := 0 to 3 do
      SkipTo[i] := -1;
            
            
   //オブジェクトの実体を生成する
   BackBmp := TBitmap.Create;
   (中略)
end;
            

手続きOneStepを書き換えます(リスト13)。

(リスト13)
            
procedure TMainForm.OneStep;
var
   brief,tempstr1,tempstr2 : string;
   j,k : Integer;
begin
   //最後のページまで進んでいなければ
   if TextPoint < AllText.Count div 5 then
   begin
      //ページ番号を1増やす
      Inc(TextPoint);
      //スクリプト部分を取得
      brief := AllText[TextPoint*5-5];

      //TextPointページのメッセージを表示
      PutText(TextPoint);

      //選択肢のページであるか否か
      k := Pos('choice',brief);  ------------------------------------(20)
      if k <> 0 then
      begin
         tempstr1 := Copy(brief,k+Length('choice')+1, 255); ---------(21)
         tempstr1 := Copy(tempstr1, 0, Pos(')',tempstr1)-1);      |
         j := 0;                                                  |
         while Pos(',',tempstr1) <> 0 do                          |
         begin                                                    |
            tempstr2 := Copy(tempstr1, 0, Pos(',',tempstr1)-1);   |
            tempstr1 := Copy(tempstr1, Pos(',',tempstr1)+1, 255); |
            if Length(tempstr2) > 0 then                          |
               SkipTo[j] := StrToInt(tempstr2);                   |
            Inc(j);                                               |
         end;  ----------------------------------------------------
         ChoiceInit;  -----------------------------------------------(22)
      end;
   end;
end;
            

スクリプト部分を変数briefに格納し、
その中にchoiceという文字列が含まれているか否かを(20)で判断します。
含まれている場合、そのページが選択肢のページであるとします。
(21)では、スクリプトの解析を行います。
この場合、スクリプト部分は

            
3 choice(9,7,,)
            

となっており、(21)によって

            
SkipTo[0] := 9
SkipTo[1] := 7
            

と代入されます。
選択肢の跳び先を設定したら、(22)から選択肢部分の初期化を行う
手続きChoiceInitに移ります(リスト14)。
メンバ関数の宣言もお忘れなく。

(リスト14)
            
//選択肢表示の初期化
procedure TMainForm.ChoiceInit;
begin
   //選択フェーズであることを明示
   phase := choice;
   //TextBmpを反転表示させるようにする
   TextBmp.Transparent := False;
   TextBmp.Canvas.FillRect(TextBmp.Canvas.ClipRect);
   PrimImage.Canvas.CopyMode := cmPatInvert;
   //現在選択している選択肢番号
   ChoiceNum := 0;
   //選択肢を反転表示
   PrimImage.Canvas.Draw(TEXT_X,TEXT_Y+TEXT_SEP*ChoiceNum,TextBmp);
end;
            

選択中の選択肢はTextBmpを反転表示して、選択中であると分かるようにします。

PrimImageMouseDownイベントを書き換えて、選択肢のページに進んだら、
クリックしても先に進ませないようにします(リスト15)。

(リスト15)
            
procedure TMainForm.PrimImageMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
            
            
   //押したボタンが左だったら
   if Button = mbLeft then
   begin
      case phase of
         //通常フェーズ
         normal : OneStep;
         //選択フェーズ
         choice :
         begin
            //ここに選択フェーズでのイベント処理を書きます。
         end;
      end;
   end;
            
            
end;
            

この時点で実行すると、6ページ目を表示したところで
メッセージウィンドウ内部が図7-2のようになり、
クリックしても先に進まなくなります。
これが、第一選択肢を選択している状態です。

図7-2:第一選択肢を選択中

次に、マウスカーソルを動かすことによって選択する選択肢を変更する処理を書きます。
マウスカーソルを動かすということは、MouseMoveイベントですね。
PrimImageMouseMoveイベントを用意して、その中に書きます(リスト16)。

(リスト16)
            
procedure TMainForm.PrimImageMouseMove(Sender: TObject; Shift: TShiftState;
  X, Y: Integer);
            
            
var
   place : Integer;
            
            
begin
            
            
   if phase = choice then
   begin
      //マウスカーソル位置によって選択している場所を取得する
      place := ChoiceNum; ------------------------------------(23)
      if (X > TEXT_X) and (X < TEXT_X+TEXT_W) then       |
         if (Y > TEXT_Y+TEXT_SEP*0)                      |
           and (Y < TEXT_Y+TEXT_SEP*0+TEXT_H) then       |
            place := 0                                   |
         else if (Y > TEXT_Y+TEXT_SEP*1)                 |
           and (Y < TEXT_Y+TEXT_SEP*1+TEXT_H) then       |
            place := 1                                   |
         else if (Y > TEXT_Y+TEXT_SEP*2)                 |
           and (Y < TEXT_Y+TEXT_SEP*2+TEXT_H) then       |
            place := 2                                   |
         else if (Y > TEXT_Y+TEXT_SEP*3)                 |
           and (Y < TEXT_Y+TEXT_SEP*3+TEXT_H) then       |
            place := 3;                                  |
                                                         |
      if place > MTo then                                |
         place := ChoiceNum; -----------------------------

      //取得した選択肢が前回取得した選択肢と異なる場合
      if place <> ChoiceNum then
      begin
         //前回の選択肢の反転表示を戻す
         PrimImage.Canvas.Draw(TEXT_X,TEXT_Y+TEXT_SEP*ChoiceNum,TextBmp);
         //今回の選択肢を反転表示する
         PrimImage.Canvas.Draw(TEXT_X,TEXT_Y+TEXT_SEP*place,TextBmp);
         //今回の選択肢を代入する
         ChoiceNum := place;
      end;
   end;
            
            
end;
            

(23)で、マウスカーソルが載っている選択肢が何行目かを取得しています。
それが前回このMouseMoveイベントで調べた選択肢と異なる場合、
前回の選択肢に掛けられていた反転表示を元に戻し、今回の選択肢を反転表示します。
ここで実行すると、選択フェーズにマウスカーソルの動きに合わせて
選択している選択肢に反転表示が掛かるようになります。

あとは選択肢を決定する処理です。
選択肢の上にマウスカーソルを置いた状態でクリックしたら、
その選択肢で決定ということにしましょう。
PrimImageMouseDownイベントに書き足します(リスト17)。

(リスト17)
            
procedure TMainForm.PrimImageMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
   //押したボタンが左だったら
   if Button = mbLeft then
   begin
      case phase of
         //通常フェーズ
         normal : OneStep;
         //選択フェーズ
         choice :
         begin
            
            
            //マウスカーソルが選択肢の上に置かれているなら
            if (X > TEXT_X) and (X < TEXT_X + TEXT_W)
              and (Y > TEXT_Y) and (Y < TEXT_Y + TEXT_SEP*MTo + TEXT_H) then
            begin
               //跳び先を代入
               TextPoint := SkipTo[ChoiceNum] - 1;
               //反転表示を解除
               TextBmp.Transparent := True;
               PrimImage.Canvas.CopyMode := cmSrcCopy;
               //通常フェーズに戻す
               phase := normal;
               //次のメッセージを表示
               OneStep;
            end;
            
            
         end;
      end;
   end;
end;
            

マウスカーソルが選択肢の上にある状態で左クリックされると、
その時選択していた行を元に配列SkipToから跳び先を求め、TextPointに代入されます。
こうして選択は終了し、通常フェーズに戻ります。
実行して、図5の流れの通りに進める事ができるか確認してください。



他にスクリプトで使用する命令文として、
立ちキャラを表示する"chara"と消去する"erase"、シナリオの終了を示す"end"の処理を実装します。
立ちキャラを表示する手続きはDrawCharaで実装済みですので、
立ちキャラを消去する手続きEraseCharaを実装します(リスト18)。

(リスト18)
            
//立ちキャラを消去する
procedure TMainForm.EraseChara;
begin
   //裏画面を転送して、表画面に描かれた画像を消す
   PrimImage.Picture.Assign(BackBmp);
   //フラグを降ろす
   SetChara := False;
end;
            

Phaseに新しく"stop"を作成し、"stop"が読み込まれた時はphaseにstopを代入しましょう。
PrimImageMouseDownで処理が定義されているのは"normal""choice"の2つなので、
phaseがこれ以外の場合は何も処理されません。

            
  //通常フェーズと選択フェーズ、シナリオストップ
  TPhase = (normal, choice, stop);
            

ただ…今回は説明のためにフェーズ"stop"を作りましたが、
実際のゲームではシナリオをストップさせるわけではなく、
他の処理(ムービー再生やエンディングロールなど)に移すことでしょう。

スクリプトの解析は手続きOneStepで行っているので、この手続きに書き足します(リスト19)。
立ちキャラの表示・消去の処理を終えてからメッセージを表示するため、
"erase""chara"の処理は手続きPutTextを呼び出す前に書き、"stop"の処理は後に書きます。

(リスト19)
            
procedure TMainForm.OneStep;
var
   brief,tempstr1,tempstr2 : string;
   j,k : Integer;
begin
   //最後のページまで進んでいなければ
   if TextPoint < AllText.Count div 5 then
   begin
      //ページ番号を1増やす
      Inc(TextPoint);
      //スクリプト部分を取得
      brief := AllText[TextPoint*5-5];
            
            
      //'erase'が含まれていれば、立ちキャラを消去する
      if Pos('erase',brief) > 0 then EraseChara;

      //'chara'が含まれていれば、立ちキャラをロードして描画する
      k := Pos('chara',brief);
      if k <> 0 then
      begin
         tempstr1 := Copy(brief,k+Length('chara')+1, 255);
         tempstr1 := Copy(tempstr1, 0, Pos(')',tempstr1)-1);
         CharaBmp.LoadFromFile(Format('..\\..\\data\\chara%.2d.bmp',
                                      [StrToInt(tempstr1)]));
         DrawChara;
      end;

      //'erase''chara'のいずれかであればメッセージウィンドウを描画する
      if Pos('erase',brief)
       + Pos('chara',brief) > 0 then DrawMessageWindow;
            
            
      //TextPointページのメッセージを表示
      PutText(TextPoint);
            
            
      //'stop'が含まれていれば、そのページでストップする
      if Pos('stop',brief) > 0 then phase := stop;
            
            
      //選択肢のページであるか否か
      (中略)
end;
            

では実行してみましょう。
シナリオファイルには5ページ目にeraseが、8,10,11ページ目にcharaが書かれています。
実際そのページを表示した時に、立ちキャラが消えたり現れたりしましたか?
そして、8ページ終了後には9ページ以降に進まず、そこで止まるようになりましたか?


第8回へ
アドベンチャーゲーム設計論トップへ戻る
開発分室へ戻る
トップページへ戻る