前回、SpriteKit Gameプロジェクトテンプレートで生成されたソースを見た。
そのソースをベースにして、今回はSpriteKitの物理エンジンを利用するコードを追加してみる。
物理エンジンにも色々あって、抽象化の方法も様々だったり構成要素の名称が色々違ってたりとかあると思うけど、一般的には仮想的な物理世界を表す「なんとかワールド」クラスのインスタンスに、剛体を表す「なんとかボディ」クラスのインスタンスを登録し、あとはワールドの時間をちょっとづつ進める事でシミレーションを行うというのが基本になると思う。
SpriteKitにも「なんとかワールド」や「なんとかボディ」に相当するクラスがあり、それぞれSKPhysicsWorldとSKPhysicsBodyとなっている。
ただしSpriteKitの物理エンジンは、物理エンジンとして独立したライブラリではなく、SpriteKitに統合されているので一般的なものと使い勝手が違う(コードの記述量が少なくて済む)部分がある。
例えば、SKPhysicsWorldのインスタンスは生成する必要がない。
というのも、SKSceneのインスタンスを生成すると自動的にそのphysicsWorldプロパティにSKPhysicsWorldのインスタンスがセットされるからである。
また、物理シミュレーションの実行を指示するコードもわざわざ書く必要がない。(その辺りは自動的に内部でうまい具合やってくれるみたい。)
というか、物理シミュレーションを1ステップずつ実行するようなSKPhysicsWorldのメソッドは公開されていない。(多分)
こんな感じで、使うのが簡単な代わりに自由度は低めであるが、まぁ簡単な2Dゲームの作成においてはこの自由度の低さが問題になる事は少ないんではないかと思う。
ちなみにSpriteKitの物理エンジンはSI単位を採用している。
静止画だとなんのこっちゃ分からんけど、まぁ要はタップした位置にボールを配置し、それが画面の下側に向かって落下するアプリを作る。
ボールの画像は以下のものを使っている。(Blenderでポリゴン数少なめで作った画像なのでカクカクしてるけど、ご自由にご利用ください。)
上記のとおり、SKPhysicsWorldを生成したりは必要じゃないので、いきなり剛体の設定のコードからはじめる。
前回の-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)eventメソッドを以下のように書き換える。
-(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]; sprite.physicsBody = pbody; sprite.size = (CGSize){radius * 2, radius * 2}; [self addChild:sprite]; } }
重要なのは[SKPhysicsBody bodyWithCircleOfRadius:radius]の部分である。
これによって、半径radiusの円の形をした剛体が生成される。
この場合radiusは20なので、画面上では半径20ポイント相当の円となる。
そしたら、sprite.physicsBodyに剛体をセットする。
最後にsprite.sizeを設定してる部分は物理エンジンとは無関係で、設定した剛体のサイズと見た目のサイズを揃えるための行である。
radiusは半径なので円全体のサイズにするために2倍している。
これだけでボールが画面下に向かって落ちるアプリが出来た。
ボールが画面下に落下するのは、SKPhysicsWorldのgravityプロパティ(2次元ベクトル)のデフォルト値が地球の重力を表す(0.0,-9.8)となっているからである。
gravityプロパティの単位はメートル毎秒毎秒である。
ちなみにSpriteKitにおいては、1ポイントは2/3センチメートルに相当するみたい(ドキュメントで明記されている部分を見つけることは出来なかったが計測したらそのくらいになった)。
剛体の質量を設定していないが、SKPhysicsBodyのdensity(密度)プロパティのデフォルト値が1キログラム毎平方メートルとなっており、剛体のサイズから質量は自動的に計算される。
質量を直接指定したければmassプロパティ(単位はキログラム)にセットすればよい。
その際、逆にdensityが自動で再計算される。
とても楽だ。
しかし、ボールは画面外に向かって落下し続けるだけで衝突反応がないのでつまらない。
ボールが画面外に出ないよう、枠を追加する。
前回の-(id)initWithSize:(CGSize)sizeメソッドを以下のように書き換える。
-(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}]; } return self; }
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:(CGRect){CGPointZero, size}]の行を追加しただけである。
これで、シーンのサイズと同じサイズの枠が生成され、シーンのphysicsBodyプロパティにセットされる。
よって、ボールが画面の外にはみ出さなくなる。
SKPhysicsBodyで表現出来る剛体は大きく分けて2種類ある。
ひとつはボリュームベース剛体で、もうひとつはエッジベース剛体である。
ボリュームベース剛体はいわゆる普通の剛体で、質量を持ち、仮想的な物理世界の中で様々な力を及ぼしたり受けたりして移動するものである。
今回の例では、これはボールが該当する。
ボリュームベース剛体の場合、SKPhysicsBodyのdynamicプロパティにNOをセットすると動かなくする事が出来る(静止している質量無限の物体のようになる)。
エッジベース剛体は、枠だけの剛体で質量ゼロであり、ボリュームベース剛体のdynamicプロパティにNOをセットした状態と同じになる(要は完全に動かない障害物となる)。
今回の例では、シーンに設定した枠が該当する。
インスタンス生成用のクラスメソッドに「Edge」という単語が含まれるものはエッジベース剛体を生成する。
それ以外はボリュームベース剛体となる。
だいぶ少ないコードで今回の目標を達成したが、コードが少なくて済んだのは各クラスのデフォルト値に頼ってるからでもある。
特に、SKPhysicsBodyの反発力(restitutionプロパティ)のデフォルト値が小さすぎてボールがあまり跳ねないのが面白くない。
他にも摩擦力や空気抵抗を表すプロパティがあるので色々試してみるとおもしろいだろう。
うーん、わかりやすい
ありがとうございます!
読みやすく、そしてわかりやすかったっす
ありがとうございます!