eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

JetBrains Riderで適用されるCodeStyle

環境ごとのデフォルト

  • Ubuntu(Linux)とWindowsでデフォルトフォーマッタが異なる
  • 大きいところではブレス class Hoge { のブレスが K&R styleBSD style で異なる
  • 基本的にはC#の基本的なコードスタイルを使いたい
  • 改行コードはLinux, Windowsで異なる
    • LF を使いたい

C# の推奨するスタイル

AndroidStudio 3.2-Canary17の所感

Moduleの依存グラフが表示されるようになっていた

  • いつからだろうか
  • Build Variants > i アイコンから見れる
  • 依存Moduleの依存は見れない
  • app > sdk > ktx のような依存グラフにはなってない
  • そのうち改善される?

Android API-28公開

  • これでライブラリ公開やGoogle Play Consoleへのアップロードが可能になった
  • APIがFIXされたので、互換性チェックも行えるだろう
  • Kotlinバージョン 1.2.41 にアップデート

ConstraintLayoutが破壊的変更

  • パッケージ名が変更された
    • NG androidx.constraintlayout.widget.ConstraintLayout
    • OK androidx.constraintlayout.ConstraintLayout

NavigationEditor対応

  • 相変わらずビルドに失敗する
  • Googleにクラッシュレポートをアップして終了

Unity学習の整理 Animatorの基本

UnityChanをアニメーションさせる

Animator

  • 複数Layerが配置できる
    • Layerは Baseが身体本体の歩きモーション 表情モーション のように組み合わせられる
    • UnityChan付属のAnimator UnityChanAnimationCheck が参考になる
  • 基本的にステートマシンとして動作する
    • おそらく同じ骨構成であれば別アバターにも適用できる構造のはず

アニメーションループ

  • アニメーションが終わったタイミングでイベントを発火というユースケースがある
    • イベント処理で使っていた
  • 現在のAnimationの位置が正規化されたTime値でわかる
    • 開始 => 終了で1.0
    • 周回を進められるので、小数点以下が正規化された現在のループ
    • 整数部がループ回数として得られる
  • アニメーションがループ設定になっていない場合でも時計は進み続ける
    • UnityChanの JUMP00 で確認できる
    • 時計は進むが、アニメーションは止まる
      • 適切な次のアクションに切り替えてあげなければならない
  • ポーズだけのアニメーションの場合、凄まじい速度で時計が進む
    • UnityChanの POSE で確認できる

UniRxでループタイミングを得る方法

  • normalizedTime をint変換することで、整数部の切り替わり = ループのタイミングとして検出できる。
  • 0は最初の開始時となる。
_animator = GetComponentInChildren<Animator>();
_animator.UpdateAsObservable()
    .Select(_ => (int) _animator.GetCurrentAnimatorStateInfo(0).normalizedTime)
    .DistinctUntilChanged()
    .Subscribe(loop => Debug.Log($"Animation Loop!! [{loop}]"));

RangeBarで始めるCustomViewとDataBindingとMVVM

どういうことがしたいのか

こんなUIをMVVMで簡潔に実現したい。

f:id:eaglesakura:20180608174849g:plain

  • ユーザーに心拍数(下限、上限)等の一定範囲データを設定させる
  • その際、実装コストを減らしたい
    • なるべく現代的に仕上げたい
  • UIは RangeBar を利用する
    • RangeBarを操作すると、対応する値が表示される
    • RangeBarでは最大値1000のようなインデックス値のみ設定でき、min-maxのような範囲は指定できない
  • ViewとModel分離の利点として、UIに設定可能な値と実際の値を分離することができる
    • Modelを単純値のシンプルなまま、UIをカスタムすることができる
    • 実際に設定されるViewの変数と保存される値の取次をViewModelが担当する

設計方針

  • Android Architecture ComponentsのMVVMを使用
  • Viewはほぼレイアウトファイルのみ
  • 入力値は範囲を絞る
    • 人間にとって心拍0はありえないので、入力値を限定させる
  • データは整数値で保存する

RangeBarをDataBinding対応にする

DataBindingを行うための拡張

  • @BindingAdapter でViewModel -> Viewの一方通行
  • @InverseBindingAdapter で View <-> ViewModel の双方向
    • ここではViewModelにRangeBarのインデックス値を設定させている
@InverseBindingAdapter(attribute = "range")
fun RangeBar.getRange(): Range<Int> {
    return Range.create(leftIndex, rightIndex)
}

@BindingAdapter("range")
fun RangeBar.setRange(range: Range<Int>?) {
    @Suppress("NAME_SHADOWING")
    val range = range ?: return

    if (range.upper <= range.lower      // validation error.
            || range.lower == leftIndex && range.upper == rightIndex    // not modified.
    ) {
        return
    }
    setThumbIndices(range.lower, range.upper)
}

@BindingAdapter("tickCount")
fun RangeBar.bindTickCount(value: Int) {
    this.setTickCount(value)
}

ViewModel

  • val heartrateRange = MutableLiveData<Range<Int>>() にViewが持つIndex値を保存する
  • commitHeartrateRange() で保存されるときにIndex値を変更してあげる
  • このViewModelでは、実際に保存するときとViewに渡される値に HEARTRATE_RANGE_OFFSET ぶんの差が発生するので、ViewModelがソレを吸収する
class FitnessViewModel : AppViewModel() {
    private val fitnessRepository: FitnessRepository by lazy { FitnessRepository.getInstance() }

    private val serivce: FitnessService by lazy { FitnessService() }

    val heartrateRange = MutableLiveData<Range<Int>>()

    override fun attach(owner: ViewModelOwner) {
        super.attach(owner)

        heartrateRange.value = serivce.heartrateRange.let {
            Range.create(it.min - HEARTRATE_RANGE_OFFSET, it.max - HEARTRATE_RANGE_OFFSET)
        }
    }

    fun commitHeartrateRange() {
        val current = heartrateRange.value ?: return
        launch {
            // HeartrateRangeはRaw値のため、ここで値を変換する
            serivce.setHeartrateRange(HeartrateRange(
                    current.lower + HEARTRATE_RANGE_OFFSET,
                    current.upper + HEARTRATE_RANGE_OFFSET
            ))
        }
    }

    companion object {
        @JvmStatic
        val HEARTRATE_RANGE_OFFSET = 50

        @JvmStatic
        val HEARTRATE_RANGE_MAX = 200
    }
}

XML設定

  • app:tickCount 固定値だが、ViewModelの定数を計算で表示している
  • app:range は双方向なので、 @={ViewModelの変数名} を使用する
<com.edmodo.rangebar.RangeBar2
    app:tickCount="@{fitness.HEARTRATE_RANGE_MAX - fitness.HEARTRATE_RANGE_OFFSET}"
    app:range="@={fitness.heartrateRange}"
    android:id="@+id/HeartrateRange"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

RangeBarを継承せずにカスタムイベントを追加する

  • RangeBarの仕様上、値が変更されるたびにコールバックが飛んでくる
    • RangeBarの値を50から100に移動させると、50,51,52.....100のようにすべての値でコールバックが飛んでくる
  • そのたびにストレージに書き込むと、無駄な書き込みが発生する
  • ユーザーが値を決定した(指を離した)際のListenerは定義されていない
  • カスタムで作り、DataBindingにも反映させる
  • app:onRangeComplete というAttributeを作成した
  • OnCompleteListener には引数付きだが、DataBinding側では引数なしのラムダ式を書いても問題なくコード生成してくれる
  • これにより、 ViewModelの commitHeartrateRange() を確定タイミングでのみ行わせることができる
interface OnCompleteListener {
    fun onIndexChangeCompleted(view: RangeBar, leftThumbIndex: Int, rightThumbIndex: Int)
}

@BindingAdapter("onRangeComplete")
fun RangeBar.setOnCompleteListener(listener: OnCompleteListener?) {
    if (listener == null) {
        setOnTouchListener(null)
    } else {
        setOnTouchListener { _, event ->
            if ((event.action and MotionEvent.ACTION_UP) != 0) {
                listener.onIndexChangeCompleted(this, leftIndex, rightIndex)
            }
            return@setOnTouchListener false
        }
    }
}
<com.edmodo.rangebar.RangeBar2
        app:onRangeComplete="@{() -> fitness.commitHeartrateRange()}"
        />

RangeBarの変更を別なViewへ反映する

  • RangeBarが変更されたら、実際の設定値をユーザーに表示させなければならない
  • これもレイアウトXMLに記述できる
    • 変更通知はLiveDataが受け持っているため、複雑な設定は不要
  • app:valueText で、テキストを設定している
    • このとき、string.xmlのフォーマッタを適用している
      • @string/HogeFuga(値)で参照できる
      • safeUnbox() で囲わないとLintに怒られる場合がある
    • 計算もできるので、表示するときには fitness.HEARTRATE_RANGE_OFFSET を加算してもとの値にしている
  • 計算がダルければ、表示テキスト用のLiveDataをViewModel側に用意しても良いかもしれない
<org.andriders.ace.component.activity.widget.AppKeyValueView
        app:keyText="Heartrate"
        app:valueText="@{@string/BodyHeartrateFormat(safeUnbox(fitness.heartrateRange.lower) + fitness.HEARTRATE_RANGE_OFFSET, safeUnbox(fitness.heartrateRange.upper) + fitness.HEARTRATE_RANGE_OFFSET)}"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <string name="BodyHeartrateFormat">%1$d bpm - %2$d bpm</string>

発生した問題

LiveDataの変更をしても値が反映されない場合

  • 生成されるLayoutBindingクラスにLifecycleOwnerを渡していないとそうなる
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = ProfileBinding.inflate(inflater, container, false).also { binding ->
            binding.setLifecycleOwner(activity)
            binding.fitness = fitnessViewModel
        }
        return binding.root
    }

Unity学習の整理 UnityEditorでC#のUnitTest

C#でコードを書くにあたって、UnitTestができたほうが安心できるので調べた。

UnitTestの方法

  • UnityEditorに組み込まれたUnitTest Runnerが使用できる
  • NUnit相当の機能が使える

テストプロジェクトに癖がある

  • Create > Testing でTestsディレクトリを作成可能
  • 作成されたテストプロジェクトは、UnityProject(ゲーム)側のdllが参照されない
    • ゲームコードのUnitTestができないのでは?
  • 公式動画を見ると、アセット内に直接UnitTestコードを追加していた
    • 成果物バイナリにUnitTestが含まれてしまうのでは?
      • 最適化で外される?
      • 試しに new HogeTest() をゲームコードから呼び出したが、リリースビルドでも動作した
      • CIなりなんなりでどうにもできるとはいえ、この構成は正しいのか?と不安になる
    • C#の仕様とも比べなければならない

非同期のテストも記述できる

  • IEnumerator を返却することで非同期(Unity Coroutine)のテストが行える
  • その場合のAttributeは [UnityTest]

Runtime(Editor外)でもテストが実行できる

  • Windowsで確認
  • Monoに限定されるだろうが、実ビルドしてEditor外でUnitTestが行える
    • ある程度安心はできそう

Rider内でテストが失敗する

  • 原因が不明
  • Unity Editorでは成功しているが、Riderは実行直前に例外で落ちる

Unity学習の整理 連携する統合開発環境

開発環境は必要に応じて最適なものを使いたい。 Unityでアプリを開発するにあたって、選定を行った。

C#プロジェクトエディタの選定

Visual Studioの利点・欠点

  • Unityプロジェクト側にプラグインを追加せずに使える
    • Riderは専用プラグインが自動的に組み込まれる
    • 「どうしてもファイルを増やしたくない」ならこっち
  • 日本語環境がある
  • Visual Studioである
    • エディタ機能は超強力
    • Xamarin等、他の環境も使いやすい
      • 話がそれるけど、iOSのLayout Editor等の環境はVisual Studioのほうが整備されている
      • こっちに慣れておけば、同じC#のXamarinを使う際の抵抗が小さくなるかも
  • ReShaperがないとどうにもならない
    • 結局Jetbrainsのお世話になるのである
    • 有料である
  • 会社規模が大きい場合、有料となる
    • ライセンス料は結構高い

Riderの利点

  • Android StudioやGolandと同様の使いやすさ
    • ベースが同じなので、概ね同じように動作する
  • 微妙にショートカットや機能が違う
  • サンプルコードだとBreakPointにアタッチできない
    • Visual Studioならできる
    • Pluginとの兼ね合いかもしれない
    • 通常のプロジェクトなら問題ない
  • Windows, Ubuntuでは正常動作

Unityの初期設定

  • Edit > Preferences
    • External Tools > External Script Editor でRiderを選択
      • Android SDKとNDKも選択
      • SDKはUnity用にStableな環境を持っておいたほうが良いかも
      • NDKは13b限定だった
    • ndk-bundleではなく、バージョン固定で持っておいたほうが良い
  • Unity2018から正式に .Net 4.5 + C#6.0 が使用できるようになった
    • なのでそれを使う
    • Edit > Project Settings > Player から変更
  • 必要な参照は NuGet for UnityAsset Store から取得する

Reactive Extension for Unity -> UniRx

  • UnityでもRxを使いたいので導入する
  • NuGetで取得すると、古いC#バージョン向けのコードが降ってくる
    • MainThreadDispatcher等が無いので、注意する

MainThreadDispatcherの実装

  • UnityMainThreadへのアタッチ方法を確認するため、コードを読む
    • GlobalなDispatcherを登録していた
    • GameObjectが取得できるのがMainThread限定なので、その特性を利用している
    • C#世界ではなく、UnityEngine側も利用して実装されている
  • MainThreadDispatcherが登録されており、それがMainThreadのPostを担当している

リポジトリ管理

  • gitを利用する
  • Windowsを使っていると改行コードがズレるので、LFで統一
    • .gitattributeで統一しておくといい
    • Visual StudioはCRLF+BOMじゃないと正常に動作しなかった気がするけど、直ったかな?
    • このあたりはプロジェクトメンバーの構成に合わせて変化するだろう
  • Mergerを設定する
    • .gitconfig を編集する
    • UnityYAMLMerger というツールが付属しているので、PATHを通してConfigを設定する

Unity学習の整理 Unityについての所見

なぜゲームエンジンを覚えるのか

2018.06時点のスキル

UnrealEngineとの比較

  • UnrealEngineは個人的にすごく好き
    • 学生時代(2005年頃)から、たまに雑誌で紹介されていたね
  • C#による手軽なコーディングが行える
    • テストコードもある程度書きやすい
    • 言語仕様はC#だが、RuntimeはUnityのために少し特殊であることを覚える
    • 処理がコードベースなので、プログラマとして見ると理解しやすい
    • プログラマとしてのキャリアを考えると、コードとの親和性の高い環境を使いたい
    • コードを書くのが好きである
  • アプリを観察してみると、モバイル分野ではUnityのほうが盛んだと思う
    • どっかでデータを見た気がするけど、ソースが見つからなかった

学習環境

  • 2018.06時点では、基本的に独習である
    • 勉強会は、子育てもあるので出づらい
  • 情報はかなりの量が出ている
  • 調べてみると、痒いところに手が届かない感じの情報量
    • UnitTestの件とか
  • 調べ尽くしてわからなければ、 コミュニティ にも頼ろう
  • リソースは標準のものやAsset Storeから無料・低額な素材を探す
    • 学習用としては十分である

Unityのインストール

Linux(Ubuntu)版の不具合

  • Floating Windowのリサイズができない
  • フォントに独特の滲みがある
    • GeForceで確認
    • Intel GPUCore i7組み込みGPU)では再現しない
    • フォント用テクスチャ生成でトチってる?
  • 同スペックマシンの場合、Windows版に比べて重い
  • 不具合ではないが、Launcherが登録されないので自分で *.desktop ファイルを書く必要がある

Windows版の所感

  • 開発環境としては最も良さそう
    • スペックが自由に選べる
    • 動作が安定している
  • ノートPC + Quadro環境ではカツカツな状態だった
    • DirectXを使っているはずなので、Quadroマシンにはあんまり期待してない
    • そもそも2015年のPCだし
    • 会社マシンがコレなので、スペック的にキツイ
    • 買い替えたいが、買い替え先の検討をしなければならない