まぁ!きれい!(感涙)

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

パーティクルシステム



第2章で作成したプロジェクトにパーティクルシステムを追加しました。

パーティクルとは粒子のこと。
細かいテクスチャを大量に描画することによって、煙や雪などの不定形な要素を描画する。

だそうです。
GPUに重い負荷をかけることになりますが、少しのコードでリッチな演出を実現できるので、便利ですね〜。
もっと違う画像を用意したり、動きの付け方に工夫が色々できそうですが、自分の頭で思い描いた通りに使いこなすのは難しそうだ・・・


パーティクルシステムを実装するときのポイントは、

  • パーティクルの数を適切に設定してやること
  • パーティクルの寿命を適切に設定してやること
  • 必要に応じてフレームごとに(あるいは数フレームに数個)パーティクルを追加していく
  • 演出に応じた動きをつけてやること

例えば、動き回るオブジェクトの座標にフレームごとにパーティクルを追加していけば軌跡みたいになるし、1箇所に多数のパーティクルをいっぺんに追加して、広がるような動きにしてやれば敵を倒したような演出になります。左右に多少ぶれながら下に落ちていくようにして、寿命を長めにすれば、雪みたいになるでしょう。

参考までに、インターフェイスObjective-C、中身は速度重視のためCで書いた実装です。
書籍のサンプル実装とはだいぶ違いますが、やってることはほぼ同じです。

Particle.h

typedef struct {
    float x, y, size;	// 位置、大きさ
    float mx, my;	// 移動量
    bool active;	// 活性フラグ
    int frame;		// 生成からのフレーム数
    int life;		// 寿命フレーム数
} PARTICLE;

// パーティクルクラス
@interface Particle : NSObject
{
    int _count;		    // 次回生成するパーティクルの数
    int _memct;		    // メモリ内に読み込まれているパーティクルの数
    int _life;		    // 1パーティクルの寿命フレーム数
    PARTICLE* _particles;   // パーティクル
}

@property(assign, nonatomic, readwrite) int count;
@property(assign, nonatomic, readwrite) int life;

// 初期化メソッド
-(id)init;

// 準備メソッド
-(void)prepare;

// パーティクル追加
-(void)addX:(float)x Y:(float)y size:(float)size moveX:(float)moveX moveY:(float)moveY;

// 描画メソッド
-(void)draw:(GLuint)texture;

Particle.mm

@implementation Particle

@synthesize count = _count;
@synthesize life = _life;

// 初期化メソッド
-(id)init {
    self = [super init];
	
    if (self) {
        _count = 0;
        _memct = 0;
        _particles = NULL;
    }
	
    return self;
}

// 破棄メソッド
-(void)dealloc {
    [self releaseParticles];
    [super dealloc];
}

// 準備メソッド
-(void)prepare {
    if (_particles) {
        [self releaseParticles];
    }
	
    _memct = _count;
    _particles = (PARTICLE*)malloc(sizeof(PARTICLE) * _memct);
	
    for (int i = 0; _memct > i; ++ i) {
        _particles[i].active = false;
    }
}

// パーティクル追加
-(void)addX:(float)x Y:(float)y size:(float)size moveX:(float)moveX moveY:(float)moveY {
    PARTICLE* p = _particles;
	
    // 非アクティブのパーティクルを1つアクティブにする
    for (int i = 0; _memct > i; ++ i) {
        if (!p->active) {
            p->active = true;
            p->x = x;
            p->y = y;
            p->size = size;
            p->mx = moveX;
            p->my = moveY;
            p->frame = 0;
            p->life = _life;

            break;
        }
		
        ++ p;
    }
}

// 描画メソッド
// ここでは移動制御、寿命管理と描画をひとまとめにしてしまっている。
-(void)draw:(GLuint)texture {
    // アクティブなパーティクルの情報を更新しながら、数を数える
    int activeParticleCount = 0;
    PARTICLE* p = _particles;
	
    for (int i = 0; _memct > i; ++ i) {
        if (p->active) {
            // パーティクルを移動
            p->x += p->mx;
            p->y += p->my;
			
            // 寿命更新
            p->frame ++;

            if (p->life <= p->frame) {
                p->active = false;
            }
            else {
                ++ activeParticleCount;
            }
        }
		
        ++ p;
    }
	
    // 頂点、色、マッピングのための配列を作成する
    // 正方形を2つの三角形で表すので、頂点の数は6×2次元となる
    GLfloat* vertices = (GLfloat*)malloc(sizeof(GLfloat) * 6 * 2 * activeParticleCount);
	
    if (!vertices) {
        NSLog(@"failed to memory allocation.");
        return;
    }
	
    GLubyte* colors = (GLubyte*)malloc(sizeof(GLubyte) * 6 * 4 * activeParticleCount);

    if (!colors) {
        free(vertices);
        NSLog(@"failed to memory allocation.");
        return;
    }
	
    GLfloat* coords = (GLfloat*)malloc(sizeof(GLfloat) * 6 * 2 * activeParticleCount);
	
    if (!coords) {
        free(vertices);
        free(colors);
        NSLog(@"failed to memory allocation.");
        return;
    }
	
    // 高速にイテレーションするためのポインタ変数を用意する
    p = _particles;		// すべてのパーティクル構造体へのポインタ
    GLfloat* vp = vertices;	// アクティブなパーティクルの頂点座標へのポインタ
    GLubyte* lp = colors;	// アクティブなパーティクルの色情報へのポインタ
    GLfloat* rp = coords;	// アクティブなパーティクルのテクスチャマッピング座標へのポインタ
	
    // 座標配列を構築する
    for (int i = 0; _memct > i; ++ i) {
        if (p->active) {
            // 座標計算
            float centerX = p->x;
            float centerY = p->y;
            float size = p->size;
            float vLeft = -0.5f * size + centerX;
            float vRight = 0.5f * size + centerX;
            float vTop = 0.5f * size + centerY;
            float vBottom = -0.5f * size + centerY;
			
            // ポリゴン1
            *vp++ = vLeft;  *vp++ = vTop;	// 左上座標
            *vp++ = vRight; *vp++ = vTop;	// 右上座標
            *vp++ = vLeft;  *vp++ = vBottom;	// 左下座標
			
            // ポリゴン2
            *vp++ = vRight; *vp++ = vTop;	// 右上座標
            *vp++ = vLeft;  *vp++ = vBottom;	// 左下座標
            *vp++ = vRight; *vp++ = vBottom;	// 右下座標
			
            // 現在のフレームがパーティクル寿命のどの段階にあるのかを判断
            float lifePos = (float)p->frame / (float)p->life;
			
            int alpha;
			
            // 寿命が半分以上ある場合はフェイドイン
            if (0.5f >= lifePos) {
                alpha = (int)(round(lifePos * 2.0f * 255.0f));
            }
            // 半分以下の場合はフェイドアウト
            else {
                alpha = 255 - (int)(round(lifePos * 255.0f));
            }

            // 色(6頂点分)
            // for文よりもべた書きの方が若干だが速い・・・はず
            *lp++ = 255; *lp++ = 255; *lp++ = 255; *lp++ = alpha;
            *lp++ = 255; *lp++ = 255; *lp++ = 255; *lp++ = alpha;
            *lp++ = 255; *lp++ = 255; *lp++ = 255; *lp++ = alpha;
            *lp++ = 255; *lp++ = 255; *lp++ = 255; *lp++ = alpha;
            *lp++ = 255; *lp++ = 255; *lp++ = 255; *lp++ = alpha;
            *lp++ = 255; *lp++ = 255; *lp++ = 255; *lp++ = alpha;

            // マッピング座標
            *rp++ = 0.0f; *rp++ = 0.0f;		// ポリゴン1 - 左上
            *rp++ = 1.0f; *rp++ = 0.0f;		// ポリゴン1 - 右上
            *rp++ = 0.0f; *rp++ = 1.0f;		// ポリゴン1 - 左下
            *rp++ = 1.0f; *rp++ = 0.0f;		// ポリゴン2 - 右上
            *rp++ = 0.0f; *rp++ = 1.0f;		// ポリゴン2 - 左下
            *rp++ = 1.0f; *rp++ = 1.0f;		// ポリゴン2 - 右下
        }
		
        ++ p;
    }
	
    // OpenGLに描画を指示する
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, texture);
    glVertexPointer(2, GL_FLOAT, 0, vertices);
    glEnableClientState(GL_VERTEX_ARRAY);
    glColorPointer(4, GL_UNSIGNED_BYTE, 0, colors);
    glEnableClientState(GL_COLOR_ARRAY);
    glTexCoordPointer(2, GL_FLOAT, 0, coords);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
	
    glDrawArrays(GL_TRIANGLES, 0, activeParticleCount * 6);
	
    // メモリ解放
    free(vertices);
    free(colors);
    free(coords);
}

// パーティクル解放
-(void)releaseParticles {
    if (_particles) {
        free(_particles);
        _particles = NULL;
    }
	
    _memct = 0;
}

@end

そんなわけで、ついにハエたたきゲーム完成しました!
ハエの軌跡は白、叩いたときのエフェクトは青にしてみました。

このゲーム面白い!