ぶーーーーん

OpenGLで作るiPhone SDKゲームプログラミング - 4章

レースゲーム(2D)

この第4章では、前のハエたたきと違って、ユーザー操作を主体としたゲームアプリの基盤を作りました。
左下の左右ボタンがハンドル操作、右下のアクセルボタンで加速し、慣性が働くようになっています。

ここ数回に渡って、ごちゃごちゃと日記を書いていく中で、ほぼ4章の終わりまでの作り込みができました。

テクスチャの読み込み
車クラスの実装
チェックポイントの表示
周回数とラップタイムの表示
パーティクルの表示


これを制御するゲームクラスと、レンダラーなどの上位クラスを用意するだけで完成。

ゲームクラスの定義

#define CHECK_POINT_NUM 8

class CpGameFacilitator
{
    typedef std::vector<CpCheckPoint*> CheckPointArray;

public:
    CpGameFacilitator();
    ~CpGameFacilitator();
    
    CpCar& GetCar();

    void SetLeftOn();
    void SetLeftOff();
    void SetRightOn();
    void SetRightOff();
    void SetAccelOn();
    void SetAccelOff();
    
    void Start();
    void Update();
    
    CpTextureObjectsDrawer& SetCheckPointsToDrawer(CpTextureObjectsDrawer&);
    CpTextureObjectsDrawer& SetNumbersToDrawer(CpTextureObjectsDrawer&);
    CpTextureObjectsDrawer& SetParticleToDrawer(CpTextureObjectsDrawer&);
    
private:
    CpCar _car;                          // ユーザーが操作する車
    CpLapCounter _lap;                   // 周回管理オブジェクト
    CpParticleSystem _particle;          // パーティクルシステムオブジェクトポインタ
    CheckPointArray _checkPoints;        // チェックポイント
    int _nextCheckPointIndex;            // 次のチェックポイントのインデックス
};

// 車の軌跡を表示するパーティクル部品クラス
class CpCarOrbit : public CpParticle
{
    typedef CpParticle inherited;
    
public:
    // 生成
    CpCarOrbit(float x, float y) : inherited(30)
    {
        // x, y座標は車の座標
        _x = x, _y = y;
        
        // それ以外は自身で決定する
        _width = _height = 0.05f;
        _mx = (RANDF() - 0.5f) * 0.002f;
        _my = (RANDF() - 0.5f) * 0.002f;
    };

    // 1フレームあたりの更新処理
    void Move()
    {
        // 最初に決定したベクトルに従って移動
        _x += _mx;
        _y += _my;
        
        // 残り寿命に応じてフェードイン、アウト
        float lifePos = (float)_frame / (float)_life;
        
        if (0.5f >= lifePos)
        {
            _a = (int)(round(lifePos * 2.0f * 255.0f));
        }
        else
        {
            _a = 255 - (int)(round(lifePos * 255.0f));
        }
    }
    
private:
    float _mx, _my;    // 1フレームあたりの移動量
};

ゲームクラスの初期化

CpGameFacilitator::CpGameFacilitator() :
_particle(30, [&]()->CpParticle*{return new CpCarOrbit(_car.GetPosX(), _car.GetPosY());})
{
    // チェックポイントを初期化する
    _checkPoints.push_back(new CpCheckPoint(0.75f, -0.5f, 0.2f));
    _checkPoints.push_back(new CpCheckPoint(0.4f, -0.25f, 0.2f));
    _checkPoints.push_back(new CpCheckPoint(0.0f, 0.8f, 0.2f));
    _checkPoints.push_back(new CpCheckPoint(-0.85f, 0.5f, 0.2f));
    _checkPoints.push_back(new CpCheckPoint(-0.3f, 0.3f, 0.2f));
    _checkPoints.push_back(new CpCheckPoint(-0.9f, -0.2f, 0.2f));
    _checkPoints.push_back(new CpCheckPoint(-0.8f, -0.9f, 0.2f));
    _checkPoints.push_back(new CpCheckPoint(0.0f, -0.9f, 0.2f));
    
    _nextCheckPointIndex = 0;
    CpCheckPoint& cp = *_checkPoints[_nextCheckPointIndex];
    cp.SetNext(true);
    
    _car._posX = _checkPoints[CHECK_POINT_NUM - 1]->_x;
    _car._posY = _checkPoints[CHECK_POINT_NUM - 1]->_y;
    
    // パーティクルシステムを初期化する
    _particle.Alloc();
}

ゲームクラスの1フレームごとの更新処理

void CpGameFacilitator::Update()
{
    // ラップタイムを計測する
    _lap.Update();

    // 車の現在位置にパーティクルを発生させる
    _particle.Generate();
    
    // パーティクルを更新する
    _particle.Update();
    
    // 車を1コマ分動かす
    _car.Move();
    
    // チェックポイントの通過をチェックする
    CpCheckPoint& nextCheckPoint = *_checkPoints[_nextCheckPointIndex];
    float dx = _car.GetPosX() - nextCheckPoint.GetX();
    float dy = _car.GetPosY() - nextCheckPoint.GetY();
    
    // 処理負荷軽減のため、平方根を取らずに、対象半径の2乗の値と比較する
    float distance = dx * dx + dy * dy;
    
    if (nextCheckPoint.GetRadius() * nextCheckPoint.GetRadius() >= distance)
    {
        // チェックポイント通過
        nextCheckPoint.SetNext(false);
        ++ _nextCheckPointIndex;
        
        if (CHECK_POINT_NUM <= _nextCheckPointIndex)
        {
            _lap.Round();
            _nextCheckPointIndex = 0;
        }
        
        _checkPoints[_nextCheckPointIndex]->SetNext(true);
    }
}

レンダラ

-(void)renderMain {
    CpGameFacilitator& game = *_game;
    CpTextureObjectsDrawer drawer;

    // ゲーム時間を1コマ進める
    game.Update();
    
    // コースを描画する
    _bg.Draw(_courseTexture.raw);
    
    // チェックポイントを描画する
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    game.SetCheckPointsToDrawer(drawer).Draw(_checkPointTexture.raw);
    glDisable(GL_BLEND);
    
    // 車の軌道を描画する
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    game.SetParticleToDrawer(drawer).Draw(_particleTexture.raw);
    glDisable(GL_BLEND);
    
    // 車を描画する
    glPushMatrix();
    glTranslatef(game.GetCar().GetPosX(), game.GetCar().GetPosY(), 0.0f);
    glRotatef(game.GetCar().GetAngle(), 0.0f, 0.0f, 1.0f);
    glScalef(game.GetCar().GetSize(), game.GetCar().GetSize(), 1.0f);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    game.GetCar().Draw([_carTexture raw]);
    
    glDisable(GL_BLEND);
    glPopMatrix();
    
    // ゲーム状態の数値を画面に表示する
    game.SetNumbersToDrawer(drawer).Draw(_numberTexture.raw);
}

意外と面倒だったのが、ラップタイムの計測処理でした。
書籍のサンプルではNSDateを使っていましたが、今回私は、ゲームクラスを始めとしてレンダラ配下にあるすべてのクラスをC++で実装したので、ラップタイムの計測、表示処理を自力でコーディングする必要がありました。
Cでのミリ秒取得関数はこちらのメモにまとめました。

【ミリ秒を3桁で取得】

// 引数でマイクロ秒を受け取り、ミリ秒の上3桁を返却する関数
int MicroSecondsToMillis(__darwin_suseconds_t t)
{
    // マイクロ秒 → ミリ秒
    int milli = t * 1000;
    
    // ミリ秒の桁数を数える
    int milliFigures = 1;
    
    for (int i = 10; 0 != milli / i; i *= 10)
    {
        ++ milliFigures;
    }
    
    // ミリ秒の上3桁を取得する
    int milli3 = 0;
    
    for (int i = 2; 0 <= i; -- i)
    {
        // 先頭1桁の値
        int n = (int)(milli / pow(10, milliFigures - 1));
        
        // 桁調整して結果に加算
        milli3 += n * pow(10, i);
        
        // 取得した桁の値を元の値から減算
        milli -= n * (int)pow(10, milliFigures - 1);
        
        // 桁数調整
        -- milliFigures;
    }
    
    return milli3;
}


う〜ん、ここは素直に秒のfloatに変換して、

int milli = (int)floor(lapTime * 1000) % 1000;

と3桁にしちゃえば良かったんですね。
ちょっとこの発想が出て来なかったので、上記のようなガチ手続きを書いてしまいました。




さて、一応見た目はレースゲームっぽいのですが、まだ当たり判定も何もない状態です。
次回、第5章は「衝突判定と衝突応答」とのことです。
楽しみです。