第9回:背景変更エフェクト(1) ブラックアウト・イン



今まで作成してきたものは、背景を最初の1枚(bg01.bmp)しか使用していませんでした。
でも、シナリオファイルの11ページを見ると、どうやら舞台は上春君の部屋の様子。
このページを表示する時には、背景を上春君の部屋(bg02.bmp)にしなくてはなりませんね。
第9回と第10回で、背景を変更する処理について説明しましょう。

背景の変更は、
  1. 裏画面に背景をロード
  2. 表画面に転送
  3. 表画面を再描画
とすることにより行えます。
例を示すと、以下のようになります。

procedure TMainForm.ChangeBackGround
begin

   BackBmp.LoadFromFile('..\..\data\bg02.bmp');
   PrimImage.Picture.Assign(BackBmp);
   PrimImage.Repaint;

end;

背景を変えたいときにこの手続き呼び出せば、背景をbg02に変更することができます。
しかし、この方法では何の前触れもなく突然背景が変わってしまいます。
意図的にそうすることもありますが、大抵は何らかのエフェクトを
交えながら背景を変えるものではないでしょうか?

第9回では、ブラックアウト・インについて説明します。
前の背景から黒一色へのフェードアウトのことを便宜上ブラックアウトと呼びます。
逆に、黒一色の状態から次の背景へのフェードインをブラックインと呼びます。
ブラックアウトの処理の流れとしては、
  1. 表画面の各ピクセルのRGB値を取得し、半分にする
  2. 表画面を再描画する
ということを8回繰り返します。
8回にする理由は、RGB値の各要素の最大値は255ですから、
8回半分にする(2で割る)ことによって255でも0になるからです。
最終的には表画面の全てのピクセルがRGB=(0,0,0)、すなわち黒一色になっています。

ブラックインの流れは次の通りです。
  1. 裏画面に切り替える背景画像をロードする
  2. 裏画面を表画面に転送する
  3. 表画面の各ピクセルのRGB値を取得し、(8 − 繰り返した回数)回だけ半分にする。
  4. 表画面を再描画する
1.を行った後、2.から4.を8回繰り返します。
ブラックアウトに比べて少々複雑になっていますので、順に説明しましょう。
2.では、切り替える背景CGを読み込んだ裏画面を表画面に転送しています。
ここではまだ再描画が行われていないため、表画面には転送した結果が反映されません。
その状態で3.の処理を行います。
ブラックアウトの時と同様、各RGB値を半分にしますが、1回目のループでは
半分にする処理を7回行い、2回目のループでは6回行う…といったように、
ループの回数によって、半分にする回数を決定します。
半分にする回数は、ループ回数が大きくなるにつれて少なくなるようにします。
この処理で、最初は黒一色に近い表画面が、徐々に裏画面の画像に近づいていきます。
最後に4.で、表画面を再描画して2.と3.の結果を反映させます。

では実際にコードを書いていきましょう。
ブラックアウトなどのエフェクトは、今後他のゲームを作成するときに
使い回すことができるように、基本部分(main.pas)ではなく新しいpascalユニットを
一つ作成して、そちらに処理を記述します。
作成したユニットは、基本部分から呼び出して使用します。

Delphiメニューの[ファイル|新規作成]から「ユニット」を選択してください。
新規ユニットの雛形が作成されます。[ファイル|名前を付けて保存]を選択し、
effect.pasとして保存してください。ユニット名がeffectとなります。

ブラックアウトやブラックインを行う手続きをまとめて、一つのクラスを作成します。
ここではTEffectクラスとしましょう。
メンバ関数は、ブラックアウトを行うBlackOutと、ブラックインを行うBlackInの二つです。
クラスの定義と、メンバ関数をユニットeffectに記述します(リスト22)。

(リスト22)
            
unit effect;

interface
            
            
uses
   Windows, Classes, SysUtils, ExtCtrls, Graphics;

type
   TEffect = class
   public
      procedure BlackOut(img : TImage);
      procedure BlackIn(img : TImage; backbmp : TBitmap);
   end;
            
            
implementation
            
            
{ TEffect }

//背景のブラックアウト
procedure TEffect.BlackOut(img : TImage);
var
   //引数imgの幅と高さ
   img_w,img_h : Integer;
   //ScanLineを受け取るポインタ
   p : PByteArray;
   //ループカウンタ
   s,x,y : Integer;
begin
   //引数imgの幅と高さを取得
   img_w := img.Picture.Bitmap.Width;  --------------------------(30)
   img_h := img.Picture.Bitmap.Height;  ----|

   //8回(255を2進数で表した時に使用するビット数)繰り返す
   for s := 1 to 8 do
   begin
      //画像の縦方向について繰り返す
      for y := 0 to img_h - 1 do
      begin
         //画像の横一列のRGB値を受け取る
         p := img.Picture.Bitmap.ScanLine[y];  ------------------(31)

         //上で取得した配列pの要素について繰り返す
         for x := 0 to img_w*3 - 1 do
            //指定した座標のRGB値を半分
            //(1ビット右シフト)にする
            p^[x] := p^[x] shr 1;  ------------------------------(32)
      end;

      //画面全体の処理が終わったら、再描画する
      img.Repaint;

      //処理が速すぎる為、ウェイトを置く
      Sleep(50);
   end;
end;

//背景のブラックイン
//裏画面(backbmp)から背景を転送した後黒くする
procedure TEffect.BlackIn(img : TImage; backbmp : TBitmap);
var
   //引数imgの幅と高さ
   img_w,img_h : Integer;
   //ScanLineを受け取るポインタ
   p : PByteArray;
   //ループカウンタ
   s,x,y : Integer;
begin
   //引数imgの幅と高さを取得
   img_w := img.Picture.Bitmap.Width;
   img_h := img.Picture.Bitmap.Height;

   //8回繰り返す
   //変数sは7→0に減少していく
   for s := 7 downto 0 do
   begin
      //裏画面から表画面に背景画像を転送する
      img.Picture.Assign(backbmp);

      //画像の縦方向について繰り返す
      for y := 0 to img_h - 1 do
      begin
         //画像の横一列のRGB値を受け取る
         p := img.Picture.Bitmap.ScanLine[y];

         //上で取得した配列pの要素について繰り返す
         for x := 0 to img_w*3 - 1 do
            //指定した座標のRGB値を
            //sビット右シフトする
            p^[x] := p^[x] shr s;  ------------------------------(33)
      end;

      //画面全体の処理が終わったら、再描画する
      img.Repaint;

      //処理が速すぎる為、ウェイトを置く
      Sleep(50);
   end;
end;

            
            
end.
            

手続きBlackOutの処理を解説しましょう。
この手続きは、引数としてTImageオブジェクトimgをとります。
これには呼び出し側の表画面、すなわちPrimImageを与えます。
(30)で変数img_wとimg_hにそれぞれimgの幅と高さを取得して代入し、8回行われるループに入ります。
このループの中では、(31)で画像のy座標についてスキャンラインを取得し、
そのy座標におけるRGB値のバイト配列pを取得しています(図9-1)。
この画像はBackBmpで24ビット画像と宣言されているため、
1ピクセルは24ビット、即ち3バイトとなります。
よって、取得したバイト配列pの添字範囲は0〜img_w*3-1となります。
この配列の中には各ピクセルのRGB値が入っているので、
(32)でそれらの値を半分に(1ビット右シフト)します。
この処理を全てのy座標について繰り返した後、表画面を再描画して1回のループ処理が終わります。

図9-1:表画面のスキャンライン

手続きBlackInでも、処理の流れはBlackOutとほぼ同じです。
引数としてTImageオブジェクトimgとTBitmapオブジェクトbackbmpをとります。
それぞれ、表画面PrimImageと切り替える背景を読み込んだ後のBackBmpを与えます。
8回行われるループでは、ループカウンタsを7から始めて0になるまで繰り返します。
ループ内処理でBlackOutと異なるのは、ループの始めに裏画面を表画面に転送していることと、
(33)でRGB値を半分にする際にループカウンタsが示す値の回数分右シフトしていることです。
1回目のループではs=7ですから7ビット右シフト、
2回目ではs=6から6ビット右シフト…ということになります。

この二つの手続きの実装が終わったところで、
これらの処理を基本部分から呼び出すことにしましょう。
ブラックアウト→インの処理は、シナリオファイルのスクリプトに

changeBG(black,n)

と書かれた箇所を読み込んだ際に行われるように実装することにします。
ここでblackとはブラックアウト→ブラックインを行うことを意味し、
nは変更する背景CGの番号を指定します。
シナリオファイルの11ページには既に"changeBG(black,2)"と記述されているので
こちらは編集しません。

スクリプトを解析しているのは、基本部分の手続きOneStepでした。
この手続きの一部を書き換えます(リスト23)。

(リスト23)
            
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];
            
            
      //スクリプト部分に'changeBG'が含まれていれば背景を変更する
      k := Pos('changeBG',brief);
      if k <> 0 then
      begin
         //エフェクト指定と背景番号を取得して
         //手続きChangeBackGroundを呼び出す
         //'changeBG()'の括弧の中身をtempstr1に格納する  -----------------(34)
         tempstr1 := Copy(brief, k+Length('changeBG')+1, 255);   |
         tempstr1 := Copy(tempstr1, 0, Pos(')',tempstr1)-1);     |
         //tempstr1の','より前をtempstr2に格納する               |
         tempstr2 := Copy(tempstr1,0,Pos(',',tempstr1)-1);       |
         //tempstr1には','より後を格納し直す                     |
         tempstr1 := Copy(tempstr1,Pos(',',tempstr1)+1,255);  ----
         
         ChangeBackGround(tempstr2,StrToInt(tempstr1));  -----------------(35)
      end;
            
            
      //'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''changeBG'のいずれかであれば
      //メッセージウィンドウを描画する
      if Pos('erase',brief)
       + Pos('chara',brief)
       + Pos('changeBG', brief) > 0 then DrawMessageWindow;  -------------(36)
            
            
      //TextPointページのメッセージを表示
      PutText(TextPoint);
      (以下省略)
          

スクリプト部分の中に'changeBG'という文字列が含まれていれば、
(34)で文字列解析を行い、エフェクト指定と変更する背景の番号を取得します。
この二つを引数とした、背景変更処理を呼び出す手続きChangeBackGroundを
(35)で呼び出しています。
背景変更処理が終わった後には再度メッセージウィンドウを描画する必要があるので
(36)でその手続きを呼び出し、描画します。

手続きChangeBackGroundを実装しましょう。
main.pasのtype節にあるTMainFormクラスの宣言内で、手続きの宣言を書き足します。

            
type
  TMainForm = class(TForm)
    (中略)
    procedure ChoiceInit;
    procedure EraseChara;
            
            
    procedure ChangeBackGround(EffectType : string; BGNumber : Integer);
            
            
  public
    { Public 宣言 }
  end;
            

手続きChangeBackGroundの処理を書きます。
main.pasの最後、"end."の前に書けば良いでしょう(リスト24)。

(リスト24)
            
//背景CGを変更する手続き
//BGNumber : 背景CGの番号
//EffectType : 変更エフェクト
procedure TMainForm.ChangeBackGround(EffectType : string; BGNumber : Integer);
var
   //TEffectクラスのインスタンス変数
   ef : TEffect;
begin
   //背景CGの番号を設定する
   CurBack := BGNumber;
   //指定された番号の背景CGをロードする
   BackBmp.LoadFromFile(Format('..\\..\\data\\bg%.2d.bmp',[CurBack]));  ---(36)

   //TEffectクラスの実体を生成する
   ef := TEffect.Create;  -------------------------------------------------(37)
   try
      //EffectTypeによる分岐
      //'black'の場合、ブラックアウト→ブラックイン
      if EffectType = 'black' then
      begin
         ef.BlackOut(PrimImage);  -----------------------------------------(38)
         ef.BlackIn(PrimImage,BackBmp);  ---------|
      end;
   finally
      //オブジェクトを解放する
      ef.Free;
   end;

   //立ち絵は消去されているためフラグを降ろす
   SetChara := False
end;
            

(36)で裏画面BackBmpに引数BGNumberで与えられた背景CGをファイルからロードします。
(37)ではTEffectクラスのインスタンスefの実体を生成しています。
その後(38)でクラスTEffectのメンバ関数であるBlackOutとBlackInの
それぞれ適切な引数を与えて呼び出し、ブラックアウト→ブラックインを実行しています。

クラスTEffectは、まだmain.pasから呼び出せるようになっていないため、
メニューの[ファイル|ユニットを使う]を選択して「effect」を選択してください。
プログラム上ではmain.pasのimplementation節の後にuses節が自動生成され、
effectユニットが加わっています。
これでクラスTEffectのインスタンスを生成できるようになりました。

ここまでできたら、実行してみましょう。
10ページ目のテキストが表示されてからマウスを左クリックすると、
画面がブラックアウトして黒一色になってから、徐々に次の背景が表示されます。

ブラックアウトする前の画面を図9-2に、
手続きBlackOutの1回目のループ終了時と2回目のループ終了時の画面を
それぞれ図9-3と図9-4に示します。
徐々に暗くなっているのがお分かりでしょうか。

図9-2:ブラックアウト処理前 図9-3:ブラックアウト処理中
(ループ1回目)
図9-4:ブラックアウト処理中
(ループ2回目)



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