A Day In The Life

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

オフスクリーンレンダリングを使って画像を動的にアトラス化する

オフスクリーンレンダリングという手法があります。この手法をつかって動的に画像をアトラス化してスプライトフレームキャッシュ(SpriteFrameCache)に乗せることができると、必要最小限の画像をアトラス化して効率よくキャッシュできるのではないかということで試してみました。
まずはオフスクリーンレンダリングとスプライトフレームキャッシュそれぞれの使い方を説明してから、その2つを組み合わせる方法を最後に紹介します。

続きを読む

【Swift1.2対応】プロの力が身につく iPhone/iPadアプリケーション開発の教科書 Swift対応版

iOS が8.3にバージョンアップしたのに伴い Xcode が6.3に Swift が1.2にバージョンアップされました。Swift 1.2ではシンタックスエラーとなりビルドできなくなるコードがあります。本書で記載しているコードだけでなく、サンプルの自動生成コードにも変更が必要な箇所がありましたので修正しました。最新のサンプルコードはすべて Swift 1.2に対応済みです。

以降は、本書に記載されているプログラムの修正箇所になります。

続きを読む

Cocos2d-x でネイティブ連携する方法

Cocos2d-x でゲームを開発していると広告を表示させたり、ランキングを追加したり、課金を入れたり、SNSTwitter と連携したり、アクセス解析をしたり、など iOSAndroid の機能を使わないと実現できない機能があります(このようにプラットフォームと Cocos2d-x を連携させることをネイティブ連携と呼びます)。Cocos2d-x には Plugin-X というネイティブ連携のための仕組みがあるのですが、提供されている機能が少なくまた導入が複雑(特に Android)で導入するメリットがあまりないのが実情です。
どうせなら自前で実装したほうがいろいろと便利だし iOSAndroid の知識も深まるのでこの記事では Cocos2d-x 3.4 Final でネイティブ連携する方法について説明します。ネイティブ連携の仕組みは一度開発してしまえば似たようなコードを書かずに、Objective-CJava を書くだけで機能追加できるようになるので覚えておいて損はないと思います。

ネイティブ連携の仕組み

ネイティブ連携は新規にクラスを作成してヘッダファイルのみ共通にして実装ファイルを iOS 用、Android 用と分けます。iOS の場合は Objective-C++ で実装を書きます。Android の場合は実装部分に JNI の呼び出しコードを書きます。実際の処理はあらかじめ Java 側に用意しておきます。

ネイティブ連携用クラスの作成

それでは連携用の NativeLauncher クラスを作成します。以下のようにヘッダと実装ファイルを作成してください。

  • NativeLauncher.h
  • NativeLauncher.mm(iOS用)
  • NativeLauncher.cpp(Android用)

例えばネイティブ連携をしてハイスコアを登録する機能を作成する場合は以下のようにクラスにメソッドを定義します。

#include "cocos2d.h"

class NativeLauncher {
public:
  // ハイスコアを登録する
  static void postHighScore(int score);
};

Xcode の設定

開発に Xcodeを使っている場合は NativeLauncher.cpp ファイルをコンパイル対象から外す必要があります。
削除手順は Project Navigator からプロジェクトのルートを選択してプロジェクトの詳細画面を表示します。次に「TARGETS」の「プロジェクト名 iOS」を選択して「Compile Sources」から NativeLauncher.cpp を削除してください。
Xcodeの画像

NativeLauncher.mm の実装

iOS の場合は Objective-C++ が使えますのでメソッド中に直接 Objective-C のコードを書いていきます。

#include "NativeLauncher.h"

#import <GameKit/GameKit.h>

void NativeLauncher::postHighScore(int score)
{
  GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
  if([localPlayer isAuthenticated]) {
    // 処理...
  }
}

Java の実装と NativeLauncher.cpp の実装

Android の場合は少し複雑でまず Java で実装を書いてからそのコードを C++ から JNI を経由して呼び出すことになります。Android のネイティブ連携コードは基本的には proj.android にある AppActivity クラスに書きます。
AppActivity クラスの詳細はこちらの記事を参照してください。

package org.cocos2dx.cpp;

import org.cocos2dx.lib.Cocos2dxActivity;

public class AppActivity extends Cocos2dxActivity {
  // 気持ち悪いけど static メソッドから参照するときに必要
  private static AppActivity me = null;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    me = this;
  }
  // JNI から呼び出されるメソッド
  public static void postHighScore(int score) {
    // UIスレッドで実行
    me.runOnUiThread(new Runnable() {
      @Override
      public void run() {
        // 処理...
      }			
    });
  }
}

あとは C++ からJNI で postHighScore メソッドを呼び出します。

#include "NativeLauncher.h"
#include "platform/android/jni/JniHelper.h"
#include <jni.h>

#define CLASS_NAME "org/cocos2dx/cpp/AppActivity"

void NativeLauncher::postHighScore(int score)
{
  JniMethodInfo methodInfo;
  if (!JniHelper::getStaticMethodInfo(methodInfo, CLASS_NAME, "postHighScore", "(I)V")) {
    return;
  }    
  methodInfo.env->CallStaticVoidMethod(methodInfo.classID, methodInfo.methodID, score);
  methodInfo.env->DeleteLocalRef(methodInfo.classID);
}

Cocos2d-x が JniHelper という便利なクラスを用意してくれているのでそれを使います。JniHelper クラスの getStaticMethodInfo メソッドの第4引数が暗号じみてよくわからないかもしれません。int 型の引数を取り戻り値は void という意味です。

【正誤表】プロの力が身につく iPhone/iPadアプリケーション開発の教科書 Swift対応版

「プロの力が身につく iPhone/iPadアプリケーション開発の教科書」をお買い上げの皆様、ありがとうございます。
読者様からご指摘を頂き、誤りがありましたので訂正させていただきます。

2-3テーブルビューとコレクションビュー

148ページ、プログラム2つ目の下から3行目

訂正前

override func tableView(tableView: UITableView,
  cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:indexPath)
      as UITableViewCell
    cell.textLabel.text = self.groups[indexPath.section][indexPath.row]
    return cell
}

訂正後

override func tableView(tableView: UITableView,
  cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:indexPath)
      as UITableViewCell
    cell.textLabel?.text = self.groups[indexPath.section][indexPath.row]
    return cell
}

加えてサンプルプログラムにも修正が必要でしたので修正しました。GitHub に push 済みです。

CollectionViewサンプルプログラム

ViewController クラス viewDidLoad メソッド、本の内容に訂正はありません。GitHub に push 済みです。
修正前

override func viewDidLoad() {
  super.viewDidLoad()      
  self.collectionView.contentInset = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0)
}

修正後

override func viewDidLoad() {
  super.viewDidLoad()      
  self.collectionView?.contentInset = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0)
}
Navigationサンプルプログラム

ViewController クラス tableView:cellForRowAtIndexPath: メソッド、本の内容に訂正はありません。GitHub に push 済みです。
修正前

override func tableView(tableView: UITableView,
  cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:indexPath)
      as UITableViewCell
    cell.textLabel.text = self.groups[indexPath.section][indexPath.row]
    return cell
}

修正後

override func tableView(tableView: UITableView,
  cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:indexPath)
      as UITableViewCell
    cell.textLabel?.text = self.groups[indexPath.section][indexPath.row]
    return cell
}

3-3データをフラッシュドライブに保存する

191ページ、プログラム2つ目の7行目(4/3追加)

訂正前

@IBAction func respondToArchiveButton() {
  // ファイルパスの取得
  let paths = NSSearchPathForDirectoriesInDomains(
    .DocumentDirectory,
    .UserDomainMask, true) as [String]
  // 保存するファイルの名前
  let filePath = String(paths[0]) + "data.dat"
  // 保存するデータ、氏名と住所
  let array = ["山田太郎", "104-0061", "東京都", "中央区", "銀座1丁目"]
  // アーカイブしてdata.datというファイル名で保存する
  let successful = NSKeyedArchiver.archiveRootObject(array, toFile: filePath)
  if successful {
    println("データの保存に成功しました。")
  }
}

訂正後

@IBAction func respondToArchiveButton() {
  // ファイルパスの取得
  let paths = NSSearchPathForDirectoriesInDomains(
    .DocumentDirectory,
    .UserDomainMask, true) as [String]
  // 保存するファイルの名前
  let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
  // 保存するデータ、氏名と住所
  let array = ["山田太郎", "104-0061", "東京都", "中央区", "銀座1丁目"]
  // アーカイブしてdata.datというファイル名で保存する
  let successful = NSKeyedArchiver.archiveRootObject(array, toFile: filePath)
  if successful {
    println("データの保存に成功しました。")
  }
}
192ページ、プログラム1つ目の3行目(4/3追加)

訂正前

    .DocumentDirectory,
    .UserDomainMask, true) as Array<String>
  let filePath = String(paths[0]) + "data.dat"
  if let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? Array<String> {
    for str in array {
      println(str)
    }
  } else {
    println("データがありません")
  }
}

訂正後

    .DocumentDirectory,
    .UserDomainMask, true) as Array<String>
  let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
  if let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? Array<String> {
    for str in array {
      println(str)
    }
  } else {
    println("データがありません")
  }
}
197ページ、プログラム1つ目の下から8行目(4/3追加)

訂正前

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToArchiveButton() {
    // 山田太郎、花子オブジェクト(住所が同じ夫婦という設定)
    let address1 = Address(zipCode: "104-0061",
      state: "東京都", city: "中央区", other: "銀座1丁目")
    let taroYamada = Person(name: "山田太郎", address: address1)
    let hanakoYamada = Person(name: "山田花子", address: address1)
    // 田中次郎オブジェクト
    let address2 = Address(zipCode: "604-8126",
      state: "京都府", city: "京都市", other: "中京区")
    let jiroTanaka = Person(name: "田中次郎", address: address2)
    // 保存するデータを配列にまとめる
    let people = [taroYamada, hanakoYamada, jiroTanaka]
    // 保存するファイルの設定
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]) + "data.dat"
    // アーカイブしてファイルに保存
    let successful = NSKeyedArchiver.archiveRootObject(people, toFile: filePath)
    if successful {
      println("データの保存に成功しました。")
    }
  }
}

訂正後

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToArchiveButton() {
    // 山田太郎、花子オブジェクト(住所が同じ夫婦という設定)
    let address1 = Address(zipCode: "104-0061",
      state: "東京都", city: "中央区", other: "銀座1丁目")
    let taroYamada = Person(name: "山田太郎", address: address1)
    let hanakoYamada = Person(name: "山田花子", address: address1)
    // 田中次郎オブジェクト
    let address2 = Address(zipCode: "604-8126",
      state: "京都府", city: "京都市", other: "中京区")
    let jiroTanaka = Person(name: "田中次郎", address: address2)
    // 保存するデータを配列にまとめる
    let people = [taroYamada, hanakoYamada, jiroTanaka]
    // 保存するファイルの設定
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
    // アーカイブしてファイルに保存
    let successful = NSKeyedArchiver.archiveRootObject(people, toFile: filePath)
    if successful {
      println("データの保存に成功しました。")
    }
  }
}
198ページ、プログラム1つ目の10行目(4/3追加)

訂正前

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToUnarchiveButton() {
    // 保存するファイルの設定
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]) + "data.dat"
    // アンアーカイブする
    let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as Array<Person>
    for person in array {
      println(person.name)
      println(person.address.zipCode)
      println(person.address.state)
      println(person.address.city)
      println(person.address.other)
    }
  }
}

訂正後

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToUnarchiveButton() {
    // 保存するファイルの設定
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
    // アンアーカイブする
    let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as Array<Person>
    for person in array {
      println(person.name)
      println(person.address.zipCode)
      println(person.address.state)
      println(person.address.city)
      println(person.address.other)
    }
  }
}
202ページ、プログラム1つ目の10行目(4/3追加)

訂正前

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToSaveButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]) + "data.plist"
    // 都道府県データ(NSArray型にキャストする)
    let array = ["北海道", "青森県", "岩手県", "秋田県", "宮城県", "山形県"] as NSArray
    let successful = array.writeToFile(filePath, atomically: false)
    if successful {
      println("データの保存に成功しました。")
    }
  }
}

訂正後

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToSaveButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
    // 都道府県データ(NSArray型にキャストする)
    let array = ["北海道",
                      "青森県",
                      "岩手県", 
                      "秋田県",
                      "宮城県",
                      "山形県"] as NSArray
    let successful = array.writeToFile(filePath, atomically: false)
    if successful {
      println("データの保存に成功しました。")
    }
  }
}
203ページ、プログラム1つ目の下から8行目(4/3追加)

訂正前

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToLoadButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]) + "data.plist"
    // データをプロパティリストから読み込む
    let array = NSArray(contentsOfFile: filePath)!
    for data in array {
      println(data)
    }
  }
}

訂正後

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToLoadButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
    // データをプロパティリストから読み込む
    let array = NSArray(contentsOfFile: filePath)!
    for data in array {
      println(data)
    }
  }
}
205ページ、プログラム1つ目の10行目(4/3追加)

訂正前

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToSaveButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]) + "data.plist"
    // 山田太郎、花子オブジェクト(住所が同じ夫婦という設定)
    let address1 = Address(zipCode: "104-0061",
      state: "東京都", city: "中央区", other: "銀座1丁目")
    let taroYamada = Person(name: "山田太郎", address: address1)
    let hanakoYamada = Person(name: "山田花子", address: address1)
    // 田中次郎オブジェクト
    let address2 = Address(zipCode: "604-8126",
      state: "京都府", city: "京都市", other: "中京区")
    let jiroTanaka = Person(name: "田中次郎", address: address2)
    let archivedTaroYamada = NSKeyedArchiver.archivedDataWithRootObject(taroYamada)
    let archivedHanakoYamada = NSKeyedArchiver.archivedDataWithRootObject(hanakoYamada)

訂正後

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToSaveButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
    // 山田太郎、花子オブジェクト(住所が同じ夫婦という設定)
    let address1 = Address(zipCode: "104-0061",
      state: "東京都", city: "中央区", other: "銀座1丁目")
    let taroYamada = Person(name: "山田太郎", address: address1)
    let hanakoYamada = Person(name: "山田花子", address: address1)
    // 田中次郎オブジェクト
    let address2 = Address(zipCode: "604-8126",
      state: "京都府", city: "京都市", other: "中京区")
    let jiroTanaka = Person(name: "田中次郎", address: address2)
    let archivedTaroYamada = NSKeyedArchiver.archivedDataWithRootObject(taroYamada)
    let archivedHanakoYamada = NSKeyedArchiver.archivedDataWithRootObject(hanakoYamada)
206ページ、プログラム2つ目の10行目(4/3追加)

訂正前

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToLoadButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]) + "data.plist"
    // データをプロパティリストから読み込む
    let array = NSArray(contentsOfFile: filePath)!
    for data in array {
      // 読み込んだオブジェクトをアンアーカイブする
      let person = NSKeyedUnarchiver.unarchiveObjectWithData(data as NSData) as Person
      println(person.name)
      println(person.address.zipCode)
      println(person.address.state)
      println(person.address.city)
      println(person.address.other)
    }
  }
}

訂正後

class ViewController: UIViewController {
  :
  : 省略
  :
  @IBAction func respondToLoadButtonClick() {
    // 保存するファイルの設定(拡張子はplist)
    let paths = NSSearchPathForDirectoriesInDomains(
      .DocumentDirectory,
      .UserDomainMask, true) as [String]
    let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")
    // データをプロパティリストから読み込む
    let array = NSArray(contentsOfFile: filePath)!
    for data in array {
      // 読み込んだオブジェクトをアンアーカイブする
      let person = NSKeyedUnarchiver.unarchiveObjectWithData(data as NSData) as Person
      println(person.name)
      println(person.address.zipCode)
      println(person.address.state)
      println(person.address.city)
      println(person.address.other)
    }
  }
}
補足

191ページから206ページにかけての訂正はすべてファイルパスの文字列連結をしている以下のプログラムが実機で実行時エラーになるのが原因です。

// 実行時エラー
let filePath = String(paths[0]) + "data.plist"

以下のようにファイル名の前にスラッシュを付けるだけで修正できますが、

let filePath = String(paths[0]) + "/data.dat"

URL の操作には以下のように stringByAppendingPathComponent メソッドを使ったほうがより安全です。

let filePath = String(paths[0]).stringByAppendingPathComponent("data.plist")

Swift 1.2の変更への対応(2015/5/25追記)

iOS が8.3にバージョンアップしたのに伴い Xcode が6.3に Swift が1.2にバージョンアップされました。Swift 1.2ではシンタックスエラーとなりビルドできなくなるコードがあります。本書で記載しているコードだけでなく、サンプルの自動生成コードにも変更が必要な箇所が発生しました。2015年5月25日にサンプルプログラムのコンパイルエラーを修正したバージョンをGitHubにアップしました。iOS8.3で開発されている読者様はGitHubのサンプルコードページより最新版をダウンロードしてください。また Swift 1.2への対応箇所を以下の記事にまとめましたのでそちらも参考にしてください。

今後、間違いを見つけ次第この記事に追記していきます。

間違いを指摘してくださいました読者様ありがとうございました。今後も本書の間違いや訂正があればこの記事に追記して行きます。訂正があったことをこの場をお借りしてお詫びします。引き続き本書をよろしくお願いします。

Androidでui::ScrollViewの背景が緑色になる時の対処方法

Cocos2d-x(3.4 Final) の ui::ScrollView クラスを使うとAndroidで背景色が緑色になる現象が発生しました(iOS ではこの現象は発生しません)。
こちらのページに載っている方法で解決することができました。

具体的な解消法は proj.android/src/org/cocos2dx/cpp/AppActivity.java に以下の行を追加するだけです。

public class AppActivity extends Cocos2dxActivity {
  :
  :  省略
  :
  @Override
  public Cocos2dxGLSurfaceView onCreateView() {
    Cocos2dxGLSurfaceView glSurfaceView = new Cocos2dxGLSurfaceView(this);
    // 追加する
    glSurfaceView.setEGLConfigChooser(5, 6, 5, 0, 16, 8);
    return glSurfaceView;
  }
}

OpenGLの設定がどうやらだめだったぽいです。

Cocos2d-xのゲームの構造がどのようになっているか紐解いてみる

Cocos2d-xでゲームの開発を始めてそろそろ5ヶ月たちます。とにかく作ってみるといった感じで基礎知識があるかもも怪しいまま(Scene と Layer の違いがわからんとか)開発を続けていました。最近になってiOS/Androidとのネイティブ連携ではまったり、iOSだと問題なく動くのにAndroidだとクラッシュしたり、BGMの制御うまくいかなかったり、まあいろいろと問題が出てきました。というわけでここらで一度Cocos2d-xとちゃんと向き合ってみるかというわけで過去に書いたこちらの記事「iOS アプリの構造がどのようになっているか紐解いてみる」と同じノリでCocos2d-xで作ったゲームの構造を紐解いてみたいと思います。

前提とする Cocos2d-x のバージョンとかもろもろ

Cocos2d-xはiOS/Androidはもちろんのこと、その他多数のプラットフォームに対応したゲームエンジンです。この記事ではスマホゲームを前提にしますのでiOSAndroid上でゲームを開発することを前提に書きます。
この記事で対象とするCocos2d-xやiOSAndroidのバージョンは以下の通りです。

Cocos2d-x プロジェクトのフォルダ構成

クラス構成について説明をする前に Cocos2d-x で作成したプロジェクトのフォルダ構成について説明します。ゲーム自体に直接関係するクラスとプラットフォーム依存のクラスの置き場が違って少し複雑なので理解しておいたほうがなにかと良いです。以下は Cocos2d-x で作成したプロジェクトのフォルダ構成一覧になります。

  • プロジェクト名(開発者が任意の名前をつける)
    • Classes
      プラットフォームに依存しないゲームに関わるC++のクラスが置かれている。開発者は基本的にここにクラスを追加していく
    • proj.android
      Android アプリに関係するクラスや設定、画像が置かれる
    • proj.ios_mac
      iOS アプリに関係するクラスや設定、画像が置かれる
    • Resources
      プラットフォームに依存しない共通のリソース(スプライト画像やUI画像、フォント、音声ファイルなど)を配置する
    • cocos2d
      cocos2d-x の本体
      • cocos
        cocos2d-x のソースコードが置かれている
        • 2d
          2Dゲーム用のライブラリ置き場
        • 3d
          3Dゲーム用のライブラリ置き場
        • physics
          物理エンジン用のライブラリ置き場
        • renderer
          描画に関わるライブラリ置き場
        • ui
          UI系ライブラリ置き場
        • platform
          プラットフォームに依存するライブラリの置き場
          • android
            Android 用ライブラリの置き場、JNI のコードもここに置かれている
          • ios
            iOS 用ライブラリの置き場

シーンとレイヤーが1つの単純なゲームの構造

シーンとレイヤーが一つでスプライトもなにもなく、ただ真っ黒な画面を表示するだけのゲームがどのような構造になっているのか見てみましょう。以下クラスの関係を図にしたものです。
Cocos2d-xクラス図
iOSAndroidの違いを吸収しているのが Application(Application-Android) クラスと GLViewImpl(GLViewImpl-Android) クラスです*1。図の中の赤色のクラスです。この2つのクラスがプラットフォームごとに用意されています。プラットフォーム固有のクラスが多いだけでCocos2d-x自体は割とシンプルなつくりだというのがわかると思います。
図の中の各クラスは以下のように4つに分類できます。

  • ゲームエンジンクラス(プラットフォームに依存しないクラス)
  • 各プラットフォームから提供されているクラス
  • Cocos2d-x が各プラットフォームのために用意している拡張クラス
  • Cocos2d-x がプロジェクト作成時に自動生成するクラス

それでは分類ごとにクラスの説明をしていきます。

ゲームエンジンクラス

Cocos2d-x のコアとなるゲームエンジン部分のクラスです。この分類のクラスはプラットフォームに依存していません。iOS 向けゲームでも Android 向けゲームでも同じです。実際にゲームを開発するとなるとここで紹介するクラス以外に Sprite クラスや Action クラスとその派生クラスなんかも必要になります。

GLView クラス

Cocos2d-x のゲームは OpenGL を使って描画をしています。その OpenGL を使ったビューを管理しているクラス。抽象クラスになっていて、プラットフォーム依存しない機能は実装があり、プラットフォームに依存する部分はメソッドの定義のみされています。プラットフォームごとに依存する部分は後述の GLViewImpl クラスに実装されています。

Director クラス

シーンとイベントを管理するためのクラス。Cocos2d-x の心臓部。シングルトンクラスなのでどこからでもオブジェクトを取得できます。シーンの遷移(画面遷移みたいなもの)だったりカスタムイベントの追加だったりするときに使います。

DisplayLinkDirector クラス

Director クラスから派生したクラス。タイマーと同期するための機能が提供されています。Director::getInstance メソッドで取得できるオブジェクトはこのクラスのオブジェクトです。

Node クラス

画面上のオブジェクトのツリー構造を管理するためのクラス。後述の Scene クラスや Layer クラスの他、画面表示に必要な機能を提供するあらゆるクラス(Sprite クラスや UI 系クラス)の親クラスになります。

Scene クラス

ゲームの場面を表すクラス。シーン単位でゲームの画面を管理するのが普通で、マップ、町の中、バトル、ステージ選択、ローディングなどゲームの見せ方が変わる単位でシーンを作成します。1画面で表示されるシーンは1つになります。なので画面上のノードオブジェクトのツリー構造をたどっていくと一番根っこにシーンオブジェクトが存在することになります。

Layer クラス

レイヤーを表すクラス。レイヤーというのはノードをグループ化するための概念だと考えると良いと思います。特定のZオーダーをまとめて管理したい時(モーダルでノードを表示するみたいな)やタッチイベントを制御するときなんかに使います。

各プラットフォームから提供されているクラス

iOSAndroid 向けのアプリ開発をしている方には馴染みのあるクラスばかりだと思います。iOS/Android に関わらず画面を1枚表示するための必要最低限の機能だけ使っている印象です。AndroidOpenGL を使った描画に色々とお作法があり、その辺りが少し複雑な印象です。

UIApplicationDelegate インターフェース(iOS)

アプリから通知されるライフサイクルに関する(アプリを立ち上げたとか、バックグラウンド状態になったとか、そこから復帰したとか)メソッドが定義されているインターフェース。

UIViewController クラス(iOS)

画面に表示しているビューオブジェクト(UIview オブジェクト)を監視する機能と他のビューコントローラに遷移させる機能を提供するクラス。Cocos2d-x でゲームを作る場合、後者の機能(画面遷移)は基本的には使いません。

UIView クラス(iOS)

画面に矩形領域を表示するための機能を提供するクラス。

Activity クラス(Android)

画面を管理するためのクラス。iOS でいうところの UIViewController に近いです。

FrameLayout クラス(Android)

画面レイアウトを表すクラス。Android では幾つかのレイアウト方法がありその方法ごとにレイアウトクラスが存在します。FrameLayout クラスは画面に1つのビューを表示する場合に使用するレイアウトです。

SurfaceView クラス(Android)

アニメーションとかゲームのような描画のパフォーマンスが必要なビューを作成するためのクラス。このクラスを使うと描画用のスレッドを持った View を作ることができます。

GLSurfaceView クラス(Android)

OpenGLレンダリングが可能なビューを作成することができるクラス。SurfaceView クラスを拡張しています。

GLSurfaceView.Renderer インターフェース(Android)

OpenGL を使って描画をするときに呼ばれるイベントメソッドを定義しているインターフェース。このインターフェースに定義されているイベントメソッドを実装して OpenGL の設定(クリアカラー、クリア深度、OpenGL の有効/無効切り替えなど)やメインループの呼び出しなどを行います。

Cocos2d-x が各プラットフォームのために用意している拡張クラス

各プラットフォーム上で OpenGL の描画を行うための機能を提供してくれるクラス群(CCEAGLView、Cocos2dxGLSurfaceView、Cocos2dxRenderer、Cocos2dxActivity クラス)と iOS/Android の違いを吸収しているクラス群(Application、GLViewImpl)があります。

CCEAGLView クラス(iOS)

UIView クラスを継承した OpenGL の描画を行うための機能を提供してくれるクラス。ゲームをマルチタッチに対応させるときはこのクラスのオブジェクトの setMultipleTouchEnabled メソッドを使います。

Cocos2dxGLSurfaceView クラス(Android)

SurfaceView クラスを継承した OpenGL の描画を行うための機能を提供してくれるクラス。

Cocos2dxRenderer クラス(Android)

GLSurfaceView.Renderer インターフェースを実装したクラス。onSurfaceCreated メソッドを実装して Cocos2d-x 側のゲーム初期化処理を呼び出したり、onDrawFrame メソッドを実装して Cocos2d-x のゲームループを呼び出したり、画面のタッチを Cocos2d-x 側に通知したり、アプリのライフサイクルイベント(アプリが起動した、サスペンドした、サスペンドから復帰したなど)を Cocos2d-x 側に通知したり、など JNI(Java Native Interface) を利用して Cocos2d-x とやりとりするための機能を提供してくれています。

Cocos2dxRenderer_nativeInit 関数(Android)

あまりにも長いので省略していますが、正式な関数名は Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit です。JNI 呼び出しで実行される関数です。クラス図では便宜上クラスとして独立させていますが実際は Cocos2dxRenderer クラスの nativeInit メソッド(static メソッド)の実態です。

Cocos2dxActivity クラス(Android)

Activity クラスを継承したクラス。Cocos2d-x のライブラリを読み込んだり、ビューをセットしたり、OpenGL のせってをしたり、Cocos2d-x のゲームに必要な下準備をするための機能を提供してくれます。

Application クラス(クラス名共通、実装別)

クラス図が複雑になるので省略しましたが iOSAndroid 版ともに ApplicationProtocol クラスというアプリのライフサイクルに関わるイベントメソッドが定義されている抽象クラス(C++なんでクラスとしていますが実質インターフェースです)を継承しています。このクラスの run メソッドを使うと applicationDidFinishLaunching メソッドが呼ばれアプリの起動が Cocos2d-x 側に伝達される仕組みになっています。iOS 版と Android 版の違いは run メソッドの中でゲームループを開始させるかどうかです。iOS 版は run メソッドの処理の中でゲームループを開始させているのに対し、Android 版は run メソッドの中でゲームループに関する処理を一切していません(Android のゲームループは GLSurfaceView が行うので必要ないのです)。

GLViewImpl クラス(クラス名共通、実装別)

GLView クラスを継承したクラス。バージョン3.3から導入されたようです。実装はだいたい同じなのですが、IME キーボードの実装だったり、ビューの初期化方法だったり、が違っています。

Cocos2d-x がプロジェクト作成時に自動生成するクラス

プロジェクト作成時に Cocos2d-x が自動生成してくれるクラスです。AppDelegate と HelloWorldScene クラスに実際のゲームのコードを書いていきます。それ以外のクラスはネイティブ連携(SNS 連携、広告表示、課金など)をするときの拡張ポイントとして使います。

AppController クラス(iOS)

UIApplicationDelegate プロトコル(インターフェース)を実装したクラス。アプリのライフサイクル(アプリが起動した、サスペンドした、サスペンドから復帰したなど)に関するイベントを受け取る機能を持っています。このクラスの application:didFinishLaunchingWithOptions: メソッドの中でウインドウやビューコントローラー、ビューの生成をしています。ネイティブ連携(SNS 連携、広告表示、課金など)の機能を追加するときに使います。

RootViewController クラス(iOS)

UIViewController クラスを継承したクラス。自動生成されるだけで特別なことはほとんど何もしていません。拡張ポイントとして用意されていると考えると良いと思います。ネイティブ連携は基本 AppController クラスに実装していくのですがたまにこのクラスを拡張するときがあります(Game Center を使うときなど)。

AppActivity クラス(Android)

Activity クラスを継承したクラス。自動生成されるだけで特別なことはほとんど何もしていません。拡張ポイントとして用意されていると考えると良いと思います。ネイティブ連携(SNS 連携、広告表示、課金など)の機能を追加するときに使います。

AppDelegate クラス(共通)

アプリのライフサイクルに関するイベントを受け取る機能を持ったクラス。ライフサイクルイベントは iOS の場合 AppController オブジェクト、Android の場合 Cocos2dxRenderer オブジェクトから通知されます。

HelloWorldScene クラス(共通)

Scene とついていますが実際は Layer クラスを継承したクラスです。Scene オブジェクトの生成機能を持っています。クラス名がわかりにくいので変更することをお勧めします。自分の場合、このややこしいクラス名のおかげで Scene と Layer の違いがわからず苦労しました。

ゲーム起動からメインループの処理が始まるまでの動き

ゲームを起動してからメインループが始まるまでの動きを図にしてみました。ループの回し方が iOSAndroid で違うのが特長です。iOS は CCDirectorCaller オブジェクトがメインループの呼び出しを行っているのに対し、Android は GLSurfaceView の仕組みを使ってメインループ呼び出しを行っています。

iOS

Cocos2d-xクラス図

メインループ中の共通処理

メインループ中の動きは iOS/Android ともに同じ動きをします。シーンが切り替わるとノードツリーをたどって onEnter、onEnterTransitionDidFinish メソッドが呼ばれます。また描画処理ではノードツリーをたどって visit メソッドを呼び出しています。onEnter、onEnterTransitionDidFinish、visit メソッドは Node クラスで定義されていて全てのノードオブジェクトから呼び出せるようになっています。
Cocos2d-xクラス図

*1:クラス図の性質上同じ名前のクラスを作成することができないため便宜的に名前を変えています。実際は iOS/Android ともに同じクラスです

Cocos2d-xでTwitterプラグインを使ってみる

Cocos2d-xにはプラグインという仕組みがあり、それを使えばTwitter連携やら広告表示なんかが割と簡単に使えるようになります。
Cocos2d-xのプラグイン導入の方法は以下のサイトが参考になりました。

自分の環境だと上記の手順に加え MediaPlayer.framework と Security.framework の追加が必要でした。
Twitter連携については以下のサイトが参考になりました。

Twitter連携プラグインを使うには SystemConfiguration.framework の追加が必要でした。
割とめんどくさかったですがいちど設定してしまえば便利ですね。