インデックスに戻る ←前のチャプター →次のチャプター

避けゲーを創る on Delphi

2.敵を創る

今回は敵弾を作って表示させていきます。

今回のソースとバイナリです。ダウンロードはこちら。今回も最初は前回のソースに以下の記述に従ってコードを追加して下さい。

敵弾を創る

まず、フルカラー、かつ、32x32の弾丸の絵を用意して、bullet.bmpとしておき、ソースと同じフォルダに置きましょう。
面倒くさければ、上でダウンロード出来るファイルの中のを使って下さい。
下のような画像がビットマップファイルで用意してあります。
敵弾画像サンプル

敵弾は自機と違ってたくさんないといけないので、レコード型(Cでいうところの構造体)の配列(ここではごく普通の静的配列)で宣言します。


const
  MAXBULLET=100;//定数の定義。C言語で言うところのマクロっぽく使う。

  //敵弾のレコード型。座標と縦横それぞれの方向へのスピード、画像、
  //そしてON/OFF状態をメンバーとして持つ。
  TBullet = record

    //敵弾に関しては、integerではなくsingleを用いる。その方が、
    //バラエティに富んだ敵弾を得られるので。
    x,y,xspeed,yspeed:single;
    Texture:TDGTexture;//敵弾画像入れ
    Flag:boolean;//ONならTrue,OFFならFalse
    end;

  //TBulletのMAXBULLET個入っている配列を定義
  Tbulletarray = array[0..MAXBULLET-1] of TBullet;

(Tbulletarray型の配列BulletをTForm1のprivate宣言。ついでに次に発射される弾丸の配列番号を表すnextbulletも。)


    nextbullet:integer;//次に何番目の敵弾を発射するか
    Bullet:TBulletarray;//敵弾データ

まず、MAXBULLETという敵弾の最大同時出現数を入れる定数を定義します。
後々敵弾の最大同時出現数を変えたくなったとき、ここの数字さえ変えればいいので(つまり、他の処理をMAXBULLETを使って書けばその処理はソースコードを変更する必要がないので)楽が出来ます。

(for文の作業用変数tmpintをローカル変数として宣言。以降、この講座でtmpintという変数が突然出現してきたら、予めローカル変数で宣言して下さい。)


var
    tmpint:integer;//for文の作業用変数

(Bulletの持つメンバー変数の一部、つまり敵弾の画像データを、Form1のOnCreateイベントで初期化。
ついでにNextBulletも初期化。
ただし、弾丸の座標、スピード、状態については、敵弾発射処理直前にどうせあとで定義するのでここでは初期化しない。)



//敵弾の状態を初期化。全てOFFに。
  for tmpint:=0 to Maxbullet-1 do
    begin
      bullet[tmpint].Flag:=false;
    end;

//敵弾発射番号の初期化
  nextbullet:=0;

//敵弾画像の初期化。自機と同様
  for tmpint:=0 to Maxbullet-1 do
    begin
      bullet[tmpint].Texture:=TDGTexture.Create(DG, DGFMT_ARGB); //できればαチャンネル付きで
      bullet[tmpint].Texture.BorderColor:=$FFFFFFFF;             //ボーダー色は透明度255
      bullet[tmpint].Texture.LoadFromFile('bullet.bmp');//bullet.bmpを読み込む
      bullet[tmpint].Texture.SetColorKey//白(=#FFFFFF)を透過色とする
        (
            Rect(0,0,bullet[tmpint].Texture.Width,bullet[tmpint].Texture.Height),
            D3DCOLOR_ARGB(0,0,0,0)
          );
    end;


次に、「最も簡単で頭の悪い方法」として、0〜99番目の弾を順番に画面上に撃ち出していきます。

当然、配列の0番目の弾が画面内に存在しているうちにまた0番目が撃ち出されたら最初の0番目の敵弾は消える事になります。

ですが、これは後で解決するということにして、とりあえずここでは無視しておきます。

とにかく、ここでは、1度処理が回ってくるごとに(つまり、1フレームごとに)順番に敵弾を発射させていきます。

それでは、まず、敵弾を発生させる手続きCreateBulletを作りましょう。
CreateBulletは、Bullet配列の中の(NextBulletに入っている値)番目の座標とスピードを決定する処理とします。
よくわからなければ、とにかく下のコードを見て下さい。(それでも解らなければ今までコーディングしたところを見直して下さい。)
後々、makegraphic手続きの中で、このCreateBullet手続きを呼ぶことにします。

(宣言部)


    { Private 宣言に追加する }
        procedure CreateBullet(x,y,xSpeed,ySpeed:single);//敵弾生成手続き

(実装部)


////////////////////
//
//敵弾発生手続き
//
/////////////////////
procedure TForm1.CreateBullet(x, y, xSpeed, ySpeed: single);
var
  tmpint:integer;
begin

      //打たれる敵弾の初期化
      Bullet[NextBullet].Flag:=true;//ON状態にする
      Bullet[NextBullet].x:=x;
      Bullet[NextBullet].y:=y;
      Bullet[NextBullet].xSpeed:=xSpeed;
      Bullet[NextBullet].ySpeed:=ySpeed;

      inc(NextBullet);
      if (NextBullet=MAXBULLET) then	NextBullet:=0;//MAXBULLETになったら0に戻す

end;

それでは、makegraphic手続きの中で、いま作ったCreateGraphic手続きを呼び出しましょう。
ついでに弾丸の移動処理も加えてしまいましょう。


////////////////////
//
//スプライト配置処理
//
/////////////////////
procedure TForm1.makegraphic;
var
  tmpint:integer;//for文の作業用
begin
//画面のクリア、色は黒で
  Scene.Clear(D3DCLEAR_TARGET, $000000, 1.0, 0);

//ここに各スプライトの配置処理を書く

////自機の座標決め


  DDIDEX1.Scan(DI_KEYB);//キー入力チェックをする

  //↑キーが入力されていれば、DDIDEX1.Stick.Y=-1
  //↓キーが入力されていれば、DDIDEX1.Stick.Y=1
  //←キーが入力されていれば、DDIDEX1.Stick.X=-1
  //→キーが入力されていれば、DDIDEX1.Stick.X=1
  //が入力されているので、以下のような条件分けが可能になる。
  //ここらへん、DDIDEXサマサマで、簡潔な記述が可能になってありがたい。

  if (DDIDEX1.Stick.X*DDIDEX1.Stick.Y=0) then//縦横どちらか入力されていない時
    begin
      player.x:=player.x+(DDIDEX1.Stick.X)*3;
      player.y:=player.y+(DDIDEX1.Stick.Y)*3;
    end
  else                 //縦横どちらも入力されている時は移動速度を遅くする。
    begin
      player.x:=player.x+(DDIDEX1.Stick.X)*2;
      player.y:=player.y+(DDIDEX1.Stick.Y)*2;
    end;


//画面の外に行っちゃったら戻す。
  if (player.x>form1.clientWidth) then player.x:=form1.clientWidth
    else if (player.x<0) then player.x:=0;
  if (player.y>form1.clientheight) then player.y:=form1.clientheight
    else if (player.y<0) then player.y:=0;
//

//自機の座標に自機スプライトの中心を置きましょう
//つまり、64の半分だけずらす。
  putsprite(player.Texture,player.x-32,player.y-32);

    CreateBullet(338,32, (random(200) mod 200/100)-1, (random(200) mod 200)/100);
//弾の座標決定
    for tmpint:=0  to MAXBULLET-1 do//全ての敵弾で
      begin
        if Bullet[tmpint].Flag=true then//状態がONなら
          begin
            Bullet[tmpint].x:=Bullet[tmpint].x+Bullet[tmpint].xSpeed;
            Bullet[tmpint].y:=Bullet[tmpint].y+Bullet[tmpint].yspeed;
            if (
                  (bullet[tmpint].x<0) or
                  (bullet[tmpint].y<0) or
                  (bullet[tmpint].x>form1.clientWidth) or
                  (bullet[tmpint].y>form1.clientHeight)
                  ) then bullet[tmpint].Flag:=false;
          end;
      end;

//敵弾を描画
    for tmpint:=0 to  MAXBULLET-1 do
      begin
        if bullet[tmpint].Flag then///状態ONの敵弾だけ
          //自機同様、敵弾も半キャラずらして、キャラの真ん中を座標に置く。
          putsprite(bullet[tmpint].Texture,round(bullet[tmpint].x)-16,round(bullet[tmpint].y-16));
      end;
end;

(上記に関連して、以下Form1のOnCreateイベントに追加)


    randomize;//乱数を初期化。敵弾各個の速度を乱数で決めるため。

上記のそれぞれ赤いところを追加します。


ここまでやってようやく弾丸を実装できたのでコンパイルして実行してみましょう。
ですが、敵弾があっというまに消えていくはずです。
今まで書いてきたソースコードを見てもらえば解りますが、一つの弾丸は、100フレーム後には、新しく作り直されてしまうからです。
これを解決するために、画面の中に表示されている弾丸は生成し直さないようにしていきましょう。

(private宣言でBulletNumを定義。BulletNumにはFlagがTrueになっているBulletの数が入る。)


    bulletnum:integer;//画面に何個敵弾が出ているか

(makegraphic手続きの中のCreateBullet手続きを呼ぶ前に、Bulletnumへ値を入れる。)


//敵弾の数を数える
	BulletNum:=0;
    for tmpint:=0 to MAXBULLET-1 do
    begin
      Bulletnum:=Bulletnum+ord(bullet[tmpint].Flag);//ここのやり方幾通りもある。
    end;                                            //integerキャストでもいいし
                                                    //if文使ってもいいし


    CreateBullet(338,32, (random(200) mod 200/100)-1, (random(200) mod 200)/100);

(敵弾が最大数に達していない時だけ、新しく敵弾を発生させる。)


////////////////////
//
//敵弾発生手続き
//
/////////////////////
procedure TForm1.CreateBullet(x, y, xSpeed, ySpeed: single);
var
  tmpint:integer;
begin

  if bulletnum<Maxbullet then//敵弾が最大数に達していなければ
    begin
      tmpint:=0;
      while (bullet[tmpint].Flag and (tmpint<MaxBullet)) do//
        begin                // (tmpint<MaxBullet))は本来いらないが、
          inc(tmpint);      //将来改造したときの安全弁
        end;                //何かの間違いで開発中bulletnumとFragに整合性がとれないと
                            //試しに実行中にゲームがそこで止まっちゃうし
                            //こういうふうにしとくと、もしtmpintがMaxBullet以上になっても
                            //番号最大の弾が消えるだけですむ

      nextBullet:=tmpint;   //次打たれる敵弾の番号決定
      inc(Bulletnum);//当然一発敵弾数増える

      //打たれる敵弾の初期化
      Bullet[NextBullet].Flag:=true;//ON状態にする
      Bullet[NextBullet].x:=x;
      Bullet[NextBullet].y:=y;
      Bullet[NextBullet].xSpeed:=xSpeed;
      Bullet[NextBullet].ySpeed:=ySpeed;

      inc(NextBullet);
      if (NextBullet=MAXBULLET) then	NextBullet:=0;//MAXBULLETになったら0に戻す
    end;
end;

さて、ここまでやってきましたが、まだ問題があります。
弾丸の速度はランダムで決められますが、これがたまたまやたら遅い弾丸が増えてくると、延々画面から消えない弾丸が目立ってきてしまいます。今度はこれを解決します。

(敵弾レコードのメンバにframeを追加。これは、その敵弾が発生してから何フレームになるかを入れる。つまり、0から始まって、1フレームごとに1ずつ増える。)


  //敵弾のレコード型。座標と縦横それぞれの方向へのスピード、画像、
  //そしてON/OFF状態と、何フレーム目で生まれたかをメンバーとして持つ。
  TBullet = record

    //敵弾に関しては、integerではなくsingleを用いる。その方が、
    //バラエティに富んだ敵弾を得られるので。
    x,y,xspeed,yspeed:single;
    Texture:TDGTexture;//敵弾画像入れ
    Flag:boolean;//ONならTrue,OFFならFalse
    Frame:cardinal;
    end;

(frameの初期化や、frameの値の増加、そして、frameが規定の数字(ここでは1800)以上になったらその弾丸を消去させる処理を追加する。)


////////////////////
//
//敵弾発生手続き
//
/////////////////////
procedure TForm1.CreateBullet(x, y, xSpeed, ySpeed: single);
var
  tmpint:integer;
begin

  if bulletnum<Maxbullet then//敵弾が最大数に達していなければ
    begin
      tmpint:=0;
      while (bullet[tmpint].Flag and (tmpint<MaxBullet)) do//
        begin                // (tmpint<MaxBullet))は本来いらないが、
          inc(tmpint);      //将来改造したときの安全弁
        end;                //何かの間違いで開発中bulletnumとFragに整合性がとれないと
                            //試しに実行中にゲームがそこで止まっちゃうし
                            //こういうふうにしとくと、もしtmpintがMaxBullet以上になっても
                            //番号最大の弾が消えるだけですむ

      nextBullet:=tmpint;   //次打たれる敵弾の番号決定
      inc(Bulletnum);//当然一発敵弾数増える

      //打たれる敵弾の初期化
      Bullet[NextBullet].Flag:=true;//ON状態にする
      Bullet[NextBullet].x:=x;
      Bullet[NextBullet].y:=y;
      Bullet[NextBullet].xSpeed:=xSpeed;
      Bullet[NextBullet].ySpeed:=ySpeed;

      Bullet[NextBullet].Frame:=0;

      inc(NextBullet);
      if (NextBullet=MAXBULLET) then	NextBullet:=0;//MAXBULLETになったら0に戻す
    end;
end;

(1フレームごとにBulletのframeを1ずつ増やし、1800を超えるようだったら画面から弾を消す)


    CreateBullet(338,32, (random(200) mod 200/100)-1, (random(200) mod 200)/100);
//弾の座標決定
    for tmpint:=0  to MAXBULLET-1 do//全ての敵弾で
      begin
        if Bullet[tmpint].Flag=true then//状態がONなら
          begin
            Bullet[tmpint].x:=Bullet[tmpint].x+Bullet[tmpint].xSpeed;
            Bullet[tmpint].y:=Bullet[tmpint].y+Bullet[tmpint].yspeed;
            inc(Bullet[tmpint].Frame);//その敵弾が何フレーム画面上に存在しているか
            if (
                  (bullet[tmpint].x<0) or
                  (bullet[tmpint].y<0) or
                  (bullet[tmpint].x>form1.clientWidth) or
                  (bullet[tmpint].y>form1.clientHeight) or
                  (bullet[tmpint].Frame>1800)//1800フレーム残っているようなら消す
                  ) then bullet[tmpint].Flag:=false;
          end;
      end;

ここまでで、あたり判定以外の部分は完成しました。
コンパイルして正常に動くか実行して試してみましょう。
step_3.png(10986 byte)


インデックスに戻る ←前のチャプター →次のチャプター