ここまでの冒険を記録しますか?

長岡技大ソフトウェア開発サークルの開発ブログ

【ゲーム製作入門】C/C++でブロック崩しを作る⑥【DXライブラリ】

書き手:肥田野

プログラミング初心者向けに、簡単なゲームの制作を通してC/C++の基本を解説していきます。

当サークルに所属しない方でも参考にしていただければ幸いです。

この記事では前回の内容を理解していることを前提に進めていきます。

前回は配列を使ってブロックの生死を判別しましたが、今回は「クラス」を使うことでよりブロックを管理しやすくしてみます。

またクラスと同時に「関数」も扱っていきます。ただし今回は関数のメリットをあまり生かし切れていないので、「こういう役割があるんだな」ぐらいの認識で大丈夫です。

関数の機能に関しては、また別の機会に詳しく解説したいと思います。

それでは今回解説するコードです。

#include "DxLib.h"
//new
class Block{
public:
    int blockX,blockY;
    int width,height;
    bool live;

    void View(){
        if(live){
            DrawBox(blockX,blockY,blockX+width,blockY+height,GetColor(255,255,255),TRUE);
        }
    }

    void liveControl(int targetX,int targetY,int targetVec){
        if(targetX>blockX && targetX<blockX+width && targetY > blockY && targetY < blockY+height){
            live = false;
        }
        if(targetVec == 0 && CheckHitKey(KEY_INPUT_SPACE)){
            live = true;
        }
    }

    Block(int setX,int setY){
        blockX = setX;
        blockY = setY;
        width = 90;
        height = 20;
        live = true;
    }
};
//newここまで

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
                        LPSTR lpCmdLine, int nCmdShow ){
    ChangeWindowMode(TRUE);
    if( DxLib_Init() == -1 )return -1 ;
    SetDrawScreen(DX_SCREEN_BACK);
    
    int ballX = 320,ballY = 240;//new
    int vecX = 0,vecY = 0;
    int px = 320,py = 400;

    Block* bl0;//new
    bl0 = new Block(80,100);//new
    Block* bl1;
    bl1 = new Block(200,100);

    while(ProcessMessage() != -1){
        int startTime = GetNowCount();
        ScreenFlip();
        ClearDrawScreen();
        
        DrawCircle(ballX,ballY,20,GetColor(255,255,255),TRUE);
        DrawBox(px,py,px+60,py+20,GetColor(255,255,255),TRUE);

        bl0->View();//new
        bl0->liveControl(ballX,ballY,vecX);//new
        bl1->View();
        bl1->liveControl(ballX,ballY,vecX);

        if(vecX != 0 && vecX != 0){
            if(ballX>640)vecX = -1;
            if(ballX<0)vecX = 1;
            if(ballY<0)vecY = 1;
            if(ballX>px && ballX<px+60 && ballY > py)vecY = -1;
            if(ballY>480){
                ballX = 320;
                ballY = 240;
                vecX = 0;
                vecY = 0;
            }
        }else{
            DrawFormatString(260,160,GetColor(255,255,255),"PUSH SPACE");
            if(CheckHitKey(KEY_INPUT_SPACE)){
                vecX = 1;
                vecY = 1;
            }
        }
        if(!bl0->live && !bl1->live){//new
            DrawFormatString(260,120,GetColor(255,255,255),"GameClear!");
            ballX = 320;
            ballY = 240;
            vecX = 0;
            vecY = 0;
        }

        ballX += 5*vecX;
        ballY += 5*vecY;

        if(CheckHitKey(KEY_INPUT_RIGHT) == 1){
            px += 10;
        }
        if(CheckHitKey(KEY_INPUT_LEFT) == 1){
            px -= 10;
        }
        
        if(CheckHitKey(KEY_INPUT_ESCAPE) == 1)break;
        int endTime = GetNowCount();
        WaitTimer((1000/60)-(endTime-startTime));
    }

    delete bl0;//new
    delete bl1;
    
    DxLib_End() ;
    return 0 ;
}

今回は解説が長くなりそうなので、落ち着いてゆっくり読み進めていってください。

class Block{

そもそもクラスとは何ぞやという話です。これは様々な例えがあるのですが、ここではスタンプを例にとって解説します。

一つのスタンプがあれば、同じ模様のイラストや文章をいくつも複製できますね。ソフトウェア上においても、クラスを使うことで「同じ役割を持つ物体(オブジェクト)」をいくつも作ることができるんです。

その原型となるのがクラスであり、クラスを宣言することにより、プログラムの実行中好きなタイミングでオブジェクトを生成することができるようになります。

プログラムは基本的に上から下へと実行されていくので、クラスを定義するのはWinMain()よりも上になります。

ちなみにクラスを定義するときは、最後の「}」の後ろに「;」をつけるのを忘れないようにしてください。

WinMainの最後は「;」がついていませんね。つまり「};」と書く理由は、それがクラスであると明確に判断させるためなんです。

public:

一応意味としては「どこからでもアクセスできる」ということですが、今は深く考えなくても大丈夫です。

この仲間に「private」と「protected」があるのですが、悩んだらpublicにするくらいの考えで何とかなるでしょう。

また末尾につけるのは「;」ではなく「:」なので注意です。

int blockX,blockY;

今回はブロックをクラスにして扱うので、X座標とY座標、ついでに幅(width)と高さ(height)の変数も宣言し、bool型での生死判定も用意します。

ここで気を付けなければならないのは、クラス内での変数(メンバと呼ぶこともあります)は、宣言することができても定義(例:blockX = 320)することはできません。

クラスはあくまで設計図であり、それを実行するのは(今回の場合)WinMainです。なのでクラスから生成されたオブジェクトの変数(blockXなど)を扱いたいときは、クラスの外側でしか扱えません。

でもよく見ると疑問がありますね。もう少し下のほうで、「blockX = setX」と代入をしています。

このあたりの疑問の解は後述しますので、ここでは気にしすぎず読み進めてください。

void View(){

これが今回初めて使う「関数」です。

voidは型の一種で、intやboolの代わりに使っています。もちろんvoidではなくint等も使うことができますが、それはまた後日ということで……

関数も変数と同じように型+名前で宣言・定義しますが、名前の後ろに「()」がついていますね。

これが関数であるという証です。WinMainやif文、for文にも()がついていますね。これも関数なんです。

関数の役割は、呼び出されたときに「{}」で囲まれた作業が実行されるということです。

主に何度も繰り返す作業(マウスのクリック判定など)を関数にまとめることで、WinMainやその中のwhile文(メインループとも呼びます)などを読みやすくすることができます。

ここではliveがtrueの時に画像を表示するだけですが、この役割はメインループが各画像を表示するより、ブロック自身が自分の生死を判定して画像を表示してくれた方が直観的かなと思い定義しました。

なお、関数も定義しただけでは自分から動いたりしないので、その関数を使いたいときに「呼び出し」を行う必要があります。

呼び出しについても後述します。

※コメントにてご指摘をいただきました。if文やfor文は()や{}が付いていますが「関数」ではありません。プログラムが実行される順番を指で追っていくと、if文は{}で定義した処理をその場で実行しているのに対し、関数は別の場所で定義した処理を呼び出しという形で実行しています。

void liveControl(int targetX,int targetY,int vec){

こちらもView()と同じく関数です。しかし、()の中にいろいろ書いてありますね。

これは「引数」と言って、関数を呼び出すときに、この引数に対応する値を設定させることで、関数に「その場の状況に応じた」処理をさせることができます。

引数はその関数の中だけで使える変数なので、型と名前が必要になります。

今回はtargetXとtargetYの表す座標がブロック内に侵入したとき、liveをfalseにしています。また、targetVec(今回はvecXを使っています)が0で、なおかつスペースキーが押されたとき、つまりゲーム開始時にliveをtrueにしています。

コードを読み進めると、このtargetXやtargetYには球の座標を代入していることが分かりますね。そうした場合は、球とブロックが接触したときにliveをfalseにするんです。

引数を使うメリットとして、例えばプレイヤーのバーが球をはじくだけでなく、ショットも撃てるように改造すると仮定しましょう。

弾の座標shotXとshotYを作って、ballXとballYの代わりにこの二つを代入すれば、ショットの当たったブロックが消えるようになります。

Block(int setX,int setY){

先ほど関数には型があるという説明をしましたが、この関数には型がありませんね。

これはクラスの内部でのみ許されている書き方で、主なルールは「型を書かない」「クラス名と同じにする」の二つです。

こうして定義した関数は「コンストラクタ」と呼んで区別します。

コンストラクタとは、そのクラスが実態を持ったとき、スタンプで言えば捺された瞬間に自動的に実行される関数です。

通常はここで、クラスの持つメンバ変数に値を代入していきます。

ここでも普通の関数同様に引数を使うことができるので、今回はブロックを配置する座標を引数で指定できるようにしました。

int ballX = 320,ballY = 240;

前回まではx,yと名前を付けていましたが、他のオブジェクトと区別しやすいように名前を変更しました。

Block* bl0;

先ほど触れた「実態化」をしているのが、こことその下の2行です。

この辺りを詳しく解説するにはメモリ領域の話から始めなければいけないため、今回はこういうものだと思って結構です。

一応名前だけ解説すると、クラスのポインタ変数になります。あっ逃げないで今日はポインタの話はしないから

WinMain内でブロックを扱うときは、bl0のblockX、live……と考えます。

bl0 = new Block(80,100);

先ほど宣言したbl0の中身を生成しているのがこの行です。クラス内のコンストラクタを呼び出していますね。

「new」についてもここでは触れません。クラスを実体化するときに必要なものだと思っておいてください。

Blockには引数setXとsetYが必要だと定義したので、それぞれ80と100を代入してやります。

これで、座標(80,100)にブロックが一つ生成されました。同じように(200,100)に二つ目のブロック「bl1」を生成しています。

bl0->View();

通常、関数を呼び出すときは関数名を書いて呼び出します。

しかし、クラス内で定義した関数の場合、どのオブジェクトの関数か区別がつかないため、bl0->View()と呼び出します。

「->」はアロー演算子と呼ばれるものですが、これも今は深い意味を考えずに矢印として扱って大丈夫です。

bl0やbl1というポインタ変数は、いくつも複製できるクラスのオブジェクトを識別するために用いるんですね。

if(!bl0->live && !bl1->live){//new

もちろん関数だけでなく、メンバ変数もアロー演算子を使って参照できます。

delete bl0;

これも詳しい意味を考え出すとメモリ領域の話に突入してしまうので、今は「newを使って作ったものはdeleteを使って消去する」と考えておいてください。

さて、駆け足で説明しましたが着いてこれたでしょうか(^_^;)

実はこのコード、クラスの実力をまだ出し切っていないんです。

前回解説した「配列」と、今回の「クラス」を組み合わせると……?

勘の良い人はもう気づいたかもしれませんね。というわけで次回は、配列を使ってオブジェクトをを自在に操る技を解説します。