パーティクルでリアルな表現

パーティクルというCGの技術があります。 炎や水といった自然界の曖昧なものを、小さな粒子の集合によって表現する手法です。

これも物理シミュレーションと同じく自分で実装しようとすると大変ですが、Sprite Kitにはこの機能も専用エディタと共にビルトインされています。

早速、炎を表示してみましょう。

New File…からSpriteKit Particle Fileを選択します。


SpriteKit Particle File

Particle templateFireを利用します。


Fire

これをfire.sksとして保存し、Xcodeで開くと以下のようなエディタが表示されます。


Particle Emitter Editor

Particle Emitter Editor上では、パーティクルをGUIで様々にカスタマイズ可能です。例えばColor Rampを修正するだけで、青い炎を作ることができます。


青い炎

sksファイルを使ってパーティクルを画面に表示するには、SKEmitterNodeを利用します。 SJParticleSceneを作成し、以下のようなコードを記載しましょう。

- (void)createSceneContents {
    NSString *firePath = [[NSBundle mainBundle] pathForResource:@"fire" ofType:@"sks"];
    SKEmitterNode *fire = [NSKeyedUnarchiver unarchiveObjectWithFile:firePath];
    fire.position = CGPointMake(30.0f, 30.0f);
    fire.xScale = fire.yScale = 0.5f;
    [self addChild:fire];
}


青い炎

たったこれだけで、リアルな炎を表示することができました。

ただ、表示するだけではありがたみが少ないので、ボールに剣があったら爆発して消えるようにしてみましょう。

まずはボールをいくつか配置し、重力を反転させて、浮かべておきます。

- (void)createSceneContents {
    /* 省略 */

    self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
    self.physicsWorld.gravity = CGVectorMake(0, self.physicsWorld.gravity.dy * -1.0f);

    for (int i = 0; i < 30; i++) {
        [self addChild:[self newBall]];
    }
}

- (SKNode *)newBall {
    SKShapeNode *ball = [SKShapeNode node];
    CGMutablePathRef path = CGPathCreateMutable();
    CGFloat r = skRand(3, 30);
    CGPathAddArc(path, NULL, 0, 0, r, 0, M_PI * 2, YES);
    ball.path = path;
    ball.fillColor = [SKColor colorWithRed:skRand(0, 1.0f) green:skRand(0, 1.0f) blue:skRand(0, 1.0f) alpha:skRand(0.7f, 1.0f)];
    ball.strokeColor = [SKColor clearColor];
    ball.position = CGPointMake(skRand(0, self.frame.size.width), skRand(0, self.frame.size.height));
    
    ball.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:r];
    
    return ball;
}


ボール

ここに剣を飛ばしてボールを破壊してみましょう。

剣の飛ばし方としては、SKActionのmoveTo:durationを使うことも考えられますが、ここでは重力を無視しておいて、力を加えることで飛ばしています。 affectedByGravityが重力の影響を受けるかどうか、velocityが加える力を設定するプロパティです。

また、自分の剣同士がぶつかるのはおかしい1ため、collisionBitMaskを設定しています。 categoryBitmaskによってそれぞれの物体にカテゴリを設定し、collisionBitMaskには衝突させたい物体のカテゴリを指定します。 これによって、ボールはボールと剣、剣はボールのみと衝突します。 ボールには何も設定していませんが、これは、デフォルトで他の物体とぶつかるようになっているためです。

なお、剣のphysicsBodyはノードの大きさそのままだと他の物体に衝突しすぎてしまうため、小さめにしています。

今回の剣のように速く動くものや、小さいものは、usesPreciseCollisionDetectionを指定することで正確に判定できます。 ただし、高コストになるので注意しましょう。

static const uint32_t swordCategory = 0x1 << 0;
static const uint32_t ballCategory = 0x1 << 1;

/* 省略 */

- (SKNode *)newBall {
    /* 省略 */
    
    ball.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:r];
    ball.physicsBody.categoryBitMask = ballCategory;
    
    return ball;
}

- (SKNode *)newSword {
    SKSpriteNode *sword = [SKSpriteNode spriteNodeWithImageNamed:@"sword"];
    sword.xScale = sword.yScale = 0.5f;
    sword.zRotation = -45.0f * M_PI / 180.0f;
    
    sword.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGRectApplyAffineTransform(sword.frame, CGAffineTransformMakeScale(0.7f, 0.7f)).size];
    sword.physicsBody.affectedByGravity = NO;
    sword.physicsBody.velocity = CGVectorMake(0, 1000.0f);
    sword.physicsBody.categoryBitMask = swordCategory;
    sword.physicsBody.collisionBitMask = ballCategory;
    sword.physicsBody.usesPreciseCollisionDetection = YES;

    return sword;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    
    CGPoint location = [touch locationInNode:self];
    
    SKNode *sword = [self newSword];
    sword.position = location;
    [self addChild:sword];
}


剣を飛ばす

これで剣を飛ばすことができるようになりました。

次に剣とボールがぶつかった時に爆発させる処理です。これには所謂当たり判定の実装が必要ですが、これにも物理エンジンが利用できます。 collisionBitMaskと同じ要領で、contactBitMaskを設定すれば2つの物体が接触したときにdelegateメソッドが呼ばれるようになるため、そこで必要な処理をおこないます。

実装は以下のようになります。 swordcontactBitMaskは、画面の枠とボールと接触するように設定します。 didBeginContact:が肝です。 渡されてくる物体の順番は順不同のため、最初のif文でswordが先に来るように2つの物体を並び替えています。 そして、swordがballと接触した場合、sparkのパーティクルを一瞬だけ表示すると共に、2つの物体を削除しています。 また画面の枠と接触した場合は、swordをただ削除するようにしています。

sparkのパーティクルはParticle templateSparkにして、デフォルトのままのものを利用しています。

static const uint32_t worldCategory = 0x1 << 2;

@interface SJParticleScene () <SKPhysicsContactDelegate>
@end

/* 省略 */
- (void)createSceneContents {

    /* 省略 */

    self.physicsWorld.contactDelegate = self;
    self.physicsBody.categoryBitMask = worldCategory;
    
    /* 省略 */
}

- (SKNode *)newSword {

    /* 省略 */
    sword.physicsBody.collisionBitMask = ballCategory;
    sword.physicsBody.contactTestBitMask = ballCategory | worldCategory;

    /* 省略 */
}

# pragma mark - SKPhysicsContactDelegate

- (void)didBeginContact:(SKPhysicsContact *)contact {
    SKPhysicsBody *firstBody, *secondBody;
    
    if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask) {
        firstBody = contact.bodyA;
        secondBody = contact.bodyB;
    } else {
        firstBody = contact.bodyB;
        secondBody = contact.bodyA;
    }
    
    if ((firstBody.categoryBitMask & swordCategory) != 0) {
        
        if ((secondBody.categoryBitMask & ballCategory) != 0) {
            NSString *sparkPath = [[NSBundle mainBundle] pathForResource:@"spark" ofType:@"sks"];
            SKEmitterNode *spark = [NSKeyedUnarchiver unarchiveObjectWithFile:sparkPath];
            spark.position = secondBody.node.position;
            spark.xScale = spark.yScale = 0.2f;
            [self addChild:spark];
            
            SKAction *fadeOut = [SKAction fadeOutWithDuration:0.3f];
            SKAction *remove = [SKAction removeFromParent];
            SKAction *sequence = [SKAction sequence:@[fadeOut, remove]];
            [spark runAction:sequence];
            
            [firstBody.node removeFromParent];
            [secondBody.node removeFromParent];
        } else if ((secondBody.categoryBitMask & worldCategory) != 0) {
            NSLog(@"contact with world");
            [firstBody.node removeFromParent];
        }
        
    }
}


ボールを破壊

このように、Sprite Kitのパーティクル機能を使えば、手軽にゲームの表現力を向上させることができます。

  1. 物理的にはおかしくないですが、ゲームでは自分の弾同士はぶつからないものが多いので、それに合わせます。 


comments powered by Disqus