SwiftでOpenGLを使ったゲームを作成する方法

## はじめに
備忘録を兼ねてます。

Swiftでゲームを作るとなると`UIView(xib)`を駆使するか、`ScenekKit`を使うか直接`OpenGL ES`を触るしかない。
`UIView(xib)`を使ってゲームを作る場合は動きが激しいゲームは作れないので、本格的にゲームを作るとなると`SceneKit`もしくは`OpenGL ES`を使うしかない。

`SceneKit`はあまり使ったことがないが`Tiled`などのツールがサポートされていない印象だ。
`OpenGL ES`を生で触るには、かなりの技術力とコーディング力が必要になる。
ということで今回は`SpriteBuilder`を使う方法について書く。

※3Dゲームを作るなら`Unity`だと思います。
※本記事は自分が使う限り更新し続ける予定です。（同じ路線でゲームを作りたい方は是非共有お願いいたします♪）

## SpriteBuilderとは？
SpriteBuilderはCocos2dのView部分を担当するUIツールだ。
作られたファイルをCocos2dで読み込むことによってプログラムを簡易化する。
xCodeで言う`xib`の部分だ。

`cocos2d`は`OpenGL ES`を簡単に操作できるプログラムだ。
このツールを使うことでSwiftで`OpenGL ES`を簡易的に使うことができる。

### デメリット
`SpriteBuilder`には重大な欠点がある。
なので、先にデメリットを伝えておく。

`SpriteBuilder`はすでに開発が終わっており、AppStoreなどでも配布されていない。
なので`GitHub`などからプログラムを持ってきてインストールする必要がある。
また、幾つかのバグがそのままになっているのだ。
（あと、基本的にはiOSアプリしか作れない。）

正気の人間ならこのツールを使う選択肢は生まれないと思う。
しかし`Swift`で2Dゲームを作ろうとした場合にほとんど方法が残されていないため、このツールを使わざるをえない状況なのだ。

### 考察
メリットの前にもう一つ考察しておく。
もしこれを読んでいる読者が`Unity`を良しとしているならこれ以上記事を読む必要はないだろう。
`Cocos2d-x`のような`C++`で書ける強者も同様だ。
`Cocos Creator`にチャレンジしている方も不要だろう。

しかしいずれの方法も`Swift`ではプログラムを書くことができない。
僕のように`Swift`でゲームを作りたいという方は今回紹介する方法を検討してみてはいかがだろうか？

### メリット
`SpriteBuilder`を使った１番のメリットは`Swift`で`OpenGL ES`のプログラミングが簡単にできるところだ。
多分現時点で一番使いやすい言語は`Swift`だと言っても良い言語を使ってプログラミングができるのは大きな魅力だろう。

`Unity`や`Cocos Creator`などを使った場合はネイティブ部分を触れないため、課金処理やスレッド処理などの込み入ったことをする時にハードルが上がる。しかし、ネイティブである`Swift`ならその辺でつまづくことはあまりない。
なので、開発効率は圧倒的に高い。

また、幸いにも`SpriteBuilder`が出力する`Cocos2d V3`は`Objective-c`が大半なので`Swift`のバージョンが上がってもあまり影響を受けない。
そのため、しばらくは使い続けれるだろう。

オープンソースなのでプログラムを変更できるのも大きな特色だ。

## やってみよう
何はともあれ、Swiftでゲームを作りたい人は乗っかってみよう。
わからないことなどがあればコメントに書いてくれればわかる範囲で回答するので。

以下、注意点をまとめます。

## インストール
昔は`AppStore`で配布されていたので、そのままインストールできます。
今は[GitHub](https://github.com/spritebuilder/SpriteBuilder)に書かれている方法でインストールできます。（筆者は`AppStore`で入れたので実践はしてませんが・・・）

## 起動後にPublishするまで
起動時に言語を選べるので`Swift`を選びます。
（`Objective-c`を選んでも`MainScene`が`Objective-c`で表示されるだけなので、どちらでもいいです。）

`File -> Project setting..`でAndroidを外します。
`Publish`すると`xCode`のプロジェクトが作られるので、そこから`Android`や`Mac`などの不要なフォルダやファイルは消しましょう。

`Published-Android`なども不要です。ガシガシ消しちゃいましょう。
（Androidのチェックを外しても、なぜか始めの一回目だけは作られてしまいます。）

## 言語コードに関して
iOS9から言語コードの扱いが変わっているため以下の対応を行う。

```objc:Source/libs/cocos2d-iphone/cocos2d-ui/CCBReader/CCBLocalizationManager.m
- (void) loadStringsFile:(NSString*) file
{
    // Load default localization dictionary
    NSString* path = [[CCFileUtils sharedFileUtils] fullPathForFilename:file];
    
    // Load strings file
    NSDictionary* ser = [NSDictionary dictionaryWithContentsOfFile:path];
    
    // Check that format of file is correct
    NSAssert([[ser objectForKey:@"fileType"] isEqualToString:@"SpriteBuilderTranslations"], @"Invalid file format for SpriteBuilder localizations");
    
    // Check that file version is correct
    NSAssert([[ser objectForKey:@"fileVersion"] intValue] == 1, @"Translation file version is incompatible with this reader");
    
    // Load available languages
    NSArray* languages = [ser objectForKey:@"activeLanguages"];
    
    // Determine which language to use
    NSString* userLanguage = NULL;
    
    NSArray* preferredLangs = [NSLocale preferredLanguages];
    for (NSString* preferredLang in preferredLangs)
    {
        if ([languages containsObject:preferredLang])
        {
            userLanguage = preferredLang;
            break;
        }
    }
    
    // Create dictionary for translations
    _translations = [[NSMutableDictionary alloc] init];
    
    // Load translations
    if (userLanguage != NULL)
    {
        NSArray* translations = [ser objectForKey:@"translations"];
        
        for (NSDictionary* translation in translations)
        {
            NSString* key = [translation objectForKey:@"key"];
            NSString* value = [(NSDictionary*)[translation objectForKey:@"translations"] objectForKey:userLanguage];
            
            if (key != NULL && value != NULL)
            {
                [_translations setObject:value forKey:key];
            }
        }
    }
}
```

このメソッドを以下のように変更します。

```objc:Source/libs/cocos2d-iphone/cocos2d-ui/CCBReader/CCBLocalizationManager.m
- (void) loadStringsFile:(NSString*) file
{
    // Load default localization dictionary
    NSString* path = [[CCFileUtils sharedFileUtils] fullPathForFilename:file];
    
    // Load strings file
    NSDictionary* ser = [NSDictionary dictionaryWithContentsOfFile:path];
    
    // Check that format of file is correct
    NSAssert([[ser objectForKey:@"fileType"] isEqualToString:@"SpriteBuilderTranslations"], @"Invalid file format for SpriteBuilder localizations");
    
    // Check that file version is correct
    NSAssert([[ser objectForKey:@"fileVersion"] intValue] == 1, @"Translation file version is incompatible with this reader");
    
    // Load available languages
    NSArray* languages = [ser objectForKey:@"activeLanguages"];
    
    // Determine which language to use
    NSString* userLanguage = NULL;
    
    NSArray* preferredLangs = [NSLocale preferredLanguages];
    for (NSString* preferredLang in preferredLangs)
    {
        // now loop thru languages from our cocosbuilder
        for (NSString *localizedLanguage in languages)
        {
            // doing range of string as we might have en-GB set in our phone and that will match our en from the activeLanguages
            if ([preferredLang rangeOfString:localizedLanguage].location != NSNotFound)
            {
                userLanguage = localizedLanguage;
                break;
            }
        }
        
        if (userLanguage != NULL) {
            break;
        }
    }
    
    // Create dictionary for translations
    _translations = [[NSMutableDictionary alloc] init];
    
    // Load translations
    if (userLanguage != NULL)
    {
        NSArray* translations = [ser objectForKey:@"translations"];
        
        for (NSDictionary* translation in translations)
        {
            NSString* key = [translation objectForKey:@"key"];
            NSString* value = [(NSDictionary*)[translation objectForKey:@"translations"] objectForKey:userLanguage];
            
            if (key != NULL && value != NULL)
            {
                [_translations setObject:value forKey:key];
            }
        }
    }
}
```

## 画面遷移
iOS9・・・というか、`Capitan`から以下のコードが落ちるようになりました。
（xCodeのバージョンのせいかもしれない。とにかく最新だと落ちます。）

```objc:Source/libs/cocos2d-iphone/cocos2d/CCTransition.m
-(void)draw:(CCRenderer *)renderer transform:(const GLKMatrix4 *)transform
{
    typedef id (*Func)(id, SEL);
    ((Func)objc_msgSend)(self, _drawSelector);
    
}
```

この箇所は以下のように書き換えると動きます。

```objc:Source/libs/cocos2d-iphone/cocos2d/CCTransition.m
-(void)draw:(CCRenderer *)renderer transform:(const GLKMatrix4 *)transform
{
    void (*Func)(id, SEL) = (void(*)(id, SEL)) objc_msgSend;
    Func(self, _drawSelector);
}
```

## iOS10から
バージョンの識別が正しくできていないので修正。
以下のコードに置き換える。（ver1と間違えて認識されてしまう。）

```objc:Source/libs/cocos2d.xcodeproj/external/ObjectAL.xcodeproj/ObjectAL/Support/IOSVersion.m
- (id) init
{
	if(nil != (self = [super init]))
	{
#if __CC_PLATFORM_IOS
		NSString* versionStr = [[UIDevice currentDevice] systemVersion];
        unichar ch = [versionStr characterAtIndex:0];
        
        version = (float)(ch - '0');
		if(ch < '0' || ch > '9' || [versionStr characterAtIndex:1] != '.')
		{
//			NSLog(@"Error: %s: Cannot parse iOS version string \"%@\"", __PRETTY_FUNCTION__, versionStr);
            // Version 10以上
            version = (float)(ch - '0') * 10 + (float)([versionStr characterAtIndex:1] - '0');
		}
		
		float multiplier = 0.1f;
		NSUInteger vLength = [versionStr length];
		for(NSUInteger i = 2; i < vLength; i++)
		{
			ch = [versionStr characterAtIndex:i];
			if(ch >= '0' && ch <= '9')
			{
				version += (ch - '0') * multiplier;
				multiplier /= 10;
			}
			else if('.' != ch)
			{
				break;
			}
		}
#else
        version = 5;
#endif
	}
	return self;
}
```

既存コードを踏襲して直したが、上記のコードはもう少し良い方法があるきがする・・・。

## Images.xcassetsについて
`Images.xcassets`は`Cocos2d`のプロジェクトでも使われています。
なので、アイコンやスプラッシュなどを設定する際には注意して設定してください。

`Cocos2d`プロジェクトの`Images.xcassets`を誤って選ばないようにしてください。
（邪魔ら消しても良いと思います。）

## 大量ワーニングについて
ほっといても良いのですが、とにかくワーニングがひたすら出ます。
以下のワーニングが30個くらい出てきます。

```
ld: warning: object file (/.../Build/Products/Debug-iphonesimulator/libcocos2d.a(IOSVersion.o)) was built for newer iOS version (9.3) than being linked (8.0)
```

これは[こちら](http://forum.cocos2d-objc.org/t/xcode7-warnings/17609)のリンクの通りで、以下の対応をすればワーニングが出なくなります。

```
The culprit is the minimum deployment target for the ObjectAL library - 
In Xcode delve into the cocos2d project -> external -> ObjectAL project: 
Goto build settings and search for 'deployment target' 
and set the value to be <= your main project minimum deployment target.
```

上記の通りで

「cocos2d project -> external -> ObjectAL」のフォルダをたどってから「build settings」を表示します。
その中の「deployment target」を自分のプロジェクトに合わせた最小ターゲットにすればOKです。

他にも出ますが、上記を削れば自分のプロジェクト内部のワーニングと切り分けれるくらい減ります。

## SpriteBuilderのプロジェクトの保存について
プロジェクトは`Ctrl+S`で保存できるのですが、、、ここにもバグがあって画像のサイズ（1xや2xなど）は保存されません。
なので、通常は`2x`で作っていてイレギュラーに`4x`などを使うと保存されずに次回開く時に`2x`で表示されてしまいます。

これは致命的なバグですが、回避策があります。
それは`close project`を行うことです。この際に画像サイズが保存されるようなので`×`で消すのではなく、`close project`を行ってから閉じてください。

## SpriteBuilderの使い方について
途中までですが[こちら](http://qiita.com/syou007/items/9e6202a88495757dda5f)に書いてます。
他にも探せばありますので、見てみると良いでしょう。

思っているよりも簡単にゲームを作れるので驚くでしょう。
（こんな名器が世に埋もれてしまうなんて・・・）

## 実績について
[こちら](http://qiita.com/syou007/items/1e5a2d920d71c039ddfd)にゲームを作った事について書いてます。
よければ参考にしてください。

ついでに、ゲームで遊んでください。
[勇者の冒険](https://itunes.apple.com/jp/app/id1163443744)
-itunesが開きます-