【unity】プログラマーがデザイナーとコンポーザーと協力してゲームを作った話【 #Economicity 】

f:id:wkpn:20170619094010p:plain
新作ゲーム「街づくりパズル エコノミシティ – ECONOMICITY -」をリリースしました!

Economicityのせいで電車を乗り過ごしたというご意見もあり、業が深いゲームを作ってしまったなと反省しております。

ダウンロードリンクはこの記事の最下部にまとめてありますので 未プレイの方はぜひダウンロードお願いします!

このアプリですが、開発はプログラマー(私 @waken )とデザイナー( Kotoriさん )とコンポーザー( Shiibaさん )の3人体制で作りました。

お世話になっている京ゆにのnaichiさんの記事を真似て自分も開発を振り返ってみます。
blog.naichilab.com

開発期間

4月頭〜5月中旬(最初のリリースまで)

2ヶ月弱。ただし本開発の前に試作をいくつか作って遊んでもらっています。

3月末に4つ作った試作を何人かに遊んでもらい、その中で一番好評だったものを製品化することにしました。そして4月頭にイラストレーターのKotoriさんに相談しました。

その後4月中旬にShiibaさんに開発中のものを見てもらい、作曲をお願いしました。

BitSummit前にリリースするのが目標だったので、一応ほぼ計画通りにリリースできました。ただ、最初は色々とバグが多かったです。

リリースしてから今に至るまで、バグの修正や演出の追加などで、ちょこちょこアップデートを繰り返しています。

役割分担

  • ゲームデザイン わけん
  • アート全般 Kotori
  • BGM Shiiba
  • UI アドバイス Kotori
  • 効果音(フリー素材を探す) わけん Shiiba
  • プログラム わけん
  • ストア準備 わけん Kotori(ストア用素材)
  • 開発環境準備 わけん
  • テキスト わけん
  • 広報 わけん
  • 英語翻訳 Laura Cronell, American Time Inc. (部活の後輩の奥さん)
  • テストプレイ ギークハウス京都東福寺住民の方々や遊びに来てくれた方々など多数


アートをKotoriさんに、BGMをShiibaさんにお願いして、残りはだいたい自分って感じです。2人にはUIとか効果音など色んな所でアドバイスを頂きました。

翻訳は最初自分で頑張ったんですが、部活の後輩で仙台で英会話教室をやっている方が、ゲームを遊んでくれた後に添削を申し出てくれました。
http://american-time.jp/com/

また、ギークハウス京都東福寺には色んな方が遊びに来てくれるので、その都度、遊んでもらって感想を頂きました。

使ったツールとか

Bitbucket

bitbucket.org

基本的に自分のバックアップ用で、他の人は使わず。

基本コマンドラインで使用。コミット数は約250。1人でやっているのでコミット単位はかなり雑。

branchはdevelopブランチを切って、そこで基本開発。わりと特殊な機能を実装する時だけfeature○○というブランチをきって、そっちで作業してdevelopにmerge。developはリリースする時にmasterにmerge。

Unity Collaborate

Unity製バージョン管理ツール。

Artを上げてもらったり、ArtやBgmを実装した後の確認をしてもらうのに利用。

まだβ版なので、たまにUnity再起動しないと変な感じになることがあるけど、超簡単、超便利。

Unity Cloud Build

自動ビルドツール。

超簡単にCollaborateで上げたやつをビルドしてくれる。iOSは証明書周りがめんどくさくてちゃんと使ってなかった。

Android版のビルドは簡単に使えたけど、実機確認したい時はクラウドビルド待ってられなくて結局ローカルビルドしていた...

というわけで、あまり有効に機能しなかった。

運用方法は今後の課題。

Slack

コミュニケーションツール。チャンネルは6つ。

economicityチャンネル

economicityに関わること全般

economicity-artチャンネル

economicityのartに関わること全般

economicity-soundチャンネル

economicityのsoundに関わること全般

economicity-storyチャンネル

economicityのstoryに関わること全般。ストーリーってほどしっかりしたものがあるわけでもないのですが、なんとなく背景のネタ出し的な感じに利用。

build-economicityチャンネル

Unity Cloud Buildの開始と結果を飛ばすチャンネル。ビルドエラーになることは皆無だったんでビルド結果情報が役に立ったということは無かったのですが、ここにメッセージが飛ぶと、あっリソース上がったなってのが分かったのが良かったです。

ego-searchチャンネル

naichiさんのパクリ。IFTTTを使ってTwitter上で「Economicity」とか「エコノミシティ」で検索した結果を飛ばしてきてる。

IFTTT

なにかをトリガーに何かをするサービス。上記のego-searchチャンネルに使用。

Google Drive, Dropbox

基本的にUnity CollaborateやSlackで事足りることが多いのだけど、たまにデータの受け渡しに使うことがありました。

タスク管理ツール

使用せず。自分がローカルでテキストエディタ使ってタスクを羅列するだけ。

音楽と画像に関してはSlackでいついつまでにこのリソース欲しいです。って言ってました。

楽ちん。

その他

タスク管理術ってほどでもないですが、マイルストーンを決めて、そっから1週間ごとの目標を決めて、そっから1日ごとの目標を決めて...っていうふうにしてました。

あとは、それぞれの目標を前倒しで達成してゆっくりするぞーって気持ちで作ってました。

それにより、ゆっくりはできなかったけど一応当初の目標であるBitSummit前にリリースできました。

ゲームを遊ぶ日

途中のマイルストーンで一緒に、製作途中のゲームをプレイする日というのを設けました。

細かい作業に没頭していると、ゲーム自体を遊ばなくなりがちなのでオススメです。

実際遊んでみるといろいろな問題が顕になって焦ります。

アートの人にも「あっこういう仕様ならこういうデザインのほうが良いかも...」っと色々気づいてもらえてgoodでした。

反省点

反省点は、ここに挙げたもの以外にも多々あるのですが、目立ったところをいくつか。参考になれば幸いです。

  • イベントギリギリに仕上げると、広報素材が間に合わないので1週間前ぐらいに仕上がるぐらいがちょうど良い。
  • バランス調整が難しそう...は鬼門。ちゃんと最後までバランス調整してみてからプロジェクトを進めるかどうか判断したほうが良い。
  • もっと仕様をFixしてからArtと音楽を発注すべきだった。会社では難しかったけど個人開発でこの規模のゲームなら、ほぼほぼ仕上げてから絵と音楽を発注ってできる気がする。(今回は〆切をBitSummitに合わせてたから難しかったけど)
  • 試遊会にて、自分が面白いでしょ!って思っているところとユーザーが楽しいって感じているところが違うことがある。今回はズレてた気がする。
  • 自分の作るゲームに関しては、試遊はゲームに慣れていなく、空気を読まずに意見を言う人にやってもらったほうが良い。つまり子供が良い。

uGUIで2色補完する(黒を○色、白を○色にする)

f:id:wkpn:20170609133118p:plain

こういう3種類の絵を出したい時に、色違いのテクスチャーを用意するのってめんどくさいですよね?

見た目の色違いだけなのに、テクスチャに色を乗算するだけではどうにもならないし... けどなんとか、左端の白黒のテクスチャ1枚で済ませたい。

黒色の部分を○○色に、白色の部分を○○色に...っと指定できたら良いのになぁ... っとは全人類が思うところです。

ということで自作。

って言っても大したことはしていません。

シェーダーの用意

まず、Shaderを用意します。Unityのビルトインシェーダーを元に作っていくので、まずは以下からダウンロード。

Unity - Update


f:id:wkpn:20170609133629p:plain


シェーダーをダウンロードしたら、中にあるUI-Default.shader(builtin_shaders-***\DefaultResourcesExtra\UI\UI-Default.shader)をUnityのプロジェクトにインポートします。

シェーダーのカスタマイズ

1. UI-DefaultをリネームしてUI-Complement2っていう名前にします。
2. Shader内の頭の部分を以下の用に修正します。
Shader "UI/Default"

Shader "UI/Complement2"
3. プロパティ部分(Properties{}の中)に以下を追加します。
_ComplementColor0 ("Complement Color 0", Color) = (0,0,0,0)
_ComplementColor1 ("Complement Color 1", Color) = (1,1,1,1)
4. CGPROGRAM〜ENDCGの間のどこかテキトーな場所に以下を追加します。
fixed4 _ComplementColor0;
fixed4 _ComplementColor1;
5. カラー計算部分を以下のように修正します。
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

half4 tex_color = tex2D(_MainTex, IN.texcoord);
half4 color;
color.rgb = lerp( _ComplementColor0, _ComplementColor1, tex_color.r ) * IN.color.rgb;
color.a = tex_color.a;

_TextureSampleAddは、Alpha8モードのときに必要らしいけど、Alpha8モードは想定していないので、除外します。

マテリアルを作って設定する

Shaderを右クリックしてCreate -> Materialとすると、そのシェーダーを使ったマテリアルができます。

インスタンスごとに色を変える

上記のマテリアルをImageに割り当てるととりあえず動くのですが、全インスタンスでマテリアルが共有されちゃうので同じ色になっちゃいます。

なので、こんなスクリプトを書いてコンポーネントとして追加します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class ComplementTestObj : MonoBehaviour {
    [SerializeField]
    Image image;

    [SerializeField]
    Material matPrehab;

    [SerializeField]
    Color color0 = Color.black;

    [SerializeField]
    Color color1 = Color.white;

	void Start () {
        var mat = Instantiate( matPrehab );
	mat.SetColor("_ComplementColor0", color0);
	mat.SetColor("_ComplementColor1", color1);
        image.material = mat;
    }
}

あとは、以下のようにImage、Material、Color0、Color1を指定すればOK。

f:id:wkpn:20170609135352p:plain

ちょいとめんどくさいけど、これでいちいち色違いのテクスチャを作らなくて済みます。



play.google.com

Unityで作ったアプリのAndroid版で set_interactable can only be called in main thread などというエラーが出て困った話

Unityで作ったアプリのAndroid版で set_interactable can only be called in main thread などというエラーが出て困っておりました。

メインスレッド以外でset_interactable呼ぶなや!ってことみたいです。

どうもAdMobのinterstitialがCloseした時のCallbackで色々やってたのが良くなかったみたいです。
具体的には、以下のようなコードのCloseCallbackの中身が良くなかったです。

interstitial = new InterstitialAd (ad_id);
request = new AdRequest.Builder ().Build ();
interstitial.LoadAd(request);
interstitial.OnAdClosed += CloseCallback;

CloseCallbackではフラグを立てるだけにして、Updateの中でflagを監視し、flagが立ったらやりたいことをする。っというようにすることで解決できました。

プラグインの用意するcallbackはメインスレッドで呼ばれない可能性があるので、中身に詳しくなければ複雑な処理はしないほうが良いなと思いました。

AOTコンパイルってなんぞ?

C#などの.Net言語はCILという中間言語コンパイルして、それをVM上で動かすようにできています。

しかし、実際にVM上で走らせなければどんなコードが動くか分からないってのはセキュリティ上よくないので、iOSではそれを許していません。

UnityはVMからJITコンパイルする部分を排除し、事前にネイティブコードを吐くようにすることでこの問題を回避しています。

事前にコンパイルしていることから、この部分のコンパイルをAOT(Ahead of Time)コンパイルと言います。

しかし、この事前コンパイル、そのそも.Net言語が想定していたものではないため(たぶん)、いろいろと制限があるそうです。

わかり易い例として、C#にはReflection.EmitというCILコードを直接生成する関数があるのですが、これはAOTでは使えません。

そのような制限があることから、UniRxを使用する際にbackendとしてMonoを選択するとエラーが発生するようです。

UniRxは正式にAOTのサポートを切っているそうなので、おとなしくIL2CPPを使うしかなさそうです。

デバッグ向けにコンパイルする時はbackendとしてMonoを選択したほうが圧倒的にビルドが早く多用していたので、ちょっとどうするか悩んでます。

参照:iPhoneでC#アプリが審査に通るワケ − @IT
参照:neue cc - UniRx 5.0 - 完全書き直しによるパフォーマンス向上とヒューマンリーダブルなスタックトレース生成

UniRxを使ってて気になったこと

AddToしているのものをIDisposable.Dispose()して、そのあとオブジェクトを廃棄したら二重解放になる?

結論、ならないので気にしなくて大丈夫な模様。

下記のコードでは、Disposeした後、AddToしたgameObjectを破棄しているが、特にエラーは発生しなかった。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;

public class Test : MonoBehaviour {
    int counter = 0;
    System.IDisposable disposer;

	void Start () {
        disposer = gameObject.ObserveEveryValueChanged( _ => counter ).Subscribe(
                x => Debug.Log( x )
                ).AddTo( gameObject );
	}
	
	void Update () {
        counter++;
        if( counter == 1 ) { disposer.Dispose(); }
        if( counter == 2 ) { Destroy( gameObject ); } //Disposeした後に、gameObjectを破棄したら二重解放でエラーになる?
	}
}

もしかしてiOS+Monoだと動かない?

iOS用 + Scripting BackendをMonoにするとエラーが発生するようです。

ExecutionEngineException: Attempting to JIT compile method 'UniRx.UnityEqualityComparer/RuntimeTypeHandlerCache`1<int>:.cctor ()' while running with --aot-only.

Rethrow as TypeInitializationException: An exception was thrown by the type initializer for RuntimeTypeHandlerCache`1
  at UniRx.ObserveExtensions.ObserveEveryValueChanged[GameObject,Int32] (UnityEngine.GameObject source, System.Func`2 propertySelector, FrameCountType frameCountType, Boolean fastDestroyCheck) [0x00000] in <filename unknown>:0 
  at Test.Start () [0x00000] in <filename unknown>:0 

デバッグ用途ではビルド時間短縮のためにMonoを使うことが多いので、コレは痛い。

どうしたものか。

vimで日本語をsortしたらうまく並び替えられなかった(解決済み)

えんこ
あやつ
うんちん
おすぷれい
いんしつ

こういう文字列を、あいうえお順に並び替えたかったのですが、いつものようにVisualモードで範囲選択した後

:'<,'>!sort

っとすると

あやつ
えんこ
いんしつ
うんちん
おすぷれい

というように、意図しない順番になっちゃいました。

:'<,'>!LC_ALL=C sort

とすることで

あやつ
いんしつ
うんちん
えんこ
おすぷれい

という意図した順番になりました。っとやり方はわかったものの、LC_ALLについてはちゃんと理解していません。すいません。

UniRxを1日勉強してみた感想

結論

まず最初に結論を。ゲーム開発がメインの私的にはObserveEveryValueChanged, Timer, AddTo(安全のため必須)あたりを使えれば十分かなぁという感想。実は、気づいていないすごい便利な使い方とかあるかもしれないので、オススメの使い方がありましたら教えて頂けると嬉しいです。

あと、ここに載せたの以外にも色々とあるらしいのですが、全然キャッチアップできていません(><)

説明

UniRxとはneueccさんが開発されている Reactive Extensions をUnity用に実装した+アルファなライブラリです。

Reactive Extensionsとは、非同期な処理を関数型プログラミングっぽい感じでかけるライブラリです。

例えば、3秒後に○○したいとか、0.5秒間隔で○○したいとか、特定の変数の値が変わった時に○○したいっていうのが簡潔に掛けます。

以下、細かい説明。

Timer

ちょっと遅延させて実行してくれる。今までは遅延させたい時はInvokeを使っていたけど、いちいちそのために関数に切り出してたので、これは便利。

var timer = Observable.Timer( System.TimeSpan.FromSeconds( 3 ) );
timer.Subscribe( _ => Debug.Log( "Hello!" ) );//3秒後にプリント

Delay, DelayFrame

ちょっと遅延させて実行してくれる、その2。
こちらは、修飾的(?)なものなので、Observableなオブジェクトに大して後ろからくっつける。

var delay = Observable.Return(Unit.Default).Delay( System.TimeSpan.FromSeconds( 3 ) );
delay.Subscribe( _ => Debug.Log( "Delayed log" ) );//3秒後にプリント
var delay = Observable.Return(Unit.Default).DelayFrame( 120 );
delay.Subscribe( _ => Debug.Log( "Delayed log" ) );//120Frame後にプリント

Interval

定期的に実行してくれる。ゲームで制限時間とか表示する時に使えそう。

var interval = Observable.Interval( System.TimeSpan.FromSeconds( 1 ) );
interval.Subscribe( x => Debug.Log( ( x + 1 ).ToString() + "秒" ) );//1秒,2秒,3秒...

EveryUpdate

毎フレーム実行してくれる。

var every = Observable.EveryUpdate();
every.Subscribe( _ => Debug.Log( "Hello!" ) );

たいていUpdate関数内に掛けば済むので使い所があまり思い浮かばない。

遅延させた後に毎フレーム実行したいときとか?

var every = Observable.EveryUpdate();
var delayed_every = every.Delay( System.TimeSpan.FromSeconds( 3 ) );
delayed_every.Subscribe( _ => Debug.Log( "Hello!" ) );

ObserveEveryValueChanged

値が変わった時に処理が走る。使いやすい。他のと違ってgameObjectの拡張メソッドになっているので注意が必要。

public int hoge;

void Start () {
    var change = gameObject.ObserveEveryValueChanged( _ => hoge );
    change.Subscribe( x => Debug.Log(x) );//hogeの値が変わると、呼ばれる
}

ReactiveProperty

ObserveEveryValueChangedのラッパーみたいなの。個人的にはObserveEveryValueChangedの方が使いやすくない?って思う。

例:スコアが変わったら、UI上の表記を変えてる

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UniRx;

public class Test : MonoBehaviour {
    [SerializeField]
    Text scoreText;

    ReactiveProperty<int> score = new ReactiveProperty<int>();

	// Use this for initialization
	void Start () {
        score.Subscribe( x => scoreText.text = x.ToString() ).AddTo( this );
	}
	
	// Update is called once per frame
	void Update () {
        score.Value += 1;
	}
}

OnClickAsObservable

ボタンをクリックしたときの処理が書ける。

var on_click = button.OnClickAsObservable();
on_click.Subscribe( _ => Debug.Log("clicked") );//クリックするとclickedがプリントされる

しかし、

button.onClick.AddListener( () => Debug.Log("clicked") );

で同じことができるので、どういうときに有効なのかが今のところ分からない。

Range

引数で指定した値の範囲分、処理を回してくれます。連番の配列を作る時に良いかも。例えば100個あるステージ名の配列を作るとか。

const int cMaxStage = 100;
string[] stage_names = new string[cMaxStage];
var range = Observable.Range(0,cMaxStage);
range.Subscribe( x => stage_names[x] = "stage" + x );

for文を使うよりもかっこいい... っと言えなくもない。
これだけだと、イマイチだけど、範囲指定の処理をいろんな場所で行うなら、一回Rangeを作っておいて、色んな場所で回すってのがスマートにできそう。

Where

処理を行うものを条件でフィルタリングする。if文掛けば済むことが多いけど、こっちの場合フィルターしたものを変数として持ち運べるのが便利。

var even_range = range.Where( x => x % 2 == 0 );
even_range.Subscribe( x => stage_names[x] = "stage" + x );
even_range.Subscribe( x => Debug.Log( stage_names[x] ) );//stage0, stage2, stage4 と出力される

Return

値を返すだけのオブザーバブルなオブジェクト。今のところ使用例が思いつかないけど、複雑なことをしたいときの道具として使うんじゃなかろうか。

var ret = Observable.Return( 10 );
ret.Subscribe( x => Debug.Log( x ) );//10

兎にも角にもなんでも良いからオブザーバブルなオブジェクトを作りたいときってときは、引数にUnit.Defaultを取るのが通例の模様。

var ret = Observable.Return( Unit.Default );
ret.Subscribe( x => Debug.Log( x ) );//()

購読停止

Subcribeで走っている処理を止めたい時は、IDisposableのDisposeを呼ぶ。
(using System;してないと、IDisposableなんてありませんよーっていうエラーになるので注意。)

var every = Observable.EveryUpdate();
IDisposable disposer = every.Subscribe( _ => Debug.Log( "Hello!" ) );
disposer.Dispose();

AddTo

AddToの説明の前にUniRxのSubscribeの仕組みについて。

Subscribeを呼ぶとDon't destroyなオブジェクトができるので、UniRxのSubscribeは、おそらくそこにコルーチンを登録しているのだと思う。

そこで、問題なのがコルーチンを登録したままGameObject等を破棄した場合。
別な言い方をすると購読が停止されないままに購読者が死んじゃった場合。
この場合、コルーチンだけ残って処理を続けようとするので、処理的に無駄だったり、死んでしまった購読者にアクセスしようとするからNullアクセス例外が発生したりする。

そういうのを回避するためにあるのが、AddTo。

Subscribe( ... ).AddTo( this )

ってしておけば、thisが死んだ時に登録しておいた処理も破棄してくれる。基本的にSubscribeとAddToはペアで呼ぶものと覚えておいたほうが良さそう。

this以外にも渡せるので、寿命をともにしたいものをAddToすると便利そう。