技術的負債を返済しつつmocopi対応した話

こんにちは!クラスター社でUnityエンジニアをしているNatsukiです。
今回は、私が担当したmocopi対応というepicについてお話しします。

特に、技術的負債を返済しつつ対応できたのが今回のepicの個人的なハイライトなので、そのことについてお話したいと思います。

mocopi対応とは

mocopi対応とは、clusterのVRで(OSCを経由して)mocopiが使えるようにするというepicです。
ただ、VRと一口に言っても、clusterではPCVRとQuestスタンドアローンの2種類があります。
このepicでは、この両方でmocopiが使えるようにする必要がありました。

www.youtube.com / 『mocopi』はソニーによるモバイルモーションキャプチャー

mocopi対応する上での課題について

さて、clusterにおけるVRにはPCVRとQuestがあるという話をしました。
PCVRとQuestでは、実装としては結構異なっている部分があります。

具体的には、CameraRigと呼ばれる、トラッキングスペースにおけるHMDやコントローラ等の階層構造を表したリグがあるのですが、その周辺実装が異なっています。
何故かと言うと、VRプラットフォーム(SteamVR、Quest)ごとに使用しているプラグインが異なるためです。

これらのプラグインにはそれぞれのCameraRigのprefabが入っており、clusterではそれらを拡張する形で使っています。

SteamVRのCameraRig(mocopi対応後)

ただ、(おそらく歴史的経緯で)このあたりの実装が複雑になっており、そのままmocopi対応するのは厳しそうでした。
何故なら、上述したとおりPCVRとQuestの両方で対応する必要があり、そうすると考慮すべき事項が(少しおおげさですが)組合せ爆発するためです。

具体的には、既存実装は以下のような状態でした。

  • 両方のリグで共通して使っているコンポーネントが「#if地獄」になっている
    • VRプラットフォームごとのプラグインを参照するため、#ifが多用されている状態
      • #ifでの分岐はIDEからも追いづらく、思わぬバグの原因になりがち
  • 内容はほぼ同じなのに各リグ専用の実装がある
    • トラッカーやコントローラ等のデバイスを表すコンポーネントとして、各VRプラットフォームのプラグインのクラスをそのまま使っているため
  • トラッカーを扱うクラスで、「Questではトラッカーを扱わない」ことを前提とした分岐がある
    • if (isQuest) returnみたいな分岐がいろんな所にある

加えて、mocopiは両VRプラットフォームで使える必要があるので、既存実装にそのまま追加する形だとさらに考慮事項が増えるのは明白でした。

「#if地獄」の様子

(上記の画像だけでは地獄感が薄いですが、このコンポーネント全体がこんな感じ且つ他クラスでの条件分岐も含めると結構地獄でした)

どのように解決したのか

さて、mocopi対応を進める上で、上記のような課題があることが分かりました。
次は、これらの課題をどのように解決したのかについて書きます。

まず、課題を踏まえた上で、取りうる方針としては以下のものを考えました。

  1. 既存実装はほぼそのままで、mocopi分の実装を追加する
    • メリット: 既存実装をほぼ改修しなくて済む(mocopi用の条件分岐等は追加する必要がある)
    • デメリット: 条件分岐が複雑で見通しの悪いコードになり、考慮すべき事項が増える
  2. リグを使わず、mocopiの姿勢データをアバターに反映する
    • メリット: 既存実装を改修しなくて済む
    • デメリット: 新規の実装が必要 + VRとの相性が悪い(HMDやコントローラの姿勢と整合させる必要があり、そうなると結局リグを使うことになりそう)
  3. 既存実装をリファクタし、mocopiのトラッカーを既存のトラッカーのロジックに載せる
    • メリット: 既存のトラッカーまわりのロジックがそのまま使える
    • デメリット: 既存実装を改修する必要がある

今回は上記の方針のうち、3を選択しました。
理由は、最も早く実装できそうだと思ったためです。

技術的負債を返済する必要はありますが、ここで負債を返済してコードの品質を上げ、将来的な拡張性も上げておきたいという思いもありました。

(こういった場合、追加の実装だけで済む1の選択肢を取りがちかと思いますが、コードが負債化して最終的には実装スピードが鈍化し、結果として実装完了が遅くなると予想しました)

さて、3の方針の具体的な内容ですが、以下の要領で実装することにしました。

  1. デバイスを表すコンポーネントを抽象化
  2. 抽象化したクラスを用いて既存実装をリファクタ
  3. トラッカーの具象クラスの1つとしてmocopiを扱う

1. デバイスを表すコンポーネントを抽象化

まず、デバイスを表すコンポーネントを抽象化することにしました。
プラットフォーム依存を考慮せずにロジックを書けるようにすることが狙いです。

例えばトラッカーの場合、VRTrackerBaseという抽象クラスを作成しました。この具象クラスとしてSteamVRTrackerOSCVRTrackerを実装する、という想定です。
また、VRTrackerBase自体はVRプラグインに依存していないのがポイントです。

VRTrackerBaseの実装(一部)

2. 抽象化したクラスを用いて既存実装をリファクタ

次に、抽象化したクラスを用いて既存実装をリファクタしました。
抽象クラスを使うことでVRプラグインへの依存を考慮する必要がなくなり、「#if地獄」が解消されました。

「#if地獄」解消後

ここで「元々#ifで行っていたVRプラグインへの依存管理はどうなったの…?」と思うかも知れません。
その役割は、今はAssembly Definitionが担っています。

例えば、VRTrackerBaseの具象クラスであるSteamVRTrackerSteamVR Pluginに依存していますが、Assembly Definitionの設定によってQuestビルドには含まれないようになっています。

このようにして、デバイス関連のクラスでプラットフォーム依存を考慮することなく、純粋なロジックに集中して書くことができるようになりました。

3. トラッカーの具象クラスの1つとしてmocopiを扱う

最後に、トラッカーの具象クラスの1つとしてmocopiを扱うようにしました。
mocopiトラッカーはOSCを経由して扱うため、OSCVRTrackerVRTrackerBaseの具象クラスとして実装しました。

OSCVRTrackerの実装(一部)

これにより、moocpiのトラッカーがclusterで扱えるようになりました。
めでたしめでたし!🎉

解決編まとめ

以上の要領で、無事にmocopiのトラッカーに対応することができました。
さて、お気付きの方もいるかも知れませんが「mocopi専用の実装」というのはほとんどしていません。
改めて念を押しますが、「ガッツリ新規実装するのではなく、既存実装をリファクタし抽象化することで、新しいトラッカーに対応できるようにした」のがこのepicの大きなポイントだと思っています。
前の章で書いた方針の「メリット」の部分が現れた形ですね。

技術的負債を理解し、それを粉砕することを許してくれる環境

今回はtech blogとして、「技術的負債を返済しつつmocopi対応した話」を書いてみました。
ただちょっと見方を変えると、「良くない実装を普通の実装にしただけじゃね?」と思われるかも知れませんが、実際にこれを業務の中で出来る環境はなかなか無いのではないでしょうか?
業務の中で書かれるコードは、理想はありつつも、納期等の理由で理想通りに実装できないことはよくあると思います。

技術的負債を理解し、それを粉砕することを許してくれる環境がクラスター社にはあります。
PMやEM、エンジニアやQAチームの理解や支えがあるからこそできることです。

今回お話ししたように、clusterはマルチプラットフォームなアプリです。
VRだけでなく、デスクトップ(Windows、Mac)やモバイル(Android、iOS)もあります。
マルチプラットフォームに対応するためには技術的に難しいこともありますが、その分やりがいがあります。
こうした環境で働いてみたい方は、クラスター社では一緒に働ける方を募集しているので、ぜひご応募ください!

recruit.cluster.mu