リュウグウノツカイのように

深海で悠々と暮らしたい

夏のDX祭/背景の移動&位置を戻す

まえがき

前回、専門学校1年生が作りそうなゲームの紹介をしました。 ryugu44.hatenablog.com

ただ、技術的に不安な学生や、そもそもそこまで習ってないし!!!!という方のために、簡単なサンプルプログラムを用意しました。例にもよって動画でアーカイブしていますが、お役に立てれば嬉しいです。

外部解説サイト様

yttm-work.jp 背景の移動ってどんなイメージなの?とピンと来ない方も安心。まずはイメージを掴みましょう。

解説動画

youtu.be 今回は動画の中でポイントもつけました。

利用素材様

■森の画像 www.ac-illust.com ※ゲーム画面に合わせて画像のサイズを変えています。

■草の画像 illust-ryokka.jp ※ゲームに合うように、画像のサイズを変えています。

それぞれ、お借り致しました。ありがとうございます。

画像は「構造体」がつきもの

 今回使った背景画像の1つでもある「草」の画像。この画像を見て想像できる「パラメータ」って、何を思いつきますか?

 例えば、DXライブラリでは、読み込んだ画像をメモリで管理すための番号である「ハンドル」が用いられます。今回の草の画像も「ハンドル」で管理されていますので、ハンドルを管理する変数が必要です。

 さらに、画像は基本的に左上を基準に、位置を決めて描画する必要があります。2Dゲームなら縦と横の位置、X位置とY位置を管理する変数も必要です。

 加えて、画像の幅と高さを取得しなければ、当たり判定はもちろんのこと、背景を次々と横に並べることも難しくなってきます。幅と高さを管理する変数も必要でしょう。

//画像
typedef struct _Image
{
    int X = 0;                //縦の位置
    int Y = 0;                //横の位置

    int Width = 0;            //幅
    int Height = 0;           //高さ

    int Handle;                //画像のハンドル
    BOOL IsDraw = FALSE;       //描画するか?
}Image;

最低限、画像を取り扱うときは、このように構造体でまとめましょう。

おまけ:「構造体」で管理しないと...

 例えば、こんな画像があるとしましょう。おそらく、シューティングゲームのプレイヤーとして利用するんじゃないでしょうか。

プレイヤーのイメージ

 この画像をプログラムで使うとき、ネットに溢れる初心者向けのサンプルプログラムなんかを参考にすると、こんなプログラムを書くかもしれません

int Handle;
Handle = LoadDivGraph("C:\\Game\\player.png");

 よく考えると、プレイヤーの情報って、他にもいくつかあるんです。例えば・・・

  • XY位置を管理しなければ、移動ができない
  • 幅と高さを管理しなければ、四角の当たり判定が計算できない
  • 移動速度を管理しなければ、スピードアップができない
  • 体力を管理しなければ、HPや残機などを実装できない などなど...

 このあたりをプログラムで無難に書くと、こうなります。

//プレイヤーのデータ
int Handle;
int x;
int y;
int width;
int height;
int speed;
int HP;

//値の初期化
Handle = LoadDivGraph("C:\\Game\\player.png");
GetGraphSize(Handle,&width,&height);
x = 0;
y = 0;
spped = 3;
HP = 5;

 プレイヤー1ついればOKのゲームであれば、コレで完璧でしょう。つまり、サンプルプログラムや学校の授業で「画像の読込を理解する」といったハナシです。しかし現実問題、ゲームには敵がいて、その敵と上手くヤり合うことによって成立する遊びなんです。プレイヤーが1ついるだけのゲームって、ゲームに非ずかもしれません。

 さて、敵の話が出てきました。敵も画像を読み込んで描画します。敵のデータとしては、どのようなものを準備しておけば良いでしょうか?

敵の例

  • XY位置を管理しなければ、移動ができない
  • 幅と高さを管理しなければ、四角の当たり判定が計算できない
  • 移動速度を管理しなければ、スピードアップができない
  • 体力を管理しなければ、HPや残機などを実装できない などなど...

おやおや、プレイヤーと同じデータではありませんか??

 例えば、先程のプログラムに敵のデータも入れてみます。

//プレイヤーのデータ
int Handle;
int x;
int y;
int width;
int height;
int speed;
int HP;

//敵のデータ
int teki_Handle;
int teki_x;
int teki_y;
int teki_width;
int teki_height;
int teki_speed;
int teki_HP;

//値の初期化
Handle = LoadDivGraph("C:\\Game\\player.png");
GetGraphSize(Handle,&width,&height);
x = 0;
y = 0;
spped = 3;
HP = 5;

//値の初期化
teki_Handle = LoadDivGraph("C:\\Game\\boss.png");
GetGraphSize(teki_Handle,&teki_width,&teki_height);
teki_x = 0;
teki_y = 0;
teki_spped = 3;
teki_HP = 5;

プログラムが一気に長くなりました。さらに、敵を追加しましょう。こんなのとか。

敵の画像Ⅱ

//プレイヤーのデータ
int Handle;
int x;
int y;
int width;
int height;
int speed;
int HP;

//敵のデータ
int teki_Handle;
int teki_x;
int teki_y;
int teki_width;
int teki_height;
int teki_speed;
int teki_HP;

//敵2のデータ
int teki2_Handle;
int teki2_x;
int teki2_y;
int teki2_width;
int teki2_height;
int teki2_speed;
int teki2_HP;

//値の初期化
Handle = LoadDivGraph("C:\\Game\\player.png");
GetGraphSize(Handle,&width,&height);
x = 0;
y = 0;
spped = 3;
HP = 5;

//値の初期化
teki_Handle = LoadDivGraph("C:\\Game\\boss.png");
GetGraphSize(teki_Handle,&teki_width,&teki_height);
teki_x = 0;
teki_y = 0;
teki_spped = 3;
teki_HP = 5;

//値の初期化
teki2_Handle = LoadDivGraph("C:\\Game\\boss2.png");
GetGraphSize(teki2_Handle,&teki2_width,&teki2_height);
teki2_x = 0;
teki2_y = 0;
teki2_spped = 3;
teki2_HP = 5;

おやおや、プログラムが読みにくくなってきませんか??

 敵が無限に登場するゲームもあるはずです。無限ということは・・・同じようなプログラム、極端な例ですが100個も書くのでしょうか?もちろん、書いたっていいのです。でも自分で管理しないと。分からないからと言って、自分で作った難解なプログラムを友人や先生に見せ泣きつくのも、なんだか自業自得というか...と思ってしまうのでした。

 プログラムを考えるのが上手な人のアタマって、おそらく、この2つが徹底されています。

  • 似たデータ同士をまとめる
  • 繰り返しを使った処理を作る

 参考に見せてもらったプログラムが、配列や構造体、クラスなんかで記述されているのを見て拒否反応を示すのであれば、まずは配列や構造体の「おさらい」からしてみるといいでしょう。使い方が分かれば、拒否反応が無くなります。また「変数しか分からない!配列なんて使いたくない!!」と意地を張るのも良くありません。配列と構造体なくして、高度なゲームプログラミングなど組めません。ゲームを作る前に、ある程度は使いこなしてから組むことを強く推奨します。そのための学校であり、授業です。

 では、先程のプレイヤーや敵を構造体にすると、どのように記述できるでしょうか?

#define TekiMax 100 //一度に出てくる敵の数

//キャラクターのデータ
struct Chara
{
 int Handle;
 int x;
 int y;
 int width;
 int height;
 int speed;
 int HP;
};

//構造体変数・配列の宣言
struct Chara player;
struct Chara teki;
struct Chara teki2;
struct Chara tekiArray[TekiMax];

//値の初期化
player.Handle = LoadDivGraph("C:\\Game\\player.png");
GetGraphSize(player.Handle,&player.width,&player.height);
player.x = 0;
player.y = 0;
player.spped = 3;
player.HP = 5;

//値の初期化
teki.Handle = LoadDivGraph("C:\\Game\\boss.png");
GetGraphSize(teki.Handle,&teki_width,&teki.height);
teki.x = 0;
teki.y = 0;
teki.spped = 3;
teki.HP = 5;

//値の初期化
teki2.Handle = LoadDivGraph("C:\\Game\\boss2.png");
GetGraphSize(teki2.Handle,&teki2.width,&teki2.height);
teki2.x = 0;
teki2.y = 0;
teki2.spped = 3;
teki2.HP = 5;

さっきよりも難しくなったじゃないか!と思うのは、まだ早い。

 プログラミングのコツは「まとめる」「繰り返す」の2つ。よくプログラミングは「データ構造」と「アルゴリズム」で、できていると学びますが、正しくその通りなんです。例えば、この構造体を使って、敵を100体つくってみましょう。

#define TekiMax 100 //一度に出てくる敵の数

//キャラクターのデータ
struct Chara
{
 int Handle;
 int x;
 int y;
 int width;
 int height;
 int speed;
 int HP;
};

//構造体変数・配列の宣言
struct Chara teki;
struct Chara teki2;
struct Chara tekiArray[TekiMax];

//値の初期化
teki.Handle = LoadDivGraph("C:\\Game\\boss.png");
GetGraphSize(teki.Handle,&teki_width,&teki.height);
teki.x = 0;
teki.y = 0;
teki.spped = 3;
teki.HP = 5;

//値の初期化
teki2.Handle = LoadDivGraph("C:\\Game\\boss2.png");
GetGraphSize(teki2.Handle,&teki2.width,&teki2.height);
teki2.x = 0;
teki2.y = 0;
teki2.spped = 3;
teki2.HP = 5;

//敵の情報をコピー
for(int i = 0; i<TekiMax; i++)
{
 tekiArray[i] = teki; 
}

//敵を描画
for(int i = 0; i<TekiMax; i++)
{
 DrawGraph(tekiArray[i].x,tekiArray[i].y,tekiArray[i].handle,TRUE);
}

 注目すべきは、敵の情報をコピーしたり、描画をする部分。上手にfor文を活用して、100体分の敵を処理しましょう。更に構造体は「同じ構造体ならば、そのまま代入(コピー)できる」性質を持っています。最初に個々の敵を作っておいて、登場させたいタイミングで、敵配列の1つへコピーする、みたいな処理をかけると、1000体でも10体でも思いのまま。ゲームのレベル設定もカンタンにできます。