NetworkedProperty(に相当する機能)のすゝめ

エンジニアの岡村です。

Unityでネットワークマルチプレイを行うアプリケーションを開発する場合、ある程度以上の規模があるのならば、その実装にはネットワークライブラリを利用するのが一般的かと思います。

その際に使われるライブラリにもいろいろな製品があるのですが、メジャーなところではPhotonやMonobitやMirror、最近ならUnity謹製のNetCode for GameObjectも選択肢に上がるでしょう。

弊社では長らくPUN Classic(PUN2ではない)を自社向けにカスタマイズしたものを使ってマルチプレイを実装していました。基本的な機能はPUNに準拠していたのですが、細かい同期の制御をコンポーネント内でのイベントとRPCの実装で行っており、大人数対応などの拡張が難しい状態になっていました。

例えばNEUTRANSでは、プレイヤーの入室時に他プレイヤーからの同期を全てRPCで実装し、プレイヤーがルーム内で描いた絵(UGC)に対して、自前で後から参加したユーザー向けの遅延同期処理もRPCで実装するなど、かなり無理のある使い方をしていました。

そのままでは入室人数を増やしたり、より複雑な機能の開発を行うには限界があったため、思い切って新しい仕組みに書き換えることを決め、新しい選択肢を求めて他のライブラリの検証をいました。その過程で触ったNetCode for GameObjectやPhoton Fuisonといった新しめのライブラリには「NetworkedProperty」というような名前で、同期する必要がある変数1個1個の単位で同期する為の機能が搭載されていることに気づきました。

(UNetにも同様の機能は存在しますが、自分がUNetを触るより前にPhotonを触っていたので知りませんでした)

UnityのマルチプレイライブラリにはRPCと、それ以外の値ベースの同期機能が搭載されていることになります。今まではRPCばかり使っていたので、今回改めてこれらの機能の存在理由について調べ、纏めてみました。

以下の内容では機能名は基本的にFusionのものを利用していますが、他のネットワークライブラリにもほぼ同様の機能が別名で存在するので、適宜読み替えてください。


Unityにおけるマルチプレイライブラリの特徴

前提として、UnityゲームエンジンはGameObjectを処理の単位としています。Unityと深く統合されたマルチプレイライブラリはその設計を汲んで、GameObjectとそこにアタッチしたコンポーネント内で簡単に同期のための機能を利用できる仕組みを搭載しています。

マルチプレイを制御するRunnerが一つと、それの子としてマルチプレイのシーンを構成するObjectが0個以上ある形になります。また、Runner内におけるインターネットの向こうにあるサーバーやクライアントとメッセージをやり取りする仕組みと、Runnerが各Objectに対して同期サービスを提供する仕組みは分離可能な実装になっていることが多いです。

NetworkObjectは基本的にGameObjectにコンポーネントを付けたPrefabの形で実装されます。同期するにはPrefab同士が同じ構造である必要があるので、それぞれのPrefabにはIDが振られており、特定のIDで特定のPrefabが取得できるようになっています。必要になったタイミングでInstantiateするとともにネットワーク上で一意となるIDを振り、Runnerと接続して初期化を行います。

NetworkedProperty

変数の同期機能を持っているライブラリは、コンポーネントに同期ロジックを実装する際、フィールドメンバーをAttribute等で同期可能な変数としてマークします。マークされた変数は同期サービス側で監視され、値の変更があれば自動でシリアライズされて他のクライアントに同期されます。

基本的にこの同期はいずれかのクライアント上の値を真として一方向のみで行われ、他のクライアント上で値を変更しても反映されません。これはどちらのクライアントが持つ値が正しいのかをハッキリさせ、お互いが値を送り合って状態が収束しなくなってしまうのを防ぐためです。

RPC(Remote Procedure Call)

RPC(Remote Procedure Call)は、同期オブジェクト側から能動的にリクエストが送られる機能です。RPCとしてマークされたメソッドを呼び出すと、その引数がシリアライズされて受け取り手に届きます。これらの処理は基本的に即時に、遅滞なく行われます。

ただし、Networked Propertyのように状態を持っておらず、送信しようとしたタイミングで送信相手がルーム内に存在する必要があります。後から入室した相手には届きません。

ちなみに、PUN2ではRPCをバッファリングして後から入室したプレイヤーにも送信する機能がありましたが、Fusionでは削除されました。この変更は恐らくRPCをステートレスにすることで、NetworkedPropertyとの使い分けを明確にしたのだと思います。

使い分け

RPCは、

  • 特にリアルタイム性が要求される同期処理(マルチプレイゲームでの攻撃処理)
  • 同期権限のないクライアントから、同期権限のあるクライアント(サーバー)へ値の更新を依頼するとき
  • 状態の変更を伴わない一時的なトリガーの同期(送信経路を選べるなら、到達保証を無しにしても良い)

一方で、NetworkedPropertyは、

  • RPCで挙げた以外の同期処理全て

これくらいの認識で使い分けをして問題ないと思います。

Fusionでも入退室時のイベントと、RPCを使うことで全てRPCで完結することもできなくはないですが、それではここでは紹介しなかったFusionの様々な便利機能を使えない、実装の手間が増える、同期時の負荷が上がるといったデメリットが色々あるので、基本的にはNetworkedPropertyとRPCを組み合わせて同期処理を作っていくのがいいでしょう。

上図のフローを参考に同期するコードを書いてみるとこのような感じになります。

using Fusion;
using UnityEngine;

public class SampleScript : NetworkBehaviour
{
    [Networked(OnChanged = nameof(OnColorChanged))]
    private Color Color { get; set; }

    public void ChangeColor(Color color)
    {
        RPC_SetColor(color);
    }

    [Rpc(sources: RpcSources.All, RpcTargets.StateAuthority, InvokeLocal = true)]
    private void RPC_SetColor(Color color)
    {
        Color = color;
    }

    private void OnColorChanged()
    {
        var block = new MaterialPropertyBlock();
        block.SetColor("_Color", Color);
        var renderer = GetComponent<Renderer>();
        renderer.SetPropertyBlock(block);
    }
}

他のプレイヤーに対する同期は全てNetworkedPropertyがやってくれるので、コードではとにかく「同期権限のあるクライアント側のNetworkedPropertyに値を入れる」「NetworkedPropertyが更新された時にビューを更新する」の2点だけを考えればよくなっています。

ちなみに、以前はこんな感じで実装していました(PUN2のコード)。

using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

public class SampleScript : MonoBehaviourPunCallbacks
{
    private Color color;

    public void ChangeColor(Color color)
    {
        photonView.RPC(nameof(RPC_SetColor), RpcTarget.All, color);
    }

    [PunRPC]
    private void RPC_SetColor(Color color)
    {
        this.color = color;
        var block = new MaterialPropertyBlock();
        block.SetColor("_Color", color);
        var renderer = GetComponent<Renderer>();
        renderer.SetPropertyBlock(block);
    }

    public override void OnPlayerEnteredRoom(Player newPlayer)
    {
        if (photonView.IsMine)
        {
            photonView.RPC(nameof(RPC_SetColor), newPlayer, color);
        }
    }
}

コード量はあまり変わりませんが、他のプレイヤーが入室したときの処理を手動でハンドリングしています。このコードではオブジェクトが大量にあった場合、入室時に現在の状態を同期する為にRPC通信が大量に飛ぶことが予想されます。小規模なアプリであれば動作に問題はないのですが、大規模な拡張をするのは難しいでしょう。

その他

NetworkedPropertyでも対応が難しいパターン

先程はNetworkedPropertyを万能かのように書いたのですが、そもそもリアルタイムネットワークの特徴として、あまり大容量のデータを扱うことは苦手です。そのため、UGC(ユーザーが作ったコンテンツ)の同期、特にマルチプレイ空間内で動的に作成される作品を同期したりといった事は苦手です。そのようなものを実装することになった場合は、大人しくUGCの同期用の仕組みを別途用意するのがいいでしょう。

NetworkedPropertyの拡張

この仕組みはルーム内の全ての状態が同期サービス側から読み書きが可能な為、その口をほんの少し拡張することで、ルーム内の状態のセーブ、ロードやルーム状態のCDNを通じたライブ配信など、様々な応用が出来るのではないか、と考えています。

おわりに

最近の同期ライブラリの情報自体は追っていたのですが、実際に使っているコードはPUN Classicをカスタマイズしたものだった為、この記事を書くにあたって調査するたびに新しい発見があって大分情報が古くなっているな、と改めて感じました。やはり手を動かしてみるのが大事ですね。

今回の記事はPhoton Fusionに限らないモダンなUnityのマルチプレイライブラリに共通した考え方について書いた(つもり)です。Photon Fusionにしかないような便利な機能は沢山あるのですが、この場での紹介は割愛しました。もしPhoton Fusionに興味がある方は、先日行われたこちらのセミナーの資料が参考になったので、是非一読をお勧めします。

photonjp.connpass.com

Unityで画像データをいい感じにロードしたい

こんにちは、エンジニアリングマネージャーの渡辺(@mochi_neko_7)です。

今回はUnityでの画像データのロード方法に関する記事になります。

たかが画像データですが、意外とまだ王道の取り回しが確立されていない印象です。

最近また画像のロード周りを触る機会があり、いろいろな方法を調べてみたのでその内容をまとめてみました。

背景

画像データのロード方法の悩み

画像データをただUnity内に取り込んで利用したいだけの場合は、JPGやPNGのファイルをUnityEditorで取り込むとTextureやSpriteに変換してくれますので、それほど困ることはないと思います。

しかしUnityのプロジェクトの外部から画像データをロードして表示したいという場合には、意外とひと手間二手間かかります。

今回私が調査した画像のロード方法で重要視した観点は以下になります。

  1. なるべくメインスレッドを止めないで、Unityの外にある画像データをUnity内で表示したい
  2. JPG、PNGを含む標準的な画像コーデックは対応したい
  3. Windows/macOS/Android/iOSなどのマルチプラットフォームで動作させたい

画像データはデータサイズが4Kや8Kまでいかなくてもデータの処理自体にそれなりに時間がかかるため、UnityのUpdateの数~数十フレーム分の負荷がかかってしまい、画面の更新が少し止まってしまうことも珍しくありません。

使用する画像が事前に分かっている場合には解像度を落とすなど最適化をする余地もありますが、外部から様々な画像データを読み込むようなユースケースでは制御が難しいです。

例えばVRのアプリケーションでは画面のフリーズはVR酔いにつながるため、致命的な体験の質の低下を起こしてしまうこともあります。

また、画像データのコーデックはいくつもあるため、それらの対応可能な範囲も気になるところです。

マルチプラットフォーム開発ができるというのがUnityの強みの一つであるので、どのプラットフォームで動作するのかも確認していきたいと思います。

Unityでの画像のロード方法の基本的な流れ

画像をロードしてUnityで表示させる処理の基本的な流れは以下のようになるかと思います。

JPGやPNGの画像データのbyte[](or Stream)を取得する

→ デコードしてピクセルデータのbyte[](+WidthやHeightなどのメタデータ)に変換する

→ UnityのTexture(2D)にピクセルデータを読み込ませる

最初の画像データの取得方法はローカルデータならSystem.IOでできますし、URLから取得するならHTTPClinetでも構いません。

また、UnityのTextureなどのUnityが管理しているオブジェクトにアクセスして読み書きするAPIは基本的にはメインスレッドでしか行うことができません。

ですので要件1のメインスレッドをなるべく止めないことを実現するためには、画像データのデコード処理をメインスレッド以外の別スレッドで実行できることはポイントの一つになります。

紹介

UnityEngine.ImageConversion.LoadImage

Unityの公式のAPIに昔からあるLoadImageです。

docs.unity3d.com

対応しているコーデックはJPG、PNGだけというのはありますが、このロード方法では画像のバイナリーデータのデコードも含めてすべてメインスレッド上で行われるため、メインスレッドを占有してしまう時間が長くなってしまいます。

UnityWebRequest(と昔のWWWクラス)からTextureを取得する方法も(おそらく)内部では同じ挙動をするため、同じ問題を持っています。

docs.unity3d.com

メリット

  • Unityに組み込まれているため特別な準備をすることなく使える

デメリット

  • メインスレッドを止めてしまう
  • JPG、PNGしか対応していない

AssetBundle/Addressables

Unity公式の機能のAssetBundleやAddressablesを使ってもTextureして外部からのロードをすることもできます。

docs.unity3d.com

docs.unity3d.com

ただし事前にAssetBundleデータのビルドをする必要があったり、プラットフォーム別にそれぞれビルドを用意する必要があります。

使用する画像データがある程度決まっていてコントロールできる場合には十分有用ですが、アプリのユーザーが画像をアップロードして使用する場合などデータが事前に用意できないケースには向きません。

メリット

  • 圧縮されている画像を扱える
  • 他でAssetBundle/Addressablesを使用している場合には乗っかれる

デメリット

  • AssetBundleビルドがプラットフォーム毎に必要
  • 事前に画像データの用意が必要

Unity.IO.LowLevel.Unsafe.AsyncReadManager.Read

比較的最近追加されたUnity公式のAPIに、AsyncReadManagerというものがあります。

ざっくり説明するとファイルのロードを非同期に、かつUnsafe(つまりUnmanaged Memory上で)でロードできるものです。

既にいくつか紹介記事もありますので詳しくはこちらなどをご覧ください。

zenn.dev

qiita.com

これも十分有効に使える場面もあるとは思いますが、後者の記事にあるようにUnity上での画像の圧縮形式を考慮する必要があるため、前者の記事のように一度ローカルにTextureデータをロードしておいたり、プラットフォームによって異なる圧縮形式の対応を考えたりする必要があります。

一度ローカルに保存するためにUnityWebRequestなどでTextureに変換してしまうとそのプロセスでメインスレッドを占有するため、リアルタイムにロードするのにはあまり向かないかもしれません。

メリット

  • Unsafeで扱えるのでメモリの負担が少ない
  • 非同期APIが用意されている

デメリット

  • 圧縮形式を考慮する必要がある

System.Drawing

C#の標準の機能を調べて見ると、System.Drawingというクラスで画像をBitMapにデコードできることが分かります。

docs.microsoft.com

ただUnityにはこのSystem.DrawingのDLLが含まれていないため、さっと使用するのは難しそうです。

qiita.com

メリット

  • C#の標準機能

デメリット

  • Unityで使用するのは大変

FreeImage

Unity公式のAPIでも、C#の標準APIでも適切なものが見つからない場合には、オープンソースのライブラリを探してみます。

比較的有名な画像処理のライブラリに、FreeImageというものがあります。

github.com

弊社のNEUTRANSというプロダクトでも採用しているライブラリです。

このライブラリの注意点は、動作する環境がStandalone(Windows/macOS/Linux)のみで、Android/iOSでは動かないという点です。

C/C++で書かれているため原理的には適切にビルドをすれば動きそうな気もしますが、弊社の別のメンバーが試したところうまくいかなかったとのことです。

メリット

  • Unsafeで扱えるのでメモリの負荷が少ない
  • 別スレッドで実行可能
  • OSS

デメリット

  • Standalone(Windows/macOS/Linux)のプラットフォームでしか動作しない
  • 自分で導入する必要がある

UnityAsynImageLoader

「Unity Image Loading」などで検索していたところ、こんなライブラリを見つけました。

github.com

READMEに書かれていることはまさに同じ課題意識のため「これは!」と思ったのですが、内部ではFreeImageを使用しているようなので、スマホで動かすのは難しそうです。

APIは綺麗に作られているので、FreeImageのWrapperとしては使いやすいのではないでしょうか。

メリット

  • FreeImageを触りやすくしてくれている
  • OSS

デメリット

  • FreeImageのデメリットを引き継いでいる

OpenCV for Unity

Twitterでいいライブラリはないものかとつぶやいていたところ、フォロワーさんからOpenCV for Unityというアセットを教えていただきました。

assetstore.unity.com

これは画像処理のOSSで有名なOpenCVをUnity向けに組み込み、拡張したアセットになります。

opencv.org

なんとWindows/macOS/Android/iOSに加えて、WebGLやUWPなどのほとんどのメジャーなプラットフォームにも対応してます。

早速会社で購入してもらい、実際に触ってみました。

結論から言うと当初の要件を満たすことはできますが、少し問題点もありました。

  1. OpenCVのピクセルデータクラスのMatへの変換処理は別スレッドで実行できるので、メインスレッドの負荷を減らせる
  2. 各プラットフォームの動作確認もできたが、iOS向けのビルドのPostProcess処理(Xcodeでのライブラリ参照など)に癖があり少しカスタマイズが必要だった
  3. Native Pluginのファイルが単体で100MBを超えるものが複数あり、GitHubだとGit LFSを使用しないといけない、かつかなりの容量を使用する。
  4. OpenCV本家のThirdPartyLicenseが多くてライセンスのチェックが大変そう(全部確認したわけではありません)
  5. ただFFMPEGは手動で入れない限り入らないのでそこの不安はなさそう
  6. いろいろな画像処理をできる反面、画像をデコードしたいだけだとややオーバースペック

すごく便利なアセットであることには間違いないので一度導入はしてみたのですが、課題も見えてきたため最終的には別のライブラリに置き換えることになりました。

メリット

  • NativePluginで主要なプラットフォームにはほとんど対応している
  • 画像のデコード以外にもOpenCVの様々な機能が使える
  • Unity向けの拡張やデモが用意されていて比較的触りやすい

デメリット

  • NativePluginのファイルサイズが100MBを超えるものが複数ある
  • iOS向けのビルドは少しケアが必要
  • 画像をロードするだけのために入れるには機能が豊富過ぎる
  • 有料

UnityのAssetStoreで検索

OpenCV for Unity以外ではあまりいいアセットが見つかりませんでした。

Native Pluginを自作する

適切なものがない場合や、パフォーマンスを重視するようなケースでは、Native Pluginを自作するのも一つの手です。

実際に凹さんが記事にしているものがあります。

tips.hecomi.com

tips.hecomi.com

tips.hecomi.com

Native Pluginは対応したいプラットフォーム向けにそれぞれ作る必要があること、そのメンテナンスも必要なことをクリアできる知識とリソースがあれば自由度が高くパフォーマンスも高い手段になるでしょう。

今回は対応プラットフォームが多く、かつ画像のロード機能はそこまでコアな機能でもないためそこまでリソースは割けませんでした。

メリット

  • パフォーマンスが良い
  • 自由にカスタマイズが可能

デメリット

  • 複数のプラットフォーム別に用意する・運用するのが大変

結局どの方法を採用したのか?

OpenCV for Unityを導入して悩んでいたのですが、まったく別のところで3Dモデルをロードする機能を作った際に利用したTriLibというアセット

assetstore.unity.com

のライセンスのチェックをしていたところ、TriLibの内部で画像のデコードに利用しているStbImageSharpというライブラリがあることを知りました。

StbImageSharp

github.com

このライブラリの特徴的なところは以下になります。

  1. JPG、PNG含むメジャーなコーデックをサポートしている(PSDとGIFに対応しているのは謎にすごい)
  2. NativePluginを使用せずPure C#で書かれている、つまりUnityのどのプラットフォームでも動作する(パフォーマンスは少し落ちる)
  3. ただしUnity向けには作られていないので、少し拡張が必要

最後のUnity向けの拡張さえあれば当初の要件が満たせそうなため、自分で作ることにしました。

StbImageSharpForUnity

github.com

画像のデコード処理を別スレッドで処理するデモも用意しました。

まだメモリの取り扱いを最適化しきれていないですが、パフォーマンスをそこまで気にしなくてもいいのであればプラットフォームを気にせずに取りまわせる使いやすいライブラリなのではないかなと思います。

というわけで結局StbImageSharpを利用した方法を採用することになりました。

メリット

  • デコード処理を別スレッドで実行できる
  • 対応コーデックが多い
  • マルチプラットフォームの対応が容易

デメリット

  • パフォーマンスはそこまで良くない

まとめ

画像データのロード方法を様々紹介しましたが、どれが完璧というわけでもないですし、プロジェクトの要件によって最適な方法は変わるかなと思います。

今回はあまり紹介できませんでしたが、メモリの最適化、非同期処理、残るTextureのAPIアクセスのオーバーヘッド、大きい画像の分割ロード、GIF画像の取り扱いなどまだまだ突き詰められる余地があります。

たかが画像データと思いきや意外と奥が深い世界です。

また、今回紹介したのは私が知っている or 検索できた範囲になっていますので、「他にもこんな方法、アセット、OSSもあるよ!」というのもありましたらTwitterなどで教えていただけると嬉しいです!

不正確な記述もありましたらご指摘いただけますと幸いです。

最後に紹介したこちらも使ってみてのフィードバックも大歓迎です。

github.com

以上、Unityで画像データをいい感じにロードしたいけど何か困っているという方の参考になれば幸いです。

NestJS+Svelteを使って簡単なSSRを試してみた

はじめに

エンジニアの松原です。普段の業務ではバックエンド開発業務が多く、たまにはフロントエンドのこともしたいと思いつつ、新しいWebフレームワークにも触れたいと考えたため、 バックエンドとしてRails風にMVCベースの設計を持っているNestJS(厳密にいうと、NestJSはViewはデフォルトでは持っていませんが)、描画エンジンとしてフロントエンドのWebフレームワークであるSvelteを組み合わせて Hello World! をやってみました。

尚、今回はとっかかりとしてのコードしか書いておらず、ちゃんとSSRを行うにはSvelteをTypeScript対応にしたり、Webpackで依存するパッケージをバンドルするなどの手続きが必要になります。 以降フォローできればと思います。

また、今回はPCに node (v16以降、と npm ) が入っていることを前提に記事を書いています。

NestJSのセットアップ

NestJSのCLIが必要なので、npmからインストールしておきます

npm install -g nest

NestJSのプロジェクト作成を作成する

CLIから新規のプロジェクトを作成します

nest new example-my-project

その際、パッケージマネージャーを何にするか聞いてくることがありますので、選択します(矢印キーで操作して選びます) 今回はyarnを使用したいので、yarnを選んでいます

⚡  We will scaffold your app in a few seconds..

CREATE example-my-project/.eslintrc.js (665 bytes)
CREATE example-my-project/.prettierrc (51 bytes)
CREATE example-my-project/nest-cli.json (118 bytes)
CREATE example-my-project/package.json (2002 bytes)
CREATE example-my-project/README.md (3340 bytes)
CREATE example-my-project/tsconfig.build.json (97 bytes)
CREATE example-my-project/tsconfig.json (546 bytes)
CREATE example-my-project/src/app.controller.spec.ts (617 bytes)
CREATE example-my-project/src/app.controller.ts (274 bytes)
CREATE example-my-project/src/app.module.ts (249 bytes)
CREATE example-my-project/src/app.service.ts (142 bytes)
CREATE example-my-project/src/main.ts (208 bytes)
CREATE example-my-project/test/app.e2e-spec.ts (630 bytes)
CREATE example-my-project/test/jest-e2e.json (183 bytes)

? Which package manager would you ❤️  to use? (Use arrow keys)
> 

プロジェクトディレクトリに移動します。

cd example-my-project

READMEが自動生成されていますので、package.jsonと合わせて、自分の情報に書き換えます。

Svelteのパッケージをインストールする

サーバーサイドでhtmlレンダリングを行うためにSvelteの設定を追加していきます。 まずは必要パッケージをインストールします。それぞれ svelte はSvelte本体、 svelte-check はSvelteのテンプレートの記述を診断するCLIツールになります。

yarn install -D svelte svelte-check

SvelteをViewEngineとして設定する

NestJsのControllerからhtmlのレンダリング処理を自分でコントロールしたい場合、描画処理の手続きを記述する必要があります。 今回はSvelteのテンプレートを使うため、Svelteのモジュールに処理を実行させるコードを書きます。 src/ 配下に svelte-view-engine.ts というファイル名を追加し、以下のコードを追加します。

import 'svelte/register';

interface NodeCallback<T> {
  (err: any, result?: undefined | null): void;
  (err: undefined | null, result: T): void;
}

export function renderWithViewEngine(filePath: string, options: any, next: NodeCallback<any>) {
  const component = require(filePath).default;
  const { html } = component.render(options);
  next(null, html);
}

src/main.ts にこのコードを反映させます。

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { resolve } from 'path';
import { AppModule } from './app.module';
import { renderWithViewEngine } from './svelte-view-engine';

async function bootstrap() {
  // const app = await NestFactory.create(AppModule); // <- 変更前の記述
  // 以降を追加
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.engine('svelte', renderWithViewEngine);
  app.setViewEngine('svelte');
  app.setBaseViewsDir(resolve('./src/views'));
  // ここからは以前と同様
  await app.listen(3000);
}
bootstrap();

Svelteのテンプレートをレンダリングできるようにする

Svelteのテンプレートを作成します。 src/ 配下に views というディレクトリを作成し、 Layout.svelte を作成し、以下のコードを追加します。 このファイルはhtmlを作成する際のひな型になります。

<script>
   export let title;
</script>

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{title}</title>
  </head>
  <body>
    <slot />
  </body>
</html>

同じく Home.svelte を作成し、以下のコードを追加します。先ほどの Layout.svelte をベースにして、 <slot /> の箇所に Home.svelte の内容が流し込まれます。

<script>
   import Layout from './Layout.svelte';
   export let message = "";
   export let title = "";
</script>

<Layout title={title}>
    <h1>{message}</h1>
</Layout>

これらのテンプレートを使ってレンダリングするためには、コントローラー側に対象のビュー設定を行うためのメソッドデコレータを指定する必要があります。 src/app.controller.ts を書き換えます。新たに Render のメソッドデコレータを追加し、描画するテンプレート ( この場合は Home -> Home.svelte という対応関係) を指定します。

import { Controller, Get, Render } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @Render('Home')
  getHello(): object {
    return {
      message: this.appService.getHello(),
      title: 'Home'
    };
  }
}

実行してみる

以下のコマンドを実行し、NestJSを開発モードでローカル実行してみます。

yarn start:dev

以下のようにNestJSのサービスが稼働していれば大丈夫です。

[10:33:31] Starting compilation in watch mode...

[10:33:33] Found 0 errors. Watching for file changes.

[Nest] 28600  - 2022/**/** **:**:**     LOG [NestFactory] Starting Nest application...
[Nest] 28600  - 2022/**/** **:**:**     LOG [InstanceLoader] AppModule dependencies initialized +26ms   
[Nest] 28600  - 2022/**/** **:**:**     LOG [RoutesResolver] AppController {/}: +5ms
[Nest] 28600  - 2022/**/** **:**:**     LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 28600  - 2022/**/** **:**:**     LOG [NestApplication] Nest application successfully started +3ms

http://localhost:3000 にアクセスします。

無事NestJS + Sveleteで Hello World! ができました。

まとめ

今回はじめてNestJSを触りましたが、NestJSコード構造は、Railsの設計にも若干似ており、(RubyとTypeScriptという開発言語の違いはありますが)Rails開発で培ってきた経験がそのまま利用できるイメージがあります。
また、NestJSにはTypeScriptで利用できる強力なデコレータが用意されており、JavaのSpringのアノテーションにも似ており、メンテナンス性を考えて丁寧に設計されていることが良く分かります。
これまでがっつりMVCベースのWebフレームワークを触ってこられた方は使いやすいWebフレームワークなのかなと思います。

公式サイト(英語)のOverviewを見ていくと、どういったWebフレームワークなのか理解を深められると思いますので、NestJSに興味を持っていただいた方は是非お読みください。

docs.nestjs.com

SvelteはReactやVueと似たようなものかと考えていましたが、実際触ってみるとPugのようなテンプレートエンジンに近いイメージを持ちました。独特な構文が少ないため、学習コストもReactやVueと比べると非常に少ないと思います。
今回はアドホックなやり方でSvelteを使っているため、ブラウザ上のロジックが動作しない(動的バインディングや関数が動作しない)のですが、本来のSvelteはSvelteはReactやVueと同様にブラウザ上にロジックを持たせることができるWebフレームワークのようですので、Svelteが気になる方は公式サイトをぜひ除いてみてください。
公式の対話型チュートリアルを通して、実際にコードに触れながら学ぶことができますので、空き時間にポチポチ触ってみると良い時間つぶしになりそうです。

svelte.jp

次回以降の記事では具体的なWebアプリケーションとして動作する仕組みを試していければと考えております。

NFTなんもわからんのでとりあえず試してみた〜スマートコントラクト実装とERC721対応〜

こんばんは、エンジニアの黒岩(@kro96_xr)です。
バックエンドを中心にフロントエンドやらインフラやら色々担当しています。

突然ですが、最近NFTやらWeb3やら良くも悪くも盛り上がっていますね。
個人的に色々思うところはありつつ、技術としてはキャッチアップしてかねばならない…と言うわけでまずは完全に理解した状態を目指しつつ検証した内容をブログに認めていこうと思います。

なお、本記事の内容は個人的に触ったものであり弊社開発物と一切関係がありませんのでご承知おきください。(一応ね)

前置き

NFTって結局なんなのよ

言葉の定義は色々あるかと思いますが、技術的には「ERC721等の規格を満たすスマートコントラクトから生成されたトークン」であると私は理解しました。ERC721という規格を満たす実装をすることで一意のトークンIDを持つ、つまり非代替性を持つ=Non Fungibleなトークンになるわけです。巷でよく見るNFTアートと呼ばれる画像はあくまでトークンに紐づいたメタデータ(トークンからリンクされた画像)であり、NFTそのものではないと言えるかもしれません。

ちなみにERC1155という代替性トークンと非代替性トークンを同時に扱える規格もあり、一概にERC721に従わなくてはならないわけではないようです。

じゃあスマートコントラクトってなんなのよ

どうやらNFTを理解するにはスマートコントラクトとは何かということを理解しないといけなさそうです。

ではこのスマートコントラクトとはなんなのでしょうか?
検索してみると自動販売機に例えている説明を目にしました。

実際に自動販売機を例にとって、順を追って説明します。(私が、100円でコーラを買う場合)

  1. 100円を投入すると、コーラが買える自動販売機を業者が設置(前項目、図解の①、契約の事前定義)

  2. 私が自動販売機に100円を投入する(前項目、図解の④、決済)

  3. コーラのボタンを押す(前項目、図解の②、イベント発生)

  4. 自動販売機からコーラが出てくる(前項目、図解の③、契約執行・価値移転)

当たり前じゃないか!馬鹿にするな!

と、怒られるかもしれませんが、これがスマートコントラクトの原点です。
“スマートコントラクトの原点は自動販売機”というお話し ~スマートコントラクトは執行可能な契約である(英LDTP)~ | 一般社団法人日本暗号通貨技能検定協会より引用

…要するに「決まった命令に対して決まった処理を行い結果を返すプログラム」というイメージでしょうか。
引用した例で言うと、下記のようになります。

  1. 自動販売機を機業者が設置=コントラクトをデプロイ
  2. 自動販売機に100円を入れてボタンを押す=コントラクトに命令を送る
  3. 自動販売機から飲み物が出てくる=処理結果が返ってくる

ん?「特定の命令=入力に対して特定の出力を返す」ってことはただの関数とも言えるのでは?
ということでサンプルコードを見てみましょう。

pragma solidity ^0.4.0;
contract SingleNumRegister {
    uint storedData;
    function set(uint x) public{
        storedData = x;
    }
    function get() public constant returns (uint retVal){
        return storedData;
    }
}

スマートコントラクトを作成し実行する - Ethereum入門より引用

このコードを見る限りオブジェクト指向言語における「クラス」のようなものみたいですね。なんとなく自分でも書ける気がしてきました。

スマートコントラクトを書いてみよう

それでは一旦NFTとERC721のことは忘れてコントラクトを実装してみましょう。

コントラクトを実装するために使用する言語がSolidityになります。
イーサリアムはEVM(Ethereum Virtual Machine)というプログラム実行環境を持っており、EVM上でコントラクトを実行します。
Solidityはコンパイラを通してEVM上で動作するコントラクトのバイトコードを生成することができます。

開発環境

今回はサクッと試してみたかったのでRemix IDEを使用しました。
こちらはブラウザ上で動くIDEであり、テスト環境へのデプロイも簡単にできる優れものです。

というわけで文字列をコントラクト上に保存して、それを取得、更新できる簡単なプログラムを書いてみました。
Solidityの記法は下記を参照してください。

コントラクト指向言語Solidity詳解 - Ethereum入門

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.14;

contract HelloWorld {
    // コントラクトに保存される変数
    string greeting = "Hello, world!";
    // 保存されている文字列を返す関数
    function getGreeting() public view returns (string memory) {
        return greeting;
    }
    // 文字列の更新を行う関数
    function setGreeting(string memory _str) public {
        greeting = _str;
    }
}

こちらをコンパイルします。
SOLIDITY COMPILERタブを開き、コンパイラのバージョンをコード内で指定したものと合わせて"Compile {ファイル名}"をクリックします。

するとコントラクタが実行できるようになるので実行してみましょう。
今回はJavaScript VMという開発環境を使用して実際のブロックチェーン上へのデプロイは行いません。
DEPLOY & RUN TRANSACTIONSタブを開きEnvironmentをJavaScript VMにしてdeployボタンを押すと…

Deploys Contractsにコントラクトが追加されます。

ここからテストができます。

ということで非常に簡単ですが、Solidityを使ったコントラクトの作成ができました。

ERC721に対応してみよう

では、実際にNFTに対応するためにERC721への対応をしていきたいと思います。
これらの規格に対応する場合はOpenZeppelinなどのライブラリを利用するのが一般的なようです。
このライブラリを使用することで非常に簡単に実装することが出来ます。

では、下記のようなコードをRemix IDEに入れて同様にコンパイル、デプロイしてみます。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.14;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract KrocksNFT is ERC721, Ownable {
    constructor() ERC721("Krocks NFT", "KRONFT") {}

    function _baseURI() internal pure override returns (string memory) {
        return "";
    }

    function safeMint(address to, uint256 tokenId) public onlyOwner {
        _safeMint(to, tokenId);
    }
}

とりあえずトークンの名前とオーナーは確認できますね。

このトークンはまだmintされていないのでsafeMintを実行します。
Environmentの下にテスト用のアカウント一覧があるので適当なアカウントのアドレスをコピーします。

toにコピーしたアドレスを、tokenIdに数値を入力して"transact"ボタンを押すと…

無事mintされて指定されたアドレスにトークンが付与されました!
分かりづらいですが、0xB〜がコントラクトのオーナー、0xA現在の所有者になります。

さらにsafeTransferFromを実行すると….

トークンが移動しました!

以上、トークンの生成、移動が実行できることが確認できました。
まだ擬似環境で検証しただけですが、技術的観点からNFTが少し理解できたかなと思います。

とりあえず...

Crypto Zombies をやりましょう。

おわりに

以上、今回はコントラクトの実装を試してみました。

コントラクトの実装は初めてだったのですが、バックエンドエンジニアと親和性が高いのではないかと思いました。
ブロックチェーン上に残る状態変数が公開データベースのようなイメージで捉えているのですがこの感覚は正しいのでしょうか…?

今後は

  • プライベートイーサリアムネットワーク上へのデプロイ
  • もう少し複雑なコントラクトの実装とフロントエンドとの連携(Dapps?)
  • Hardhatを使った開発

など引き続き試していきたいなと考えています。

参考文献

以下の記事を参考にさせていただきました、ありがとうございます!

ブロックチェーンEthereum入門 3 | NTTデータ先端技術株式会社

コントラクト指向言語Solidity詳解 - Ethereum入門

NFT, Web3完全に理解した になるためのチュートリアル|DJ RIO|note

NFTを自分で作ってみた~Remix上でERC721、ERC1155トークンを実装 - 引きこもり系上海駐在員の日常

Serverless Frameworkを使ってlambdaとAPI GatewayでGoで書かれた簡単なAPIを作る

ごあいさつ

エンジニアのうぃすきー(@Whisky_shusuky)と申します。弊社ではインフラ・バックエンド・フロントエンドとWeb周りを全般的に対応しております。

前回Serverless Frameworkを使ってlambdaでメール用のワーカーをデプロイする記事を書きました。ワーカーだけではなくてlambdaとAPI Gatewayを組み合わせたら簡単なAPIも作成できたので紹介します。

前回のサンプルコードに追記したのでこれに則って記載します。 github.com

構成の紹介

api/main.goに実際にLambdaで実行するコードを置いています。クエリパラメータでparameterの項目を受け取ってその値をそのままjsonにして返す簡単なAPIです。

...
func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    // queryを取得
    parameter := aws.String(request.QueryStringParameters["parameter"])

    var resp events.APIGatewayProxyResponse
    body := Body{
        Parameter: *parameter,
    }
    jsonData, _ := json.Marshal(body)
    resp.StatusCode = 200
    resp.Body = string(jsonData)

    return resp, nil
}
...

こちらのコードをServerless Frameworkを使ってデプロイしています。 serveless.ymlにこのように記載することによってAPI Gatewayが作成されます。ちょっと詰まった箇所としてはquerystringsにて使用するクエリパラメータparameterを明示的に記述する必要がある点です。

...
  api:
    name: api
    handler: bin/api/main
    events:
      - http:
          path: /sample
          method: get
          cors: true
          # クエリストリングの設定
          request:
            parameters:
              querystrings:
                parameter: true
...

この状態でmake deployを実行するとMakefileの内容が実行されてデプロイされます。

するとAPI Gatewayが構築されます。ステージタブを開くとエンドポイントが表示されるのでそれ+/sample(Serverless Frameworkで指定したpath)を叩けば先ほどのAPIが実行されます。

実際はこのエンドポイントを直接使用することは無くてRoute53で管理しているドメインと連携するといった対応をするケースが多いでしょう。その場合Route53側の設定を行う前にAPI Gateway側でカスタムドメインを作成する必要があります。こちらの設定を行うとRoute53側のAレコード設定時に 作成したAPI Gatewayが設定できるようになります。

なお、serverless-domain-managerというpluginを使用すればカスタムドメイン・Route53も一緒に設定できるようです。ただし、インフラがプラグインに依存することになるので使用する際は念頭に置く必要があるでしょう。

結び

このようにして単純なAPIを作成することができました。静的ファイルでは物足りないもののわざわざサーバーを立てるまでもない時に使い所がありそうです。今回は値を返すだけのAPIですが勿論DynamoDBなどAWSのマネージドサービスと連携することも可能なので本格的なこともできそうです。 また試すにあたりこちらのブログを大いに参考にさせていただきました。

Unity上で外部デバイスとの同期処理を行うための考察(タイムコード読み取り編)

はじめに

エンジニアの松原です。Unityと外部デバイスの間で同期処理を取ろうとするとき、単純に時間のタイミングだけ測って同期を取ろうとしても、デバイス内部で利用しているクロックジェネレータの仕様によって、時間が経つほどズレが大きくなってくることがあります。
同期ズレをできる限り無くしたい、または同期ズレが起きた場合でも後で修正する仕組み(またはマスタリング)として時間の尺度としてタイムコード(Linear time code、またはSMPTE timecode)を利用するアイデアがあります。
前回の記事に引き続き、今回はマイク入力経由でタイムコード信号生成器から音声データとして受け取ったタイムコードを読み取る方法について記事としてまとめました。

synamon.hatenablog.com

ちなみに、60fps以上のフレームレートに対応したタイムコードについての規格についてはSMPTE ST 12-3:2016やITU-RのRecommendation BT.1366-3のドキュメントで読めますが、SDI信号として送受信するアンシラリータイムコード(ATC)についての記述がメインで、音声信号経由で送受信できるかについては書かれていませんでした。(SDI信号はGHzレベルの周波数で信号を送っているため、音声信号として受け取ることができません)

タイムコード信号を波形画像から読み取る

いきなりUnity側の処理に移ると何をしているか分かりづらいと思うので、まずはタイムコード信号を波形画像として表示し、画像から波形に含まれているデータを読み取る方法について解説します。

事前セットアップ

前回の記事でも紹介しましたスマホからタイムコードの信号を生成してくれる、「TimeCode Generator」を使います。

timecodesync.com

3.5mmオーディオジャックを使ってスマホのイヤフォン出力をPCの音声入力につなげた状態で、タイムコード信号をAudacityで信号をキャプチャします。

www.audacityteam.org

実際のタイムコードの信号の波形

タイムコードの信号をAudacityでキャプチャした際の波形はこのようになります。タイムコード信号はパルス波で送られてきます。

これだけでは分からないので、この信号について解説します。タイムコード信号は長い波長を0、短い波長の正負のペアをとしてみなす、Biphase Mark Code(Differential Manchester encodingのエンコードバリエーション)というデジタルデータのエンコード方式が利用されています。(以下の画像はWikipediaの画像の抜粋になります)

先ほどの波形信号を読み取ると、以下のようになります。今回利用するタイムコードはLinear timecode(SMPTE timecode)の規格に則っているので、一つのタイムコードのデータは80ビットで表現されます。赤の縦線が入っている間に80bit分のタイムコードの波形データが含まれています。それぞれの波形をBiphase Mark Codeとして読み取ったものが赤で書かれた[0, 1]の文字になります。

ビットデータをタイムコードに変換する

Wikipediaにタイムコードのデータフォーマット表があるので、これを元に読み取ってみたいと思います。もっと詳しく知りたい方は、ITU-RのRecommendation BR.780-2に詳しく解説されているので、ご興味があればお読みください。

波形画像からビットデータを取り出し、整理すると以下のようになります。(時間以外のデータの表記については省略しています)

これをタイムコードとして数値表現すると、「06:18:36:28」になります。音声の波形からタイムコードを読み取ることができました。

Unity上でタイムコード信号を読み取る

上記ではAudacityでタイムコード信号を直接波形から読み取る方法について紹介しましたが、これ以降はUnity上で動作するスクリプトからタイムコード信号を処理してタイムコードとして取得する方法について解説します。
今回はGistにソースコードを置いております。このコードをベースに記事を書いておりますので、参考しながら記事を読むのをお勧めします。

Unity上で音声信号からLinear timecodeに変換するヘルパーメソッド · GitHub

事前セットアップ

必要パッケージを追加する

今回のコードに含まれる計算処理の一部に com.unity.mathematicsを使っているため、PackageManagerから追加しておく必要があります。
追加方法はPackageManagerの【Add package from git URL...】から com.unity.mathematics を入力して【Add】ボタンを押してインストールします。

AudioのDSP Buffer Sizeを変更する

Project Settingsから、AudioのDSP Buffer Sizeを【Best latency】に変更しておきます。これはマイク入力の遅延を減らすことができます。

信号をUnityで受け取る

前回の記事でも書きましたが、マイク入力経由でタイムコード信号を受け取る処理ですが、一つのソースコードに書くと全体が長くなりがちなので、信号を処理すること自体は別のコードで行うようにしました。

https://gist.github.com/blkcatman/35fffc8e566a81aad09045ec24ee7bb4#file-microphonegrabber-cs

    private void OnAudioFilterRead(float[] data, int channels) {
        // オーディオデータの処理を外部に移譲する
        OnAudioDataRead?.Invoke(data, channels);

        // そのままだとTimecodeの音声が鳴り響くので、ゲインを0にする
        for (int i = 0; i < data.Length; i++)
        {
            data[i] *= 0f;
        }
    }

処理側はTimecodeDecorderクラスのProcessAudioData()メソッド内で行うようにしています。

https://gist.github.com/blkcatman/35fffc8e566a81aad09045ec24ee7bb4#file-timecodedecoder-cs

    private void ProcessAudioData(float[] data, int channels)
    {
        // ここにTimecode取得の処理を記述する
        ...
    }

以降はTimecodeDecorderのProcessAudioData()メソッド内の処理をベースに解説していきます。また、メソッド内ではヘルパー的にLtcHelper.csの処理を利用しています。

https://gist.github.com/blkcatman/35fffc8e566a81aad09045ec24ee7bb4#file-ltchelper-cs

無信号時を検出する

信号が来ていない、または無信号の時も処理が走ってしまうので、ProcessAudioData()のメソッド内でタイムコードの信号の解析をする前に無信号状態を検出しています

        // 信号の振幅が一定未満なら、無信号とみなす
        if (!LtcHelper.HasSignal(maxLevels, 0.2f))
        {
            if (currentBinarySize > 0)
            {
                Span<float> source = new Span<float>(binaryBuffer, 0, currentBinarySize);
                source.Fill(0f);
                currentBinarySize = 0;
            }
            return;
        }

タイムコード信号のチャンネルデータを取り出す

OnAudioFilterRead経由で取得したデータはステレオになっていますが、タイムコード信号は左側のみチャンネルしか受け取っていないため、右側のデータを削除します

        // チャンネル一つぶんの信号を取り出す
        var monauralDataLength = data.Length / channels;
        float[] monauralData = new float[monauralDataLength];
        Span<float> monauralSource = new Span<float>(monauralData);
        LtcHelper.GetMonauralData(data, ref monauralSource, channels, 0);

信号を2値化する

タイムコード信号はマイク入力経由で受け取っているため、一度音声信号としてアナログ変換されます。それをPC側で量子化してデジタルデータ化しているため、ノイズが混入したり、パルス信号の波形がなまる(立ち上がり時間が増える)問題があります。
このため、信号処理ではパルス幅を測定する際に振幅(Amplitude)にしきい値(下限)を設けるか、フィルタリング処理が必要になります。
本来であればパルス整形フィルタ(例:ガウスフィルタ)などを掛けるのがベストですが、Unity側で処理を簡略化するため、雑ですが信号そのものを1.0と-1.0に2値化して、パルス整形フィルタの代わりにします。

        // 信号を2値化する
        float[] binarizedData = new float[monauralDataLength];
        Span<float> binarizedDest = new Span<float>(binarizedData);
        LtcHelper.Binarize(monauralSource, ref binarizedDest);

2値化した後はこんなイメージになります。

2値化した信号をバッファに溜める

OnAudioFilterRead()メソッド経由で受け取るバッファサイズはDSP Buffer Sizeに影響を受けるため、OnAudioFilterRead()から呼び出された1回分の信号データにはタイムコードをデコードできるほどのデータ量が含まれていないことがあります。
このため、2値化したデータをある程度のデータ量になるまでバッファに保存します。一定以上データがたまったら信号をタイムコードとしてデコードする処理を行います。

        // 2値化データをバッファにコピーする
        var storeDest = new Span<float>(binaryBuffer, currentBinarySize, monauralDataLength);
        binarizedDest.CopyTo(storeDest);
        currentBinarySize += monauralDataLength;
        
        // 一定以上データをバッファに蓄積したら処理を行う
        if (currentBinarySize > 2000)
        {
        ...
        }

各パルスのパルス幅を取得する

パルスが0のビットを表現しているのか、それとも1のビットを表現しているかを知るためには、事前に各パルスのパルス幅を計算する必要があります。信号は既に2値化されているので、パルス幅の測り方は信号の符号の入れ替わりで計算することができます。プロセスについては以下のようになります。

  1. 信号のカウントを開始する
  2. 前後の信号を比較し、信号どうしの符号が同じなら、同じパルスと見なせる
  3. 同じ符号を持つ信号をカウントする
  4. 比較対象の符号が異なる時、それまでの信号のカウントをパルス幅として確定し、以降は別のパルスと見なす
  5. 1~4の処理を繰り返す

コードでも同様に、パルス幅の判定に連続する信号の符号を利用し、波長を計算しています。

            // 信号の波長を取得する
            var signalSource = new ReadOnlySpan<float>(binaryBuffer, 0, currentBinarySize);
            var waveLengthArray = 
                LtcHelper.GetWaveLengthArrayFromBinarizedFloats(signalSource, samplingRate);

パルス幅からビットを推定する

タイムコードの信号はBiphase Mark Code方式でエンコードされているので、0か1のビットを判断するにはパルス幅で判断する必要がありますが、タイムコード(Linear time code)はフレームレートによってパルス幅が変わってきます。

Wikipediaではパルス幅は30フレーム時2400Hz (= 416.66マイクロ秒)という表現になっていました。

Made up of 80 bits per frame, where there may be 24, 25 or 30 frames per second, LTC timecode varies from 960 Hz (binary zeros at 24 frames/s) to 2400 Hz (binary ones at 30 frames/s), and thus is comfortably in the audio frequency range.

ITU-Rのドキュメントでは30フレーム時の0ビットのパルス幅は417マイクロ秒、1ビットのパルスは片側208.5マイクロ秒と記述がありました。

流石に今回の記事でパルス幅を自動的に判断する方法までは入れられなかったのと、30fps以下にタイムコードを設定する意味も薄いため、タイムコード信号発生器のフレームレートは30fps固定として、0のビットと1のビットを判定するパルス幅は決め打ちで設定しました。
コードではノイズ等でパルス幅が長くなったり短くなったりすることを想定して、それぞれのビットのパルス幅の判定基準に上限下限のしきい値を設けてビットデータの配列に変換しています。

            // ビットデータ配列に変換する
            var bitArray = LtcHelper.DecodeWaveLengthToBits(waveLengthArray,
                0.0004f, //0ビットとして判定するパルス幅の下限
                0.0005f, //0ビットとして判定するパルス幅の上限
                0.0002f, //1ビットとして判定するパルス幅の下限
                0.00026f //1ビットとして判定するパルス幅の上限
            );

ビットデータ配列からSync wordを探す

Sync wordはタイムコード信号の始まりから終わりまでを区別するための固定の16ビット(0011111111111101)のデータです。Sync wordの前後にタイムコードの実データが含まれているので、ビット変換後はSync wordを頼りにタイムコードのデータの区切りを判別することができます。
Sync wordはタイムコードの実データ部の後ろに付与されているので、Array.LastIndexOf()メソッドを使って要素の最後から検索しています。

            // ビットデータ配列内にSyncWordが含まれているかチェックする
            var syncWordPosition = bitArray.LastIndexOf(LtcHelper.SyncWord, StringComparison.Ordinal);
            // SyncWordが入っていない場合は処理を中断する
            if (syncWordPosition < 0)
            {
                // 1秒以上バッファにデータがたまっているときはバッファを初期化する
                if (currentBinarySize > samplingRate)
                {
                    var clearSource = new Span<float>(binaryBuffer);
                    clearSource.Fill(0f);
                    currentBinarySize = 0;
                }
                return;
            }

Sync wordより前のデータ(64bit)をタイムコードしてデコードする

Sync wordの間にあるビットデータはタイムコードの実データになるので、Sync wordが見つかったビットのインデックス位置よりも前の64bit分のデータをデコードすることでタイムコードを取り出すことができます。
コードではタイムコードのデータフォーマットに従ってデコードしています。また、冗長な処理かもしれませんが、コードではSync word分も取り出して、最後にバリデーションをかけています。

            // SyncWordが含まれている箇所より前にタイムコードを計算できるデータ量が含まれている時
            if (syncWordPosition >= 64)
            {
                // タイムコード80bitぶんのデータを切り出す
                var bitData = bitArray.Substring(syncWordPosition - 64, 80);
                // タイムコードに変換する
                if (LtcHelper.DecodeBitsToTimecode(bitData, out var timecode))
                {
                    CurrentTime = timecode;
                }
            }

2値化した信号データをバッファから削除する

一度デコードしてタイムコード化したデータは不要になるため、タイムコードとして認識した信号データよりも前の2値化した信号データをバッファから削除します。

            // 後処理: バッファの先頭からSyncWordの末尾の位置を取得する
            var segmentPosition = waveLengthArray[syncWordPosition + 16].Position;
            var tempBufferSize = currentBinarySize - segmentPosition;
            if (tempBufferSize > 0)
            {
                // SyncWordの末尾からのデータをバッファの先頭に移動する
                var tempBuffer = new float[tempBufferSize];
                var temp = new Span<float>(tempBuffer);
                // Tempバッファにデータをコピー
                var tempSource = new Span<float>(binaryBuffer, segmentPosition, tempBufferSize);
                tempSource.CopyTo(temp);
                // Tempバッファを2値化データ用のバッファの先頭にコピーする
                var bufferDest = new Span<float>(binaryBuffer, 0, tempBufferSize);
                temp.CopyTo(bufferDest);
                currentBinarySize = tempBufferSize;
            }

できあがったもの

タイムコードの表示がUnity上でできるようになりました。マイク入力にOSネイティブのドライバ(ASIOなど)を使ったり、コードをチューニングすることで遅延をもっと減らすこともできると思います。

まとめ

この記事ではタイムコード信号生成器から発生した信号をマイク入力経由でUnity上でもタイムコードを扱えるようにする内容について紹介しました。次回以降はこのタイムコード表示を使った同期方法の検討や、タイムコード以外の方法を使った同期方法についても記事にできればと思います。

InputCompositeBindingのTouchscreenでの応用例の紹介

こんにちは、エンジニアリングマネージャーの渡辺(@mochi_neko_7)です。

前回、Unityの(New) Input Systemでデバイス入力をカスタマイズできるInputCompositeBindingを紹介しました。

synamon.hatenablog.com

こちらの記事ではInputCompositeBindingをそもそも知らない方が多いと思い、基本的な説明から行ったため、あまり応用の具体的な話ができませんでした。

今回はまだ低レベルなAPIしか用意されていないTouchscreenの入力の具体的な応用例を2つ紹介しながら、読者の方がよりInputCompositeBindingを理解し自身で実装ができるイメージを持てることをゴールにしたいと思います。

それではさっそく見ていきましょう。

1. TouchscreenでのTapした位置の入力

スマートフォン向けのアプリで、指でタップした位置にある3D空間内のオブジェクトになにかのインタラクションをしたい、というケースを想定しましょう。

それを実現するための一つの例として、以下のようなフローが考えられます。

指でタップしたスクリーン位置を取得する

→ スクリーン位置をCamera.ScreenPointToRay(...)で3D空間のRayに変換する

→ Rayを使って3D空間内でRaycastなどをしてオブジェクトを検索する

→ 見つかったオブジェクトになにかのインタラクションをする

ところがUnity標準のTouchscreenのBindingでは、Touch入力は0~9の10個のTouchControlが提供されていて、指定したTouchのTapやPositionは取得できますが、「TapしたTouchのPosition」を取るのは素直にはできません。

docs.unity3d.com

このような時にはInputCompositeBindingの出番です。

実際にソースコードを見ながら解説しますが、基本的な部分は前回の記事で丁寧に紹介したため、ここではカスタマイズのコアな部分のみ紹介します。

自作したクラスの内部に以下のような処理を記述します。

...
        // Please input path:"<Touchscreen>/*touch/tap" to update value.
        [InputControl(layout = "Button")] public int anyTap;

        public override Vector2 ReadValue(ref InputBindingCompositeContext context)
        {
            var touches = Touchscreen.current.touches;
            for (var index = 0; index <= 9; index++)
            {
                var touch = touches[index];
                if (IsEffectiveTap(touch))
                {
                    return touch.position.ReadValue();
                }
            }

            return Vector2.zero;
        }

        public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
        {
            return ReadValue(ref context).magnitude;
        }

        private static bool IsEffectiveTap(TouchControl touch)
        {
            return touch.tapCount.ReadValue() > 0;
        }
...

最も重要な入力の加工処理は、public override T ReadValue(...) で行います。

今回はTapした位置を取得したいため、TにはVector2を指定してクラスを定義しています。

ReadValue(...)では、はじめにTouchscreen.current.touchesで現在のTouchControl10個の配列を取得します。

それからfor文で順番にTouchControlを見て、IsEffectiveTapがtrue、つまりTapをしているTouchを判定して、それが見つかった際にTouchControl.position.ReadValue()でPositionを返すようにしています。

ロジックとしてはとてもシンプルかと思います。

ただ注意しなければならないのが、ReadValue(...)以外の点です。

EvaluateMagnitude(...)のoverrideの実装ですが、これはReadValue(...)の結果を流用しているだけなのですが、この実装とセットでActionに設定する際にInteractionsのTapを設定することでTap操作としての振る舞いをさせることができるようになります。

次に最初から用意されているTouchControl.tap.ReadValue()というTap判定が(不具合なのか?設定が悪いのか?)正常に取得できませんでした。

そのため厳密には正しくはないのですが、Tapすると数が増えていくTouchControl.tapCount.ReadValue()を見てTapしているかどうかの判定としています。

ここの部分は不具合の可能性もあるため今後は改善されるかもしれません。

最後に、本来ReadValue(...)内部で使用すべきものですがここでは使用していない[InputControl(layout = "Button")] public int any;を説明します。

ご覧のとおり今回の処理では特にInputActionAsset上でのBindingsのセットアップをすることなく入力の取得ができるように見えると思います。

しかしこのBindingsを一つも定義しない場合、InputActionAsset上で自作したInputCompositeBindingにControlSchemeの指定欄がないため、ControlSchemeの指定ができない、つまりどのデバイス入力に対応して入力を流すのか指定できないこととなり、そもそも入力がまったく入らなくなってしまいます。

では適当に定義しておくとして何でも良いかというとそうではなく、そこで指定したBindingsの入力が更新されるタイミングで、自作したBindingsの入力も更新される仕様となっているため、自作した入力を利用したいタイミングと同じタイミングで値が更新されるBindingsを指定する必要があります。

今回は何かしらのTap操作のタイミングで入力が渡せられればよいので、ActionAssetのEditorで <Touchscreen>/*touch/tap をBindingsに設定します。

Tのアイコンを押すとテキストで指定できます、* はワイルドカードです。

ここは動かしてみないと分からない点なので気を付けていただきたいのと、後から見ても何が目的なのか分からないためコメントを残しておくことをおすすめします。

...

以上がTouchscreenでのTapした位置の入力のBindingのカスタマイズ例になります。

後半で説明したやや複雑な部分もありはしますが、ロジック自体は簡潔かと思います。

ちなみにこのようなCustom Bindingを作ると、例えばPCでのマウスのクリックとスマホでのタップ操作の切り替えをInput System側だけで完結させることができ、入力を利用する側ではデバイスの差異を気にすることなる処理をすることができるようになります。

2. Touchscreenの仮想Button入力

次はスクリーン上の仮想ボタンを押して何かのアクションをさせたいケースを想定しましょう。

スクリーン上のどこかに円形のボタンがあり、その領域内を指で押す判定を取りたいのですが、既存のBindingsでは領域を絞った入力を取ることは難しいです。

そのため、やはりこのケースでもInputCompositeBindingで自作をする必要があります。

1と同じくコアな部分の実装例だけ見ていきましょう。

...
        // Please input path:"<Touchscreen>/touch*/press" to update value
        [InputControl(layout = "Button")] public int any;

        public Vector2 centerPosition = Vector2.zero;
        public float radius = 1f;

        public override float ReadValue(ref InputBindingCompositeContext context)
        {
            var touches = Touchscreen.current.touches;
            for (var index = 0; index <= 9; index++)
            {
                var touch = touches[index];
                if (IsEffectiveTouch(touch))
                {
                    return touch.press.ReadValue();
                }
            }

            return 0f;
        }

        private static bool IsEffectiveTouch(TouchControl touch)
        {
            return touch.isInProgress
                   && IsInsideOfCircle(touch.position.ReadValue(), centerPosition, radius);
        }

        public static bool IsInsideOfCircle(Vector2 touchPosition, Vector2 centerPosition, float radius)
        {
            return Vector2.SqrMagnitude(touchPosition - centerPosition) <= radius * radius;
        }

        public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
        {
            return ReadValue(ref context);
        }
...

まず一番重要な ReadValue(...) から見ていきます。

TouchControlの配列を取得して、0から順番に見ているのは変わりません。

そして IsEffectiveTouch(...) がtrueになるTouchControlを探して、その TouchControl.press の値を返すという処理をしています。

基本的な考え方はTap位置の取得の時と変わりませんね。

大事なのは、private static bool IsEffectiveTouch(TouchControl touch)の判定部分です。

一行目の TouchControl.isInProgress はそのTouchが今何かしらの入力状態にあるかどうか(厳密にはTouchPhaseがBegan/Moved/Stationaryのいずれかにあるかどうか)をチェックしています。

二行目の IsInsideOfCircle(...) でTouch位置が円形のボタン位置の内側にあるかどうかをチェックしています。

ちなみに意味合いとしては Vector2.Distance(...) のほうが直感的なのですが、内部で平方根の計算をしているため処理が少し重いため、代わりに Vector2.SqrMagnitude(...) を使用するほうがパフォーマンスが良いです。

[InputControl(layout = "Button")] public int any;EvaluateMagnitude(...) の実装は1の場合と同じですね。

注意事項もいくつかあります。

まずさせたい振る舞いがButtonなのでboolを入力として取り扱いたくなるのですが、あくまで 0~1の値を取る float を指定し、Actionに設定する際にInteractionsにPressを設定することでButtonの振る舞いをさせることができます。

また、Buttonの位置をcenterPositionradiusで指定していますが、GUIを出したい場合uGUI×EventSystemで扱う場合と違ってGUIの配置と一致しているとは限らないため、Binding側での判定領域とGUIの配置の整合性を自分で保障してあげる必要があります。

決め打ちなら何も問題はないのですが、実行時にSafeAreaを考慮するなどしたい場合はBinding側に位置の計算ロジックを持たせて、GUI側がそれを見てuGUIを動かすなどしてあげるとよいかもしれません。

また他の入力操作との重複を避ける必要もあると思いますのでご注意ください。

...

以上がスクリーン上での仮想Button入力の取得の実装例とその説明になります。

こちらも1でお作法が分かってしまえばシンプルなロジックで実装できることが分かるかと思います。

また、このBindingを使用すると、PCでのキー入力とスマホでのボタン操作の切り替えを容易に実装できることが分かるかと思います。

実装の注意点のまとめ

ロジック以外の部分で実装時に注意すべき項目を整理しておきます。

  • 基本的なお作法に則っているか?
    • これは前回の記事で紹介した範囲です
  • <T>の指定は適切か?
    • ActionAssetで思った通りに設定できるか試してみれば分かると思いますが、Actionの取り扱う型とCompositeBindings<T><T>を一致させる必要があります
    • 特にButtonはboolではなくfloatです
  • [InputControl] public int ***を定義し、ActionAssetで適切に設定できているか?
    • ロジックで使用しなくても必要です
    • 普通に利用する際は、型の指定がButtonなどの文字列である点に注意してください
  • EvaluateMagnitude(...)のoverrideは実装しているか?
    • 実装の有無で振る舞いが変わります
    • なぜか実装しないほうが良いケースもあるため注意してください
  • GUIとの整合性は取れているか?
    • EventSystemを経由しないため、実行時に調整をする際には気を付けてあげる必要があります

おわりに

InputCompositeBindingは特に低レベルのAPIしかないTouchまわりでは有効なのですが、まだまだ初見では分かりづらいポイントや落とし穴があり、とっつきづらい部分も多いです。

そもそも他デバイスとの共通化をまったく考えなくてもいいのであれば、Actionをただの入力として扱い、Actionを利用する側に入力処理のロジックを持たせればわざわざこのような実装はしなくても済みます。

ですが何度も説明しているように、逆にうまく使いこなすことができればデバイスの差異をうまくInputSystem側で吸収することができ、アプリケーション全体の設計として綺麗にまとまりやすくなると思います。

現在はこうやって自作する必要がありますが、将来的にTouchのジェスチャー系の実装の予定もあるとか聞きますし*1、もっと手軽に扱えるようになるかもしれませんね。

それを待たずともこのあたりの処理を汎用化してライブラリにしてあげるといいのかもしれませんが、特に入力領域を指定する場合に入力処理同士の重複を避ける処理がなかなか汎用化しづらいのが難点です。

今回はTouchでの応用例のみ紹介しましたが、もちろんそれ以外でも標準のBindingsでは欲しい形で入力を取得できない場合には有用ですので、困ったときには使用を検討してみるとよいかと思います。

以上、前回の記事と併せてInputCompositeBindingを使ってみたい方の参考になれば幸いです。

*1:> 新しいInput Systemは現時点では基本的なタッチ入力しかサポートしていませんが、将来的にはジェスチャー入力等より高度な表現にも対応する予定とのことです。> https://forpro.unity3d.jp/unity_pro_tips/2021/05/20/1957/