2008/07/12
初級ゲームプログラミング完全マニュアル [vol.0054 2008/07/12]
┏┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳┓
┣┛ ┗┫
┃ 挫折不可能! 初級ゲームプログラミング完全マニュアル ┃
┃ ┃
┃ 第 54 号 2008/07/12 ┃
┣┓ ┏┫
┗┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻┛
- Ads space -
┌┬───────────────────────────────┬┐
││ はじめに ││
└┴───────────────────────────────┴┘
みなさま、こんにちは!
個人ゲーム制作アドバイザーの Byerkut です!
きゃー! ただいま雷鳴がとどろいております。
私の住んでいる地域は雷が有名なのです。
この時期になるともうごっすんごっすん雷が落ちます。
停電でデータが飛ぶのが怖いのでノートパソコンは手放せません。
みなさまも油断なさらぬよう…
さて、今回は地球防衛ゲームの主人公が弾を乱射します。
┌┬───────────────────────────────┬┐
││本日のラインナップ ││
└┴───────────────────────────────┴┘
・今日のメインテーマ
【全弾発射!倍返しだぁぁぁぁ!】
・考えようによっては、ためになるコラム
【リスト構造】
・あとがき
┌┬───────────────────────────────┬┐
││ みんなの備忘録 ││
└┴───────────────────────────────┴┘
■Visual C++ 2008 Express Edition をインストールする手順
http://www.game-create.com/archives/235
■Visual C++ 2008 Express Edition でプロジェクトを新規作成する手順
http://www.game-create.com/archives/270
■VC++ 2005 EE でプロジェクトにソースファイルを登録する手順
http://blog.mag2.com/m/log/0000240151/108824854.html
■画像ファイルを LoadImage() 関数で読み込めるようにする手順
http://www.game-create.com/archives/308
■ゲーム用ウィンドウのテンプレート
http://www.game-create.com/menu/downloads
■ビットマップ学習用クラス - Study::Bitmap
http://www.game-create.com/menu/downloads
┏┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳┓
┃┃ ┃┃
┃┃ 今日のメインテーマ ┃┃
┃┃ ┃┃
┃┃ 【全弾発射!倍返しだぁぁぁぁ!】 ┃┃
┃┃ ┃┃
┗┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻┛
前回のメルマガで、地球防衛ゲームの主人公、まどかが弾を撃てるように
なりました。しかし、誰の目から見ても不自然さは明らかです。
たとえば発射された弾が画面に表示されている間に、もう一度
スペースキーを押してみてください。なんと、発射された弾が消えて
しまうではありませんか!これはいったいどういうことでしょう?
実はこれは当たり前の話なのです。
先週のプログラムではそうなって当然なのです。
原因はひとつです。原因は…
弾の情報を格納する変数がひとつしかない!
…ということです。
先週のプログラムで弾を格納する変数は player_bullets.cpp の冒頭に
ある…
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
static PlayerBullet playerBullet;
------------------------------------------------------------------
…のひとつだけです。ひとつだけであるにもかかわらず、
新しく弾を発射する段階で…
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
void ShotPlayerBullet(Player &player)
{
playerBullet.x = player.x + ((player.width / 2) - (playerBullet.width / 2));
playerBullet.y = player.y;
}
------------------------------------------------------------------
…と、たったひとつしかない変数を書き換えてしまっているのです。
その結果、「まだ画面に残っている弾」のことなどおかまいなしに、
弾は、「新しく発射された弾」としてまどかの近くに移動してしまいます。
こうやって「発射」を演出していたのが前回のプログラムでした。
では、この不自然さを解決するためにはどうしたらいいのでしょうか?
簡単です。弾の情報を保存する変数がたくさんあればいいのです。
たとえば…
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
static PlayerBullet playerBullet1;
static PlayerBullet playerBullet2;
static PlayerBullet playerBullet3;
static PlayerBullet playerBullet4;
static PlayerBullet playerBullet5;
static PlayerBullet playerBullet6;
static PlayerBullet playerBullet7;
static PlayerBullet playerBullet8;
static PlayerBullet playerBullet9;
static PlayerBullet playerBullet10;
------------------------------------------------------------------
…とすると、合計で 10 発まで画面内に弾を発射できるようになります。
ただし、実際にはこうはやりません。
これを見て「面倒だなぁ…」と感じられた方はかなりスジが良いです。
実際、こういう場合には配列を使って…
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
static PlayerBullet playerBullet[10];
------------------------------------------------------------------
…とするのが一般的です(もっと実践的な方法はコラム参照)。
なぜかというと実際に書いてみればわかります。
たとえば「弾を上へ移動させる」ための UpdatePlayerBullets() という
関数があったと思います。
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
void UpdatePlayerBullets(HWND hWindow, HDC hBackBuffer)
{
playerBullet.y -= PLAYER_BULLETS_MOVE_SPEED;
}
------------------------------------------------------------------
これを最初の方法で書くと次のようになります。
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
void UpdatePlayerBullets(HWND hWindow, HDC hBackBuffer)
{
playerBullet1.y -= PLAYER_BULLETS_MOVE_SPEED;
playerBullet2.y -= PLAYER_BULLETS_MOVE_SPEED;
playerBullet3.y -= PLAYER_BULLETS_MOVE_SPEED;
playerBullet4.y -= PLAYER_BULLETS_MOVE_SPEED;
playerBullet5.y -= PLAYER_BULLETS_MOVE_SPEED;
playerBullet6.y -= PLAYER_BULLETS_MOVE_SPEED;
playerBullet7.y -= PLAYER_BULLETS_MOVE_SPEED;
playerBullet8.y -= PLAYER_BULLETS_MOVE_SPEED;
playerBullet9.y -= PLAYER_BULLETS_MOVE_SPEED;
playerBullet10.y -= PLAYER_BULLETS_MOVE_SPEED;
}
------------------------------------------------------------------
変数が 10 個あるのですから、これは当然です。
しかし、配列を使うと次のように書くことができます。
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
void UpdatePlayerBullets(HWND hWindow, HDC hBackBuffer)
{
for (int i = 0; i < 10; i++) {
playerBullet[i].y -= PLAYER_BULLETS_MOVE_SPEED;
}
}
------------------------------------------------------------------
うわああああ、信じられません!3行です!!!
最初の方法では 10 行必要だったコードが3行になってしまいました!
これがプログラミングの極意、醍醐味、おもしろみです。
この配列と for 文に関する詳細は当ブログの記事を参照ください。
http://www.game-create.com/archives/108
http://www.game-create.com/archives/398
http://www.game-create.com/archives/397
では早速、先週のプログラムを配列を使って書き換えてみましょう。
今回もサンプルを用意いたしましたのでダウンロードください。
http://www.game-create.com/archives/420
今回、変更を加えたのは player_bullets.h と player_bullets.cpp です。
重要なポイントに絞って解説をしていきます。
まずは player_bullets.cpp ですが、冒頭部分をご覧ください。
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
static Study::Bitmap playerBulletGraphics;
static PlayerBullet playerBullets[PLAYER_BULLETS_MAX];
------------------------------------------------------------------
先ほど解説のように配列を作っています。
PLAYER_BULLETS_MAX は player_bullets.h で定義されている通り…
------------------------------------------------------------------
player_bullets.h
------------------------------------------------------------------
#define PLAYER_BULLETS_MAX 80
------------------------------------------------------------------
…ですので、 80 発の弾が撃てるというわけです。
80 連装ってすごいですね。マシンガンのようです。
おや、見慣れない playerBulletGraphics という変数がありますね?
実はこれは弾の画像を読み込む場所なのです。
以前は playerBullet という変数の中にあったのですが、
playerBullet は今回 playerBullets という配列になってしまったので、
画像を読み込む場所を新しく作ったというわけです。
「別に配列の中に読み込んでも良いのでは?」と
思われるかもしれませんが、前述のように今回の配列は、
中身が 80 個もあるため、 80 回も画像を読み込むのはメモリの無駄に
なってしまうからです。なんせ同じ画像ですから。
次に InitializePlayerBullets() 関数です。
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
void InitializePlayerBullets(HWND hWindow, HDC hBackBuffer)
{
// 弾の画像をひとつだけ読み込む( playerBullets 内で使い回す)
playerBulletGraphics.load(hBackBuffer, TEXT("player_bullet.bmp"), RGB(255, 0, 255));
// すべての弾を未使用にする
for (int i = 0; i < PLAYER_BULLETS_MAX; i++) {
PreparePlayerBullet(playerBullets[i]);
}
}
------------------------------------------------------------------
今回からちゃんとコメントをつけていますので、だいぶわかりやすいかと
思います。やっていることは、弾の画像を読み込んで、弾をすべて
初期化しているという流れになります。弾を初期化するのは
PreparePlayerBullet() という関数に「お願い」しています。
PreparePlayerBullet() 関数はソースの下の方に書いてあります。
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
void PreparePlayerBullet(PlayerBullet &playerBullet)
{
// 弾を初期化する
playerBullet.x = -PLAYER_BULLETS_CHIP_WIDTH;
playerBullet.y = -PLAYER_BULLETS_CHIP_HEIGHT;
playerBullet.width = PLAYER_BULLETS_CHIP_WIDTH;
playerBullet.height = PLAYER_BULLETS_CHIP_HEIGHT;
playerBullet.status = Unuse;
}
------------------------------------------------------------------
中身は前回 InitializePlayerBullets() に書いていた内容と同じです。
わざわざ関数化するメリットは「後々のため」「自分のため」です。
忘れてしまった方は6月分のメルマガを参照ください。
…ん? playerBullet.status = Unuse; という謎の1行がありますね。
実はこれは「列挙型」というれっきとした「変数」なのです。
詳しくは当ブログの記事を参照ください。
http://www.game-create.com/archives/417
このデータ型の定義は、やはり player_bullets.h にあります。
------------------------------------------------------------------
player_bullets.h
------------------------------------------------------------------
typedef enum {
Unuse,
Emitted
} PlyBltStatus;
------------------------------------------------------------------
要約すると、このデータは「中身が Unuse か Emitted のどちらかになる
データ型」という意味になります。 Unuse は「未使用」を意味し、
Emitted は「発射済み」を意味します。
つまり、このデータ型は「現在弾がどんな状態にあるか?」を保持する
変数として使っているのです。もう一度関数を見てください。
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
void PreparePlayerBullet(PlayerBullet &playerBullet)
{
// 弾を初期化する
playerBullet.x = -PLAYER_BULLETS_CHIP_WIDTH;
playerBullet.y = -PLAYER_BULLETS_CHIP_HEIGHT;
playerBullet.width = PLAYER_BULLETS_CHIP_WIDTH;
playerBullet.height = PLAYER_BULLETS_CHIP_HEIGHT;
playerBullet.status = Unuse;
} ~~~~~ ← 注目!
------------------------------------------------------------------
この PreparePlayerBullet() は「初期化」するための関数ですので、
まず「現在弾がどんな状態にあるか?」という情報を保持する
status という変数を「未使用( Unuse )」にセットしているわけです。
初期化の段階では、まだ弾は1発も撃っていないはずですから。
これは、後々の関数の中で使っています。
次は UpdatePlayerBullets() 関数を見てください。
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
void UpdatePlayerBullets(HWND hWindow, HDC hBackBuffer)
{
for (int i = 0; i < PLAYER_BULLETS_MAX; i++) {
if (playerBullets[i].status == Emitted) {
// 弾を画面情報へ移動する
playerBullets[i].y -= PLAYER_BULLETS_MOVE_SPEED;
// 画面外へ出たら未使用に戻す
if ((playerBullets[i].y + playerBullets[i].height) < PLAYER_BULLETS_SCREEN_TOP) {
playerBullets[i].status = Unuse;
}
}
}
}
------------------------------------------------------------------
これは今回のメルマガの最初の方で例に出した通りです。
全 80 発の弾のY座標を PLAYER_BULLETS_MOVE_SPEED の分だけ
マイナスして「弾が画面の上に進んでいる」ように見せているところです。
ただし! status が Emitted の弾に限定しています。
なぜかというと使ってもいない弾まで動かしてしまうと、
処理時間の無駄だからです。 status はこのようにして、
必要な弾を分別するために新しく追加されたというわけです。
もうひとつ、この関数の中で画面外へ出た弾の status を
Unuse (未使用)に戻すこともやっています。
画面外へ出た弾は放っておいても良いのですが、
画面に見えないのに移動させるのは、やはり処理時間の無駄ですし、
実はバグの原因にもなります。多くの場合、画面外へ出た弾を放っておくと
しばらくして画面の下から出現するようになります。
これはコンピュータの特性で仕方のないことですので、
「画面外へ出た弾」は「未使用」にすることをおすすめします。
次に DrawPlayerBullets() 関数を見てください。
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
void DrawPlayerBullets(HWND hWindow, HDC hBackBuffer)
{
for (int i = 0; i < PLAYER_BULLETS_MAX; i++) {
if (playerBullets[i].status == Emitted) {
// 弾画像の形にくりぬく
BitBlt(hBackBuffer, playerBullets[i].x, playerBullets[i].y,
playerBullets[i].width, playerBullets[i].height,
playerBulletGraphics.getMask(), 0, 0, SRCAND);
// 弾画像を描画する
BitBlt(hBackBuffer, playerBullets[i].x, playerBullets[i].y,
playerBullets[i].width, playerBullets[i].height,
playerBulletGraphics.getBitmap(), 0, 0, SRCPAINT);
}
}
}
------------------------------------------------------------------
ここは単純です。 UpdatePlayerBullets() 関数と同様に、
status が Emitted 、つまり「発射済み」の弾を画面に描画しています。
BitBlt() 関数を2回呼んでいるのは「弾の背景色を透過するため」です。
詳しくは後日解説します。
次は ShotPlayerBullet() 関数を見てください。
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
void ShotPlayerBullet(Player &player)
{
// 未使用の弾を探して…
for (int i = 0; i < PLAYER_BULLETS_MAX; i++) {
if (playerBullets[i].status == Unuse) {
// 発射する
playerBullets[i].x = player.x + ((player.width / 2) - (playerBullets[i].width / 2));
playerBullets[i].y = player.y;
playerBullets[i].status = Emitted;
break;
}
}
}
------------------------------------------------------------------
ここはちょっと他の関数と毛色が違います。
ShotPlayerBullet() 関数は「新しく弾を発射する」ための関数ですから
まず「未使用の弾」を探して、見つかったらそれを「発射」します。
もし、見つからなかったら何もしません。というかできません。
「どうやって未使用の弾を見つけるの?」と思われるかもしれませんが、
それも status でわかります。 status の Unuse が「未使用」の意味
ですので、 for 文を使って playerBullets 配列のデータを
ひとつひとつ検証し、 status が Unuse のデータを見つければ良いのです。
見つかったら座標をプレイヤーの近くにセットし、
status を Emitted (発射済み)に変更すれば完了です。
みごと新しい弾が発射されます。
さて、今回のポイントはすべてお伝えすることができました。
以前のシューティングゲームサンプルでは解説を省略した部分にも
焦点を当てましたので、さらに理解度が深まることと思います。
もし、今回よくわからなくても、今後のプログラムでも
中身は同様の構造になっているので、まだまだ理解するチャンスは
たくさんあります。安心してください。
来週は待望の敵キャラを作ってみたいと思います。
お楽しみにお待ちください。
また、オリジナルの敵キャラを作りたい場合は、
あらかじめビットマップ画像を用意してください。
きっと愛着がわくと思います。
今回も最後まで読んでいただきましてありがとうございます!
それでは!
Byerkut.
┌┬───────────────────────────────┬┐
││ 考えようによっては、ためになるコラム ││
││ ││
││ 【リスト構造】 ││
└┴───────────────────────────────┴┘
今回、配列を駆使して多くのデータを保存して管理する方法を
お伝えしましたが、実際のゲームではこういった用途に配列は
ほとんど使われません。なぜかというと、配列を使っても、
まだ、処理時間の無駄が発生してしまうためです。
たとえば今回のプログラムの DrawPlayerBullets() 関数などでは
たとえ画面上に弾が1発しか発射されていなくても
80 回ものループがかならず実行されてしまい、
79 回分のループが全くの無駄だからです。
ではどうしたらいいのかというと、リストというデータ構造を使います。
配列は中身のデータの数が固定であるのに対して、
リストは中身のデータの数が可変です。
つまり、1発しか弾が発射されていないのであれば、
リスト構造の中身のデータはひとつだけにすることができるのです。
もしこれが配列の場合は1個の「発射済み」データと
79 個の「未使用」データになってしまいます。
リスト 配列
┌────┐ ┌────┐
│発射済み│ │発射済み│
└────┘ ├────┤
│ 未使用 │
├────┤
│ 未使用 │
├────┤
│ 未使用 │
├────┤
│ 未使用 │ ←この辺、使ってないよ!!
├────┤
│ 未使用 │
├────┤
│ 未使用 │
├────┤
│ 未使用 │
├────┤
〜〜〜〜〜〜〜〜
〜〜〜〜〜〜〜〜
│ 未使用 │
└────┘
もし、今回の場合、実際にリストを使うためには次のように書きます。
------------------------------------------------------------------
player_bullets.cpp
------------------------------------------------------------------
static vector<PlayerBullet> playerBullets;
------------------------------------------------------------------
使い方はちょいと高度ですので、今回は紹介程度にとどめておきます。
興味のある方は "C++ STL vector" で検索してみてください。
Byerkut.
┌┬───────────────────────────────┬┐
││ 重要記事のダイジェスト ││
└┴───────────────────────────────┴┘
■C++ は計算と記憶しかできない
■外部の機能(関数)を借り受けるには #include を使う
■#include で借り受けた機能を使うには関数を呼び出す
http://archive.mag2.com/0000240151/20080203171350000.html
■Windows プログラムには必ず WinMain() 関数から開始する
http://archive.mag2.com/0000240151/20080209215231000.html
■プログラムは関数の集まり
■プログラムは関数を増やしてソフトウェアを作る
■関数には次の要素がある(名前・機能・引数・戻り値)
http://archive.mag2.com/0000240151/20080216090000000.html
■関数を作には次の要素を決める(名前・機能・引数・戻り値)
http://archive.mag2.com/0000240151/20080608200909000.html
■プログラムはソースコードを分散させて大きなソフトを作る
http://archive.mag2.com/0000240151/20080525121425000.html
http://archive.mag2.com/0000240151/20080531212858000.html
■ゲームを作るためには「入力」「出力」「条件分岐」が必要
http://archive.mag2.com/0000240151/20080301221623000.html
■画像ファイルの絵を画面に表示する際にはメモリに読み込む
http://archive.mag2.com/0000240151/20080406202024000.html
■LoadImage() 関数はコピペで使い回せる
■LoadImage() 関数の第2引数は
メモリに読み込みたい画像ファイルの名前を渡す
■LoadImage() 関数で読み込むファイルは
プロジェクトディレクトリにおいてある必要がある
http://archive.mag2.com/0000240151/20080413174135000.html
┌┬─────┬─────────────────────────┬┐
││ あとがき │ 発行者のつぶやきです ││
└┴─────┴─────────────────────────┴┘
ペンタブレットを買いました。
これでゲームのキャラクターがうまく描けると良いですね。
ドット絵って独特のノウハウが必要なのでとても難しいです。
ドット絵にこだわるのは昔の温かい感じの 2D ゲームが好きだったから
なのですが、最近のトゥーンシェード 3D も好きです。
その間って何とか作れないものでしょうか…
┌┬───────────────────────────────┬┐
├┘ └┤
│ 購読の解除はこちら↓ │
│ http://www.game-create.com/contents/gp_beginners_ml │
├┐ ┌┤
└┴───────────────────────────────┴┘
- Ads space -
┌┬───────────────────────────────┬┐
├┘ └┤
│ 「挫折不可能!初級ゲームプログラミング完全マニュアル」は、 │
│ 「いちばんやさしいゲームの作り方」が運営しています。 │
│ │
│ サイト : http://www.game-create.com/ │
│ 発行元 : http://www.game-create.com/menu/about │
│ 発行者 : http://www.game-create.com/menu/profile │
│ 問い合わせ : http://www.game-create.com/menu/contact │
│ 利用規約 : http://www.game-create.com/informations/agreement │
├┐ ┌┤
└┴───────────────────────────────┴┘
POWERED BY LIBERTIASTER GAME STYLE.


