挫折不可能!初級ゲームプログラミング完全マニュアル  RSSを登録する

「ゲームが作りたいけどプログラミングなんて全然知らない」「文系だし数学や物理が苦手だけどゲームが作れるだろうか?」そんなアナタのためのメールマガジンです。PCの電源を入れてからどうすればいいのか?から自分のゲームが完成するまでを応援します!

最新号をメルマガでお届けします    
登録 解除

規約に同意して

登録した方には、まぐまぐの公式メルマガ(無料)をお届けします。
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.
最新号をメルマガでお届け
登録 解除

規約に同意して

登録した方には、まぐまぐの公式メルマガ(無料)をお届けします。

最近の記事

上へ戻る