Hatena::ブログ(Diary)

A Day In The Life RSSフィード Twitter

2015-01-24

ストーリーメーカー 創作のための物語論

結構前に読んだストーリー作りのための本です。

ストーリーを作るためのテクニックについて書かれています。著者はただ闇雲に物語を書くのではなく、セオリーに則ってストーリーを組み立てると未経験者でもそれなりのストーリーを作ることが出来ると主張しています。

この本に載っているストーリー作りのテクニックの1つが以下の図です。

ゲーム制作でもストーリー作りはかなり重要なのでこういった本があるととてもたすかります。

2015-01-20

プロの力が身につく iPhone/iPadアプリケーション開発の教科書が発売されました

1月17日に「プロの力を身につける iPhone/iPadアプリケーション開発の教科書」の改訂版が発売されました。改訂内容は以下の通りです。

よろしくお願いします。

本書の特徴

昨年の12月から Swift に対応した本が次から次へと発売されています。多くの本が400ページを超える大型本です。400ページを超える本はとても丁寧に細かく沢山のことが説明されているもののすべてを読んで理解するのに時間がかかります。

本書はページ数を350ページ程度に抑えコンパクトに読める内容になっています。これだけの短いページ数で Swift の構文から UI 構築、データ永続化、課金まで説明している本は他にないと自負しています。iOS アプリ開発を効率よく学びたい方はぜひとも本書を手に取って読んでみてください。また Swift の構文説明は Objective-C と比較しながら解説しているのでこれまで Objective-C を使って開発されていた方にも読みやすい内容となっています。

2014-12-02

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オーダーを設定してやると上手くいきます。

通常の座標からマップ上に座標に変換する方法は以下のサイトが参考になります。

2014-11-26

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

最近 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になってずいぶん使いやすくなったと思います。

2014-04-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

関連記事