A Day In The Life

とあるプログラマの備忘録

Isometric MapのZオーダーの指定方法

Cocos2d-xでIsometric Mapを使ったゲームのサンプルを作成していたところz-order(zorder, zindex, z-index)の指定で結構ハマったのでメモです。
Isometric Mapというのはよくある2.5D的な1マスが菱形になっているマップのことです。こんな感じのやつです。
Isometric Map
Isometric Mapを使った有名なゲームだとクラッシュオブクランなんかがあります。
ゲームで使用するマップデータの作成を無料のTiled Map Editorというツールを使ってCocos2d-xと連携させて使っていました。
TiledMap Editorを使うとわりと簡単にマップを作成することはできるものの、マップの上にキャラだったり建物だったりを配置するとZオーダーの問題が出てきます。
そこで以下の図のようにマップ座標のxとyを足した値をZオーダーに設定するとうまくいきました(図は40マス×40マスのマップの例です)。
Isometric MapのZオーダー
プログラムにすると以下のような感じになります。

bool HelloWorldScene::init()
{
  //////////////////////////////
  // 1. super init first
  if ( !Layer::init()) {
    return false;
  }
  auto tiledMap = TMXTiledMap::create("tiledmap.tmx");
  this->addChild(tiledMap, 1, "tiledMap");  
  auto layer = tiledMap->getLayer("layer_name");
  Size size = tiledMap->getMapSize();
  // マップのサイズ分まわす
  for (int i = 0; i < size.width; i++) {
    for (int j = 0; j < size.height; j++) {
      Vec2 pos = Vec2(i, j);
      // タイルオブジェクトを取得
      auto tile = layer->getTileAt(pos);
      // タイルが存在する場合
      if (tile) {
        // GIDを取得
        auto gid = layer->getTileGIDAt(pos);
        // GIDごとに建物を配置
        if (gid == 1) {
          auto sprite = Sprite::create("building.png");
          // タイルの真ん中の位置にスプライトをセット
          Vec2 midPoint = tile->getPosition() + (tile->getContentSize() / 2);
          sprite->setPosition(midPoint);
          // Zオーダーを指定してスプライトをタイルマップに追加する
          tiledMap->addChild(sprite, i + j);
        }
      }
    }
  }
}

上記は動かないスプライトの例ですが、スプライトを動かす場合はポジションからマップ上の座標を計算してZオーダーを設定してやると上手くいきます。
通常の座標からマップ上に座標に変換する方法は以下のサイトが参考になります。

特定のスプライトすべて削除する

最近 Cocos2d-x で2Dゲームを作ってます。
スプライトに名前つけて追加したあと、その名前のスプライトを検索して削除する時にハマったのでメモです(Cocos2d-x のバージョンは3.3です)。
まずはダメなコードから

bool HalloWorldScene::init()
{
  // nameという名前で大量にスプライトを追加する
  for (int i = 0; i < 100; i++) {
    auto sprite = Sprite::create("hoge.png");
    this->addChild(sprite, 0, "hoge");
  }

  // タッチイベント
  auto listener = EventListenerTouchOneByOne::create();
  listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this);
  auto dip = this->getEventDispatcher();
  dip->addEventListenerWithSceneGraphPriority(listener, this);
  dip->setPriority(listener, 0);
}

bool HelloWorld::onTouchBegan(Touch* touch, Event* event)
{
  // だめ。たまに消えないスプライトがある
  for (auto node : this->getChildren()) {
    if (node->getName() == "hoge") {
      node->removeFromParent();
    }
  }
  // 以下もダメ
  this->enumerateChildren("hoge", [](Node *node) -> bool {
    node->removeFromParent();
    return false;
  });
  return true;
}

以下がうまく行ったコードです(initの処理は同じなので省略してます)。

bool HelloWorld::onTouchBegan(Touch* touch, Event* event)
{
  // nameという名前のスプライトを検索する
  this->enumerateChildren("hoge", [](Node *node) -> bool {
    // スプライトを削除するアクション
    auto action = RemoveSelf::create();
    node->runAction(action);
    return false;
   });  
  return true;
}

RemoveSelf アクションは結構使いどころが多くて重宝してます。その他にも特定のアクションを実行されてからスプライトを削除する時なんかに使ってます。例えばこんな感じです。

bool HelloWorld::onTouchBegan(Touch* touch, Event* event)
{
  auto sprite = Sprite::create("hoge.png");
  this->addChild(sprite, 0, "hoge");
  auto moveTo = MoveTo::create(1, Vec2(100, 100));
  auto seq = Sequence::create(moveTo, RemoveSelf::create(), NULL);
  sprite->runAction(seq);
}

パーティクルを作成するときなんかに使えます。

C++のコード

このブログでC++のコードを書いたことがなかったんですね(話題にしたことはあったんですが)。記念すべき第1回目となりました。C++も11になってずいぶん使いやすくなったと思います。

SpriteKitではじめる2Dゲームプログラミング

先日こちらの記事にも書きましたが「Hedgehog Drive」というゲームをリリースしました。このアプリのゲーム部分は SpriteKit を使って実装しました。初めて SpriteKit を使ったので備忘録的にこのフレームワークの使い方をまとめてみました。

2D ゲームで使用される基本用語

SpriteKit の説明の前に 2D ゲーム開発でよく使われる用語とその説明をします。SpriteKit でも以下で説明する用語が使われていますので覚えておくと良いと思います。

  • ゲームループ
    ゲームではキャラクタや背景などが常に動いています。このような常に変化する状態を実現するために、ゲームの状態をプログラムで処理する必要があります。これを処理するための仕組みがゲームループ(またはメインループ)です。ゲームループの処理は常に一定時間ごとに呼び出されます
  • FPS
    Frames Per Second の略。1秒間に何回フレームが処理されるかを表す単位のことです。ゲールループはFPSごとに呼び出されます。
  • シーン(Scene)
    ゲームの場面のことです。RPG のマップ、町の中、バトル、ムービーなどゲームの見せ方が変わる単位です。ストーリーボードで使われるシーンと考え方は同じです
  • トランジション
    シーンの切り替え時のアニメーションのこと
  • スプライト(Sprite)
    シーンの上で動き回るオブジェクトや障害物のことです。スーパーマリオのマリオやクリボー、土管やハテナボックスがスプライトです
  • パーティクル(Particle)
    iOS ではエミッター(Emitter)とも言われています。シューティングゲームで敵を撃破したときの爆発とか、飛行機やロケットのエンジンから噴射される炎なんかを指します。スプライトが爆発する時の演出に使ったりスプライトの動きをよりリアルに見せる為に使うことが多いです
  • アクション(Action)
    スプライトの動き、アニメーションのこと
  • テクスチャ(Texture)
    スプライトの見た目や質感を表現するための模様やパターン、画像のこと

シーンとトランジションの関係を図にすると以下のようになります。
シーンとトランジション
シーンとスプライト、アクションの関係を図にすると以下のようになります。
スプライトとアクション

SpriteKit クラス構成

SpriteKit ではシーン、スプライト、パーティクルがツリーで構成されています。それぞれシーンが SKScene クラス、スプライトが SKSpriteNode クラス、パーティクルが SKEmitterNode クラスとなっています。またツリーを構成する要素のことをノードと呼んでいます。ノードにあたるクラスが SKNode クラスで、SKScene、SKSpriteNode、SKEmitterNode クラスは SKNode クラスを継承しています。
また SpriteKit では UIView オブジェクト上にゲーム画面を配置できるように SKView というクラスを提供しています。SKView オブジェクト上にシーンオブジェクトを配置します。
以下は SpriteKit から提供されている主なクラスの関係を図にしたものです。
SpriteKitクラス図

ゲームプログラム作成の流れ

ゲームプログラム作成の流れは大まかに以下の順番で行います。

  • シーンの配置と表示
  • スプライトの配置
  • スプライトに動きや音をつける

シーンの配置と表示

シーンをビューに配置して表示するには SKView と SKScene クラスを使います。SKView はビューとシーンの橋渡しのためのクラスです。シーンの配置と表示は以下のように UIViewController の viewDidAppear: メソッドで行います。

@implementation MyViewController {

- (void)viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:animated];
    
  // SKViewオブジェクトの生成と追加
  SKView *skView = [[SKView alloc] initWithFrame:self.view.frame];
  skView.showsFPS = YES; // FPSの表示(デバッグ用設定)
  skView.showsNodeCount = YES; // 配置されているノードの数を表示(デバッグ用設定)
  [self.view addSubview:skView];
    
  // SKSceneオブジェクトの生成と配置
  SKScene *scene = [MyScene sceneWithSize:skView.bounds.size];
  scene.scaleMode = SKSceneScaleModeAspectFill;
  scene.userInteractionEnabled = YES;
    
  // シーンの表示
  [skView presentScene:scene];
}

@end

はじめに SKView オブジェクトを生成してビューに追加します。その後、SKScene オブジェクトを生成して SKView オブジェクトの presentScene: メソッドを使って表示します。なお SKView オブジェクトの生成と追加はストーリーボード上で行うことも出来ます。

トランジションを使ったシーンの切り替え

SKTransition クラスを使うとシーンの切り替えにちょっとした演出を加えることが出来ます。
以下は UIViewController のタッチイベントでシーンを切り替えるプログラムの例です。

@implementation MyViewController

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  // シーンオブジェクトの生成
  SKScene *scene = [[SKScene alloc] initWithSize:self.stageView.frame.size];
    
  // トランジションオブジェクトの生成(ドアオープン)
 SKTransition *transition = [SKTransition doorsOpenHorizontalWithDuration:2];
    
  // トランジションをつかったシーンの切り替え
  [self.stageView presentScene:scene transition:transition];
}

@end

トランジションはここで紹介したドアオープン以外にもいろいろあります。

SKScene クラスの拡張

シーンはゲームの場面ごとに分かれていて、各場面ごとに各種イベント処理やスプライトの配置(後ほど説明します)をする必要があります。従って各場面ごとに SKScene クラスを継承してシーンを作成する必要があります。

@interface MyScene : SKScene

@end
タッチイベントの受け取り

SKScene オブジェクトは以下のようにタッチ系イベントを受け取ることが出来ます。

@implementation MyScene {

// Touch Began イベント
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  // シーン上の位置を取得する
  UITouch *touch = [touches anyObject];
  CGPoint location = [touch locationInNode:self]; 
  NSLog(@"%@", NSStringFromCGPoint(location));
}
// Touch Moved イベント
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
}
// Touch Ended イベント
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
}

@end

基本 UIView オブジェクトでタッチイベントを受け取る方法と同じです。UITouch オブジェクトの locationInNode: メソッドを使うとシーン上のどの位置がタッチされたかがわかります。

ゲームループイベントの受け取り

また SKScene オブジェクトでは以下のようにゲームループイベントを受け取ることが出来ます。

@implementation MyScene {

// ゲームループイベント
-(void)update:(CFTimeInterval)currentTime {
  // FPSごとに呼び出される
  NSLog(@"%g", currentTime);
}

@end

スプライトの移動やスプライト同士の衝突判定なんかをするときに使います。

スプライトの配置

シーンを表示しただけではなにも表示されませんので、シーンにスプライトを配置します。スプライトの配置には SKSprite クラスを使います。スプライトをシーン上に配置するとスプライトが画面に表示されます。スプライトの配置は以下のようにシーンのコンストラクタメソッドで行います。

@implementation MyScene

-(instancetype)initWithSize:(CGSize)size {
  if (self = [super initWithSize:size]) {
    // 画像名を指定してスプライトオブジェクトを生成する
    SKSprite *sprite = [[SKSprite alloc] initWithImageNamed:@"hedgehog"];
    // シーンの中央に設定
    sprite.position = CGPointMake(CGRectGetMidX(self.frame),
                                  CGRectGetMidY(self.frame));
    // スプライトに名前をつける
    sprite.name = @“hedgehog”;
    // スプライトを配置する
    [self addChild:sprite];
  }
}

@end

スプライトに名前をつけるとシーンオブジェクトの処理でスプライトを検索する時に役立ちます。

スプライトの検索

スプライトに対する処理は通常、シーンオブジェクトのタッチイベントかゲームループイベントで行います。各イベントでスプライトオブジェクトを参照する場合はシーン上のスプライトを検索する必要があります。
SKScne クラスの childNodeWithName: メソッドを使うとシーン上に配置されているスプライトを検索してオブジェクトを取得することが出来ます。

@implementation MyScene {

-(void)update:(CFTimeInterval)currentTime {
  // hedgehogという名前のスプライトを探してオブジェクトを取得する
  SKNode *sprite = [self childNodeWithName:@“hedgehog”];
}

@end

スプライトに動きや音をつける

SKAction クラスを使うとスプライトに簡単に動きや音をつけることが出来ます。スプライトをシーンに配置しただけではスプライトは動かずゲームらしくないのでスプライトにアクションをつけていきます。スプライトにアクションを実行させるには以下のようにシーンのイベントメソッドを実装します。

@implementation MyScene {

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  // シーン上の位置を取得する
  UITouch *touch = [touches anyObject];
  CGPoint location = [touch locationInNode:self]; 
  // hedgehogスプライトの取得する
  SKNode *sprite = [self childNodeWithName:@“hedgehog”];
  // タッチした位置に移動するアクションの生成
  SKAction *move = [SKAction moveTo:location duration:1];
  // アクションを実行する
  [sprite runAction:move];
}

@end
複数のアクションを順番に実行する

シーケンスを使うと、複数のアニメーションを連続して実行することができます。シーケンスは SKAction クラスの sequence メソッドを使って作成することが出来ます。また作成したシーケンスオブジェクトは通常のアクションを実行するのと同じように runAction メソッドを使って実行することが出来ます。シーケンスを使ったアニメーションの実行では前のアクションが実行されるまで次のアクションは実行されません。
以下はスプライトの向きを変えてから移動するプログラムの例です。

@implementation MyScene {

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  // シーン上の位置を取得する
  UITouch *touch = [touches anyObject];
  CGPoint location = [touch locationInNode:self];
  // hedgehogスプライトの取得する
  SKNode *sprite = [self childNodeWithName:@“hedgehog”];
  // タッチした位置に移動するアクションの生成
  SKAction *move = [SKAction moveTo:location duration:1];
  // 角度の計算
  CGFloat dx = location.x - self.position.x;
  CGFloat dy = location.y - self.position.y;
  CGFloat angle = atan2(dx, dy);
  // 角度を変えるアクションの生成
  SKAction *rotate = [SKAction rotateToAngle:-angle duration:1];
  // シーケンスの生成
  SKAction *seq = [SKAction sequence:@[rotate, move]];
  // アクションの実行(角度が変わってから移動する)
  [self runAction:seq];
}

@end
複数のアクションを同時に実行する

グループを使うと、複数のアニメーションを同時に実行することができます。グループは SKAction クラスの group メソッドを使って作成することが出来ます。また作成したグループオブジェクトは通常のアクションを実行するのと同じように runAction メソッドを使って実行することが出来ます。
以下はスプライトの向きを変えながら移動するプログラムの例です。

@implementation MyScene {

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  // シーン上の位置を取得する
  UITouch *touch = [touches anyObject];
  CGPoint location = [touch locationInNode:self];
  // hedgehogスプライトの取得する
  SKNode *sprite = [self childNodeWithName:@“hedgehog”];
  // タッチした位置に移動するアクションの生成
  SKAction *move = [SKAction moveTo:location duration:1];
  // 角度の計算
  CGFloat dx = location.x - self.position.x;
  CGFloat dy = location.y - self.position.y;
  CGFloat angle = atan2(dx, dy);
  // 角度を変えるアクションの生成
  SKAction *rotate = [SKAction rotateToAngle:-angle duration:1];
  // グループの生成
  SKAction *group = [SKAction group:@[rotate, move]];
  // アクションの実行(角度が変わってから移動する)
  [self runAction:group];
}

@end
音の再生

アクションを使って音を再生することもできます。AVFundation フレームワークや AudioToolbox フレームワークを使って音を再生せるよりも簡単にできます。
以下はシーンでBGM音源を再生させるためのプログラム例です。

@implementation MyScene

-(instancetype)initWithSize:(CGSize)size {
  if (self = [super initWithSize:size]) {
    // 音声ファイルのパスを指定してアクションを生成
    SKAction *sound = [SKAction playSoundFileNamed:@“bgm.m4a" waitForCompletion:YES];
    // 音を再生する
    [self runAction:sound];
  }
}

@end

ゲーム開発初心者がはじめて2Dゲームを作ってみてわかったこと

こちらの記事にも書きましたが先日「Hedgehog Drive」という2Dのゲームをリリースしました。

アプリと違ってゲーム開発は初めてということもあり、苦労したところが多かったのでその辺りをまとめてみました。

どんなゲームをつくったか

指でハリネズミを操作して画面上のセルを消していくゲームです。
画面キャプチャ
ハリネズミが消したセルの数を競います。
セルをすべて消すとアイテムが出現します。最後に消したセルの色によって以下のアイテムが出現します。

  • 最後に消したセルの色が紫だったら「ブドウ」
  • 最後に消したセルの色が黄だったら「チーズ」
  • 最後に消したセルの色が赤だったら「ニンジン」

これらのアイテムを使ってゲームを有利に進めることが出来ます。
動画を見るとどんなゲームかわかりやすいと思うのでプレイ動画を貼付けておきます。

価格は無料で広告表示もありません。

開発のきっかけとプログラミング

ちょうど一年ほど前 iOS6 の時代に、ゲーム開発の勉強をするために Objective-C で UIView を使ったゲームフレームワークを作ったのがきっかけでした。

ゲームエンジン用のサンプルアプリとしてじゃんけんゲーム、タワーディフェンスゲーム、ハリネズミのセル消しゲームなどたしか7個ぐらい作りました。中でも特に気に入っていたセル消しゲームをちゃんとゲームとしてつくってみようと思い去年の7月に開発を開始しました。ゲームの名前はハリネズミが自由に動き回ることから「Hedgehog Drive」としました。
そうこうしてるうちに9月になり、iOS7がリリースされてアップル純正の Sprite Kit という素晴らしいゲームフレームワークの存在を知りました。自分の作ったゲームフレームワークはすぐに開発中止にして「Hegehog Drive」を Sprite Kit に移植することにしました*1
開発当初のハリネズミはこんな感じの絵でした。
初代ハリネズミ

開発でハマったところ

Hedgehog Driveで使った技術は以下の通りです。

  • UIKit
  • Core Data
  • Sprite Kit
  • Game Kit

一番ハマったのが UIKit の Autolayout でした。iPhone 4S発売から2年経過しているのでマルチデバイス対応するメリットがどこまであるのかはきちんと考えた方がよいかもしれません。iPhone 5以上をターゲットにしてAutolayoutを使わずに開発効率をあげるのもありな気がしました。
Autolayout でハマったときに書いた記事が以下にあります。

Sprite Kit を使った感想

必要最低限の機能しかない小さめのフレームワークのわりに、アクションを使ってサウンドを再生でるなどかゆいところに手が届く設計になっていてとても使いやすかったです。はじめて本格的なゲームプログラミングをしましたがあまりハマらずすんなりと使うことができました。

  • シーンの制御
  • スプライトの配置
  • アクションを使ってスプライトを動かす
  • サウンドの再生方法

上記がわかれば何となく作れます。Sprite Kit 関連で読んだ資料はアップル公式ドキュメント「Sprite Kit Programming Guide」です。これを読めばなんとかなります。

SpriteKitの基本的な使い方は以下の記事にまとめました。

どこまで Sprite Kit を使うのか

すべて Sprite Kit で作るのかストーリーボードを組み合わせるのかは結構迷いどころです。自分は画面遷移やスコアの表示なんかは普通にストーリーボードをつかって UIViewController + UIView で作成しました。ゲーム部分のみ Sprite Kit を使いました。Autolayout なんかを考えるとすべて Sprite Kit で開発するのは現実的ではないかもしれません。
ゲームを Sprite Kit で開発するにしてもゲーム以外の部分で UIView や UIViewController を使うのは普通のアプリ開発と変わらないです。ということで引き続き拙著「プロの力が身につく iPhone/iPadアプリケーション開発の教科書」をよろしくお願いします(Swiftに対応した改訂版が発売されました)。

Sprite Kit の説明はありませんが UIView や UIViewController、Core Data などアプリ開発の基礎を学ぶことが出来ます。

ゲームの骨格が決まったのでそれをどうアレンジするか

ゲーム性

ハリネズミを操作してセルを消すという単純なものをどうやって面白くするかはかなり悩みました。はじめ消えたセルをランダムに再生していましたがまったく面白みがありませんでした。セルを生き物のように増殖させたくてセルオートマトンをベースに創発のアプゴリズムを加えて実装しました。
セル増殖のアイデアを実装するときに役にたったのが以下の書籍です。


制限時間内にひたすらセルを消していくだけだと面白くないと感じたので、アイテムをつかってゲームを有利にすすめることが出来ないか考えました。
画面上のセルをすべて消すアイテムや時間を止めるアイテムなどいろいろ考えましたが最終的に以下の3つに決定しました。

  • 残り時間をプラスするブドウ
  • ハリネズミが巨大化するチーズ
  • ハリネズミのスピードがアップするニンジン

個人的にはハリネズミが巨大化するチーズがお気に入りでした。巨大化チーズはたまたまプログラム開発中に間違えてハリネズミのサイズを大きくしたことで生まれました。

ゲームのユーザーインターフェース

当時 iOS7 リリース前後ということもありフラットデザインという言葉が流行していました。フラットデザインの勉強をかねてゲームをフラットな感じの UI でまとめることにしました。ゲームの UI をフラットにして大丈夫かというのが不安でフラットデザインっぽいゲームをいろいろと探しました。そのなかでDotsというゲームにかなり感銘をうけ、セルの色や UI など参考にしました。
Hedgehog Drive の開発をしていたころ iOS7 のデザインについて勉強がてら書いた記事がこちらです。

iOS7 に合ったゲームにしたかったというのが一番つよかったです。

ゲームバランスや難易度をどうやって検証したか

ゲームの実装がひととおり終わったのが12月はじめごろでした(そのときはまだ巨大化チーズはありませんでした)。娘(当時3才、現在4才)にネズミさんのゲームがあるよってことを認識してもらうことからはじめて、ちょっとした変更を加えるだびに遊んでももらいました。子供の反応は素直で面白くなければすぐにホームボタンを押されました。ゲーム中にホームボタンを押される経験を何度か繰り返しましたが改善するにつけてちゃんと最後まで遊んでくれるようになりました。巨大化チーズを見せた時の反応が良かったのでそのまま採用しました。
娘がHedgehog Driveで遊んでいる様子が以下です。

開発当初は子供向けゲームということは意識していませんでしたが、娘に遊ばせているうちに子供でも遊べるようなゲームに仕上げることが出来ました。

イラスト

本職はプログラマなので絵はあまり得意ではありません。いままでアプリのアイコンはほとんど自分でなんとか描いてきたので今回も何とかなるだろうと考えていました。描いたイラストはハリネズミと3種類のアイテムだけです。
ハリネズミブドウチーズニンジン
すべてベクター画像ツールInkscape を使いました。

自分のような素人がイラスを描くコツは二つあります。

  • イラストは円と四角の組み合わせでなんとか描ける
    単純に組み合わせるだけじゃなく差分を使うとぐっと表現力が増します。三角は四角と四角の差分で描けます
  • サイズを何となく決めないで極力黄金比白銀比にする
    素人が感覚だけで絵が描けるわけがないので理論でカバーします

たったこれだけです。あとは自分の描きたいイラストを頭の中でイメージして Google 画像検索の中の膨大なのイラストを参考に円と四角を使って絵を描けばいけます。どうしてもうまくいかない時だけベジェをつかってみると良いとおもいます。Hedgehog Drive で描いたイラストのなかでブドウの弦の部分だけはベジェを使いました。

ハリネズミ画像の変遷

完全蛇足ですがハリネズミ画像は何度か描き直しました。変遷は以下のようになっています。
初代ハリネズミ2代目ハリネズミ3代目ハリネズミ4代目ハリネズミ5代目ハリネズミ

カラーマネジメント

セルの色をどうするかハリネズミの色をどうするかボタンのいろどうするか最後までかなり悩みました。バランスのとれた色合いとはどういうものかいくつかの書籍をよみいろんなサイトの色についての記事を読みましたが未だに上手く色を決めることができません。配色に関しては今後の課題です。1つわかったことはRGBで色を考えるよりもHSV(HSB)を使った方が色の根拠を決めやすいと言うことでした。
配色については現在も以下の書籍を読んで勉強しています。

サウンド

ゲームをつくるにあたりアイデア、プログラミング、デザイン、イラストここまでは正直自分の力でなんとかなりました。ただサウンドだけはどうしても上手くいかず最終的にフリーの効果音と有料の効果音素材を購入しました。
はじめはGrage Bandを使ってなんとか効果音を作ってみたのですが、どうしても自分の作成した音に納得がいかず効果音を自力で作成するのをあきらめました。サウンドがゲームの面白さに直結するので妥協できませんでした。こんな音にしたいというイメージがあってもそれをどうやって作れば良いのか全くわかりませんでした。結局、BGMのみ自作しました。
効果音を探すにあたり参考にしたのが以下のサイトです。

On-Jin は個人開発であれば無料で音をダウンロードして使うことが出来ます。Soundstock は個人開発関係なく商用のゲームで使用する場合は500円(税抜)で気に入った音を購入することができます。セルの破裂音を On-Jin でその他効果音を Soundstock で調達しました。気に入った音、自分のイメージに近い音を探すコツは根気よくいろんな音を探して聞きまくることです。かなり地味な作業ですが良い効果音に出会うとかなりうれしくなります。

開発期間

2013年7月はじめに開発をはじめて2014年3月7日にバージョン1.0.0の承認がおりました。開発開始からリリースまで8ヶ月かかりました。京都にいた(9月まで)ころは大阪までの通勤時間(往復1時間半)と仕事の昼休み、東京に転勤してからは単身赴任中(10月から)は昼休みと仕事終わりの時間、単身赴任後(11月から)は仕事の昼休み時間を利用して開発しました。

開発費用

Soundstock から購入した効果音7種類の費用3500円プラス消費税のみです。それ以外はかかりませんでした。

ダウンロード数

iOS ゲームはすぐに埋もれると聞いてはいたものの本当に厳しくほとんどダウンロードされていません。
無料ゲームであるにも関わらずダウンロード数は2014年3月29日時点で54です。しかもダウンロードの半分が自分の知り合いというかなり厳しい結果です。
前作「即興ピアノ」のときはなんのプロモーションをしなくても毎日30-50ぐらいのダウンロードがあって今でも平均20ぐらいのダウンロードがあります。ここまで悲惨とは思いませんでした。

最後に

最後まで読んでいただきありがとうございます。この記事を読んで少しでもHedgehog Driveに興味をもたれた方はダウンロードお願いします。無料です。広告表示もありません。

*1:iOS7のベータ版の調査をまじめにやっていればこんなことにならなかったわけで、当時はめちゃくちゃ凹みました

ハリネズミと遊ぼう!指でなぞるだけのシンプルな2Dゲームをリリースしました


先日、iOS向けの2Dゲーム「Hedgehog Drive」をリリースしました。
画面を指でなぞって丸いセルを消していくシンプルなゲームです。アプリは無料で広告表示もありません。

4才の娘がルールを理解して遊べるようにゲームのバランス調整をするのが大変でした。せっかくなので娘と実際にHedghog Driveで遊んでいる動画を撮影してみました。

Hedgehog Drive は UIKit と Sprite Kit を組み合わせて開発しました。
はじめのゲーム開発で苦労したことも多かったですが無事にリリースまでこぎ着けることができて良かったです。ぜひダウンロードして遊んでみてください。

Autolayout に対応した画面で UIScrollView を使う時のコツ

iOS 6から導入された Autolayout(オートレイアウト)を最近になって本格的に使うようになりました。
4-inch の iPhone が主流になりつつある中、アプリを Autolayout に対応させるのはほぼ必須といっても良いと思います。Storyboard と格闘してなんとかコツはつかめたものの UIScrollView だけは一筋縄ではいかず苦労しました。
というわけで Autolayout に対応した画面で UIScrollView を使う時のコツをまとめてみました。
なおこの記事で説明する内容は Storyboard 上で Autolayout がオンになっていることが前提になっています。
Autolayoutをオンにする

Autolayout 対応画面で UIScrollView を使うための2種類の方法

Autolayout に対応した画面で UIScrollView を使うには以下の2つの方法があります。

  • Mixed Approach
    Autolayout と AutoresizingMask(iOS 6以前からあるレイアウト方法)を組み合わせる方法
  • Pure Auto Layout Approach
    Autolayout だけを使う方法

どちらのアプローチで実装するにしても UIScrollView オブジェクトは Autolayout を使って配置します。違いは UIScrollView オブジェクトの中に配置するビュー(Content View といいます)を AutoresizingMask を使って配置するか Autolayout を使って配置するかです。
Mixed Approach と Pure Auto Layout Approach に関する詳細は以下のアップル公式ドキュメントを参考にしてください。

サンプルアプリの概要

以下のサンプルアプリを実装しながら説明していきます。
アプリのイメージ
画面の中央に 320 × 170 サイズの UIScrollView オブジェクトを配置して、その中に UIView オブジェクトを2つ横に並べています。表示範囲を横スクロールさせるアプリです。

前準備

それぞれの実装方法を説明する前に準備として UIScrollView オブジェクトを配置しておきます。

UIScrollView オブジェクトの配置

Storyboard を使って、320 * 170サイズの UIScrollView オブジェクトを配置します。
UIScrollView を配置する

Constraints を追加

配置した UIScrollView オブジェクトを選択した状態で Constraints を追加します。まずはじめに UIScrollView オブジェクトの大きさを固定します。
大きさを固定する
次に UIScrollView オブジェクトを画面の真ん中にくるように設定します。
真ん中に設定する

プロパティを追加して IBOutlet 接続する

以下のように UIViewController の子クラス(ここでは仮に ViewController クラスとします)に UIScrollView 型のプロパティを追加します。

@interface ViewController : UIViewController

@property (nonatomic, strong) IBOutlet UIScrollView *scrollView;

@end

Storyboard で追加したプロパティを IBOutlet 接続します。
以上で前準備は完了です。それでは両アプローチの実装方法を見ていきましょう。

Mixed Approach を使って実装する

Autolayout と AutoresizingMaskを組み合わせる方法です。UIScrollView オブジェクトを Autolayout で配置して、スクロールビューの中に配置するビューを Autolayout を使わずに配置します。
以下のように viewDidLoad メソッドを実装します。

@implementation ViewController

- (void)viewDidLoad
{
  [super viewDidLoad];

  /*
   * ナビゲーションコントローラを使って
   * ビューコントローラを管理している場合は下記設定が必要
   */
  self.automaticallyAdjustsScrollViewInsets = NO;

  // 1つ目のビューのインスタンス生成
  UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(40, 0, 240, 170)];

  // 2つ目のビューのインスタンス生成
  UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(300, 0, 240, 170)];

  // スクロールビューにビューを追加する
  [self.scrollView addSubview:view1];
  [self.scrollView addSubview:view2];

  // スクロールビューのコンテンツサイズを設定する
  self.scrollView.contentSize = CGSizeMake(580, 170);
}

@end

UIScrollView オブジェクトにコンテントビュー(view1 と view2)を配置して contentSize を設定するおなじみ方法です。注意点としてはコメントにも書いた通り、ビューコントローラがナビゲーションコントローラーに管理されている場合、ビューコントローラの automaticallyAdjustsScrollViewInsets プロパティに NO を設定しないとコンテントビューの位置がずれてしまうことです(モーダルビューの場合はこの設定は不要です)。どうやら iOS 7 特有の問題のようです。詳しくはこちらの記事を参照してください。

Pure Auto Layout Approach を使って実装する

純粋に Autolayout だけを使って実装する方法です。Mixed Approach に比べると少しコードが長くなる傾向にあります。Autolayout は出来る限り Storyboard を使って設定したい所ではありますが UIScrollView に関して想定通りに動いたためしがないのでコードで書くことにします。
以下のように viewDidLoad メソッドを実装します。

@implementation ViewController

- (void)viewDidLoad
{
  [super viewDidLoad];
    
  /*
   * ナビゲーションコントローラを使って
   * ビューコントローラを管理している場合は下記設定が必要
   */
  self.automaticallyAdjustsScrollViewInsets = NO;
    
  // 1つ目のビューのインスタンス生成
  UIView *view1 = [[UIView alloc] init];
    
  // 2つ目のビューのインスタンス生成
  UIView *view2 = [[UIView alloc] init];
    
  // スクロールビューにビューを追加する
  [self.scrollView addSubview:view1];
  [self.scrollView addSubview:view2];
    
  // AutoresizingMaskをオフにする
  self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
  view1.translatesAutoresizingMaskIntoConstraints = NO;
  view2.translatesAutoresizingMaskIntoConstraints = NO;
    
  // Constraint(制約)を追加
  [self.scrollView addConstraints:
   [NSLayoutConstraint constraintsWithVisualFormat:@"|-40-[view1(==240)]-20-[view2(==240)]-40-|"
                                             options:0
                                             metrics:0
                                               views:NSDictionaryOfVariableBindings(view1, view2)]];
  [self.scrollView addConstraints:
   [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[view1(==170)]"
                                             options:0
                                             metrics:0
                                               views:NSDictionaryOfVariableBindings(view1)]];
  [self.scrollView addConstraints:
   [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[view2(==170)]"
                                             options:0
                                             metrics:0
                                               views:NSDictionaryOfVariableBindings(view2)]];
}

@end

UIScrollView オブジェクトの contentSize はAutolayout によって自動で計算されるため設定する必要がありません。また UIScrollView オブジェクトとその中に配置するコンテントビューの translatesAutoresizingMaskIntoConstraints プロパティはすべて NO に設定する必要があります。

制約を記述する Visual Format Language

Autolayout ではビューの位置を Constraint(制約)を使って指定します。Constraint を指定するには NSLayoutConstraint の constraintsWithVisualFormat:options:metrics:views: メソッドを使います。このメソッドの第1引数には Visual Format Language と呼ばれる特殊な文字列を渡します。サンプルコード上の1つ目の Visual Format である |-40-[view1(==240)]-20-[view2(==240)]-40-| を図に表すと以下のようになります。
Visual Formatの意味
左端からマージンを40あけて1つ目のビューを240の幅で配置し、さらにマージンを20あけてから2つ目のビューを240の幅で配置、最後に右端から2つ目のビューまで40あけるという意味になります。
残り2つの Visual Format の V:|-0-[view1(==170)]V:|-0-[view2(==170)] は指定しているビューが違うだけで意味は同じです。図に表すと以下のようになります。
Visual Formatの意味その2
縦位置のマージンは0、ビューの高さは170という意味になります。「V」はおそらく Vertical の V です。

参考

Autolayout と UIScrollView についての詳細は以下の Apple 公式ドキュメントを参考にしてください。

UIViewController クラスの automaticallyAdjustsScrollViewInsets プロパティについては以下の記事を参考にしてください

Autolayout の基礎的な内容については以下の記事が参考になりました。

Autolayout と UIScrollView についての Stackoverflow の議論

サンプルソース

この記事で使用したサンプルプログラムをこちらに置いておきます。