前のチャプターで、自機と敵弾の表示ができるようになりました。そこで、ここのチャプターでは、ゲームではつきもののタイトル画面を作って、キーボードのZキーを押したらゲーム開始、ゲーム中、自機が弾に当たったら、タイトル画面に戻るというシステムを作っていこうと思います。やっとこれでゲームっぽくなりますね。
今回も本チャプターの内容まで取り入れたソースとバイナリを用意しましておきました。 ダウンロードはこちらから。ですが、最初は、前回のソースに、以下の記述に沿ってコードを書き加えて下さい。
思い浮かべてもらえば解りますが、一般的なゲームのタイトルには、大概ゲームのタイトル名と「Push Start Key」とかの指示が書いてあります。
こういったことをタイトル画面に表示する際、テキストを表示させる方法や、画像を作っておいてそれを表示させる方法とかいろいろあります。
テキストを表示させる方法は、Quadruple Dを使って実現できるのですが、若干難しいので、今回は簡単に画像を用意しておいて、自機や敵弾と同じように表示させてみましょう。
(今回タイトル用で用意した絵。フルカラー640x480で作成してtitle.bmpの名前でソースファイルと同じフォルダに置く。)
(タイトル画像を入れる変数を宣言)
titleTexture:TDGTexture;//タイトルの画像をいれます
Form1のOnCreateイベントで、titleTextureをcreate、初期化を行う。
//タイトル画像の初期化
titletexture:=TDGTexture.Create(DG,DGFMT_ARGB);
titletexture.BorderColor:=$00000000; //ボーダー色は透明
titletexture.LoadFromFile('title.bmp');
titletexture.SetColorKey//黒(=#000000)を透過色とする
(
Rect(0,0,titletexture.Width,titletexture.Height),
D3DCOLOR_ARGB(0,0,0,0)
);
(Form1のOnDestroyイベントでfreeするのも忘れずに)
////////////////////
//
//アプリケーションが終了時呼び出される
//
/////////////////////
procedure TForm1.FormDestroy(Sender: TObject);//Form1のOnDestroyイベント
var
tmpint:integer;//for文の作業用
begin
//以下、Freeについては、クラスの知識を習得するまでは、
//createしたものに対して行う必要があるおまじない程度に考えておいて良い。
player.texture.Free;//自機の画像を格納していたメモリ領域を開放
//敵弾の画像を格納していたメモリ領域を開放
for tmpint:=0 to Maxbullet-1 do
begin
bullet[tmpint].Texture.Free;//
end;
titletexture.free;
TimeEndPeriod(1);//TimeBeginPeriod(1)とセットで記述する。
end;
前のチャプターまでに、Form1のOnDestroy自機・敵弾のfreeをまだ書き加えていなかったら、上記のように書き加えておいて下さい。
createしたら、freeしないと、無駄なメモリ領域を保持し続けます。
さて、タイトル画像の変数の準備はできたので、これから、MakeGraphic手続きで書き込んでいくわけです。
しかし、今まではMakeGraphic手続きは自機と敵弾を書く処理、つまりゲーム中の処理のみを行う手続きでした。
そのままだと、混乱しますので、mode変数を作って、タイトル画面なのか、ゲーム中なのか判別させてmakegraphic手続きで条件分岐させましょう。
ついでにタイトル画面でキーボードのZ-キーを押したらゲームが始まるようにします。
(private宣言でgamemode変数を宣言。)
gamemode:integer;//0ならタイトル画面、1ならゲーム画面を入れる
(Form1のOnCreateイベントでgamemodeをタイトル画面に初期化する。)
//ゲームモードをタイトル画面に初期化
gamemode:=0;
(makegraphic手続きの中で条件分岐させてタイトル画面を描画。Zキーでゲーム開始させる。)
////////////////////
//
//スプライト配置処理
//
/////////////////////
procedure TForm1.makegraphic;
var
tmpint:integer;//for文の作業用
begin
application.processmessages;//他の待機中のプログラムの処理を行わせる。
case gamemode of//gamemodeに入っている値でモードを変える
0://タイトル画面
begin
DDIDEX1.Scan(DI_KEYB);//キー入力のチェック
if DDIDEX1.Buttons[DI_B1] then//DDIDEXでは、デフォルトで、Zキーは
//DI_B1に割り振られている
begin
//自機と敵弾の初期化
//自機の初期座標を決める
player.x:=320;
player.y:=240;
//敵弾の状態を初期化。全てOFFに。
for tmpint:=0 to Maxbullet-1 do
begin
bullet[tmpint].Flag:=false;
end;
//敵弾発射番号の初期化
nextbullet:=0;
gamemode:=1;//ゲームモードにする。
exit;
end;
//画面のクリア、色は黒で
Scene.Clear(D3DCLEAR_TARGET, $000000, 1.0, 0);
//タイトル画像を表示
putsprite(titletexture,0,0);
end;
1://ゲーム画面
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);
//敵弾の数を数える
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);
//弾の座標決定
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;
//敵弾を描画
for tmpint:=0 to MAXBULLET-1 do
begin
if bullet[tmpint].Flag then//状態ONの敵弾だけ
//自機同様、敵弾も半キャラ分(16)だけずらす。キャラの真ん中を座標に置く。
putsprite(
bullet[tmpint].Texture,round(bullet[tmpint].x)-16,
round(bullet[tmpint].y-16)
);
end;
end;
end;//case終わり
end;
ちなみに上の
if DDIDEX1.Buttons[DI_B1] then
の所ですが、
if (DDIDEX1.Buttons[DI_B1]=true) then
と書くのと同じことです。
不思議に思うかもしれませんが、まあ、こうも書けるんだよと覚えといて下さい。
ところで、10年弱続いた対戦格闘ゲームブームで有名になった当たり判定という言葉、御存知でしょうか。
これは2つのキャラクターが重なり合っているかを判定するためにキャラクターに持たせるデータです。
シューティングゲームの場合、多くはキャラクターの大きさより小さく設定されることが多いです。
例えば下の絵を見て下さい。
STGの自機や敵機・敵弾の当たり判定を青い部分だけ設定して、ここの部分が重なり合わないと2つのキャラクター当たった、と判定しないわけです。
理由としては、
真四角なキャラクター以外では、表示されていない部分に当たり判定を持ってしまうことと
敵を派手にたくさんの出現させながら当たり判定を小さくすることで、いかにもすごい弾避けプレイをできている、という錯覚をプレイヤーに持たせて爽快感を与えることが挙げられます。
自機の当たり判定の例。当たり判定のある部分が実際に青く表示されるわけではありません。
同様に敵弾の当たり判定
この状態では、画像同士は重なっていますが、当たり判定は重なっていませんので当たったとは判定しません
当たり判定が重なっているので、当たったと判定します。
自機と敵弾の当たり判定がぶつかった場合にゲーム終了としましょう。
まず、自機の当たり判定をどう定義するか考えます。
自機は既に自機の中心が座標上に表示されるようになっています。
これを利用して、既に定義してあるPlayerレコードのメンバー変数hit_x、hit_yだけ上下左右に離れた正方形を当たり判定としましょう。
絵で見るとわかりやすいので、下に載っけときます。
例えば、8x8ドットの画像の場合、中心の座標は(画像の幅/2,画像の高さ/2)で求められます。
ここに、6x6の当たり判定を設定するとしましょう。
この場合、当たり判定の四辺までの距離が、(画像の幅/2,画像の高さ/2)の座標から上下左右に3ずつになるわけです。
以上のことを踏まえて、当たり判定を実装していきましょう。
当たり判定の定義は既に自機・敵機のレコードでそれぞれ定義済みですので、Form1のOnCreateイベントで値を初期化していきます。hit.x、hit.yはそれぞれ中心から、当たり判定を持つ矩形(四角形)範囲の上辺、左辺までの距離にしておきましょう。
//自機のあたり判定の初期化
player.hitx:=2;
player.hity:=2;
//↑この場合、自機画像の中の4x4ピクセルが当たり判定となる。
//同様に敵弾にもあたり判定を与える。
for tmpint:=0 to MaxBullet-1 do
begin
bullet[tmpint].hitx:=2;
bullet[tmpint].hity:=2;
end;
これで、自機・敵機の当たり判定を持つ矩形(四角形)範囲が確定しました。
では、自機と敵機それぞれの当たり判定が重なっているか判定しましょう。
便利なことに、2つの矩形が重なっているか調べるためのIntersectRectという関数がありますので、これを利用した関数collisionを作ってみましょう。
2つの四角形データを入れて重なったてたらTrueを返し、重なっていなかったらFalseを返り値として返すようにするのです。
Delphiでは、矩形(四角形)の位置とサイズを変数として持たせる場合にTRect型変数を使います。
具体的に四角形をデータとして持たせるには、
TRect型の変数:=Rect( 左辺のx座標 , 上辺のy座標 , 右辺のx座標 , 下辺のy座標 );
と入力します。
関数IntersectRectは、第1引数に作業用のTRect変数、第2引数と第3引数に2つの比較したい四角形を変数と持つTRect変数を持ちます。
なお、IntersectRect関数の第1引数のTrect変数には、IntersectRect関数が呼ばれた後、第2引数と第3引数の2つの矩形が重なっている部分の矩形が入力されます。ただし、今回は使いませんので、この第一引数のTRectは適当に用意してほっとくことにします。
(private宣言でCollision関数を宣言)
//あたり判定を行うために、2つの四角形が 重なっているかチェックする
//重なっていればtrueを返す
function Collision(RectA,RectB:TRect):boolean;
(実装部)
////////////////////
//
//あたり判定チェック
//
/////////////////////
function TForm1.Collision(RectA, RectB: TRect): boolean;
var
tmprect:TRect;//intersectRectの作業用としてのみ使用
begin
//これで重なっていればtrueが返る。
//ちなみにtmprectには重ね合わさった部分の矩形データが入るが、今回はそのデータは使わない
Result:=IntersectRect(tmprect,RectA,RectB);
end;
なお、当たり判定が重なっているかどうかは、自機と敵弾の位置が決まった直後がよいでしょう。
以下のコードを自機と敵弾の座標を決めた後に追加してください。
(makegraphic手続きの中の敵弾座標決定後、かつ、敵弾の描画前の部分に追加)
//自機と敵弾のあたり判定をチェック。
for tmpint:=0 to MaxBullet-1 do
begin
if bullet[tmpint].Flag then
begin
if collision(//自機と敵弾それぞれのあたり判定が重なっているかチェック
Rect(
player.x-player.hitx,
player.y-player.hity,
player.x+player.hitx,
player.y+player.hity
),
Rect(
round(bullet[tmpint].x-bullet[tmpint].hitx),
round(bullet[tmpint].y-bullet[tmpint].hity),
round(bullet[tmpint].x+bullet[tmpint].hitx),
round(bullet[tmpint].y+bullet[tmpint].hity)
)
) then
begin
gamemode:=0;//タイトル画面に戻らせて
exit;//今の手続きはここで処理を終了させる。
end;
end;
end;
これで、自機と敵弾が当たると、タイトル画面に戻るようになります。
以上で、一通り、ゲームの体裁が出来ました。コンパイル、実行して試してみて下さい。