Hatena::ブログ(Diary)

A Day In The Life RSSフィード Twitter

2015-08-31

OpenGL ES入門 -仕組みから実装まで その1-

最近 Cocos2d-x を触っているとどうしても細かいところで OpenGL の知識がないと理解できないみたいなことが多いです。いままで避けてきた感がある OpenGL の勉強をそろそろ始めてみようかなと思い少しづつ日記的にまとめていこうと思います。OpenGL と言っても自分はモバイルゲームの開発に興味があるので OpenGL ES について勉強していくことにしました。参考図書として購入した本はこちらです。

わりと新しく出た本ってのと解説がものすごくわかりやすかったので選びました。第1回は OpenGL ES の基本的なところから画面を色で塗りつぶすまで解説します。

OpenGL ES って何?

OpenGL は三角形以下(三角形、線、点)の図形を3D空間に高速に描画するための API です。Ope GL ES(OpenGL for Embedded Systems) は主にモバイル端末向けに OpenGL の冗長な機能を取り除いた簡略版です。

OpenGL ES のバージョン

  • 1.1
    基本的な固定機能グラフィックスパイプラインを提供する
  • 2.0
    プログラマブルシェイダを使用することができる。1.0 との互換性はない
  • 3.0
    iOS7以降で使用することができる。マルチレンダーターゲット機能やマルチサンプルアンチエイリアス(MSAA)を標準サポート。バージョン2.0と互換性があり

本当は3.0の勉強をしようと思っていたのですが、3.0について解説している日本語の本が現状(2015/8現在)ないのと、2.0を学んでおけば3.0にも活かせるとこができる、というわけで2.0で進めます。

OpenGL で描画できる図形

OpenGL で描画できる図形は以下の3つです。

  • 三角形

四角形以上の図形は三角形の組み合わせで描画します。

ゲームループ

OpenGL を使ったグラフィックの描画処理はゲームループの中で行います。ゲームループとはある一定時間ごとに呼び出される関数(またはメソッド)です。一般的なゲームでは1秒間に60回、関数が呼び出されます。これを60FPS(Frames Per Second)といいます。

ダブルバッファリング

OpenGL はダブルバッファリングという手法を使って描画処理を行います。ダブルバッファリングとは表示されている描画領域(表示領域)で直接描画処理を行うのではなく、表示領域と同じサイズのバッファ領域をメモリ上に用意してそこで描画処理を行う手法です。一般的には表示領域をフロントバッファ、描画処理を行う領域をバックバッファと呼んでいます。ゲームループの処理の中で描画処理をバックバッファで行い、描画処理が終わったらバックバッファとフロントバッファと入れ替えてグラフィックを表示しています。

OpenGL では描画処理を行うバックバッファのことをフレームバッファ、描画処理結果を表示するフロントバッファのことをレンダーバッファといいます。また描画されている空間のことをサーフィスと呼びます。図にすると以下のようになります。

フレームバッファ

描画処理を行うフレームバッファは役割により以下の3つのバッファに分かれています。

ピクセルの情報を保存する領域(メモリ空間)のことを OpenGL の用語でバッファ(Buffer)と呼びます。図にすると以下のようになります。

OpenGL ES を使うための下準備(for iOS)

OpenGL ES のプログラムを書くための下準備をします。ここは対応 OS によって実装が変わるので適宜 OS ごとに実装を用意する必要があります。とりあえず iOS(Objective-C) で実装するのであればどうなるかを紹介します。iOS といっても GLKit を使わない前提でプログラムします。iOS の Open GL ES の下準備は以下の流れで行います(流れは他の OS でもだいたい同じになると思います)。

プログラムは UIView クラスを継承したビューを作成してその中で行います。クラスの宣言部分は以下のようになります。

#import <UIKit/UIKit.h>
#import <OpenGLES/ES2/gl.h>

@interface GLSurfaceView : UIView

@end

では実際の OpenGL ES のセットアップ処理を実装部分に書いていきます。

#import "GLSurfaceView.h"

@implementation GLSurfaceView {
  // OpenGL コンテキストオブジェクト
  EAGLContext *_context;
}
// OpenGL ES 用のレイアーを使うことを明示する
+ (Class)layerClass {
  return [CAEAGLLayer class];
}
// ストーリーボードから呼ばれるイニシャライザ
- (instancetype)initWithCoder:(NSCoder*)coder
{
  if ((self = [super initWithCoder:coder])) {
    [self setUp];
    // ゲームループ60FPS
    [NSTimer scheduledTimerWithTimeInterval:1/60.0f target:self selector:@selector(update:) userInfo:nil repeats:YES];
  }
  return self;
}
// 各種セットアップ処理
- (void)setUp
{
  // レイヤーのセットアップ(iOS特有の処理)
  CAEAGLLayer *layer = (CAEAGLLayer*)self.layer;
  layer.opaque = YES;
  // コンテキストオブジェクトのセットアップ(iOS特有の処理)
  _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
  [EAGLContext setCurrentContext:_context];
  // レンダーバッファーのセットアップ
  GLuint renderBuffer;
  glGenRenderbuffers(1, &renderBuffer);
  glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
  [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
  // フレームバッファーのセットアップ
  GLuint frameBuffer;
  glGenFramebuffers(1, &frameBuffer);
  glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
  glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBuffer);
}
// ゲームループメソッド(1/60秒ごとに呼び出される)
- (void)update:(NSTimer *)timer
{
  // ここに描画処理を書く...
}

@end

OpenGL はコマンド単位で描画処理を行います。OpenGL はステートマシンで描画処理の状態を管理しています。コマンド実行後の状態はコンテキスト(Context)という名前のオブジェクトによって管理されます。コンテキストオブジェクトは各 OS ごとに用意する必要があるので、セットアップ方法は OS によって変わります。例えば iOS では以下のようになります。

// OpenGL ES 2.0用のコンテキストオブジェクトを生成する
[[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

またコンテキストオブジェクトスレッドごとに1つ生成する必要があります。

色を塗りつぶすプログラム

下準備がおわったので、画面全体を塗りつぶすプログラム書いてみましょう。描画処理はゲームループから呼び出されるメソッドの中に書いていきます。先ほどのセットアップ処理で作成した update メソッドに描画処理を実装します。塗りつぶす色を指定してカラーバッファに塗りつぶしを行います。プログラムにすると以下のようになります。

// ゲームループから呼び出される関数
- (void)update:(NSTimer *)timer
{
  // 色をセット
  glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
  // カラーバッファを塗りつぶす
  glClear(GL_COLOR_BUFFER_BIT);
  // フレームバッファの内容を描画する(OS によって実装方法が異なります)
  [_context presentRenderbuffer:GL_RENDERBUFFER];
}

はじめに glClearColor 関数を呼んでコンテキストオブジェクトに色の情報を保存します。

次に glClear 関数を呼んでコンテキストオブジェクトに保存されている色情報を使って引数で指定されたバッファの色を塗りつぶします。

最後にフレームバッファの内容をレンダーバッファに渡して(バックバッファの内容をフロントバッファに置き換え)描画を行います。

コンテキストに設定されている色情報の取得

コンテキストオブジェクトに設定されている色情報を確認するには以下のように glGetFloatv 関数を使います。

GLfloat rgba[4];
glGetFloatv(GL_COLOR_CLEAR_VALUE, rgba);
NSLog(@"%f %f %f %f", rgba[0], rgba[1], rgba[2], rgba[3]);

実行結果

ここまでのプログラムを実行すると以下のようになります。

サンプルプログラム

今回作成したサンプルプログラムをこちらに置いておきます。

次回は

第1回はここまでにします。最後までお付き合いいただきありがとうございました。次回は三角形を描画するプログラムを書いてみます。三角形を描くにはシェーダの設定が必要になるので、シェーダの基本的な説明ができればと考えています。

参考記事

2015-07-18

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

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

まずはオフスクリーンレンダリングスプライトフレームキャッシュそれぞれの使い方を説明してから、その2つを組み合わせる方法を最後に紹介します。

オフスクリーンレンダリング

オフスクリーンレンダリングとは、テクスチャを動的に変更するためのテクニックです。動的にテクスチャを生成する時に画面の表示領域でやるのではなく画面の表示されないところで行います。オフスクリーンと言われるのはそのためです。

Cocos2d-x でオフスクリーンレンダリングを行うには RenderTexture クラスを使います。RenderTexture オブジェクトの beginWithClear メソッドと end メソッドの間に Sprite オブジェクトの描画処理(visit メソッド)を挟みます。

// オフスクリーンレンダリング用テクスチャ
Size winSize = Director::getInstance()->getWinSize();
RenderTexture *texture = RenderTexture::create(winSize.width, winSize.height);
// レンダリング開始
texture->beginWithClear(0, 0, 0, 0);

Size size = Director::getInstance()->getWinSize();
for (std::string fileName : files) {
  // スプライト生成
  auto sprite = Sprite::create("hoge.png");
  // 上下反転させる
  sprite->setScaleY(-1.0f);
  sprite->setPosition(Vec2(rand()%(int)size.width, rand()%(int)size.height));
  // 書き込み
  sprite->visit();
}

// レンダリング終了
texture->end();

RenderTexture オブジェクトと Sprite オブジェクトになんの関連もないのでこれでいいのか感はありますが、裏で Director オブジェクトがうまいことやってくれてます。あと RenderTexture には begin というメソッドもあるのですがたまにゴミが残って透過画像の描画でおかしくなる時があるので beginWithClear メソッドを使いましょう。Sprite オブジェクトに scale をかけて上下反転させているのは OpenGLテクスチャ座標が左上原点だからです。

スプライトフレームキャッシュ

画像を一つ一つ個別で描画するよりも、複数の画像をまとめた(アトラス化された)画像を使うことで描画効率が良くなります(ドローコールが減るなど)。Web でいうところの CSS スプライトに近い発想です。Cocos2d-x でアトラス化された画像を扱う場合は SpriteFrameCache クラスを使います。アトラス画像とその画像の中にどのような画像が配置されているかを管理するための plist ファイル(中身はXMLです)を開発者側であらかじめ用意してから SpriteFrameCache オブジェクトに plist ファイルを読み込ませます。そのようにすることでキャッシュからスプライトを生成することができます。プログラム的には以下のようになります。

// plistからキャッシュ生成
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("atlas.plist");
// スプライト生成
auto sprite = Sprite::createWithSpriteFrameName("hoge.png");

アトラス画像と plist ファイルの生成にはいろいろな方法があります。個人的には ShoeBox という無料のアトラス画像制作ツールをおすすめします。

複数の画像を選択して ShoeBox にドラッグするとアトラス化された画像と plist ファイルが生成されます。plist ファイルの中身は以下のようになっています(hoge.png と piyo.png 画像をまとめて atlas.png にまとめた例)。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>frames</key>
    <dict>
      <key>hoge.png</key>
      <dict>
        <key>frame</key>
        <string>{{0,0},{256,256}}</string>
        <key>offset</key>
        <string>{0,0}</string>
        <key>rotated</key>
        <false/>
        <key>sourceColorRect</key>
        <string>{{1,3},{447,173}}</string>
        <key>sourceSize</key>
        <string>{256,256}</string>
      </dict>
      <key>piyo.png</key>
      <dict>
        <key>frame</key>
        <string>{{0,0},{128,128}}</string>
        <key>offset</key>
        <string>{0,0}</string>
        <key>rotated</key>
        <false/>
        <key>sourceColorRect</key>
        <string>{{1,3},{447,173}}</string>
        <key>sourceSize</key>
        <string>{128,128}</string>
      </dict>
    </dict>
    <key>metadata</key>
    <dict>
      <key>format</key>
      <integer>2</integer>
      <key>size</key>
      <string>{512,512}</string>
      <key>textureFileName</key>
      <string>atlas.png</string>
    </dict>
  </dict>
</plist>

plist の形式や中のタグの意味なんかは以下のページを参考にしてください。

動的にアトラス画像を生成する

オフスクリーンレンダリングで生成した RenderTexture オブジェクトからテクスチャを取り出して、それをスプライトフレームキャッシュに追加すると画像を動的にアトラス化できます。

#include "cocos2d.h"

using namespace cocos2d

// ファイル(動的に変わる想定)
std::vector<std::string> files;
files.push_back("hoge.png");
files.push_back("piyo.png");
files.push_back("fuga.png");
files.push_back("foo.png");
files.push_back("bar.png");
files.push_back("buz.png");

// テクスチャの最大サイズ
Size winSize = Director::getInstance()->getWinSize();
int x = 0;
int y = 0;
int maxHeight = 0;

// オフスクリーンレンダリング用テクスチャ
RenderTexture *texture = RenderTexture::create(winSize.width, winSize.height);
texture->beginWithClear(0, 0, 0, 0);

// plist中の画像に関するデータを書き込むためのMap
ValueMap frames;

for (std::string fileName : files) {
  // スプライト生成
  auto sprite = Sprite::create(fileName);
  if (x + sprite->getContentSize().width > winSize.width) {
    y += maxHeight;
    x = 0;
    maxHeight = 0;
  }
  
  // 上下反転するので
  sprite->setScaleY(-1.0f);
  sprite->setAnchorPoint(Vec2(0, 1));

  sprite->setPosition(Vec2(x, y));
  // 書き込み
  sprite->visit();
  Director::getInstance()->getTextureCache()->removeTextureForKey(fileName);

  // plist情報の生成
  ValueMap frame;
  frame["frame"] = StringUtils::format("{{%f, %f}, {%f, %f}}",
                                       sprite->getPosition().x,
                                       sprite->getPosition().y,
                                       sprite->getContentSize().width,
                                       sprite->getContentSize().height);
  frame["offset"] = StringUtils::format("{%d, %d}", 0, 0);
  
  frame["rotated"] = false;
  frame["sourceColorRect"] = StringUtils::format("{{%f, %f}, {%f, %f}}", 0.0f, 0.0f,
                                                 sprite->getContentSize().width,
                                                 sprite->getContentSize().height);
  frame["sourceSize"] = StringUtils::format("{%f, %f}",
                                            sprite->getContentSize().width,
                                            sprite->getContentSize().height);
       
  x += sprite->getContentSize().width;
    
  if (maxHeight < sprite->getContentSize().height ) {
    maxHeight = sprite->getContentSize().height;
  }
  frames[fileName] = frame;
}

// レンダリング終了
texture->end();

// plistのメタ情報を生成
ValueMap plist;
plist["frames"] = frames;
ValueMap metadata;
metadata["format"] = 2; // いろいろ種類があるみたいだけど今回は2で
metadata["size"] = StringUtils::format("{%d, %d}", winSize.width, winSize.height);
metadata["textureFileName"] = "atlas.png";
plist["metadata"] = metadata;

std::string filePath = "atlas.plist";
// plist ファイルを保存
FileUtils::getInstance()->writeToFile(plist, filePath);
// キャッシュを生成
SpriteFrameCache::getInstance()->removeSpriteFramesFromFile(filePath);
SpriteFrameCache::getInstance()->addSpriteFramesWithFile(filePath,
                               texture->getSprite()->getTexture());

// スプライとを使うときはこんな感じ
auto sprite = Sprite::createWithSpriteFrameName("hoge.png");

アトラス画像用の plist ファイルを生成する部分が複雑ですがそれ以外は特に問題ないかと思います。

すべての画像をアトラス化するには画像が多すぎて現実的ではないとか、サーバからの情報で使用する画像が動的に変わるとか、そんな時に使うと有効だと思います。

問題点が...。

今回紹介したオフスクリーンレンダリングの方法では、端末の画面サイズ以上の画像を生成することができません。次の記事でこのあたりを改良してみます。

// こっちじゃなくて
Size winSize = Director::getInstance()->getWinSize();
// こんな感じで画面サイズ以上の領域でも使いたい
GLint glSize;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, & glSize);
const int TEXTURE_SIZE = a_val;

参考

2015-06-08

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

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

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

Chapter2-1

p112 プログラム 4行目

訂正前

let button = UIButton.buttonWithType(UIButtonType.System)
    as UIButton

訂正後

let button = UIButton.buttonWithType(UIButtonType.System)
    as! UIButton
p112 プログラム 12行目

訂正前

fun respondToButtonClick(sender: UIButton!) {

訂正後

func respondToButtonClick(sender: UIButton) {
p115 1つ目のプログラム3行目

訂正前

func respondToButtonClick(sender: UIButton!) {

訂正後

func respondToButtonClick(sender: UIButton) {
p115 2つ目のプログラム5行目

訂正前

let button = UIButton.buttonWithType(UIButtonType.System)
    as UIButton

訂正後

let button = UIButton.buttonWithType(UIButtonType.System)
    as! UIButton
p115 2つ目のプログラム最後から1行目

訂正前

func respondToButtonClick(sender: UIButton!) {

訂正後

func respondToButtonClick(sender: UIButton) {

Chapter2-2

p127 2つ目のプログラム4行目

訂正前

func application(application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {

訂正後

func application(application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
p138 1つ目のプログラム2行目

訂正前

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {

訂正後

override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
p138 1つ目のプログラム4行目

訂正前

override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {

訂正後

override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
p138 1つ目のプログラム6行目

訂正前

override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {

訂正後

override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
p138 1つ目のプログラム8行目

訂正前

override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {

訂正後

override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {

Chapter2-3

p148 2つ目のプログラム4行目

訂正前

let cell = tableView.dequeueReusableCellWithIdentifier("Cell",
    forIndexPath:indexPath) as UITableViewCell

訂正後

let cell = tableView.dequeueReusableCellWithIdentifier("Cell",
    forIndexPath:indexPath) as! UITableViewCell
p159 1つ目のプログラム2行目

訂正前

as UICollectionViewCell

訂正後

as! UICollectionViewCell
p159 2つ目のプログラム12行目

訂正前

let header = collectionView.dequeueReusableSupplementaryViewOfKind(
    kind,
    withReuseIdentifier: "Header",
    forIndexPath: indexPath) as UICollectionReusableView

訂正後

let header = collectionView.dequeueReusableSupplementaryViewOfKind(
    kind,
    withReuseIdentifier: "Header",
    forIndexPath: indexPath) as! UICollectionReusableView

Chapter3-2

p178 1つ目のプログラム5行目

訂正前

let button = UIButton.buttonWithType(UIButtonType.System)
    as UIButton

訂正後

let button = UIButton.buttonWithType(UIButtonType.System)
    as! UIButton

Chapter3-3

p191 2つ目のプログラム5行目

訂正前

let paths = NSSearchPathForDirectoriesInDomains(
    .DocumentDirectory,
    .UserDomainMask, true) as Array<String>

訂正後

let paths = NSSearchPathForDirectoriesInDomains(
    .DocumentDirectory,
    .UserDomainMask, true) as! Array<String>
p192 1つ目のプログラム2行目

前のページからの続きになっている箇所です。

訂正前

let paths = NSSearchPathForDirectoriesInDomains(
    .DocumentDirectory,
    .UserDomainMask, true) as Array<String>

訂正後

let paths = NSSearchPathForDirectoriesInDomains(
    .DocumentDirectory,
    .UserDomainMask, true) as! Array<String>
p196 1つ目のプログラム2行目

訂正前

self.name = aDecoder.decodeObjectForKey("name") as String 
self.address = aDecoder.decodeObjectForKey("address") as Address

訂正後

self.name = aDecoder.decodeObjectForKey("name") as! String 
self.address = aDecoder.decodeObjectForKey("address") as! Address
p196 2つ目のプログラム19行目

訂正前

self.zipCode = aDecoder.decodeObjectForKey("zipCode") as String
self.state = aDecoder.decodeObjectForKey("state") as String
self.city = aDecoder.decodeObjectForKey("city") as String
self.other = aDecoder.decodeObjectForKey("other") as String

訂正後

self.zipCode = aDecoder.decodeObjectForKey("zipCode") as! String
self.state = aDecoder.decodeObjectForKey("state") as! String
self.city = aDecoder.decodeObjectForKey("city") as! String
self.other = aDecoder.decodeObjectForKey("other") as! String
p197 1つ目のプログラム20行目
p198 1つ目のプログラム9行目

訂正前

let paths = NSSearchPathForDirectoriesInDomains(
  .DocumentDirectory,
  .UserDomainMask, true) as [String]

訂正後

let paths = NSSearchPathForDirectoriesInDomains(
  .DocumentDirectory,
  .UserDomainMask, true) as! [String]
p198 1つ目のプログラム12行目

訂正前

let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as Array<Person>

訂正後

let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as! Array<Person>
p202 1つ目のプログラム9行目
p203 1つ目のプログラム9行目
p205 1つ目のプログラム9行目
p206 2つ目のプログラム9行目

訂正前

let paths = NSSearchPathForDirectoriesInDomains(
  .DocumentDirectory,
  .UserDomainMask, true) as [String]

訂正後

let paths = NSSearchPathForDirectoriesInDomains(
  .DocumentDirectory,
  .UserDomainMask, true) as! [String]
p206 2つ目のプログラム15行目

訂正前

let person = NSKeyedUnarchiver.unarchiveObjectWithData(data as NSData) as Person

訂正後

let person = NSKeyedUnarchiver.unarchiveObjectWithData(data as! NSData) as! Person
p219 1つ目のプログラム1行目

訂正前

let addressList = defaults.arrayForKey("address-list") as [NSData]

訂正後

let addressList = defaults.arrayForKey("address-list") as! [NSData]
p219 1つ目のプログラム3行目

訂正前

let person = NSKeyedUnarchiver.unarchiveObjectWithData(data as NSData) as Person

訂正後

let person = NSKeyedUnarchiver.unarchiveObjectWithData(data as NSData) as! Person
p229 1つ目のプログラム11行目

訂正前

let applicationDocumentsDirectory: NSURL = urls[urls.count-1] as NSURL

訂正後

let applicationDocumentsDirectory: NSURL = urls[urls.count-1] as! NSURL
p230 1つ目のプログラム2行目

訂正前

let model = NSEntityDescription.insertNewObjectForEntityForName("Event", inManagedObjectContext: context) as NSManagedObject

訂正後

let model = NSEntityDescription.insertNewObjectForEntityForName("Event", inManagedObjectContext: context) as! NSManagedObject
p252 2つ目のプログラム3行目

訂正前

let object = self.fetchedResultsController.objectAtIndexPath(indexPath) as NSManagedObject

訂正後

let object = self.fetchedResultsController.objectAtIndexPath(indexPath) as! NSManagedObject
p254 2つ目のプログラム7行目

訂正前

let person = NSEntityDescription.insertNewObjectForEntityForName(entity.name!, inManagedObjectContext: context) as NSManagedObject

訂正後

let person = NSEntityDescription.insertNewObjectForEntityForName(entity.name!, inManagedObjectContext: context) as! NSManagedObject
p257 1つ目のプログラム5行目

訂正前

let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell

訂正後

let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
p277 1つ目のプログラム16行目

訂正前

let dict = NSMutableDictionary()

訂正後

var dict = [String: AnyObject]()

Chapter4-2

p312 1つ目のプログラム3行目から4行目

訂正前

let ope = object as HttpOperation
let responseString: String = NSString(data: ope.responseData, encoding:NSUTF8StringEncoding)!

訂正後

let ope = object as! HttpOperation
let responseString: String = NSString(data: ope.responseData, encoding:NSUTF8StringEncoding) as String!

Chapter4-3

p318 2つ目のプログラム9行目

訂正前

let payment = SKPayment(product: product as SKProduct)

訂正後

let payment = SKPayment(product: product as! SKProduct)
p319 1つ目のプログラム3行目
p323 1つ目のプログラム3行目
p327 1つ目のプログラム22行目

訂正前

for transaction in transactions as [SKPaymentTransaction] {

訂正後

for transaction in transactions as! [SKPaymentTransaction] {
p333 1つ目のプログラム8行目

訂正前

let productRequest = SKProductsRequest(productIdentifiers: set)

訂正後

let productRequest = SKProductsRequest(productIdentifiers: set as Set<NSObject>)
p334 1つ目のプログラム1行目

訂正前

let payment = SKPayment(product: product as SKProduct)

訂正後

let payment = SKPayment(product: product as! SKProduct)
p334 1つ目のプログラム7行目、29行目、44行目

訂正前

for transaction in transactions as [SKPaymentTransaction] {

訂正後

for transaction in transactions as! [SKPaymentTransaction] {

関連記事

参考

2015-03-18

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 という意味です。

参考

関連記事

2015-03-13

【正誤表】プロの力が身につく 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への対応箇所を以下の記事にまとめましたのでそちらも参考にしてください。

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

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

関連記事

参考