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

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

【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公式表示すれば終わりじゃね?」とか思ったけど深く考えないことにしました。
でも逆に考えるとブラウザで出来ることは何でもいけそうってことです。夢が広がりんぐですね!