【ゲーム製作入門】C/C++でブロック崩しを作る⑧【DXライブラリ】
書き手:肥田野
プログラミング初心者向けに、簡単なゲームの制作を通してC/C++の基本を解説していきます。
当サークルに所属しない方でも参考にしていただければ幸いです。
この記事では前回の内容を理解していることを前提に進めていきます。
今回はクラスをもっと活用して、よりソースコードを読みやすくしてみます。
また、タイトル画面やゲームオーバー画面も実装して、ゲームとして完成させましょう。
それでは今回解説するコードです。
#include "DxLib.h" #define BLOCK_NUM 24 #define WINDOW_X 640 #define WINDOW_Y 600 class Block{ public: int x,y; int width,height; int gh; bool live; void View(){ if(live){ DrawGraph(x,y,gh,TRUE); } } Block(int setX,int setY){ x = setX; y = setY; gh = LoadGraph("Block.png"); GetGraphSize(gh,&width,&height); live = true; } void All(){ View(); } }; class Player{ public: int x,y,width,height; int speed; int gh; Player(){ x = 320; y = 550; speed = 10; gh = LoadGraph("Player.png"); width = 80; height = 20; } void Move(){ if(CheckHitKey(KEY_INPUT_RIGHT) && x<640){ x += speed; } if(CheckHitKey(KEY_INPUT_LEFT) && x>0){ x -= speed; } } void View(){ DrawExtendGraph(x,y,x+width,y+height,gh,TRUE); } void All(){ Move(); View(); } }; class Ball{ public: int x,y,r; int vecX,vecY; int speed; Ball(){ x = 320; y = 300; r = 5; vecX = 0; vecY = 0; speed = 5; } void Move(){ x += speed*vecX; y += speed*vecY; } void View(){ DrawCircle(x,y,r,GetColor(255,255,255)); } void All(){ Move(); View(); } }; class GameControl{ public: Block* bl[BLOCK_NUM]; Player* pl; Ball* ba; int life; int state; int titlegh; int gameovergh; int cleargh; bool pushFlag; bool PushSpace(){ if(CheckHitKey(KEY_INPUT_SPACE)){ if(!pushFlag){ pushFlag = true; return true; } }else{ pushFlag = false; } return false; } GameControl(){ life = 2; state = 0; titlegh = LoadGraph("Title.png"); gameovergh = LoadGraph("GameOver.png"); cleargh = LoadGraph("Clear.png"); pushFlag = false; for(int i=0;i<BLOCK_NUM;i++){ bl[i] = new Block(140+(i%4)*100,10+(i/4)*50); } pl = new Player(); ba = new Ball(); } ~GameControl(){ for(int i=0;i<BLOCK_NUM;i++){ delete bl[i]; } delete pl; delete ba; } void Title(){ DrawGraph(0,0,titlegh,TRUE); if(PushSpace()){ state = 1; ba->x = 320; ba->y = 300; } } void Game(){ for(int i=0;i<BLOCK_NUM;i++){ bl[i]->All(); } pl->All(); ba->All(); for(int i=0;i<life;i++){//残機表示 DrawCircle(20+i*(life+ba->r+10),20,ba->r,GetColor(255,255,255)); } for(int i=0;i<BLOCK_NUM;i++){//ゲームクリア判定 if(bl[i]->live)break; if(i == BLOCK_NUM-1){ ba->vecX = 0; ba->vecY = 0; state = 3; } } if(ba->vecX != 0 && ba->vecX != 0 && life>0){ if(ba->x>WINDOW_X)ba->vecX = -1; if(ba->x<0)ba->vecX = 1; if(ba->y<0)ba->vecY = 1; if(ba->x>pl->x && ba->x<pl->x+pl->width && ba->y+ba->r > pl->y){ ba->vecY = -1; } if(ba->y>WINDOW_Y){ ba->x = 320; ba->y = 300; ba->vecX = 0; ba->vecY = 0; life--; } for(int i=0;i<BLOCK_NUM;i++){ if(bl[i]->live){ if(ba->x > bl[i]->x && ba->x < bl[i]->x+bl[i]->width && ba->y+ba->r > bl[i]->y && ba->y+ba->r < bl[i]->y+bl[i]->height){//上 bl[i]->live = false; ba->vecY *= -1; } if(ba->x > bl[i]->x && ba->x < bl[i]->x+bl[i]->width && ba->y-ba->r > bl[i]->y && ba->y-ba->r < bl[i]->y+bl[i]->height){//下 bl[i]->live = false; ba->vecY *= -1; } if(ba->x+ba->r > bl[i]->x && ba->x+ba->r < bl[i]->x+bl[i]->width && ba->y > bl[i]->y && ba->y < bl[i]->y+bl[i]->height){//左 bl[i]->live = false; ba->vecX *= -1; } if(ba->x-ba->r > bl[i]->x && ba->x-ba->r < bl[i]->x+bl[i]->width && ba->y > bl[i]->y && ba->y < bl[i]->y+bl[i]->height){//右 bl[i]->live = false; ba->vecX *= -1; } } } }else if(life > 0){//球が止まって残機がある=ミス DrawFormatString(260,360,GetColor(255,255,255),"PUSH SPACE"); if(CheckHitKey(KEY_INPUT_SPACE)){ ba->vecX = 1; ba->vecY = 1; } }else{//残機がない=ゲームオーバー state = 2; } } void GameOver(){ DrawGraph(0,0,gameovergh,TRUE); if(CheckHitKey(KEY_INPUT_SPACE)){ state = 0; life = 2; for(int i=0;i<BLOCK_NUM;i++){ bl[i]->live = true; } } } void GameClear(){ DrawGraph(0,0,cleargh,TRUE); if(CheckHitKey(KEY_INPUT_SPACE)){ state = 0; life = 2; for(int i=0;i<BLOCK_NUM;i++){ bl[i]->live = true; } } } void All(){ if(state == 0)Title(); if(state == 1)Game(); if(state == 2)GameOver(); if(state == 3)GameClear(); } }; int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ){ ChangeWindowMode(TRUE); SetGraphMode(WINDOW_X,WINDOW_Y,32); if( DxLib_Init() == -1 )return -1 ; SetDrawScreen(DX_SCREEN_BACK); GameControl* ga = new GameControl(); while(ProcessMessage() != -1){ int startTime = GetNowCount(); ScreenFlip(); ClearDrawScreen(); ga->All(); if(CheckHitKey(KEY_INPUT_ESCAPE) == 1)break; int endTime = GetNowCount(); WaitTimer((1000/60)-(endTime-startTime)); } delete ga; DxLib_End() ; return 0 ; }
全体的に大きく構成が変わったので、重要なところを中心に満遍なく解説していきます。
#define BLOCK_NUM 24
これはマクロと呼ばれる、いわば「置き換え」です。
ファイルの冒頭でこのように定義しておくと、以降のソースコード内で「BLOCK_NUM」と書いた部分を、コンパイル時に自動的に置き換えてくれます。
今回はブロックの数にマクロを使っています。
for文でブロックの数だけループさせたいときに、for(int i=0;i<BLOCK_NUM;i++)と書くと、プログラマがブロックの数を意識しなくても24回ループをしてくれるんです。
またブロックの数を変更したければ、#defineの24を書き換えてやるだけで、ブロックに関する全ての処理が正しく変更されます。
同じように、ウィンドウのサイズもマクロで定義したので、この数値をいじればゲーム画面のサイズを好きなように調節できます。
gh = LoadGraph("Block.png");
新しい関数が出てきました。
これまではDrawBoxでブロックを描画していましたが、実際にゲームを作る場面では専用の画像を用意することがほとんどです。
今回はPNG形式の画像をペイントで簡単に作成したので、これをDLして使ってください。
もちろん、自分で画像を用意しても大丈夫ですよ。
画像はDrawBoxとは違って、LoadGraphとDrawGraphの二つの関数を使わなければなりません。
どうしてかと言うと少し難しそうな話になるのですが、大事なことなので覚えておいてください。
コンピュータがデータを記憶する場所は、大きく分けて「主記憶装置」と「補助記憶装置」の二つです。
私たちが普段使っているHDDやUSBメモリは補助記憶装置に該当します。
では主記憶装置は何なのかというと、CPUが演算をする時に必要とする記憶領域のことです。
例えばint a;と変数を宣言すれば、主記憶装置の中に4バイト分(int型の変数の容量)の領域が確保されます。
主記憶装置は補助記憶装置に比べて、コンピュータの演算部との通信が速いのが特徴です。
それなら音楽や映像などあらゆるデータを主記憶装置に保存すればいいじゃないかと思いますが、残念ながらそれはできません。
主記憶装置に保存したデータは、コンピュータの電源を落とすと全て消えてしまうのです。
逆に補助記憶装置は、主記憶装置に比べて通信速度が劣るものの、電源を切ってもデータが消失することはありません。その為、ソフトウェアやメディアのデータは補助記憶装置に保管されるのです。
しかし、このゲームは60FPS、つまり1秒間に60回も描画処理をしているので、その都度補助記憶装置から画像のデータを呼び出していたのでは到底間に合いません。
そのため、補助記憶装置にある画像を一時的に主記憶装置に読み込ませるのがLoadGraphという関数なんです。
また、int型で宣言したghは、この画像を参照したい時に識別するための変数です。
GetGraphSize(gh,&width,&height);
これは読み込んだ画像から、幅と高さを求めてint型の変数に代入する関数です。
widthとheightの前に&が付いています。そういえばscanfの引数にも&がありましたね。
これはアドレス演算子と呼ばれるもので、widthやheightの変数の中身ではなく、変数としての住所(アドレス)を表現しています。
つまりGetGraphSizeの引数にはghの中身と、「widthとheightがメモリのどこに保存されているか」という情報が必要なんです。
では何故アドレスが必要なのでしょうか。簡単に言えば「その変数を書き換えるから」なのですが、これだけではピンと来ない方も多いでしょう。
関数が引数を利用するときは、引数の値を読み込むだけで、代入元の変数を変更することはできないんです。
第一、int型の引数には変数だけでなく直接数字を入れることもありますからね。それを変更しろと指示されてもコンピュータは困ってしまいます。
しかし、関数内で外部の変数の値を変更したいという事はよくあります。そういう時に使うのがアドレス演算子です。
まあ今回はここ以外に使っていないので、アドレス演算子を使った関数のお話は、別の機会に詳しくできればなと思います。
void All(){
この関数は中でViewを呼び出しているだけで、一見意味がないように思えますね。
書き方は人それぞれなのですが、私の場合クラスの中で「毎フレーム事項させたい処理」は全て「All」の関数にまとめておいて、WinMainで実行させる時にAllだけ呼び出せばいいようにしています。
class Player{
新たに作成したプレイヤークラスです。
ゲーム中に登場する物体は、全てクラスとして扱うのが便利です。
もちろんプレイヤーは1つしか実体化させませんが、クラスにまとめることでプレイヤー関係の処理を修正したい時などにすぐ分かるようになります。
プレイヤーの画像はこちらになります。
同様にBallクラスも作成しました。特に新しい要素はないので確認程度に目を通しておいてください。
class GameControl{
これもクラスなのですが、ゲームの動作処理をクラス内の関数(メソッド)で実行しています。
そのため、GameControlクラスのメンバ変数として、BlockクラスやPlayerクラスなどのポインタを用意しています。
bool PushSpace(){
これはスペースキーが押されたかどうかを判定するとき、長押しによる連続入力を防ぐための関数です。
CheckHitKeyは「そのキーが押されているかどうか」しか判定できないので、押されているあいだは何度でも1(true)を返してしまうんです。
しかし実際のゲームでは、長押ししているだけで連打扱いになったら困りますよね。そんな自体を回避したいときのテクニックをご紹介します。
関数の方にboolが使われていますね。そして関数内を見ると「return」という単語があります。これは演算結果を返すタイプの関数で、今回の場合スペースキーが最初に押された時だけtrueを返します。
これまで作った関数はvoidと書いていましたが、voidは辞書で引くと「空」という意味があります。つまり、関数の中だけで処理を完結させていて、結果を外部に出力しない関数だったのです。
今回は逆に、スペースキーが最初に押されたかどうかの結果が必要なので、bool型の関数になったというわけです。
GameControlクラスのメンバ変数として「pushFlag」というのを用意しました。初期設定はfalseです。
まずCheckHitKeyでスペースキーが押されているか判定し、押されていたらpushFlagを参照します。
この時pushFlagがfalseならば、pushFlagをtrueにしてからreturn trueを実行します。
ちなみにreturnをするとその瞬間に関数を抜け出してしまうので、この二行の順番を変えると正しく動作しません。
スペースキーが押された時にpushFlagがtrueだった場合、既に一度return trueを実行していることになるので、if文ではねられて最後の行まで行きreturn falseします。
そしてスペースキーが押されていない時にはpushFlagをfalseにして、次に押されたときtrueを返せるようにします。
GameControl(){
注目してほしいのは後半です。
GameControlクラスのコンストラクタでブロックやプレイヤー、ボールを実体化させています。
GameControlが実体化するのと同時にゲームのオブジェクトも実体化されるので、間違いがなくていいですね。
同時に、WinMainで実体化するのはGameControlだけでいいということでもあります。
~GameControl(){
新しい記号が出てきましたね。
これはコンストラクタに対応したデストラクタと呼ばれるもので、そのオブジェクトがdelete演算して消去される時に実行される処理を定義できます。
今回はコンストラクタで実体化させたものを全てdeleteしています。
よって、WinMainではGameControlのポインタをdeleteするだけでよくなりますね。
void Title(){
タイトル画面時の処理をまとめています。
先程解説したPushSpace関数がtrueを返したら、int型の変数stateを1にしています。
このstateはGameControlのメンバ変数です。ゲームの状態を管理するための番号として設定しました。
0の時にタイトル画面、1ならゲーム画面、2はゲームオーバーで3はゲームクリアです。
GameControlのAll()では、この値を参照してどの関数を実行するかを判断しています。
なお、タイトル画面の画像にはこちらをお使いください。
void Game(){
state == 1の時に実行されるゲーム本編の処理です。
基本的に前回と変わっていませんが、タイトルとゲームオーバー、ゲームクリア時にはstateを変更して別の関数に移行させるので、幾分処理が簡単になったかと思います。
void GameOver(){
ゲームオーバーの画像はこちらです。
void GameClear(){
ゲームクリアの画像はこちらです
void All(){
この関数が、stateに応じてゲームの場面を切り替えています。
WinMainで実行するのはこの関数だけでよいということです。
SetGraphMode(WINDOW_X,WINDOW_Y,32);
随分短くなったWinMainにも、新しい要素が入っています。
これはウィンドウのサイズを変更する関数です。引数のウィンドウサイズは、冒頭でマクロ定義した値になります。
さて、今まで長々と処理を書いていたwhile文ですが、どうでしょう。わずか1行でメインの処理が終わってしまいました。
このようにクラスを有効活用することで、プロジェクトの規模が大きくなっていっても管理がしやすくなるんです。
C/C++でブロック崩しを作る連載はひとまずここまでにしますが、このゲームにはまだ改良の余地がありますね。
いくつかのステージ構成にして、ブロックの数を変化させたり、一度球が当たっただけでは壊れないブロックというのも面白そうです。