
【裏話】Android OSのコードネームに隠された秘密とは!
目次
おはようございます。
システム事業部 かっちゃんです。
平時はiOSアプリの開発に携わっています。
今回のこの記事もiOSアプリに関するものです。
目次を見て興味をひかれるものがあれば、どうぞ先へお進みください。
最初は準備と説明回です。
タイトルの通り、今回はMac PCとXcodeでブロック崩しのiOSゲームアプリを実際に作ってみたいと思います。
Xcode(エックスコード)は、Apple社が開発しているアプリ開発ツールです。
開発言語は『Swift』または『Objective-C』。
今回は『Objective-C』を使います。
XcodeはMac App Storeから無料でダウンロードできます。
アプリをストアでリリースするには開発者登録が必要になりますが、アプリの開発とシミュレーターや実機で動かすまでなら無料でできます。
ちなみに今回は長くなるので、Xcodeのインストールや使い方の説明は省略します。
ここからはXcodeを使い実際にアプリを作っていきます。
段階的にコードと実行結果の画像を載せるので、参考にしていただければと思います。
これから作るゲームの大まかな画面の流れは以下の通りです。
作成フロー①:タイトル画面
作成フロー②:プレイ画面
作成フロー③:ゲームオーバー画面
作成フロー④:クリア画面
今回はタイトル画面から順番に作っていきます。
基礎となるGameViewControllerとタイトル画面であるTitleSceneを作ります。
AppDelegate.mからGameViewControllerを開き、続いてGameViewController.mからTitleScene開くよう記述を追加します。
※ここからコードがずらっと並びます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//【AppDelegate.m】 #import "AppDelegate.h" #import "GameViewController.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; GameViewController *viewController = GameViewController.new; _window.rootViewController = viewController; [self.window makeKeyAndVisible]; return YES; } // 省略... |
1 2 3 4 5 6 7 8 9 10 11 12 |
//【GameViewController.h】 @import UIKit; @import SpriteKit; @interface GameViewController : UIViewController @property (strong, nonatomic) SKScene *gameScene; @property (weak, nonatomic) IBOutlet SKView *skView; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
//【GameViewController.m】 #import "GameViewController.h" #import "TitleScene.h" @interface GameViewController () @end @implementation GameViewController - (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; skView.showsPhysics = NO; // 物理演算適応の可視化(デバッグ用) SKScene *scene = [TitleScene sceneWithSize:self.view.bounds.size]; [skView presentScene:scene]; } - (void)viewWillLayoutSubviews { CGRect screenRect = [[UIScreen mainScreen] bounds]; CGFloat screenWidth = screenRect.size.width; CGFloat screenHeight = screenRect.size.height; if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) { int safeAreaTop = 20; int safeAreaBottom = 20; // iOS11以降のみ実行可能 if (@available(iOS 11, *)) { if(self.view.safeAreaInsets.top > safeAreaTop) { safeAreaTop = self.view.safeAreaInsets.top; } if(self.view.safeAreaInsets.bottom > safeAreaBottom) { safeAreaBottom = self.view.safeAreaInsets.bottom; } } self.view.frame = CGRectMake(self.view.bounds.origin.x, safeAreaTop, screenWidth, screenHeight - safeAreaTop - safeAreaBottom); // frameを設定 } } - (BOOL)prefersStatusBarHidden { return YES; } @end |
1 2 3 4 5 6 7 8 9 |
//【TitleScene.h】 @import UIKit; @import SpriteKit; @interface TitleScene : SKScene @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
//【TitleScene.m】 #import "TitleScene.h" @interface TitleScene () @end @implementation TitleScene - (id)initWithSize:(CGSize)size { self = [super initWithSize:size]; if (self) { // ゲームタイトル用ラベル SKLabelNode *titleLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"]; // 表示するテキスト titleLabel.text = @"ブロックくずし"; // ラベルの座標 titleLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame) + CGRectGetMidY(self.frame) / 4); // 文字サイズ titleLabel.fontSize = 50.0f; // 画面にラベルを追加 [self addChild:titleLabel]; // ゲーム開始方法の説明用ラベル SKLabelNode *subLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"]; // 表示するテキスト subLabel.text = @"画面をタッチしてスタート!"; // ラベルの座標 subLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame) / 6); // 文字サイズ subLabel.fontSize = 20.0f; // 画面にラベルを追加 [self addChild:subLabel]; } return self; } @end |
ひとまずここまでで一度動かしてみましょう。
タイトル画面が表示されました。
画面下部に書いてある通り、この後コードを追加してゲームを開始できるようにします。
次はプレイ画面であるPlaySceneを作り、タイトル画面から遷移するようTitleSceneに記述を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//【TitleScene.m】 #import "TitleScene.h" #import "PlayScene.h" // 省略... # pragma mark - Event // 画面がタッチされた時に働く - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // ゲームのプレイシーンを作成 SKScene *scene = [PlayScene sceneWithSize:self.size]; // シーン遷移の演出を作成, pushWithDirection:SKTransitionDirectionUp:シーン遷移の演出, duration:演出時間 SKTransition *transition = [SKTransition pushWithDirection:SKTransitionDirectionUp duration:1.0f]; // シーンを遷移する [self.view presentScene:scene transition:transition]; } @end |
タイトル画面をタッチした時にプレイ画面へ遷移する処理が用意できました。
続いてPlaySceneを作成し、ブロックとパドルを配置。
更に画面がタッチされたらパドルの上にボールが出現するよう記述します。
1 2 3 4 5 6 7 8 9 10 11 |
//【PlayScene.h】 @import UIKit; @import SpriteKit; @interface PlayScene : SKScene <SKPhysicsContactDelegate> - (id)initWithSize:(CGSize)size; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
//【PlayScene.m】 #import "PlayScene.h" @interface PlayScene () @end @implementation PlayScene - (id)initWithSize:(CGSize)size { self = [super initWithSize:size]; if (self) { [self addBlocks]; [self addPaddle]; } return self; } static NSDictionary *config = nil; + (void)initialize { NSString *path = [[NSBundle mainBundle] pathForResource:@"config" ofType:@"plist"]; if (!config) { config = [NSDictionary dictionaryWithContentsOfFile:path]; } } # pragma mark - Block - (void)addBlocks { int rows = [config[@"block"][@"rows"] intValue]; int cols = [config[@"block"][@"col"] intValue]; CGFloat margin = [config[@"block"][@"margin"] floatValue]; CGFloat width = [config[@"block"][@"width"] floatValue]; CGFloat height = [config[@"block"][@"height"] floatValue]; 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; } - (NSArray *)blockNodes { NSMutableArray *nodes = @[].mutableCopy; [self enumerateChildNodesWithName:@"block" usingBlock:^(SKNode *node, BOOL *stop) { [nodes addObject:node]; }]; return nodes; } # 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; } } @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
//【config.plist】 <dict> <key>block</key> <dict> <key>margin</key> <string>16.0</string> <key>width</key> <string>34.0</string> <key>height</key> <string>16.0</string> <key>rows</key> <string>5</string> <key>col</key> <string>7</string> <key>max_life</key> <string>5</string> </dict> <key>paddle</key> <dict> <key>width</key> <string>70.0</string> <key>height</key> <string>14.0</string> <key>y</key> <string>40.0</string> </dict> <key>ball</key> <dict> <key>radius</key> <string>6.0</string> <key>velocity</key> <dict> <key>x</key> <string>50.0</string> <key>y</key> <string>120.0</string> </dict> </dict> </dict> |
ひとまずここまでで一度動かしてみましょう。
プレイ画面への遷移アニメーションです。
タイトル画面が上に押し出されてプレイ画面が下から出てきます。
ブロックとパドル、ボールが表示されました。
ここからはゲームの要の部分を作ります。
画面上部に残りライフと現在のステージ数とSCORE(破壊したブロック数)を表示するようPlaySceneに記述を追加。
更に、ブロックとパドル、ボールにそれぞれ物理演算・衝突判定を設定します。
ボールが当たったブロックは破壊エフェクトを出して消え、ボールが画面下部に到達したらライフが1減るようにしてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//【PlayScene.h】 @import UIKit; @import SpriteKit; @interface PlayScene : SKScene <SKPhysicsContactDelegate> @property (nonatomic) int life; @property (nonatomic) int stage; @property (nonatomic) int score; - (id)initWithSize:(CGSize)size life:(int)life stage:(int)stage score:(int)score; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
//【PlayScene.m】 #import "PlayScene.h" // カテゴリビットマスク static const uint32_t BLOCK_CATEGORY = 0x1 << 0; // ブロック static const uint32_t BALL_CATEGORY = 0x1 << 1; // ボール @interface PlayScene () @end @implementation PlayScene - (id)initWithSize:(CGSize)size life:(int)life stage:(int)stage score:(int)score { self = [super initWithSize:size]; if (self) { self.life = life; self.stage = stage; self.score = score; [self addBlocks]; [self addPaddle]; [self addStageLabel]; [self addLifeLabel]; [self updateLifeLabel]; [self addScoreLabel]; // 画面端に物理ボディを設定してボールが飛び出さないよう壁にする 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 score:0]; } // 省略... # pragma mark - Block // 省略... - (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"; // 物理演算の設定 block.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:block.size]; block.physicsBody.dynamic = NO; // 物理演算を受けて動くか否か(NOで固定される) block.physicsBody.categoryBitMask = BLOCK_CATEGORY; int life = (arc4random() % maxLife) + 1; block.userData = @{ @"life" : @(life) }.mutableCopy; [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]; [self removeNodeWithSpark:block]; // 破壊演出 // ブロック1つ破壊につき、1点スコアを加算する。 self.score++; [self updateScoreLabel]; // スコア表示を更新 } // 省略... # 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]; } // 省略... # 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 = BALL_CATEGORY; ball.physicsBody.contactTestBitMask = BLOCK_CATEGORY; ball.physicsBody.velocity = CGVectorMake(velocityX + self.stage, velocityY + self.stage); // 加速 CGPathRelease(path); [self addChild: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 = fabs(x - [self paddleNode].position.x); CGFloat duration = speed * diff; SKAction *move = [SKAction moveToX:x duration:duration]; [[self paddleNode] runAction:move]; } # 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 & BLOCK_CATEGORY) { if (secondBody.categoryBitMask & BALL_CATEGORY) { [self decreaseBlockLife:firstBody.node]; } } } # 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"]; } - (void)addScoreLabel { CGFloat margin = [config[@"label"][@"margin"] floatValue]; CGFloat fontSize = [config[@"label"][@"font_size"] floatValue]; SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"HiraKakuProN-W3"]; label.text = [NSString stringWithFormat:@"SCORE %d", self.score]; label.verticalAlignmentMode = SKLabelVerticalAlignmentModeBottom; label.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft; label.position = CGPointMake(margin, margin); label.fontSize = fontSize; label.zPosition = 1.0f; label.name = @"scoreLabel"; [self addChild:label]; } - (void)updateScoreLabel { [self scoreLabel].text = [NSString stringWithFormat:@"SCORE %d", self.score]; } - (SKLabelNode *)scoreLabel { return (SKLabelNode *)[self childNodeWithName:@"scoreLabel"]; } # pragma mark - Callbacks - (void)update:(NSTimeInterval)currentTime { // 5秒ごとにボールの速度を速くする 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 { // ボールが画面最下部に行った時、破壊してライフを減らす処理を行う。0になればゲームオーバーを表示する if ([self ballNode] && [self ballNode].position.y < [config[@"ball"][@"radius"] floatValue] * 2) { [self removeNodeWithSpark:[self ballNode]]; // 破壊演出 _life--; // ライフ減少 [self updateLifeLabel]; } } # 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]; } @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
//【config.plist】 <dict> <key>block</key> <dict> // 省略... </dict> <key>paddle</key> <dict> // 省略... <key>speed</key> <string>0.005</string> </dict> <key>ball</key> <dict> // 省略... </dict> <key>label</key> <dict> <key>margin</key> <string>5.0</string> <key>font_size</key> <string>14.0</string> </dict> <key>max_life</key> <string>5</string> </dict> |
【spark.sks】
今回の破壊エフェクトはテンプレートをそのまま使用します。
新しいファイルの作成から、ファイルのテンプレート『SpriteKit Particle File』を選択し、その後のパーティクルのテンプレートは『Spark』を選択。
ひとまずここまでで一度動かしてみましょう。
ボールが動き、壁やパドルに当たって跳ね返り、ボールが当たったブロックは破壊され、ボールを落とせばライフが減るようになりました。
ここまででも既に十分ゲームらしい見た目になったと思いますが、もうちょっとだけ続きます。
続いてゲームオーバー画面の作成。
ゲームオーバー画面であるGameOverSceneを作り、ライフが0になった時に遷移するようPlaySceneにも記述を追加します。
1 2 3 4 5 6 7 8 9 10 11 |
//【GameOverScene.h】 @import UIKit; @import SpriteKit; @interface GameOverScene : SKScene - (id)initWithSize:(CGSize)size; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
//【GameOverScene.m】 #import "GameOverScene.h" #import "TitleScene.h" @interface GameOverScene () @end @implementation GameOverScene - (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) + CGRectGetMidY(self.frame) / 4); // 文字サイズ titleLabel.fontSize = 50.0f; // 画面にラベルを追加 [self addChild:titleLabel]; // ゲーム開始方法の説明用ラベル SKLabelNode *subLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"]; // 表示するテキスト subLabel.text = @"画面をタッチしてタイトルへ戻る"; // ラベルの座標 subLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame) / 3); // 文字サイズ subLabel.fontSize = 20.0f; // 画面にラベルを追加 [self addChild:subLabel]; } return self; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { SKScene *scene = [TitleScene sceneWithSize:self.size]; SKTransition *transition = [SKTransition pushWithDirection:SKTransitionDirectionUp duration:1.0f]; [self.view presentScene:scene transition:transition]; } @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
//【PlayScene.m】 #import "PlayScene.h" #import "GameOverScene.h" // 省略... # pragma mark - Callbacks // 省略... - (void)didSimulatePhysics { // ボールが画面最下部に行った時、破壊してライフを減らす処理を行う。0になればゲームオーバーを表示する 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)gameOver { SKScene *scene = [[GameOverScene alloc] initWithSize:self.size]; SKTransition *transition = [SKTransition pushWithDirection:SKTransitionDirectionDown duration:1.0f]; [self.view presentScene:scene transition:transition]; } // 省略... @end |
ひとまずここまでで一度動かしてみましょう。
ライフが0になったらゲームオーバー画面が表示され、タッチするとタイトル画面へ戻るようになりました。
最後にステージクリア画面と次ステージへ進む処理を作ります。
PlaySceneに記述を追加していきましょう。
全てのブロックを破壊した時にステージクリアの表示を出し、タッチすると次のステージへ進むようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
//【PlayScene.m】 #import "PlayScene.h" #import "GameOverScene.h" static const int SCORE_BONUS = 100; // ステージクリアボーナス // 省略... # pragma mark - Block // 省略... - (void)decreaseBlockLife:(SKNode *)block { int life = [block.userData[@"life"] intValue] - 1; block.userData[@"life"] = @(life); [self updateBlockAlpha:block]; [self removeNodeWithSpark:block]; // 破壊演出 // ブロック1つ破壊につき、1点スコアを加算する。 self.score++; [self updateScoreLabel]; // スコア表示を更新 if ([self blockNodes].count < 1) { // ステージクリアの表示を出す [self stageClear]; } } // 省略... # pragma mark - Utilities // 省略... - (void)stageClear { // ステージ数にクリアボーナスを追加する self.score += self.stage * SCORE_BONUS; [self updateScoreLabel]; // スコア表示を更新 // ボールを停止する if ([self ballNode]) { SKNode *ballNode = [self ballNode]; ballNode.physicsBody.dynamic = NO; } // ステージクリアの表示を出す SKLabelNode *clearLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"]; clearLabel.text = [NSString stringWithFormat:@"%d Stage Clear !", self.stage]; clearLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)); clearLabel.fontSize = 40.0f; [self addChild:clearLabel]; } - (void)nextLevel { // 次のステージへ進める PlayScene *scene = [[PlayScene alloc] initWithSize:self.size life:self.life stage:self.stage + 1 score:self.score]; SKTransition *transition = [SKTransition doorwayWithDuration:1.0f]; [self.view presentScene:scene transition:transition]; } // 省略... @end |
これでブロックくずしの完成です!
動かしてみましょう。
ブロックを全て破壊して次のステージへ進むことができるようになりました!
ゲームアプリ『ブロックくずし』の作成は以上になります。
機能や演出を追加する、BGMやSEを鳴らす、解像度差を考慮してブロックの大きさやマージンを調整するなど改善点はまだまだありますが、今回の説明はここまで。
この先は自由に手を加えてみてください。
![]() |
![]() |
![]() |
最後に、こんな仕様を追加してみました。
機会があればまた別のゲームを作ってみたいと思います。