代官山らへんで働くengineerのUnityブログ

サーバサイドやってきたエンジニアがUnityとか触って遊ぶだけのブログ

【Unity】PostprocessBuildPlayer + mod_pbxprojでiOSビルド時に自動でframeworkを追加したりする【iOS】

UnityからiOSにビルド後にframeworkの追加が必要になったりした場合
Unity5からはPBXProjectというAPIを使えば済む話なのですが、Unity4以前だとまだ実装されていないため、PostProcessBuildmod_pbxprojなどのどっかのすごい人達が作った外部のツールを利用必要があります。

今回は残念なことにUnity4環境でframeworkの追加やファイル個別のARC設定の自動化が必要になったため、やったことのメモです。

前提

・Unity4以前の環境
・ビルド後のiOSプロジェクトにframeworkを組み込んで、ファイルごとにARC設定を行う必要がある
・Jenkinsをビルドに使っているなどの理由でframework組み込みと設定も自動化したい
Pythonアレルギーがない(mod-pbxprojはPythonで書かれてるので...)

準備

githubリポジトリからmod-pbxprojを取ってきて、UnityのEditorフォルダ以下に置く
・UnityのEditorフォルダ以下に「PostprocessBuildPlayer」というファイルを拡張子なしで作成する

PostprocessBuildPlayerを書く

UnityのEditorフォルダ以下にPostprocessBuildPlayerというファイルを置いておくだけでビルド後に勝手に実行されます
こいつは基本的にどんな言語でも書けるらしいのですが、今回はmod-pbxprojがPythonで書かれてるのでこいつもPythonです。マジカヨ...

#!/usr/bin/env python

import os, sys
from mod_pbxproj import *

proj = XcodeProject.Load(proj_path + '/Unity-iPhone.xcodeproj/project.pbxproj')
PROJ_ROOT = proj.source_root

#ファイルが存在するかチェック
test_framework_is_readable = os.access(proj_path + '/Libraries/TestFramework', os.R_OK)
if compliance_sdk_is_readable:

    #TestFrameworkをグループとして取り込み
    proj.add_folder(PROJ_ROOT + '/Libraries/TestFramework', parent = lib_group)

    #取り込んだframeworkのファイルに「-fobjc-arc」を付与してARC有効にする
    fileNameList = []
    fileNameList.append('TestFrameworkViewController.m')
    fileNameList.append('TestFrameworkController.m')
    fileNameList.append('TestFrameworkVerificationViewController.m')
    fileNameList.append('TestFrameworkAgreementManager.m')


    for fileName in fileNameList:
        fileId = proj.get_files_by_name(fileName)
        files = proj.get_build_files(fileId[0].id)
        for f in files:
            f.add_compiler_flag('-fobjc-arc')


あとはビルドを実行するだけでこいつが勝手に動いてくれるはず
mod_pbxprojの詳しい使い方はgithubを読むとなんとなくわかるはず

【Unity】僕の考えた最強のエディタ拡張【AdventCalendar】

CYBIRDエンジニア Advent Calendar 2015 8日目書きます。 @stks こと くそすけ です。
昨日はもう残り少ない同期である@asukyの「(このクラウド時代に)自作サーバで重複排除やストレージ階層化を試してみる話」でした。
彼は社内システム管理系の部署に移ってからは同フロアの女性社員さん達と仲良くやってるらしいです。爆発すればいいのにね。

自分もなんだかんだもう4年もここにいるらしいです。やばいですね。色々と。
去年までの3年間はずっとサーバサイドエンジニアとしてPHPばっかり書いていたのですが、
今は色々あって”Unityエンジニア”という肩書で”Unityをほとんど触らず”に”シェルとPHP”を書いています。
この肩書にとらわれない働き方が出来るFreedomさが弊社のいいところだと思います。Unity触りたい。
f:id:chroske:20151207210828p:plain:w300
なんて言っておきながら少しはUnityにも触っております。
主にエディタ拡張を使っての自動化や運用効率化がメインですが
というわけでUnityの中でもちょっとマニアックなエディタ拡張の秘められた真価を解放すべく、
僕が考えた最強にROCKでMANIACでCOOLなエディタ拡張を作ってみたいと思います。
よっしやるぞお~~~~



そもそもエディタ拡張とは?

簡単に言うとUnity自体を改造して日々の開発を効率化して楽しようってものである
詳しくはここを見るとよろしいかと

エディタ拡張を使ってゲーム開発をしながらアニメを見よう!

ゲーム開発は己との戦いである。長期的な開発は否応なしに開発者の精神をすり減らし廃人にする。
またゲーム開発でボロボロになったメンタルを自らケアすることもエンジニアの使命なのだ。
そのケアの手法として今最も注目を集めているのが昨今の日本文化を代表するアニメーションである。
アニメであれば開発の手を止めることなく受動的かつ継続的に開発者のメンタルを癒やすことが可能だ。
これがいわゆる継続的インテグレーションというやつである。


f:id:chroske:20151206231019g:plain:w400
個人的におすすめしたいのが未確認で進行形というアニメだ ましろたん舐めたい

さっそくこの永久機関を現実のものとすべくUnityエディタ上での動画再生APIを探してみたのだがどうもそんなAPIは存在しないらしい。万事休す。
だがまだ諦めるのは早い、僕はどこかでUnityエディタから動画を見た記憶があったのだ。
それはAssetStoreから見れるyoutubeのAssetのサンプル動画だった。
f:id:chroske:20151207103922p:plain:w350
↑こういうやつ

あのAssetStoreはおそらくWebViewのようなものを呼び出して表示しているだけだろう。
つまり動画再生APIはなくともWebを表示させる機能を使えばWebView経由で動画が再生出来るということだ。

public class UnityDougaPlayer : EditorWindow {

	static BindingFlags Flags = BindingFlags.Public | BindingFlags.Static;
	
	[MenuItem("Window/UnityDougaPlayer")]
	static void Open ()
	{
		var type = Types.GetType ("UnityEditor.Web.WebViewEditorWindow", "UnityEditor.dll");
		var methodInfo = type.GetMethod ("Create", Flags);
		methodInfo = methodInfo.MakeGenericMethod (typeof(UnityDougaPlayer));
		methodInfo.Invoke (null, new object[]{
			"UnityDougaPlayer",
			"/Users/kusosuke/unity_projects/hogehoge/Assets/Editor/DougaPlayer.html", //なぜか相対パスが通らない?
			150, 180, 400, 600
		});
	}
}

WebViewを呼び出すだけなのでコードはとてもシンプルです。
実はWebViewを呼び出すAPIもUnityからは公開されていないのだが、AssetStoreで使っている限りどこかにあるはずである。
そこで今回はリフレクションという方法を使って本来触ることができない(Publicクラスではない)未公開のAPIを呼び出しています。

<video controls="true" width="320">
	<source src="tomadoi_recipe.webm">
</video>

WebViewで呼び出すHTMLファイルはこんなものでいいだろう。
HTML5のvideo要素を呼び出してやればいいのだ。
ファイル拡張子によっては再生できないものも結構あるので気をつけていただきたい。

f:id:chroske:20151207000229g:plain
跳ねまわる天使達をご覧いただけただろうか? 夜ノ森さん家の子になりたいよぉ

無事ローカルに保存しておいた未確認で進行形OP「とまどい→レシピ」を再生することが出来ている。
このgif画像からも動画工房と藤原佳幸監督が神であることがおわかりいただけることだろう。
嗚呼、心が洗われてゆく...

エディタ拡張で見たアニメの感想をエディタ拡張からtwitterに投稿しよう!

ゲーム開発の最中、最高のアニメを見たあとにすることといったらなんだろうか。
日々高度な技術を学び続ける意識の高いエンジニア諸兄であれば既におわかりいただけているだろう。
それは心の底より湧き上がる熱いパトスを140字以内にしたためツィートすることに他ならない。

f:id:chroske:20151207203320g:plain:w350
そうだ、次はUnityでツイッターだ!

private IEnumerator Reload (){
	string urlstr = "https://api.twitter.com/1.1/statuses/home_timeline.json";
	Dictionary<string, string> parameters = new Dictionary<string, string>();

	parameters.Add("oauth_version", oauth_version);
	parameters.Add("oauth_nonce", oauth_nonce);
	parameters.Add("oauth_timestamp", GenerateTimeStamp());
	parameters.Add("oauth_signature_method", oauth_signature_method);
	parameters.Add("oauth_consumer_key", oauth_consumer_key);
	parameters.Add("oauth_consumer_secret", oauth_consumer_secret);
	parameters.Add("oauth_token", oauth_token);
	parameters.Add("oauth_token_secret", oauth_token_secret);
	parameters.Add("count", count);

	string oauth_signature = GenerateSignature("GET", urlstr, parameters);

	parameters.Add("oauth_signature", oauth_signature);

	var sortedParameters = from p in parameters
		where OAuthParametersToIncludeInHeader.Contains(p.Key)
			orderby p.Key, UrlEncode(p.Value)
			select p;

	StringBuilder authHeaderBuilder = new StringBuilder("OAuth ");

	int i = 0;
	string kanma = "";
	foreach (var item in sortedParameters)
	{
		if(i != 0){
			kanma = ",";
		}
		authHeaderBuilder.AppendFormat(kanma+"{0}=\"{1}\"", UrlEncode(item.Key), UrlEncode(item.Value));
		i++;
	}
		
	authHeaderBuilder.AppendFormat(",oauth_signature=\"{0}\"", UrlEncode(parameters["oauth_signature"]));

	WWWForm form = new WWWForm();
	Dictionary<string, string> headers = form.headers;
	headers ["Authorization"] = authHeaderBuilder.ToString ();

	string url = urlstr + "?" + "count=" + count;
	if(since_id != "0"){
		parameters.Add("since_id", since_id);
		url += "&since_id=" + since_id;
	}

	WWW www = new WWW(url, null, headers);
	yield return www;
	if (www.error == null) {
		IList TimeLineList = (IList)Json.Deserialize(www.text);
		List<Dictionary<string,string>> HomeTimeLineListBlock = new List<Dictionary<string,string>>();

		int j = 0;
		foreach(IDictionary person in TimeLineList){
			Dictionary<string,string> TimeLineListDatas = new Dictionary<string,string>();
			if(j == 0){
				since_id = person["id"].ToString();
			}

			IDictionary datas = (IDictionary)person["user"];
			if(datas["screen_name"] != null){
				TimeLineListDatas.Add ("user_id",datas["screen_name"].ToString());
			}

			TimeLineListDatas.Add ("tweet_text",person["text"].ToString().Replace("\n"," "));
			HomeTimeLineListBlock.Add (TimeLineListDatas);
			j++;
		}
		HomeTimeLineListBlock.AddRange(HomeTimeLineList);
		HomeTimeLineList = HomeTimeLineListBlock;
	}
}

private static readonly string[] SecretParameters = new[]{
	"oauth_consumer_secret",
	"oauth_token_secret",
	"oauth_signature"
};

private static readonly string[] OAuthParametersToIncludeInHeader = new[]{
	"oauth_version",
	"oauth_nonce",
	"oauth_timestamp",
	"oauth_signature_method",
	"oauth_consumer_key",
	"oauth_token",
	"oauth_verifier",
	"screen_name",
	"count"
};

private static string GenerateSignature(string httpMethod, string url, Dictionary<string, string> parameters){
	var nonSecretParameters = (from p in parameters
		                       where !SecretParameters.Contains(p.Key)
		                       select p);
		
	string signatureBaseString = string.Format(CultureInfo.InvariantCulture,
		                                       "{0}&{1}&{2}",
		                                       httpMethod,
		                                       UrlEncode(NormalizeUrl(new Uri(url))),
		                                      UrlEncode(nonSecretParameters));

	string key = string.Format(CultureInfo.InvariantCulture,
		            "{0}&{1}",
		             UrlEncode(parameters["oauth_consumer_secret"]),
		             parameters.ContainsKey("oauth_token_secret") ? UrlEncode(parameters["oauth_token_secret"]) : string.Empty);
		
	HMACSHA1 hmacsha1 = new HMACSHA1(Encoding.ASCII.GetBytes(key));
	byte[] signatureBytes = hmacsha1.ComputeHash(Encoding.ASCII.GetBytes(signatureBaseString));
	return Convert.ToBase64String(signatureBytes);
}

これがタイムライン取得部分。
長ったらしく書いてはいますが、やっていることは送信パラメータから署名(oauth_signature)を作ってWWWメソッドでGET送信。
いわゆるtwitter REST APIのお約束をやっているだけ。
ただUnityはエディタ上でStartContinueでのコルーチンが使えないため、こちらで書かれているクラスを丸々流用させてもらっています(ありがたや〜
ツイート処理の部分は今回省略しますが、リクエストがGETからPOSTになったことと送信パラメータが少し違う以外は基本的に上と同じ処理をしています。
あとは取得したjsonをListやらDictionaryに詰めなおしてからEditorGUIに渡してあれこれしてやると
f:id:chroske:20151207205547g:plain:w350
おお、なんか普通のTwitterクライアントっぽい! やったよましろたん

普通にそれっぽく出来てしまいました。
TwiterとUnityがここまでのハーモニーを奏でるとは、もはや開発した僕の想像を完全に超えてます。実は同じ会社なんじゃないでしょうか。
というかなんだこの空間は! アニメ見れてtwitter出来てゲームも作れてあとはピザさえ注文できればUnityで人生のすべてが完結してしまうこともありえてしまうのでは....Unityとは僕らの想像を遥かに超えた超自然的な存在だったのかもしれない.........終

まとめ

すげえ要約するとUnityエディタでもちょっとがんばれば動画再生したりPOSTやGETが送れたりしますよってことでした。
標準APIで出来ないからって諦めたらそこで試合終了ですよって安西先生も言ってましたね。
ただこちらのエディタ拡張を利用して被ったいかなる被害も当方責任を負いかねますのでなにとぞ...
さすがに弊社も仕事中にアニメ見ながらtwitterしてたら怒られると思う(怒られてすむのか?)
あと弊社ではUnity触れるエンジニアが僕含めて3〜4人くらいしかいません。絶滅危惧種です。
きっと絶賛募集中だと思うので入ってやってください。なにとぞなにとぞ。

CYBIRDエンジニア Advent Calendar 2015 明日は、 @gucchonさん の「スマホ向けWEBフロントエンド開発でお世話になったChromeデベロッパーツールの機能たち」だそうです。
@gucchonさんとは仕事で全く絡んだことがないのですが、一度記事を書けばはてぶトップに躍り出ること間違いなしのスゥパァイケメンエンジニアらしいです。楽しみですね。


f:id:chroske:20151207143647g:plain:w300
未確認で進行形 Blu-ray BOX絶賛予約受け付け中です!
君もこの冬をましろたんと過ごそう!

補足

twitterAPIで一番めんどくさいのは認証部分なのですが、今回はアクセストークンなどをtwitterのdevelopersサイトから手動生成してプログラム内にベタで書いております。
いずれ認証部分もちゃんと書いてtwitter連動ライブラリみたいな感じでAssetStoreなりどこかに公開したいですな。

twitterクライアント作ってる途中で「WebView表示できるならTwitter公式表示すれば終わりじゃね?」とか思ったけど深く考えないことにしました。
でも逆に考えるとブラウザで出来ることは何でもいけそうってことです。夢が広がりんぐですね!

【Unity】UNETについてわかったことメモ【P2P】

今年の夏くらいにUnity5.1から実装されたマルチプレイ用のネットワークシステムUNETですが
思ったよりネットの文献も少ないし本も出てないし公式マニュアル読んでもよくわからんしで
色々自分なりに調査してみた結果をまとめてみます

そもそもUNETとは?

マルチプレイ用ゲームのネットワーク関連の機能を実装できるクラス
マルチプレイとは言うのもの、基本的にはプレイヤーのうち誰かがホストとなり、ゲストプレイヤーがそれにぶら下がるP2P通信タイプのゲームのこと
ホストの接続が落ちたらそれにぶら下がったゲストプレイヤーも接続が切れるやつである

サーバ→クライアントのような方式には出来ないのか?

出来るには出来る
しかしnode.jsやサーバサイド開発言語などを使った一般的なリアルタイムゲーム(白猫みたいな)とは大きな違いがある
まずサーバ用の言語を必要とせず、サーバで動くのはクライアント用にビルドしたものと同じUnityのプロジェクトだ
イメージ的にはP2Pのホストプレイヤーの役割をサーバが行っている感じだ
Minecraftやterrariaのようなプレイヤーが一時的にサーバとなり、そこに他のプレイヤーが接続するゲームの開発を想定しているのだろう
確かにそういったゲームを作るならお手軽感がある
サーバを経由せず通信を行うため、ハッキングに弱いのではないかと思われる。自分がホストになってしまえばある意味やりたい放題出来る可能性がある。

便利機能がいっぱい

ゲーム内のオブジェクトの距離に寄って、離れていたらそもそも生成しなかったりデストロイしたり
通信する情報ごとに通信のクオリティを決められたり(早くてテキトーとか遅くて正確とか)
ネットワークのパケットの動きを可視化できたり
P2P通信でのユーザ同士のマッチングサーバをUnityが用意してくたり(100人までは無料!)
サーバ側で動くメソッドとクライアント側で動くメソッドをそれぞれ準備出来たり
P2P通信ゲームを作るなら使わない手はないだろう

まとめ

まだまだ機能不足ではあると思うし、バグも多いようなので今後に期待というところだろうか。
個人でちょっとした通信機能がついたゲームを作るならばPhotonよりもお手軽感があるかもしれない。
P2P通信でなおかつマッチングサーバをUnityが用意してくれるならサーバまわりの運用は必要なくなる
つくづく個人デベロッパーに優しい機能をつけてくれたわけである サンキューUnity!

※記事を書いている現在Unity Multiplayerはまだプレビュー版のみの公開なのでマッチングサーバーは100人までしか使えないし有料版みたいなものもないです

【C#】WriteLineとWriteの違い【Unity】

StreamWriter sw = new StreamWriter(AUTOBUILD_RESULT_FILE_PATH,true);
	sw.WriteLine("CHATWORK_MESSAGE_ON_SUCCESS = " + _assetBuildResultStr);
	sw.Flush();
	sw.Close();

こんな感じで使うことが多いと思います
しかしこんなところにも罠が

Write:ファイルに文字を書き込む
WriteLine:ファイルに文字を書き込んで最後に改行を入れる


この改行のせいで今日は半日潰れた...
なんとなくでメソッド使ってると痛い目にあうっていう久々の教訓でした

【Unity】リアルタイム通信の戦車ゲームを作り始めた【Node.js】

今こんな感じ
もうちょっと開発環境等整理してまとまったら記事にしようと思う
f:id:chroske:20151020165949g:plain

思った以上にお手軽に出来て楽しくなってきた(つまらんとこではいっぱい躓いたけど)

【Unity】AssetBundleの自動ビルド化をやってみた【Jenkins】

AssetBundleの自動ビルドシステムを作ることになり
実際にやってみてのポイントなどを書いていく
ほとんど自分用メモです


■UnityはBatchModeオプションで起動する

/Applications/Unity/Unity.app/Contents/MacOS/Unity -batchmode -quit -projectPath $プロジェクトのパス/ -executeMethod ExportAssetBundles.AutoExportResource -logFile ./BuildTools/Log/build.log; cat ./BuildTools/Log/build.log

BatchModeオプションをつけることでUnityのエディタ自体を立ち上げないままメソッドを直接呼び出すことが出来る。
ビルドログの出力先もここで指定する。
ちなみにJenkinsからシェルを実行させるには設定の『ビルド手順の追加』から『シェルの実行』を選択してやると記述欄が出てくる

f:id:chroske:20150909214736p:plain



■テクスチャ素材のインポート設定を自動化する
テクスチャ素材によって設定が違ったりしたので、置かれるディレクトリ名で判別して自動設定されるようにした。

public class CustomImportSettings : AssetPostprocessor 
	{
		void OnPreprocessTexture() {
			Debug.Log("Importing texture to: " + assetPath);
			string[] pathSplitArrayData = assetPath.Split('/');

			List<Dictionary<string,string>> buildParamList = LoadCsv(AUTOBUILD_SETTING_CSV_PATH); //csv読み込みメソッド

			string textureType = "";
			string textureFormat = "";

			foreach(Dictionary<string,string> buildParam in buildParamList){
				if(Array.IndexOf(pathSplitArrayData, buildParam["resource_name"]) != -1) {
					textureType = buildParam ["texture_type"];
					textureFormat = buildParam ["texture_format"];
					break;
				}
			}

			TextureImporter textureImporter = assetImporter as TextureImporter;

			textureImporter.textureType = ManageTextureImportType.getTextureImporterType(int.Parse(textureType)); //int型のidからtextureTypeを取得するメソッド
			textureImporter.textureFormat = ManageTextureImportFormat.getTextureImporterFormat(int.Parse(textureFormat)); //int型のidからtextureFormatを取得するメソッド
		}
}

AssetPostprocessorクラスのOnPreprocessTextureメソッドを実装すると、画像がインポートされる瞬間に呼び出されて任意の処理を行うことが出来る。
assetPathで画像が置かれたパスを取得できるので、そこから設定を判別し、textureTypeとtextureFormatを設定する




■Jenkinsのワークスペースは2つ用意する
これはスマホゲームのようなマルチプラットフォーム対応のプロジェクトに限ったもの
御存知の通りUnityのSwitchPlatformは死ぬほど時間がかかる
そこで一つのJenkinsジョブに同一リポジトリワークスペースを2つ用意し、交互にビルドを行うことでSwitchPlatformをやらずにマルチプラットフォームのビルドが行える
実際これをやらないと数個のAssetBundleだとしてもAssetが多いプロジェクトならば15分とかはザラだ
もちろんほとんどSwitchPlatformにあてられた時間である

f:id:chroske:20150909214216p:plain
やり方はJenkinsの設定『ソースコード管理』から任意のバージョン管理を選択し、『ロケーションの追加』したあとにそれぞれ『ローカルモジュールディレクトリ』を設定してやるだけでいい




■自動化してみて
うちのプロジェクトでは何年も手動でAssetBundleを作っていたらしい
なんだよそれ、AssetBundle職人かよ
正直自分に引き継がれた時はコイツァは人間様のする仕事じゃねえ!!以外の感想は出てこなかった
そして自動化することを決めた、自分が幸せになり人間としての尊厳を取り戻すために
幸い自動化システムを作っているときはとても楽しかった
皆もガンガン自動化して人間は人間の尊厳を保った人間らしい仕事をしていこう

【Unity】Assetsディレクトリ以下にないファイルを一覧取得する【エディタ拡張】

Resources.Load()とかAssetDatabase.FindAssets()とか
Edita拡張からAssetsディレクトリ以下のデータにアクセスするメソッドはよく見つかるのですが
それより上の階層は無理なのかなーと思ったら普通に出来ました
Unityのメソッドとして探していたのが悪かっただけで普通にC#メソッドを使えばよかったのだ!!

string[] result = System.IO.Directory.GetFiles(path,"*検索ファイル名とか*");



すごい普通だった!
でもUnityからC#はじめた人なんかはなかなか盲点なのかなーと
C#のFileクラスと組み合わせても色々やれそうですな
File クラス (System.IO)