SHOT Object Class(list) で弾丸を一括管理



STL(Standard Template Library) の list を使って SHOT Object Class を作成します。
SHOT Class で「連射弾/円形弾/誘導弾/渦巻き弾」を発射します。

前田稔(Maeda Minoru)の超初心者のプログラム入門

プログラムの説明

  1. SHOT Object Class のファイル(Shot.h Shot.cpp)をプロジェクトに格納して下さい。
    Shot.h, Shot.cpp は、このページの説明を参考にして各自で作成して下さい。
    My.lib に登録された Object Class を使うので my.lib をリンクして下さい。
    弾の描画は IMAGE Object Class を継承した SHOT Object Class のメンバー関数を使っています。
    ID_TIMER は弾丸を移動するタイマのIDで、ID_TIMER1 は弾を登録するタイマのIDです。
    *Back は BACKBUF Object Class の定義です。
    *Shot は SHOT Object Class の定義です。
    xp,yp は目標となる標的の座標で、矢印キーの操作で移動します。
    弾丸の発射座標は、画面中央に設定しています。
    発射された弾丸が矩形構造体(rc) の外に出ると list から削除します。
        /*************************************/
        /*★ STL で弾を管理する    前田 稔 ★*/
        /*************************************/
        #include    <windows.h>
        #include    "shot.h"
        #define     ID_TIMER    (WM_APP + 0)
        #define     ID_TIMER1   (WM_APP + 1)
        #pragma     once
        #pragma     comment(lib,"my.lib")
    
        BACKBUF     *Back= NULL;        //Back Buffer Object
        SHOT        *Shot= NULL;        //Shot Object Class
        float       xp,yp;              //目標の座標(発射は画面中央)
        RECT        rc= { 5, 5, 780, 580 };
        
  2. WM_CREATE: で Object を生成して弾の画像を入力します。
    tama4.gif が弾丸の画像で、4種類の色が異なる画像が横方向に並んでいます。
    弾丸の移動と弾を発射するタイマの間隔を設定します。
            case WM_CREATE:
                Back= new BACKBUF(hWnd);
                Shot= new SHOT(hWnd);
                Shot->LoadFile("tama4.gif",4,1);
                xp=yp= 50.0f;
                SetTimer(hWnd,ID_TIMER,10,NULL);
                SetTimer(hWnd,ID_TIMER1,200,NULL);
                break;
        
  3. WM_TIMER: には二種類のタイマがポストされます。
    ID_TIMER でキー操作による目標の移動と弾丸の座標の更新を行います。
    Homing() は誘導弾の軌道を目標に向けて再計算する関数です。
    2 が誘導弾の種別で xp,yp が目標の座標で 0.3f が移動速度です。
    Rot() は渦巻き状に回転する弾丸の座標を計算する関数です。
    3 が渦巻き弾の種別で 5.0f が回転角度で 400.0f,300.0f が回転軸の中心です。
    Move() 関数が弾の座標を更新する関数で、8.0f が弾丸の移動速度です。
            case WM_TIMER:
                if (wParam == ID_TIMER)
                {   if (GetAsyncKeyState(VK_ESCAPE)<0)  PostMessage(hWnd,WM_CLOSE,0,0);
                    if (GetAsyncKeyState(VK_UP)<0)      yp-= 2;   //目標の移動
                    if (GetAsyncKeyState(VK_DOWN)<0)    yp+= 2;
                    if (GetAsyncKeyState(VK_LEFT)<0)    xp-= 2;
                    if (GetAsyncKeyState(VK_RIGHT)<0)   xp+= 2;
                    Shot->Homing(2,xp,yp,0.3f);         //誘導弾の方向を修正する
                    Shot->Rot(3,5.0f,400.0f,300.0f);    //list に登録された弾の回転
                    Shot->Move(8.0f,&rc);               //list に登録された弾の移動
                }
        
  4. ID_TIMER1 でキーの押し下げを調べて list に弾丸を登録します。
    Add() が登録する関数で「弾の種別/発射座標/目標座標」を引数として渡します。
    AddRot() は誘導弾を登録する関数で、弾の種別と発射座標を引数として渡します。
    Circle() は円形弾を発射する関数で、一度に同心円状に16個の弾を登録します。
    WM_PAINT: から描画するとチラツクので WM_TIMER: から ShotView() で直接描画します。
                if (wParam == ID_TIMER1)                //弾を登録する
                {   if (GetAsyncKeyState(VK_NUMPAD0)<0) Shot->Add(0,400.0f,300.0f,xp,yp);
                    if (GetAsyncKeyState(VK_NUMPAD1)<0) Shot->Circle(1,400.0f,300.0f);
                    if (GetAsyncKeyState(VK_NUMPAD2)<0) Shot->Add(2,400.0f,300.0f,xp,yp);
                    if (GetAsyncKeyState(VK_NUMPAD3)<0) Shot->AddRot(3,400.0f,300.0f);
                }
                ShotView();
                break;
        
  5. WM_DESTROY: ではタイマを止めてオブジェクトを開放して下さい。
            case WM_DESTROY:
                KillTimer(hWnd,ID_TIMER1);
                SAFE_DELETE(Shot);
                SAFE_DELETE(Back);
                PostQuitMessage(0);
                return 0L;
        
  6. 画面を描画する ShotView() 関数です。
    BackBuf を LTGRAY_BRUSH でクリアして、目標座標と発射座標に円を描画しています。
    本来なら自機や敵機を描画するのでしょうが、プログラムを解かりやすくするために円にしました。
    View() が list に登録された弾丸を描画する関数です。
        void  ShotView()
        {   HBRUSH      hOldBrush;
            Back->Clear((HBRUSH)GetStockObject(LTGRAY_BRUSH));
            hOldBrush= (HBRUSH)SelectObject(Back->hBackDC,GetStockObject(WHITE_BRUSH));
            Ellipse(Back->hBackDC,xp,yp,xp+20,yp+20);       //目標座標
            SelectObject(Back->hBackDC,GetStockObject(BLACK_BRUSH));
            Ellipse(Back->hBackDC,400,300,440,340);         //発射座標
            SelectObject(Back->hBackDC,hOldBrush);
            Shot->View(Back->hBackDC);
            Back->Blt();
        }
        
  7. WinMain() ではウインドウサイズを 800*600 に設定して、何時もと同じように表示します。

SHOT Object Class

  1. SHOT Object Class のヘッダファイルです。
    IMAGE Object Class を継承しています。
    構造体の説明と STL の基礎は list を使って弾丸の動きを管理する を参照して下さい。
        /*★ SHOT Object Header File   前田 稔 ★*/
        #include    <list>
        #include    "my.h"
        typedef struct
        {   int     n;                  //弾の種別
            float   x,y;                //弾の座標
            float   dx,dy;              //弾の移動量
        }   SHSTR;
    
        class SHOT : public IMAGE
        { protected:
            std::list             v;
            std::list::iterator   p,q;
            SHSTR   wk;
    
          public:
            SHOT(HWND hWnd);            //Constructor
            ~SHOT();                    //Destructor
            void    View(HDC hdc);
            void    Move(float sp, RECT *rc);
            void    Add(int no,float x,float y,float xp,float yp);
            void    AddRot(int no,float x,float y);
            void    Circle(int no,float x,float y);
            void    Homing(int no,float xp,float yp,float speed);
            void    Rot(int no,float rt,float cx,float cy);
        };
        
  2. View は list に登録された弾丸を描画するメンバ関数で、IMAGE Object Class の関数を呼び出します。
    n が弾(画像)の種別で、x,y が描画する座標です。
  3. list に登録された弾の移動を行うメンバ関数です。
    sp は弾丸の移動速度です。実際にプレイしながら調整して下さい。
    *rc の矩形領域から外に出た弾丸は削除しています。
        void  SHOT::Move(float sp, RECT *rc)
        {
            for(p=v.begin(); p!=v.end(); p++)
            {   p->x+= p->dx*sp;
                p->y+= p->dy*sp;
                if(p->x < rc->left || p->x > rc->right ||
                   p->y < rc->top  || p->y > rc->bottom)
                {   q= p;
                    p--;
                    v.erase(q);
                }
            }
        }
        
  4. 一般的な弾丸を登録する Add() 関数です。
    no が弾丸の種別で x,y が弾丸を発射する座標で xp,yp が目標の座標です。
    d に目標までの距離を求めて、dx,dy に移動係数を設定して list に登録します。
        void  SHOT::Add(int no,float x,float y,float xp,float yp)
        {   float   d;
            ZeroMemory(&wk,sizeof(wk));
            d= (float)sqrt((x-xp)*(x-xp)+(y-yp)*(y-yp));
            wk.x= x;
            wk.y= y;
            wk.dx= (xp-x)/d;
            wk.dy= (yp-y)/d;
            wk.n= no;
            v.push_back(wk);
        }
        
  5. 渦巻き弾を登録する AddRot() 関数です。
    no が弾丸の種別で x,y が弾丸を発射する座標です。
    渦巻き弾の軌道は Rot() 関数で計算するので dx, dy はゼロになっています。
        void  SHOT::AddRot(int no,float x,float y)
        {   ZeroMemory(&wk,sizeof(wk));
            wk.x= x;
            wk.y= y-20;
            wk.dx= 0.0f;
            wk.dy= 0.0f;
            wk.n= no;
            v.push_back(wk);
        }
        
  6. 円形弾を登録する Circle() 関数です。
    no が弾丸の種別で x,y が弾丸を発射する座標です。
    x,y を中心に16個の回転座標を求めて list に登録します。
    list の登録が終われば、後は dx,dy に従って弾を移動するだけです。
        void  SHOT::Circle(int no,float x,float y)
        {   int     i;
            float   rad,rad_step;
    
            rad_step= 6.28f/16.0f;
            ZeroMemory(&wk,sizeof(wk));
            wk.x= x;
            wk.y= y;
            wk.n= no;
            for (i=0, rad=0.0f; i<16; i++,rad+=rad_step)
            {   wk.dx= cos(rad);
                wk.dy= sin(rad);
                v.push_back(wk);
            }
        }
        
  7. 誘導弾の方向を修正する Homing() 関数です。
    no が弾丸の種別で xp,yp が目標の座標で speed が移動速度です。
    誘導弾に通常の速度で追跡されると避け切れないので、speed で調整できるようにしています。
    誘導弾が目標に衝突すると爆発するのでしょうが、今回は list から削除しています。
    また何時までも追撃を許すとゲームにならないので、ある一定の角度以上は誘導できないようにします。
    次の行をコメントにして試してみて下さい。
    if(rx<10.0f && rx>0.1f && ry<10.0f && ry>0.1f)
    10.0f や 0.1f の値を調整して、最適な値を求めて下さい。
    この値は、描画サイクルや移動速度にも関連するので、総合的な調整が必要です。
        void  SHOT::Homing(int no,float xp,float yp,float speed)
        {   float   d,wx,wy,rx,ry;
    
            for(p=v.begin(); p!=v.end(); p++)
            {   if(p->n==no)
                {   d= (float)sqrt((p->x-xp)*(p->x-xp)+(p->y-yp)*(p->y-yp));
                    if(d<5.0)                   //目標に衝突
                    {   q= p;
                        p--;
                        v.erase(q);
                        continue;
                    }
                    wx= (xp-p->x)/d;            //目標の方向を求める
                    wy= (yp-p->y)/d;
                    rx= wx/p->dx;
                    ry= wy/p->dy;
                    if(rx<10.0f && rx>0.1f && ry<10.0f && ry>0.1f)
                    {   p->dx= wx*speed;
                        p->dy= wy*speed;
                    }
                }
            }
        }
        
  8. 渦巻き弾の座標を計算する Rot() 関数です。
    no が弾丸の種別で rt が回転角度で cx,cy が回転軸の中心です。
    1.01f が渦巻きの広がり係数で、これを削除すると円状に回転が続きます。
    1.01f の値を調整して、最適な値を求めて下さい。
    大きくカーブするような弾丸の動きを設定することも出来ます。
    この値は、描画サイクルや回転角度にも関連します。
        void  SHOT::Rot(int no,float rt,float cx,float cy)
        {   float   px,py;
    
            for(p=v.begin(); p!=v.end(); p++)
            {   if(p->n==no)
                {   px= (p->x-cx)*1.01f;
                    py= (p->y-cy)*1.01f;
                    p->x= float(px*cos(rt/180*3.14)-py*sin(rt/180*3.14)+cx);
                    p->y= float(px*sin(rt/180*3.14)+py*cos(rt/180*3.14)+cy);
                }
            }
        }
        

【演習】

  1. SHOT Object Class を作成してプログラムを完成させて下さい。
  2. 欲張らずに一種類ずつプログラムして下さい。
  3. 渦巻き弾では回転軸の中心座標と回転角度と広がり係数を構造体に持たすことも考えられます。
    本来なら回転軸の中心はシップを発射した座標で、シップを移動しながら発射すると個々の弾ごとに中心座標が変わるのが本当でしょう。
  4. 誘導弾は連射されるときついので、単発でミサイルのような形状が良いかも知れません。
    ミサイルのような形状の場合には、向きを変えて描画するために構造体に回転角度を持たせます。
    また忍者の手裏剣などでも回転角度を変化させて、回転しながら飛んでいく方が面白いでしょう。

超初心者のプログラム入門(Win32API C++)