SpriteKitを使う
by K.I
Index
- iOS標準の、2Dゲーム用フレームワークである SpriteKitを使ってみる。
- iOS7でのみ動作する。開発環境もXcode5以降が必要
- 基本的には、小さな画像(スプライト)を高速に動かすことが出来るが、特徴としては、
- スプライトの2次元の物理的な動きをシミュレーションする
- 等速運動とか、重力の影響、衝突時の反射や当たり判定等
- 炎や爆発等を、小さな粒子の運動の集合でシュミレートするための、パーティクルという機能がある
- サウンドプレイバック対応 →どういう機能なのか不明
[top]
- なにもわからないので、とにかくまずは、どんなものかサンプルを探してみた。
- SpriteKitでブロック崩しが、とても解り易かったのでそのままやってみた。
- というか、まんま追っかけてるだけなので、tnantokaさんのオリジナルのページを見た方が解り易いと思う。
- 基本的に、InterfaceBuilderは全く使用しない
- まず基本になるプロジェクトを作成する。
- EmptyApplicationでプロジェクトを新規に作成、
- New File...で、Object-C classを追加、ViewContollerを作成、
- loadViewで、SKViewを作成して、self.viewに設定
- viewDidLoadで表示の設定後、SKSceneをインスタンス化して表示
- さらにStatusBarを非表示とする
#import "BFViewController.h"
@import SpriteKit;
@implementation BFViewController
- (void)loadView {
SKView *skView = [[SKView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.view = skView;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
SKView *skView = (SKView *)self.view;
skView.showsDrawCount = YES;
skView.showsNodeCount = YES;
skView.showsFPS = YES;
SKScene *scene = [SKScene sceneWithSize:self.view.bounds.size];
[skView presentScene:scene];
}
- (BOOL)prefersStatusBarHidden {
return YES;
}
@end
- AppDelegateで、作成したViewControllerのインスタンス変数を追加して、
#import <UIKit/UIKit.h>
#import "BFViewController.h" // ViewControllerクラスをインポート
@interface BFAppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong,nonatomic) BFViewController *viewController; // viewControllerのインスタンスを保持するためのプロパティ追加
@end
#import "BFAppDelegate.h"
#import "BFViewController.h" // ViewControllerクラスをインポート
@implementation BFAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[BFViewController alloc] init];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {}
- (void)applicationDidEnterBackground:(UIApplication *)application {}
- (void)applicationWillEnterForeground:(UIApplication *)application {}
- (void)applicationDidBecomeActive:(UIApplication *)application {}
- (void)applicationWillTerminate:(UIApplication *)application {}
@end
- これを実行すると、黒い画面の下の方に、
- 0 nodes 0 draws **.*fps と表示される
- 最初のタイトルシーンを追加する
- TitleSceneクラスを追加、SKLabelNodeを1つ追加する
#import <Foundation/Foundation.h>
@import SpriteKit;
@interface BFTitleScene : SKScene
@end
#import "BFTitleScene.h"
@implementation BFTitleScene
- (id)initWithSize:(CGSize)size {
self = [super initWithSize:size];
if (self) {
SKLabelNode *titleLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
titleLabel.text = @"BREAKOUT!";
titleLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
titleLabel.fontSize = 50.0f;
[self addChild:titleLabel];
}
return self;
}
@end
- それで、ViewController で、追加したシーンを表示する
#import "BFViewController.h"
#import "BFTitleScene.h"
@import SpriteKit;
@implementation BFViewController
- (void)loadView {
SKView *skView = [[SKView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.view = skView;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
SKView *skView = (SKView *)self.view;
skView.showsDrawCount = YES;
skView.showsNodeCount = YES;
skView.showsFPS = YES;
// SKScene *scene = [SKScene sceneWithSize:self.view.bounds.size];
SKScene *scene = [BFTitleScene sceneWithSize:self.view.bounds.size];
[skView presentScene:scene];
}
- (BOOL)prefersStatusBarHidden {
return YES;
}
@end
- ブロックの設定はJSONで記述する。。ところで、JSONって何?
- 調べてみると、JavaScript Object Notationの略で、元々はJavaScript用のデータフォーマットだが、汎用的に使われているらしい
- 確かに、XMLなんかより扱い易そう。
で、ブロックの設定は、emptyファイルで追加して、ファイル名config.jsonに、こんな感じで記述する
{
"block" : {
"margin" : 16.0,
"width" : 34.0,
"height" : 16.0,
"rows" : 5,
"max_life" : 5
}
}
- よく分からないなりにPlaySceneの記述を追って見る
- まず、initWithSizeメソッドは、最初に実行されるみたいだ。で、addBlocksメソッドが実行される
- initializeメソッドは、クラス生成時に実行される?のかな。ここで、config.jsonをNSDictionary型の変数configに読み込んでいるようだ
- #pragma mark -Blockってなんだろう?
- addBlocksメソッドは、
- 最初にconfigのデータの読み取り(連想配列みたいな感じかな)
- frameの幅と、Blockのmargin,widthから、列数colsを計算して
- frameの高さと、Blockのmargin,heightから、行数yを計算
- それで、newBlockメソッドでブロックを作って並べると
- newBlockメソッドは、
- やはり最初にconfigデータを読み取り
- cyanColorのwidth,heightのブロックを生成して、
- 乱数でlifeを決めて、userData1に入れておく
- でも、@{ @"life" : @(life) }.mutableCopy; って書き方の意味がわからん。。"life"って名前で、lifeの値を保存しているんだと思うけど
- それで、updateBlockAlphaメソッドで透明度を設定
- 次の addChildってメソッドは何だろう?実際は、ここでブロックが生成されているのかな?
- updateBlockAlphaメソッドは、
- blockのuserData[life]で、アルファ値を設定する
#import "BFPlayScene.h"
@implementation BFPlayScene
- (id)initWithSize:(CGSize)size {
self = [super initWithSize:size];
if (self) {
[self addBlocks];
}
return self;
}
static NSDictionary *config = nil;
+ (void)initialize {
NSString *path = [[NSBundle mainBundle] pathForResource:@"config" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:path];
if (!config) {
config = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
}
}
# pragma mark - Block
- (void)addBlocks {
int rows = [config[@"block"][@"rows"] intValue];
CGFloat margin = [config[@"block"][@"margin"] floatValue];
CGFloat width = [config[@"block"][@"width"] floatValue];
CGFloat height = [config[@"block"][@"height"] floatValue];
int cols = floor(CGRectGetWidth(self.frame) - margin) / (width + margin);
CGFloat y = CGRectGetHeight(self.frame) - margin - height / 2;
for (int i = 0; i < rows; i++) {
CGFloat x = margin + width / 2;
for (int j = 0; j < cols; j++) {
SKNode *block = [self newBlock];
block.position = CGPointMake(x, y);
x += width + margin;
}
y -= height + margin;
}
}
- (SKNode *)newBlock {
CGFloat width = [config[@"block"][@"width"] floatValue];
CGFloat height = [config[@"block"][@"height"] floatValue];
int maxLife = [config[@"block"][@"max_life"] floatValue];
SKSpriteNode *block = [SKSpriteNode spriteNodeWithColor:[SKColor cyanColor] size:CGSizeMake(width, height)];
block.name = @"block";
int life = (arc4random() % maxLife) + 1;
block.userData = @{ @"life" : @(life) }.mutableCopy;
[self updateBlockAlpha:block];
[self addChild:block];
return block;
}
- (void)updateBlockAlpha:(SKNode *)block {
int life = [block.userData[@"life"] intValue];
block.alpha = life * 0.2f;
}
@end
- あとは、TitleSceneにtouchesBeganメソッドを追加、画面をタッチした時にPlaySceneに遷移する様に記述する
- pushWithDirection:SKTransitionDirectionUpなので、押されるように画面が遷移:上方向にということになる
#import "BFTitleScene.h"
#import "BFPlayScene.h"
@implementation BFTitleScene
- (id)initWithSize:(CGSize)size {
self = [super initWithSize:size];
if (self) {
SKLabelNode *titleLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
titleLabel.text = @"BREAKOUT!";
titleLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
titleLabel.fontSize = 50.0f;
[self addChild:titleLabel];
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
SKScene *scene = [BFPlayScene sceneWithSize:self.size];
SKTransition *transition = [SKTransition pushWithDirection:SKTransitionDirectionUp duration:1.0f];
[self.view presentScene:scene transition:transition];
}
@end
- BlockNodeをSKSpriteNodeのサブクラスとして作成した場合は、(よく分かっていないけど)
- UIImageViewのプロパティuserInteractionEnabledをYESに設定しておかないといけないらしい。
- config.jsonに、パドルとボールの設定を追加する
"paddle" : {
"width" : 70.0,
"height" : 14.0,
"y" : 40.0,
},
"ball" : {
"radius" : 6.0,
},
- PlaySceneの、initWithSizeメソッドに、addPuddleを追加
[self addPaddle];
- さらに、addPaddle,puddleNode,addBall,ballNode,touchesBeganメソッドを追加
# pragma mark - Paddle
- (void)addPaddle {
CGFloat width = [config[@"paddle"][@"width"] floatValue];
CGFloat height = [config[@"paddle"][@"height"] floatValue];
CGFloat y = [config[@"paddle"][@"y"] floatValue];
SKSpriteNode *paddle = [SKSpriteNode spriteNodeWithColor:[SKColor brownColor] size:CGSizeMake(width, height)];
paddle.name = @"paddle";
paddle.position = CGPointMake(CGRectGetMidX(self.frame), y);
[self addChild:paddle];
}
- (SKNode *)paddleNode {
return [self childNodeWithName:@"paddle"];
}
# pragma mark - Ball
- (void)addBall {
CGFloat radius = [config[@"ball"][@"radius"] floatValue];
SKShapeNode *ball = [SKShapeNode node];
ball.name = @"ball";
ball.position = CGPointMake(CGRectGetMidX([self paddleNode].frame), CGRectGetMaxY([self paddleNode].frame) + radius);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, NULL, 0, 0, radius, 0, M_PI * 2, YES);
ball.path = path;
ball.fillColor = [SKColor yellowColor];
ball.strokeColor = [SKColor clearColor];
CGPathRelease(path);
[self addChild:ball];
}
- (SKNode *)ballNode {
return [self childNodeWithName:@"ball"];
}
# pragma mark - Touch
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (![self ballNode]) {
[self addBall];
return;
}
UITouch *touch = [touches anyObject];
CGPoint locaiton = [touch locationInNode:self];
CGFloat speed = [config[@"paddle"][@"speed"] floatValue];
CGFloat x = locaiton.x;
CGFloat diff = abs(x - [self paddleNode].position.x);
CGFloat duration = speed * diff;
SKAction *move = [SKAction moveToX:x duration:duration];
[[self paddleNode] runAction:move];
}
- タイトル画面で、ブロックとパドルが表示され、
- さらにクリックすると、ボールが表示されて、クリックした位置にパドルが移動するようになった。
1これは、たぶん汎用的に使えるデータなんだろうと思う。
[top]
- SpriteKitは、2次元の簡単な物理運動をシミュレートする物理エンジンを持っているので、
- 重力を設定して、ボールに放物運動をさせたり、壁との反射、ブロックとの当たり判定も可能。
- SpriteKitの物理エンジンデバッグ用のライブラリPhysicsDebuggerを使ってみる。
- ライブラリをプロジェクトに設定するのに、Object-Cのライブラリ管理ツール CocoaPodsを使う
- githubとか、GoogleCodeなどのライブラリをダウンロードしてプロジェクトに追加する作業をやってくれるみたいだ。
$ sudo gem update --system
- Xcodeに、CommandlineToolをインストール
- Xcode→Preference→Downloadで、ConponentにCommandlineToolがあるかどうか確認
- 無い。でもインストール出来ない。何でだろう?と思ったら、インストール方法が変わったらしい
$ xcode-select --install
$ sudo gem install cocoapods
$ pod setup
インストールの確認。バージョン番号が表示されたらOK
$ pod --version
0.29.0
- CocoaPodsの使い方は、まず読み込むライブラリを記述したPodfileというファイルを作成する
- .xcodeprojのあるディレクトリに、Podfileを置いて、以下のコマンドでライブラリがインストールされる
- Xcodeは、終了しておく。
$ pod install
- 正常にインストールされた場合は、Podsディレクトリと、.xcworkspaceファイルが作成されている
- Xcodeを起動して、.xcworkspaceファイルを開く(.xcodeprojではないので注意!)
- ProjectNavigatorで、元々のプロジェクトと、Podsプロジェクトの2つが見えればOK
- .xcworkspaceは、Podfileで指定したライブラリがリンクされたプロジェクトになっているというわけ。
- あとは、YMCPhysicsDebugger.hをインポートして、
- もちろん、これはでバッグの時しか必要ないし、重いので、
- ボールやブロック、パドルに物理的な設定をする
- ボールは、無重力で等速運動させる
- ブロックやパドルは、基本的に動かない様に固定する
- まず、config.jsonのpaddleにspeed、ballにvelocity の設定を追加
"paddle" : {
"width" : 70.0,
"height" : 14.0,
"y" : 40.0,
"speed" : 0.005
},
"ball" : {
"radius" : 6.0,
"velocity" : {
"x" : 50.0,
"y" : 120.0
}
},
- ブロック・パドルは、固定するために dynamicをNOに設定する
- ボールは重力を無視するために、affectedByGravityをNOにする
- velocityで初速を与える。等速運動なので、減衰restitutionは1とする
- 物理演算させるためには、まず最初にContactDelegateを使うことを宣言する
@interface BFPlayScene () <SKPhysicsContactDelegate>
そして衝突時のDelegeteで、自分自身のクラスのメソッドを呼出すようにする
self.physicsWorld.contactDelegate = self;
#import "BFPlayScene.h"
static const uint32_t blockCategory = 0x1 << 0;
static const uint32_t ballCategory = 0x1 << 1;
@interface BFPlayScene () <SKPhysicsContactDelegate>
@end
@implementation BFPlayScene
- (id)initWithSize:(CGSize)size {
self = [super initWithSize:size];
if (self) {
[self addBlocks];
[self addPaddle];
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
self.physicsWorld.contactDelegate = self;
}
return self;
}
static NSDictionary *config = nil;
+ (void)initialize {
NSString *path = [[NSBundle mainBundle] pathForResource:@"config" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:path];
if (!config) {
config = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
}
}
# pragma mark - Block
- (void)addBlocks {
int rows = [config[@"block"][@"rows"] intValue];
CGFloat margin = [config[@"block"][@"margin"] floatValue];
CGFloat width = [config[@"block"][@"width"] floatValue];
CGFloat height = [config[@"block"][@"height"] floatValue];
int cols = floor(CGRectGetWidth(self.frame) - margin) / (width + margin);
CGFloat y = CGRectGetHeight(self.frame) - margin - height / 2;
for (int i = 0; i < rows; i++) {
CGFloat x = margin + width / 2;
for (int j = 0; j < cols; j++) {
SKNode *block = [self newBlock];
block.position = CGPointMake(x, y);
x += width + margin;
}
y -= height + margin;
}
}
# pragma mark - Paddle
- (void)addPaddle {
CGFloat width = [config[@"paddle"][@"width"] floatValue];
CGFloat height = [config[@"paddle"][@"height"] floatValue];
CGFloat y = [config[@"paddle"][@"y"] floatValue];
SKSpriteNode *paddle = [SKSpriteNode spriteNodeWithColor:[SKColor brownColor] size:CGSizeMake(width, height)];
paddle.name = @"paddle";
paddle.position = CGPointMake(CGRectGetMidX(self.frame), y);
paddle.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:paddle.size];
paddle.physicsBody.dynamic = NO;
[self addChild:paddle];
}
- (SKNode *)paddleNode {
return [self childNodeWithName:@"paddle"];
}
# pragma mark - Ball
- (void)addBall {
CGFloat radius = [config[@"ball"][@"radius"] floatValue];
CGFloat velocityX = [config[@"ball"][@"velocity"][@"x"] floatValue];
CGFloat velocityY = [config[@"ball"][@"velocity"][@"y"] floatValue];
SKShapeNode *ball = [SKShapeNode node];
ball.name = @"ball";
ball.position = CGPointMake(CGRectGetMidX([self paddleNode].frame), CGRectGetMaxY([self paddleNode].frame) + radius);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, NULL, 0, 0, radius, 0, M_PI * 2, YES);
ball.path = path;
ball.fillColor = [SKColor yellowColor];
ball.strokeColor = [SKColor clearColor];
ball.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:radius];
ball.physicsBody.affectedByGravity = NO;
ball.physicsBody.velocity = CGVectorMake(velocityX, velocityY);
ball.physicsBody.restitution = 1.0f;
ball.physicsBody.linearDamping = 0;
ball.physicsBody.friction = 0;
ball.physicsBody.usesPreciseCollisionDetection = YES;
ball.physicsBody.categoryBitMask = ballCategory;
ball.physicsBody.contactTestBitMask = blockCategory;
CGPathRelease(path);
[self addChild:ball];
}
- (SKNode *)ballNode {
return [self childNodeWithName:@"ball"];
}
# pragma mark - Touch
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (![self ballNode]) {
[self addBall];
return;
}
UITouch *touch = [touches anyObject];
CGPoint locaiton = [touch locationInNode:self];
CGFloat speed = [config[@"paddle"][@"speed"] floatValue];
CGFloat x = locaiton.x;
CGFloat diff = abs(x - [self paddleNode].position.x);
CGFloat duration = speed * diff;
SKAction *move = [SKAction moveToX:x duration:duration];
[[self paddleNode] runAction:move];
}
- (SKNode *)newBlock {
CGFloat width = [config[@"block"][@"width"] floatValue];
CGFloat height = [config[@"block"][@"height"] floatValue];
int maxLife = [config[@"block"][@"max_life"] floatValue];
SKSpriteNode *block = [SKSpriteNode spriteNodeWithColor:[SKColor cyanColor] size:CGSizeMake(width, height)];
block.name = @"block";
int life = (arc4random() % maxLife) + 1;
block.userData = @{ @"life" : @(life) }.mutableCopy;
block.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:block.size];
block.physicsBody.dynamic = NO;
block.physicsBody.categoryBitMask = blockCategory;
[self updateBlockAlpha:block];
[self addChild:block];
return block;
}
- (void)decreaseBlockLife:(SKNode *)block {
int life = [block.userData[@"life"] intValue] - 1;
block.userData[@"life"] = @(life);
[self updateBlockAlpha:block];
}
- (void)updateBlockAlpha:(SKNode *)block {
int life = [block.userData[@"life"] intValue];
block.alpha = life * 0.2f;
}
# 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 & blockCategory) {
if (secondBody.categoryBitMask & ballCategory) {
[self decreaseBlockLife:firstBody.node];
}
}
}
@end
- ボールやパドル、ブロックに属性を設定するだけで、ちゃんと画面上で動くというのは、なかなか凄い
- @interfaceの行の <SKPhysicsContactDelegate> はプロトコルって奴かな。
- これによって、didBeginContactが呼び出されて、contact(オブジェクト同士の接触)した時、lifeを一つ減らすということをしている。
- ここらへんの既述が、イマイチわかんないんだけども。。BitMaskの大小判定はどういう意味があるんだろう?と思ったけど、
- 単に大きな方がSecondになるように、ソーティングしてるだけなのか。
- よく見ると、ちゃんと説明されてたけど、すぐにはわかんなかった。。。
- でも、Paddleには何故カテゴリを付けないんだろう?コンタクト検出の必要が無いからかなぁ。
- エフェクト用の、パーティクルを生成する
- New File...→Resource→SpriteKit Particle Fileを選択
- Particle templeteで、Sparkを選択すると、.sksファイルが出来る
- すると、花火のような派手なエフェクトが表示される。
- lifeがゼロになった時、ブロックを消すようにして、
- その時に、パーティクルファイル(spark.sks)を読み込んで表示する
- (void)decreaseBlockLife:(SKNode *)block {
int life = [block.userData[@"life"] intValue] - 1;
block.userData[@"life"] = @(life);
[self updateBlockAlpha:block];
if (life < 1) {
[self removeNodeWithSpark:block];
}
}
# pragma mark - Utilities
- (void)removeNodeWithSpark:(SKNode *)node {
NSString *sparkPath = [[NSBundle mainBundle] pathForResource:@"spark" ofType:@"sks"];
SKEmitterNode *spark = [NSKeyedUnarchiver unarchiveObjectWithFile:sparkPath];
spark.position = node.position;
spark.xScale = spark.yScale = 0.3f;
[self addChild:spark];
SKAction *fadeOut = [SKAction fadeOutWithDuration:0.3f];
SKAction *remove = [SKAction removeFromParent];
SKAction *sequence = [SKAction sequence:@[fadeOut, remove]];
[spark runAction:sequence];
[node removeFromParent];
}
- xScaleとyScaleで、パーティクルの大きさを変更できる
- またDurationの値を大きくすると、パーティクルが表示される時間が長くなる。
- パーティクルは、ただのエフェクトだろうと思っていたが、
- Node数を見ると、飛び散るパーティクルのひとつひとつがNodeになっているらしい。
- sksファイルを選択して、右側のウィンドウで SKNode Inspectorで、
[top]
- ここからは、ゲームとしての体裁を整えるための、記述を追加している
- 残機数(life)や、ステージ数(stage)のプロパティを追加
"label" : {
"margin" : 5.0,
"font_size" : 14.0,
},
"max_life" : 5
#import <Foundation/Foundation.h>
@import SpriteKit;
@interface BFPlayScene : SKScene;
@property (nonatomic) int life;
@property (nonatomic) int stage;
- (id)initWithSize:(CGSize)size life:(int)life stage:(int)stage;
@end
- initWithSizeは、追加したイニシャライザinitWithSize:life:stageを呼び出すようになっている。
- Labelの表示は、zPositionを1に指定して、ボールとかより手前に表示させるようにしている。
- こういう細かい事も参考になるなぁ。。
#import "BFPlayScene.h"
#import "BFGameOverScene.h"
static const uint32_t blockCategory = 0x1 << 0;
static const uint32_t ballCategory = 0x1 << 1;
@interface BFPlayScene () <SKPhysicsContactDelegate>
@end
@implementation BFPlayScene
- (id)initWithSize:(CGSize)size life:(int)life stage:(int)stage {
self = [super initWithSize:size];
if (self) {
self.life = life;
self.stage = stage;
[self addBlocks];
[self addPaddle];
[self addStageLabel];
[self addLifeLabel];
[self updateLifeLabel];
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
self.physicsWorld.contactDelegate = self;
}
return self;
}
- (id)initWithSize:(CGSize)size {
return [self initWithSize:size life:[config[@"max_life"] intValue] stage:1];
}
static NSDictionary *config = nil;
+ (void)initialize {
NSString *path = [[NSBundle mainBundle] pathForResource:@"config" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:path];
if (!config) {
config = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
}
}
# pragma mark - Block
- (void)addBlocks {
int rows = [config[@"block"][@"rows"] intValue];
CGFloat margin = [config[@"block"][@"margin"] floatValue];
CGFloat width = [config[@"block"][@"width"] floatValue];
CGFloat height = [config[@"block"][@"height"] floatValue];
int cols = floor(CGRectGetWidth(self.frame) - margin) / (width + margin);
CGFloat y = CGRectGetHeight(self.frame) - margin - height / 2;
for (int i = 0; i < rows; i++) {
CGFloat x = margin + width / 2;
for (int j = 0; j < cols; j++) {
SKNode *block = [self newBlock];
block.position = CGPointMake(x, y);
x += width + margin;
}
y -= height + margin;
}
}
# pragma mark - Paddle
- (void)addPaddle {
CGFloat width = [config[@"paddle"][@"width"] floatValue];
CGFloat height = [config[@"paddle"][@"height"] floatValue];
CGFloat y = [config[@"paddle"][@"y"] floatValue];
SKSpriteNode *paddle = [SKSpriteNode spriteNodeWithColor:[SKColor brownColor] size:CGSizeMake(width, height)];
paddle.name = @"paddle";
paddle.position = CGPointMake(CGRectGetMidX(self.frame), y);
paddle.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:paddle.size];
paddle.physicsBody.dynamic = NO;
[self addChild:paddle];
}
- (SKNode *)paddleNode {
return [self childNodeWithName:@"paddle"];
}
# pragma mark - Ball
- (void)addBall {
CGFloat radius = [config[@"ball"][@"radius"] floatValue];
CGFloat velocityX = [config[@"ball"][@"velocity"][@"x"] floatValue];
CGFloat velocityY = [config[@"ball"][@"velocity"][@"y"] floatValue];
SKShapeNode *ball = [SKShapeNode node];
ball.name = @"ball";
ball.position = CGPointMake(CGRectGetMidX([self paddleNode].frame), CGRectGetMaxY([self paddleNode].frame) + radius);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, NULL, 0, 0, radius, 0, M_PI * 2, YES);
ball.path = path;
ball.fillColor = [SKColor yellowColor];
ball.strokeColor = [SKColor clearColor];
ball.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:radius];
ball.physicsBody.affectedByGravity = NO;
ball.physicsBody.velocity = CGVectorMake(velocityX + self.stage, velocityY + self.stage);
ball.physicsBody.restitution = 1.0f;
ball.physicsBody.linearDamping = 0;
ball.physicsBody.friction = 0;
ball.physicsBody.usesPreciseCollisionDetection = YES;
ball.physicsBody.categoryBitMask = ballCategory;
ball.physicsBody.contactTestBitMask = blockCategory;
CGPathRelease(path);
[self addChild:ball];
}
- (SKNode *)ballNode {
return [self childNodeWithName:@"ball"];
}
# pragma mark - Touch
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (![self ballNode]) {
[self addBall];
return;
}
UITouch *touch = [touches anyObject];
CGPoint locaiton = [touch locationInNode:self];
CGFloat speed = [config[@"paddle"][@"speed"] floatValue];
CGFloat x = locaiton.x;
CGFloat diff = abs(x - [self paddleNode].position.x);
CGFloat duration = speed * diff;
SKAction *move = [SKAction moveToX:x duration:duration];
[[self paddleNode] runAction:move];
}
- (SKNode *)newBlock {
CGFloat width = [config[@"block"][@"width"] floatValue];
CGFloat height = [config[@"block"][@"height"] floatValue];
int maxLife = [config[@"block"][@"max_life"] floatValue];
SKSpriteNode *block = [SKSpriteNode spriteNodeWithColor:[SKColor cyanColor] size:CGSizeMake(width, height)];
block.name = @"block";
int life = (arc4random() % maxLife) + 1;
block.userData = @{ @"life" : @(life) }.mutableCopy;
block.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:block.size];
block.physicsBody.dynamic = NO;
block.physicsBody.categoryBitMask = blockCategory;
[self updateBlockAlpha:block];
[self addChild:block];
return block;
}
- (void)decreaseBlockLife:(SKNode *)block {
int life = [block.userData[@"life"] intValue] - 1;
block.userData[@"life"] = @(life);
[self updateBlockAlpha:block];
if (life < 1) {
[self removeNodeWithSpark:block];
}
if ([self blockNodes].count < 1) {
[self nextLevel];
}
}
- (NSArray *)blockNodes {
NSMutableArray *nodes = @[].mutableCopy;
[self enumerateChildNodesWithName:@"block" usingBlock:^(SKNode *node, BOOL *stop) {
[nodes addObject:node];
}];
return nodes;
}
# pragma mark - Label
- (void)addStageLabel {
CGFloat margin = [config[@"label"][@"margin"] floatValue];
CGFloat fontSize = [config[@"label"][@"font_size"] floatValue];
SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue-Bold"];
label.text = [NSString stringWithFormat:@"STAGE %d", _stage];
label.verticalAlignmentMode = SKLabelVerticalAlignmentModeTop;
label.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeRight;
label.position = CGPointMake(CGRectGetMaxX(self.frame) - margin, CGRectGetMaxY(self.frame) - margin);
label.fontSize = fontSize;
label.zPosition = 1.0f;
[self addChild:label];
}
- (void)addLifeLabel {
CGFloat margin = [config[@"label"][@"margin"] floatValue];
CGFloat fontSize = [config[@"label"][@"font_size"] floatValue];
SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"HiraKakuProN-W3"];
label.verticalAlignmentMode = SKLabelVerticalAlignmentModeTop;
label.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
label.position = CGPointMake(margin, CGRectGetMaxY(self.frame) - margin);
label.fontSize = fontSize;
label.zPosition = 1.0f;
label.color = [SKColor magentaColor];
label.colorBlendFactor = 1.0f;
label.name = @"lifeLabel";
[self addChild:label];
}
- (void)updateLifeLabel {
NSMutableString *s = @"".mutableCopy;
for (int i = 0; i < _life; i++) {
[s appendString:@"◆"];
}
[self lifeLabel].text = s;
}
- (SKLabelNode *)lifeLabel {
return (SKLabelNode *)[self childNodeWithName:@"lifeLabel"];
}
# pragma mark - Callbacks
- (void)update:(NSTimeInterval)currentTime {
if((int)currentTime % 5 == 0) {
CGVector velocity = [self ballNode].physicsBody.velocity;
velocity.dx *= 1.001f;
velocity.dy *= 1.001f;
[self ballNode].physicsBody.velocity = velocity;
}
}
- (void)didEvaluateActions {
CGFloat width = [config[@"paddle"][@"width"] floatValue];
CGPoint paddlePosition = [self paddleNode].position;
if (paddlePosition.x < width / 2) {
paddlePosition.x = width / 2;
} else if (paddlePosition.x > CGRectGetWidth(self.frame) - width / 2) {
paddlePosition.x = CGRectGetWidth(self.frame) - width / 2;
}
[self paddleNode].position = paddlePosition;
}
- (void)didSimulatePhysics {
if ([self ballNode] && [self ballNode].position.y < [config[@"ball"][@"radius"] floatValue] * 2) {
[self removeNodeWithSpark:[self ballNode]];
_life--;
[self updateLifeLabel];
if (_life < 1) {
[self gameOver];
}
}
}
# pragma mark - Utilities
- (void)removeNodeWithSpark:(SKNode *)node {
NSString *sparkPath = [[NSBundle mainBundle] pathForResource:@"spark" ofType:@"sks"];
SKEmitterNode *spark = [NSKeyedUnarchiver unarchiveObjectWithFile:sparkPath];
spark.position = node.position;
spark.xScale = spark.yScale = 0.3f;
[self addChild:spark];
SKAction *fadeOut = [SKAction fadeOutWithDuration:5.0f];
SKAction *remove = [SKAction removeFromParent];
SKAction *sequence = [SKAction sequence:@[fadeOut, remove]];
[spark runAction:sequence];
[node removeFromParent];
}
- (void)updateBlockAlpha:(SKNode *)block {
int life = [block.userData[@"life"] intValue];
block.alpha = life * 0.2f;
}
# 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 & blockCategory) {
if (secondBody.categoryBitMask & ballCategory) {
[self decreaseBlockLife:firstBody.node];
}
}
}
- (void)gameOver {
SKScene *scene = [BFGameOverScene sceneWithSize:self.size];
SKTransition *transition = [SKTransition pushWithDirection:SKTransitionDirectionDown duration:1.0f];
[self.view presentScene:scene transition:transition];
}
- (void)nextLevel {
BFPlayScene *scene = [[BFPlayScene alloc] initWithSize:self.size life:self.life stage:self.stage + 1];
SKTransition *transition = [SKTransition doorwayWithDuration:1.0f];
[self.view presentScene:scene transition:transition];
}
@end
- ボールが外に飛び出さない様に、枠を設定しているのが、
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
- SpriteKitの動作は以下の処理を繰り返すようになっているらしい
- Update
- アクションの処理
- didEvaluateActions
- 物理演算
- didSimulatePhysics
- 描画
- 空色の処理は、ソース中で Callbackとマークされているが、
- ここで、ユーザ処理を行うことが出来るようになっている様だ
- このソースの場合、以下のような処理をさせている
- update : 5秒毎にボールの速度を速める
- didEvaluateActions : 画面外に出たバドルを戻す
- didSimulatePhysics : ボールが画面下部に行ったら破壊して、lifeを減らす。ゼロになったらGameOver
- Titleシーンと同様に、GameOverシーンを作成して、タップで画面を遷移させる。
#import <SpriteKit/SpriteKit.h>
@interface BFGameOverScene : SKScene
@end
#import "BFGameOverScene.h"
#import "BFPlayScene.h"
@implementation BFGameOverScene
- (id)initWithSize:(CGSize)size {
self = [super initWithSize:size];
if (self) {
SKLabelNode *titleLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
titleLabel.text = @"GAVE OVER...";
titleLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
titleLabel.fontSize = 40.0f;
[self addChild:titleLabel];
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
SKScene *scene = [BFPlayScene sceneWithSize:self.size];
SKTransition *transition = [SKTransition pushWithDirection:SKTransitionDirectionUp duration:1.0f];
[self.view presentScene:scene transition:transition];
}
@end
- こちらのページで、パドルの移動と、玉の発射をコントロールする改良をしていたので、そのままマネさせていただく。
- 指の動きに追従させるために、touchesMovedメソッドを追加する。
- touchesBeganで、ballが無ければ生成しているが、これをコメントアウトして、
[top]
- このプログラムは良く出来てるけど、音がないので、ちょっと物足りない。
- iOSには、afconvertというツールが元々インストールされているらしい。
- 変換するファイル名を指定する。オプションは、最低限、以下の指定
- -f :ファイル形式を指定(aiff,wav,caff)
- -d :フォーマットを指定(aac,ima4)
- -o :出力ファイル名の指定
- terminalで、音を確認するには、afplayを使う
$ afplay pi.caf
- Xcodeのプロジェクトに、サウンドファイルを追加する
- とりあえず、Supporting Files の中に.cafファイルをドラッグ&ドロップする
- これだけではダメみたいで、Project Navigatorで、プロジェクト名を選択、さらに真ん中の画面で、TARGETでプロジェクト名を選択すると、
- General, Capabilities, Info, Build Settings, Build Phases, Build Rules と項目が表示される
- Info, Build Settings しか表示されていない場合は、右向き三角ボタンを押してTARGETでプロジェクト名を選択する
- Build Phasesを選択、Copy Bundle Resourcesを開いて、cafファイルが参照されているかどうか確認
- なければ、下方の+を押して、cafファイルを追加する
- 最初、プロジェクトに追加しても、サウンドファイルが読み込まれずに、かなり悩んだ。。
- Bundle Resourcesの確認は、必ず行うこと!
- 右クリックで、Add File...すれば、大丈夫なのかもしれない。
- SpriteKitには、Sound発生用のメソッドが用意されているので、
- これは、SpriteKit以外、特にImportとかしなくても、使えるみたいだ。
- 例えば、以下のような箇所に効果音を付加する
- Sceneの開始時、例えば initWithSizeメソッド
- ゲームオーバーへの遷移、gameOverメソッド
- ネクストレベルへの遷移、nextLevelメソッド
- ブロックの破壊時、decreaseBlockLifeメソッドで、Lifeが0になった時
- 衝突時、didBeginContactメソッドで、PhisicsBodyのカテゴリ毎に音を変えて
- 効果音を出すのはうまく行ったが、しばらく遊んでいると、Thread 1:signal SIGABRTのエラーになる事があるみたいだ。
*** Terminating app due to uncaught exception 'Failed to Load Resource', reason: 'Resource pi.caf can not be loaded'
- 毎回読み込むのは無理があるのか。あるいはメモリーリークしてるのか。。
- それでは、実際に効果音を追加するのはどうするのが良いんだろう。
- 最初にプロパティで、実行するアクションのポインタ変数projectileSoundEffectActionを用意して
@property (nonatomic, strong) SKAction *projectileSoundEffectAction;
- 初期設定時に、実行するアクションのポインタを入れておき、
self.projectileSoundEffectAction = [SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO];
- 効果音を鳴らすときは、ポインタを実行する様にする
- サンプルでは、オブジェクトの動きと同時に音を発生させている2らしく、終わったら、オブジェクトを消しているが、
SKAction *moveAction = [SKAction moveTo:realDest duration:realMoveDuration];
SKAction *projectileCastAction = [SKAction group:@[moveAction,self.projectileSoundEffectAction]];
[projectile runAction:projectileCastAction completion:^{
[projectile removeFromParent];
[self.projectiles removeObject:projectile];
}];
- 実質上、音を鳴らすにはこれだけで良いみたいだ。
[self runAction:self.projectileSoundEffectAction];
- この方が、実行時にエラーが出にくい気がする(このようにしてからは、今のところ、エラーが起こっていない)
- バッググラインド・ミュージックを鳴らし続けるにはどうすれば良いんだろう。
- これは、SpriteKitに含まれるものでは無いみたいだが、結構、簡単に出来る。
- 最初に、AVFoundationをimportして、AVAudioPlayerのプロパティを用意する
#import <AVFoundation/AVFoundation.h>
@property (nonatomic, strong) AVAudioPlayer *bgmPlayer;
- 初期設定で、曲データを読み込み、ループ再生にして、演奏を開始する
[self.bgmPlayer stop];
self.bgmPlayer = nil;
2こんな風に、ノードの動きと音をグループ化?して実行すると、何か御利益があるのかなぁ。
[top]
- ここまで、tnantokaさんの SpriteKitでブロック崩しをまんま追っかけて来たわけだけど、
- 解説がとても解り易かったので、プログラムの動作をある程度理解出来た。
- 通常のプログラムは、Windowに、階層的にViewを作って、それを切り替えて画面を変えるけど、
- SpriteKitの場合は、その代わりにSceneを切り替えて、画面を変えて、その中にNode(部品)を置いていく
- Sceneも、そもそもがNodeを継承したクラスなので、Viewの代わりにNode(部品)を階層的に作る感じ
- SpriteKitのノード同士の衝突時の動作は、
- そのノードの持つカテゴリのフラグと、他のノードのカテゴリに対する挙動を示すフラグを定義して、フラグ演算で処理される
| BitMask | default | description |
| categoryBitMask | 0xFFFFFFFF | そのノードがどのカテゴリか示す(デフォルトでは全てのカテゴリに含まれる) |
| contactTestBitMask | 0x00000000 | どのカテゴリのノードと衝突した場合に、デリゲートメソッドを呼び出すか示すフラグ |
| collisionBitMask | 0xFFFFFFFF | どのカテゴリのノードと衝突した場合に、反射運動させるかを示すフラグ |
- categoryBitMask と contactTestBitMask の論理積が0でなければ、衝突時に当たりと判定して、
- SKPhysicsContactオブジェクトが生成され、デリゲートの didBeginContact等のメソッドが呼び出される
- categoryBitMask と collisionBitMask の論理積が1ならば、衝突時に反射動作する
[top]
- 無効なポインタをアクセスして、Abortしたとかいう感じ?なのかな。
- なにか間違ってるのは確かなんだけど、かなり厄介。原因が分かりづらい。。
- 例えば、ファイル名を間違えただけでも、ファイルpathがnullになって、このエラーになったりする。
- とにかく、エラーが出る直前に編集した箇所を良くみてみよう。
*** Terminating app due to uncaught exception 'Failed to Load Resource', reason: 'Resource pi.caf can not be loaded'
- このエラーは、何回も読み込んでいると発生するみたいだけど、原因が良くわからない。
- 読み込みが間に合わない、或はメモリーリークがあるのかもしれない
- .xcconfigのプロジェクトファイルを開いて、コンパイルした際のエラー
***.xcodeproj The file “Pods.xcconfig” couldn’t be opened because there is no such file. (***.xcconfig)
The file “Pods.xcconfig” couldn’t be opened because there is no such file. (***.xcconfig)
・
[top]
[top]
[プログラムの部屋に戻る]
⇒ Disqusの広告がうるさすぎるので基本は非表示にしました