前回、SpriteKitの物理エンジンを利用した簡単なアプリを作成した。
今回はそのアプリに、SpriteKitのパーティクルシステムを利用したちょっとしたエフェクトを追加してみる。
ボールにある一定以上の衝撃が加わったら火花を散らす。
今回の場合だと、ボールが画面の枠やボール同士でぶつかったときに火花が発生するようになる。
どういうパーティクルにするかはコードでも指定できるが、Xcode 5からSpriteKitのパーティクルをGUIでデザインできる機能が追加されたので、それを使ってみる。
まず、新しいファイルの追加で Resource > SpriteKit Particle File を選択する。
そしたら次にパーティクルのタイプを選ぶ。
今回は火花を散らせたいので Spark にした。
で、新しく追加されたsksファイル(今回はMyParticle.sksという名前にした)を見てみると、動いてる状態のパーティクルを見ることが出来る。
画面右側の Emitter Node パネルの数値等をいじるとリアルタイムでパーティクルの見た目や動きが変わる。
Particle Texture として spark.png という画像が使用されているが、これはsksファイルを追加したときに自動で追加されるので、自前で用意する必要はない。
もちろんパーティクルの見た目を変えるためにオリジナルの画像に差し替える事も出来る。
あとはこのパーティクルファイルをコードで読み込んで、ボールが衝突したタイミングで衝突した場所に表示してあげればいい。
衝突したタイミングで衝突の情報を受け取るためには、 SKPhysicsBody の contactDelegate プロパティに SKPhysicsContactDelegate プロトコルのメソッドを実装したクラスのインスタンスをセットする必要がある。
今回は簡単のため MyScene クラスに SKPhysicsContactDelegate プロトコルのメソッドを実装する。
まず、 MyScene.h には <SKPhysicsContactDelegate> を追加してプロトコル準拠を示すだけである。
#import <SpriteKit/SpriteKit.h> @interface MyScene : SKScene <SKPhysicsContactDelegate> @end
そしたら、 MyScene.m に実装を書く必要があるが、 SKPhysicsContactDelegate プロトコルのメソッドは -(void)didBeginContact:(SKPhysicsContact *)contact と -(void)didEndContact:(SKPhysicsContact *)contact の2つしかない。
それぞれ衝突開始時と衝突終了時に呼ばれるメソッドだが、今回のようにパーティクルを表示するだけならどちらを使ってもあまり差は無いと思う。
今回は didBeginContact を使う。
-(void)didBeginContact:(SKPhysicsContact *)contact { if (contact.collisionImpulse > 10.0f) { static SKEmitterNode* spark = nil; if (!spark) { NSString *path = [[NSBundle mainBundle] pathForResource:@"MyParticle" ofType:@"sks"]; spark = [NSKeyedUnarchiver unarchiveObjectWithFile:path]; spark.position = contact.contactPoint; spark.numParticlesToEmit = contact.collisionImpulse; [self addChild:spark]; } else { spark.position = contact.contactPoint; spark.numParticlesToEmit = contact.collisionImpulse; [spark resetSimulation]; } } } -(void)didEndContact:(SKPhysicsContact *)contact { }
SKPhysicsContact のインスタンスである contact からは、ぶつかった2つの剛体への参照や、衝突時の衝撃(単位はニュートン秒)、衝突点(スクリーン座標における位置)の情報を得ることが出来る。
上のコードではまず、衝撃が10ニュートン秒より大きい場合に、sksファイルを読み込んで、衝突点に衝突時の衝撃に応じた数のパーティクルを生成している。
パーティクルの生成には SKEmitterNode を使っているが、その position プロパティでパーティクルの生成位置を、 numParticlesToEmit プロパティでパーティクルの生成数を指定することが出来る。
SKEmitterNode の生成とシーンへの addChild を初回だけ行うようにするためにstatic変数とifを使ってることに注意。
説明を簡単にするために今回はstatic変数を使ったが、メンバ変数にしたりすべきだろう。
初回は、 [self addChild:spark]; した時点でパーティクルの生成が開始され、2回目以降は [spark resetSimulation]; でパーティクルが再度生成される。
SKEmitterNode のインスタンスひとつを使い回してるため、短時間に連続でパーティクルを生成すると火花が広がりきる前に次のパーティクル生成によって消えてしまう点に注意。
この問題を軽減するには、 SKEmitterNode のインスタンスを配列などで複数持ち、それらを順番に使うようにするといいだろう。
ちなみに numParticlesToEmit のデフォルト値は0となっており、そのままだと無限にパーティクルが生成され続けるので注意が必要である。
あと、GUIで設定出来るsksファイルの項目は SKEmitterNode のプロパティとしても用意されてるので、コードからもパーティクルの見た目を色々いじれるはずである。(未確認)
これでパーティクルの表示の仕方は分かったが、実はこの状態で実行してもまだ何も起こらない(didBeginContactが呼ばれない)。
衝突が起きたタイミングで didBeginContact や didEndContact を呼ばれるようにするため、まず SKPhysicsWorld の contactDelegate をセットする必要がある。
前回の initWithSize メソッドを以下のように書き換える。
-(id)initWithSize:(CGSize)size { if (self = [super initWithSize:size]) { /* Setup your scene here */ self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0]; SKLabelNode *myLabel = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"]; myLabel.text = @"Hello, World!"; myLabel.fontSize = 30; myLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)); [self addChild:myLabel]; self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:(CGRect){CGPointZero, size}]; self.physicsWorld.contactDelegate = self; } return self; }
self.physicsWorld.contactDelegate = self; の行を追加しただけである。
ここでいう self は 上で SKPhysicsContactDelegate プロトコルを実装した MyScene クラスのインスタンスなのでこれが可能となっている。
これで didBeginContact が呼ばれる、ようになれば楽なんだが、もうひと手間(もうひと理解)が必要である。
答えを言ってしまえば、 SKPhysicsBody のインスタンスを生成した際にその contactTestBitMask プロパティに常に1をセットすればいい。
前回の touchesBegan メソッドを以下のように書き換える。
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { /* Called when a touch begins */ for (UITouch *touch in touches) { CGPoint location = [touch locationInNode:self]; SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:@"Ball"]; sprite.position = location; const CGFloat radius = 20.0f; SKPhysicsBody *pbody = [SKPhysicsBody bodyWithCircleOfRadius:radius]; pbody.contactTestBitMask = 1; sprite.physicsBody = pbody; sprite.size = (CGSize){radius * 2, radius * 2}; [self addChild:sprite]; } }
pbody.contactTestBitMask = 1; の行を追加しただけである。
これで didBeginContact メソッドが呼ばれるようになり、衝突点から火花が散るようになったはずである。
なぜこれだけで良いのかを知るためにには、もうひとつのビットマスクである categoryBitMask について知る必要がある。
categoryBitMask はその剛体がどのカテゴリに属するかを表すビット値で、デフォルト値は0xffffffffである。
SKPhysicsWorld の内部では、剛体同士がぶつかった場合に、片方の剛体の contactTestBitMask ともう片方の categoryBitMask で論理積(AND)をとり、その結果が0でなければ SKPhysicsContact オブジェクトを生成し、デリゲートの didBeginContact メソッドなどが呼ばれるようになっている。
contactTestBitMask のデフォルト値は0x00000000なので、そのままだと didBeginContact メソッドなどがよばれる事は一切無い。
手軽さを志向しているSpriteKitにおいては、珍しく面倒な仕様だが、衝突判定から衝突オブジェクト生成、デリゲートメソッド呼び出しの一連の処理が比較的重いのでこういう仕様になっているようである。
contactTestBitMask はその名のとおりビットマスクなので、例えば剛体A(敵A)と剛体B(敵B)同士に関しては特別な衝突時の処理は必要ないが、剛体C(プレイヤー)と剛体Aもしくは剛体Bの衝突時には特別な処理を行いたいというような場合にも例えば以下のような設定にすれば対応出来る。
player.categoryBitMask = 1; enemyA.categoryBitMask = 2; enemyB.categoryBitMask = 2; player.contactTestBitMask = 2; enemyA.contactTestBitMask = 1; enemyA.contactTestBitMask = 1;
実は、 SKPhysicsBody のプロパティに BitMask と付くものがもうひとつある。
collisionBitMask である。
collisionBitMask はどのカテゴリの剛体と衝突するかを指定するためのビットマスクでデフォルト値は0xffffffffとなっている。
2つの剛体の collisionBitMask と categoryBitMask で論理積をとった結果が0になるとその剛体同士はすり抜ける。
SpriteKitの使い方について簡単なアプリを作りながら見てきたが、非常に簡単に使える事が分かったと思う。
しかしまだサウンド関連やアクション関連、あとパフォーマンス周りは全然見ていない。
その辺りも調べたら記事を書くかもしれないが、多分今回のようにちゃんとは書かないだろう(疲れるのでw)。
SpriteKitで、ゲーム作成者人口が増えるといいなぁ(*´∀`*)ポワワ
SpriteKitの使用例を検索していてこのページに来ました。
ありがとうございました!
わかりやすくて為になりました。
どうも!ありがとうございます。
めっさ分かりやすい
ありがとうございます!
ありがとうございます!