ドメイン切替対応でリダイレクト用途だけのサーバをCloudfront+S3で作った話

はじめに

こんにちは、エンジニアのクロ(@kro96_xr)です。バックエンドを中心にフロントエンドやらインフラやら色々担当しています。
今回は弊社ティザーサイトのドメイン変更時にリダイレクト用のサーバが必要になり、Cloudfront+S3という構成で作成した話をしたいと思います。どちらかというと社内SEやコーポレートエンジニアの方向けっぽい内容ですかね。

背景

元々弊社ティザーサイトはhttps://metaverse.synamon.jp/という形でコーポレートサイトのサブドメインで運用していました。
しかし、メタバースブランディングプラットフォームとして「SYNMN」を公開するにあたり、https://synmn.biz/という新規ドメインを取得、運用することになりました。

prtimes.jp

その際に問題となったのが、旧サイトからのリダイレクトをどうするかという問題です。

ティザーサイト自体は外部のSaaSを使って構築されており、自社でサーバ管理をしているわけではありませんでした。そのため、弊社でリダイレクト用のサーバを用意することが必要でした。

方法を色々検討してみたものの、リダイレクトのためにEC2を建ててホスティングするのもなぁ…ということでS3の静的ホスティング+リダイレクト設定を使用することにしました。
また、S3の静的サイトホスティングではHTTPSに対応できないため、CloudfrontとACMも使用する必要がありました。

対応

使用したサービスは以下の通りです。

  • S3
  • ACM
  • CloudFront

DNS設定も必要ですが、コーポレートサイトドメインに関してはAWS外で設定しているため今回は除外します。
必要な場合はRoute53で設定すると良いのではないかと思います。

S3の設定ついて

Amazon S3では静的ウェブサイトをホスティングすることができます。 docs.aws.amazon.com

そしてこの静的ウェブサイトホスティング機能のオプションとしてリダイレクト機能があります。 docs.aws.amazon.com

公式リファレンスにもある通りS3では以下のようなリダイレクト設定ができます。

  • バケットのエンドポイントに対するリクエストを別ドメインにリダイレクトする
  • リダイレクトルール設定に基づきリダイレクトする
  • オブジェクトのリクエストをリダイレクトする

このあたりはClassmethodさんの記事が詳しいのでぜひ参考にしてみてください。 dev.classmethod.jp

今回は、ドメイン単位でのリダイレクトかつ複雑なルールも不要なので1つめの対応を行いました。

設定方法はバケットのプロパティから以下のように設定するだけです。

  • 静的ウェブサイトホスティング:有効にする
  • ホスティングタイプ:オブジェクトのリクエストをリダイレクトする
  • ホスト名:リダイレクト先の新ドメイン
  • プロトコルオプション:HTTPS

設定するとバケットウェブサイトエンドポイントが表示されるのでこちらをCloudfrontで使用します。

ACMについて

Cloudfrontで使用するためAWS Certificate Manager(ACM)で旧ドメインの証明書を取得します。
Cloudfrontで使用する場合はバージニア北部(us-east1)で取得する必要がありますのでご注意ください。

Cloudfrontについて

Cloudfront経由でS3にアクセスするためディストリビューションを作成します。
設定内容は以下の通りです。書いていない部分は特に変更していません。

  • オリジンドメイン:S3で発行されたバケットウェブサイトエンドポイント
  • 代替ドメイン名 (CNAME):旧ドメイン
  • カスタムSSL証明書:ACMで発行された証明書

これでAWS側の設定は完了です。

DNSの設定について

最後にCloudfrontのエンドポイントをDNSに設定してやります。
Route53で管理している場合は、「Cloudfrontディストリビューションへのエイリアス」を選択すれば作成したディストリビューションが出てくるかと思います。
弊社の場合はAWS外のDNSを使用しているため、以下のようなDNSレコード設定をしています。

  • サブドメイン:metaverse
  • 種別:ALIAS
  • 内容:ディストリビューションのエンドポイント

これで設定は完了です。お疲れさまでした!

最後に

普段はサービスのバックエンド開発をしているのですが、S3のリダイレクト機能は使ったことがなかったので勉強になりました。
AWSを触ったことがある方であればさほど難しくない内容かと思いますので、ドメイン変更時などリダイレクトが必要になった場合はぜひ試してみてください!

Oculus IntegrationからQuest Proの新機能を触ってみる(視線追従)

はじめに

エンジニアの松原です。運よくQuestProの発売日の直後(10/26)に届き、Quest2で積みっぱなしになっていた釣りゲームを週末やっていました。
ふと、最近Unity上でHMDを使った開発をしていないことに気づき、この機会にQuest Proの新機能に触れる目的で、久しぶりにOculus Integrationを利用しました。Unityの空のプロジェクトファイルから、視線追従機能を利用するまでの手続きを記事にしてみました。
今回利用した環境(+パッケージ)は以下の通りです。

  • Unity 2022.1.21f1
  • XR Plugin Management 4.2.1
  • Oculus XR Plugin 3.2.1
  • Oculus Integration 46
  • Windows Platformで実行

Quest Proを開発者モードにする

今回は直接UnityEditor上で動作しているアプリをQuestLink(旧OculusLink)経由でQuest Proの機能を利用することになるのですが、 Quest Proを開発者モードにしておく必要があります。開発者モードが有効になっていない場合、実行時にQuest Pro本体側に「com.oculus.bodyapiservice が停止しました」などのダイアログが出て、正常にQusetProの機能が利用できません。

Quest Proの端末自体を開発者モードにする方法として、スマホの「Meta Quest」アプリからQuest Proを登録しておき、開発者モードに切り替えます。

アプリのメニューから、デバイスをタップします。

ヘッドセットの設定の項目から、開発者モードの箇所をタップします。

開発者モードのトグルをONにします。

これでQuest Pro本体側の開発者モードは有効になりましたが、引き続き以下のQuestのアプリの設定を変更する必要があります。

PC側のOculusアプリでQuestLink(旧OculusLink)の設定を変更する

WindowsPC用のQuestアプリから設定を変更します。

設定 > ベータ のタブから、「開発者ランタイム機能」と「Oculus Link経由でのアイトラッキング」のトグルをONにします。

その他の項目は任意ですが、Quest Proの開発をしたい場合は周囲確認や自然な表情機能のトグルをONにしていても良いかもしれません。

QuestLinkを有効にする

QuestLinkに関してはQuest Pro本体側で操作する必要があるため、以下のURLを参考にしてQuestLinkを有効にしてください。 リンク先の内容は最新のものではないですが、QuestProの操作参考にはなると思います。

www.meta.com

以上でUnity以外のアプリ関係の設定は完了です。

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

ここからはUnityの作業になります。空のUnityプロジェクトを作成し、Package Managerから Oculus Integrationをダウンロード、インポートします。

続いてXR Plugin ManagementとOculus XR Pluginをダウンロードします。

Project SettingsのXR Plug-in ManagementからOculusのPluginを有効にします。

パッケージ関連の設定は以上で大丈夫です。

HMDを利用できるようにする

ここから最低限、HMDを動作させるための作業を行います。

OVRCameraRigをHierarcyにセットする

Oculus Integrationを使う場合、あらかじめ用意されているHMDのPrefabを利用することで簡単にHMDとして動作させることができます。 Assets > Oculus > VR > Prefabs 配下の OVRCameraRigをHierarchyにコピーします。

Unityを実行する

Unity Editor上で再生ボタンを押します。UnityのGame画面がHMDに追従していて、OVRCameraRig内のCenterEyeAnchorの数値が変わっていれば初期セットアップは成功しています。

確認が終わったらUnity Editor上での再生を止めます。

視線追従のスクリプトを追加する

Oculus Integration(version 46)にはOVR Eye Gazeというスクリプトが入っており、これを眼球として動かしたいGameObjectにアタッチすることで、Quest Proで取得した眼球運動をモデルにアタッチできます。

詳しくはMetaの公式サイトにこのスクリプトについて解説が載っています。

https://developer.oculus.com/documentation/unity/move-eye-tracking/

実際にはこのスクリプトだけだと制御が難しいため、フィルター処理を掛ける必要があるのと、まばたきの情報は取れないので、もう一つのフェーストラッキングの機能と組み合わせる必要が出てきそうです。 ここに関しては利用するモデルに大きく影響するため、具体的なフィルター処理の内容に関しては今回割愛させていただきます。

できたもの

今回キャラクターにSDユニティちゃん © UTJ/UCLを利用させていただきました。

取得した眼球運動そのものをアニメ長のモデルに直接あてはめてしまうとモデルが破綻してしまうため、今回はかなり強めにフィルタ処理を掛けています。そのため眼球運動の変化が分かりづらくなっていますね。 他、目蓋の開き具合が変わらないため、不自然さが残っているため、次回やるとしたら綺麗に見えるようにフェーストラッキングによる調整も掛けることを検討したいです。

さいごに

Quest Pro単体でアイトラッキングや表情検出、またはボディトラッキングなどできるようになることが増える反面、細かい調整に関してはやはりエンジニアが行っていく必要がありそうです。
引き続きQuest Proのキャッチアップを続けていきたいです。

ソフトウェアライセンスでヒヤッとした事例から学んだ教訓

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

少し前にTwitterで、GitHub CopilotがLGPLライセンスのコードをライセンス表記なしに出力する、という話が話題になったのをご存じですか?

これを見て、最近リリースしたばかりの弊社プロダクトのSYNMN

synmn.biz

の開発でもとあるライブラリを導入する際にソフトウェアライセンスに関してヒヤっとした事例があったことを思い出しました。

結果的にリリース前に解決ができたため問題はなかったのですが、これをきっかけに自身のソフトウェアライセンスに対する危機意識が高まったこともあり、記事化して振り返ってみたいと思います。

遭遇した事例

SYNMNでは動画コンテンツをメタバース空間内で使用できる機能を仕込んでいるのですが、動画プレイヤーにはUnity標準のものではなく、AVPro VideoというAsset Storeで販売されているライブラリを使用しています。

このライブラリはWindows、Android、macOS、iOS、UWP、WebGLとかなり幅広いプラットフォームに対応しているのが特徴の一つなのですが、その各プラットフォーム別にネイティブライブラリを内部で使用していることもあり、内部で多くの3rd Partyライブラリを使用しています。

AVPro Video自体はAsset Storeで販売されていることもありStandard Unity Asset Store EULAというライセンスに準拠しますが、このライブラリを使用したアプリケーションを配布する際には内部で使用しているライブラリのライセンスにも準拠する必要があります。

つまりライセンスは依存関係をもつので、使用するライブラリ自体のライセンスが問題なくても、その内部で使用しているライセンスもきちんと確認をする必要があります。

当時ver2.5.1の時点でAVProが内部で使用しているライブラリを入念に確認をしていたのですが、HapAVFoundationというライブラリが更にその内部でREAL-TIME YCOCG DXT COMPRESSIONというコードを含んでいて、そのライセンスがGNU Lesser General Public License(LGPL)と表記されていることを発見しました。

実際に発見をしたのは自分ではなく同僚のベテランエンジニアの方なのですが、その方がソフトウェアライセンスに詳しいこともあり、問題の発覚に至りました。

LGPLライセンスはどう問題なのか

LGPLライセンスの公式のリンクはこちらです。

opensource.org

LGPLライセンスの解説などはこちらなどを参照してください。

office54.net

GPLライセンス(およびその緩和版のLGPLライセンス)の恐ろしい特性はCopy Left、つまり「GPLライセンスのプログラムを使用した場合、制作物もGPLライセンスで配布する必要がある」というものです。

もし自作のソフトウェアにGPL/LGPLライセンスのソースコードを組み込んだ場合には、(LGPLは条件付きですが)組み込んだソフトウェア自体もGPL/LGPLライセンスで「公開」する必要があるため、特にソースコードを公開したくない商用のソフトウェアを開発する際にはかなり注意する必要があります。

今回の件ではLGPLの緩和条件である「動的リンクで使用する場合は例外」が、AVPro Video自体がDLLなどのバイナリーにビルドされていることもあり、自身で直接確認することができませんでした。

開発元に問い合わせた結果

LGPLライセンスの緩和条件が確認できない以上、そのままライブラリを組み込むのはリスクがあります。

分からないものは仕方がないので、開発元であるRenderHeads社にメールで問い合わせをしました。

やり取りは英語だったので意訳になりますが、返答は「REAL-TIME YCOCG DXT COMPRESSIONは使ってないけど、macOSのプラグインにリンクが残っているから、次のリリースで削除するよ」というものでした。

そして問い合わせから約9日後にはver2.5.2がリリースされ、無事該当の変更が取り込まれ、AVPro VideoからLGPLライセンスのコードが完全に取り除かれました。

github.com

Removed LGPL licensed code dependency from HapInAVFoundation

まとめ

今回遭遇した事例をまとめると以下になります。

  • UnityのAsset Storeで購入したライブラリにLGPLライセンスの表記を含むことが発覚
  • 調査するも、LGPLライセンスの緩和条件に該当するか判明せず
  • 開発元に問い合わせると、使用はしていないものの部分的にリンクが残っていることを確認
  • 次のリリースで修正をしてもらう

そしてこの出来事から得られた教訓は以下になります。

  • ソフトウェアライセンスは原文をちゃんと読んで内容を確認すること
  • ソフトウェアライセンスにも依存関係があるので、内部で使用しているライブラリのライセンスもきちんと確認すること
  • GPL/LGPLライセンスは特に取り扱いが注意なので、見かけた場合にはきちんと確認をすること
  • 確証が得られない場合は開発元に問い合わせること

ソフトウェアライセンスは適切に扱わないとソースコード開示の事態につながってしまったり、会社や開発者の信用を損なってしまう恐れがあります。

最近のOSSではMITライセンスなど比較的緩いライセンスが用いられることも多いですが、今回登場したLGPLライセンスなど種類によっては取り扱いの注意が必要なものもあるため、外部のライブラリやアセットを利用する際には入念に確認をしたほうがよいでしょう。

また逆に自身の書いたコードやリポジトリにちゃんとライセンスを明記することも重要です。

自分もまだまだ勉強不足な部分も多いので、今後もソフトウェアライセンスには気を配っていきたいと思います。

メタバース企業でのバックエンドエンジニアの役割について

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

今回はいつもと少し毛色を変えて、イベントレポ兼採用寄りのお話をしたいと思います。
なお、タイトルでメタバース企業と銘打ってしまいましたが、他社の状況はわかりませんのであくまでSynamonの場合と捉えていただけますと幸いです。

はじめに

Synamonでは様々な求人を出しており、その中にバックエンドエンジニア(=サーバサイドエンジニア)の求人もあります。

↓記事執筆時点での求人↓ herp.careers

しかしながら、採用チームから「メタバース企業のバックエンドエンジニアって何をやっているのかイメージが付きづらい」という声があると聞きました。

たしかに、VRあるいはメタバースというとUnityやUnrealEngineといったゲームエンジンを使った開発をイメージする方が多いのかもしれません。しかし、実際にはこれまでのWebサービス開発と同様にバックエンドエンジニアが活躍する機会は多くあると思っています。

今回はそのような疑問を少しでも払拭すべく記事を書いていきたいと思います。

本題に入る前に

SynamonではSYNMN(シナモン)というメタバースアプリを開発、オープンベータ版を公開しています。これ以降で出てくる"メタバースプロダクト"あるいは"プロダクト"といった言葉はこちらを指しますので、触ってみていただけるとイメージが湧きやすいかもしれません。

synmn.app

バックエンドエンジニアの役割について

まずはじめにバックエンドエンジニアの業務内容や役割について簡単に見ていきます。

システム概要図(超簡略版)

プロダクトのシステムを超簡略化して各エンジニア、モデラーの役割を割り振ると下記のようになります。

※書いてから気付きましたがWeb管理画面のWebサーバや、ストレージ等の記載が漏れています。超簡略図なので許してください。

バックエンドエンジニアは主にWebAPIの開発を担当しています。どうでしょう、この図を見ると他のWebサービスやスマホアプリ開発と変わらないのではないでしょうか?

バックエンドエンジニアとして要件定義、設計、実装、テスト、運用の経験があればスキル的には大きなズレなく活躍できるのではないかと思います。

また、Web管理画面のデザインや実装については他企業ではデザイナーやフロントエンドエンジニアの領域かと思いますが、Synamonではバックエンドエンジニアが業務委託の方と一緒に作っています。ここは少し特殊な点かもしれません。

インフラについて

そして、当然のことですがAPIサーバやデータベースなどを動かすためにはインフラ構築が必要となります。

インフラチームが別である会社も多いかと思いますが、Synamonではサーバサイドチームとしてバックエンドエンジニアが兼任しています。

Synamonでは主にAWSを使っていますが、AWSに限らずGCPやAzure等のクラウドを用いたインフラ構築、運用の経験をお持ちの方はスキルが活かせると思います。むしろ教えてください。

メタバースでの具体例

それではメタバースっぽい具体例を挙げてみます。

メタバースを構築する要素としてまず思い浮かぶのがアバターなどの3Dモデルかと思います。 SYNMNでは空間に必要な3Dモデルを事前にクラウドストレージにアップロードしています。

そして、ネイティブアプリ側で使用する前に必要なファイルをダウンロードしています。

実際にはファイルの種類によって処理が変わっていたりセキュリティ考慮したり色々やっていますがここでは省略しています。

書いてみると当たり前かもしれませんが、このような機能の実装にバックエンドエンジニアが関わっています。イメージ湧きましたでしょうか?

そして、これから正式リリースに向けて様々な機能開発が待っています!

続いて、同様のテーマについて話すトークイベントあったのでそちらについても触れておきます。

イベントレポ

実は、バックエンドエンジニアの役割がわかりづらいという声は前からあり、9月末にTwitterスペースで『バックエンドエンジニアがメタバース業界に飛び込んで、活かせたスキルと新たに身に着けたスキル』というテーマでトークイベントを行いました。

このイベントでは、SIer⇒Web業界とキャリアを築き、現在Synamonでバックエンドエンジニアとして働いているうぃすきーさんと私が話をしています。

twitter.com

このトークの中から、いくつか簡単に抜粋していきたいと思います。

Q.Web開発の経験で活きたところは?

管理画面やネイティブアプリから叩かれるWebAPIを開発しているため、スキルセット的にはWeb業界でもメタバース業界でもあまり変わらない。一般的なWebAPI開発に必要な要件定義、設計、実装、テスト周りの経験は活きていると思う。
補足)こちらはこの記事の前半で書いた通りです。

Q.メタバース業界だから苦労する点は?

同期処理や大規模トラフィックあたりは今まであまり経験が無いところなので、やろうとしたら苦労したと思う。Synamonではリアルタイムネットワークチームが別にあるのでそのあたりは分業化されている。
オンラインゲームやソシャゲのバックエンド経験がある方であればバックエンド、リアルタイムネットワーク両面で活躍できのではないか。

Q.どういう人であればメタバース業界のバックエンドエンジニアとして活躍できると思うか?

・メタバースに興味をもっていること。そしてそれに対して自分のスキルを活かして貢献したいというモチベーションがある方。
・スタートアップ全般に言えることだと思うが、自分でなんでもやりたい方、そしてやりきれる方。

Q.入社して成長したなと思うことは?

・スタートアップのため、分野を問わず自分が責任をもってやっており全方位に成長していると思う。
補足)例えば私の場合は、AWSでインフラ構築をした経験がほぼありませんでしたが、現在ではゴリゴリに触っています。
・何が事業成長に繋がるのかわからない、勝てるかもわからない中で「未知に立ち向かう力」が養われている。

以上、短いですがイベント内でお話しした内容をまとめてみました。

結論としては、

  • メタバースを創っているからといってバックエンドエンジニアは既存のシステム開発から大きく離れたことはしていない
  • むしろスタートアップへの適正(幅広い業務範囲への対応、自分で業務を進める推進力など)の方が重要かもしれない

といったところでしょうか。

おわりに

今回は、メタバース企業(=Synamon)におけるバックエンドエンジニアの役割について簡単にまとめてみました。
もし、ご質問やご指摘等ございましたらTwitterのDM等でご連絡いただけますと幸いです。

そして興味を持っていただけた方はお気軽にカジュアル面談にお越しください!

meety.net

Unityの設定を一括で切り替える為に作ったEditor拡張の紹介

エンジニアの岡村です。

Unityで開発をしていると、ビルドや実行を行う時に設定を一括で切り替えたいという事がよくあると思います。例えばデバッグビルドとリリースビルドで接続先のエンドポイントやAPIキー、アプリの名前を変えるといったものです。

その場合の対応方法としては、設定リストをメモに書いておく、UnityのPreset機能を使う、Editor拡張を自作するなど色々ありますが、弊社ではEditor拡張で専用のメニューを作成しています。これが実装されてから数年程経ち、使い勝手も良くなってきたため、今回この記事で紹介させて頂きます。

EnvironmentContextSwitcher

各設定を纏めたオブジェクト(コンテキスト)を複数用意し、簡単に切り替えられるよう、以下の画像のようなEditor拡張を作成しました。

図の左のリストがコンテキスト一覧、右が現在選択しているコンテキストの中身です。左下にはロック機能のアイコン、その右のチェックボックスはコンテキストの自動適用機能のON/OFFを切り替えます。

主な機能

コンテキストの追加、削除

左側のリストはReorderableListになっていて、ボタンでコンテキストの追加や並べ替えが出来るようになっています。

 

コンテキストの適用

コンテキストをダブルクリックすることで、コンテキストの内容を現在のプロジェクトに適用することが出来ます。コンテキストが持っている適用処理によって、選択されたコンテキストの内容がコピーされて書き込まれます。

コンテキストの定義の編集

コンテキスト定義用のC#クラスを用意しており、その内容を編集することで定義を増やすことが出来ます。SerializeFieldに対応した型のフィールドであれば何でも定義可能です。

public sealed class Context : ContextBase
{
    // コンテキストで管理する変数をSerializeFieldで記述する
    [Header("Package Information")]
    [SerializeField] private string displayName = default;
    [SerializeField] private string packageName = default;
    [SerializeField] private string packageVersionName = default;
    [SerializeField] private int packageVersionCode = default;

    [Header("Build Option")]
    [SerializeField] private bool generateAAB = false;

    // Applyメソッドの中に、コンテキストを適用したときの挙動を記述する
    public override void Apply()
    {
        PlayerSettings.productName = displayName;
        PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.Standalone, packageName);
        PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.Android, packageName);
        PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.iOS, packageName);
        PlayerSettings.bundleVersion = packageVersionName;
        PlayerSettings.macOS.buildNumber = packageVersionCode.ToString();
        PlayerSettings.iOS.buildNumber = packageVersionCode.ToString();
        PlayerSettings.Android.bundleVersionCode = packageVersionCode;

        EditorUserBuildSettings.buildAppBundle = generateAAB;
    }
}

ビルド、再生時の自動適用

ウィンドウ左下、ロックアイコンの右にあるAuto Applyトグルにチェックを入れることで、PlayボタンでのEditor上での再生時や、ビルド時に自動で選択されている(Unityマークが付いている)コンテキストを適用することが出来ます。この機能が有効になっている時は、タブに●アイコンが表示されます。

C#スクリプトからのコンテキストの適用

CI等のワークフローからもアクセスできるように、C#コードでコンテキストの適用ができるよう、文字列でコンテキストを検索して適用することが可能です。この機能を使い、開発ビルドとリリースビルドを出し分けています。

// EnvironmentContextの自動適用を抑制する(ビルド後に元に戻す)
AutoContextApplier.SetAutoApply(false);

var contextName = GetCommandLineArgument("-environmentContext");
var context = ContextStore.Instance.FromName(contextName);
context.Apply();

実装

開発での使い易さを優先したため、このツールはプロジェクトに埋め込まれており、アセットとして汎用的なものにはなっていません。その代わり、コアロジック以外の頻繁に触る必要がある部分は前述の通りContextBaseを継承したContextクラスだけを弄ればよく、シンプルになっています。

public sealed class Context : ContextBase
{
    // コンテキストで管理する変数をSerializeFieldで記述する

    public override void Apply()
    {
        // コンテキストを適用したときの挙動を記述する
    }
}

ContextSwitcherは、このContextクラスをインスタンス生成したScriptableObjectをContextStoreというScriptableObjectで管理しています。それをContextSwitherWindowから参照して、適用するインスタンスのApply()を呼び出しています。

また、コンテキストはEditorOnlyのScriptableObjectとして実装しています。その為、コンテキストに開発用のWeb APIのエンドポイントや、ダミーのプレイヤー情報などを入れていたとしても、リリースビルドには含まれません。

その他細かい実装の話

ContextSwitcherWindowからContextStoreを参照する

ContextSwitherWindowは生成された時やスクリプトがリロードされた際にContextStoreの参照を見つける必要がありますが、Assets以下のファイルをstringのパスで参照すると万一フォルダ構造が変わったときにエラーになってしまいます。

毎回手動でパスを書き換えてもいいのですが、出来れば余計なメンテナンスはしたくないので、フォルダにラベルを付けて、ラベル検索を行うことでContextStoreの配置されているフォルダを探しています。

var guids = AssetDatabase.FindAssets("l:EnvironmentContext");
if (guids.Length == 0)
{
    return null;
}
foreach (var id in guids)
{
    var dir = AssetDatabase.GUIDToAssetPath(id);
    if (Directory.Exists(dir))
    {
        dataPath = dir + Path.DirectorySeparatorChar + "ContextStore.asset";
        return dataPath;
    }
}

コンテキストを自動的に適用する

EditorがPlayModeに入ったタイミングはEditorApplication.playModeStateChangedで取得できるので、そのタイミングでコンテキストを適用します。

[InitializeOnLoadMethod]
public static void Register()
{
    EditorApplication.playModeStateChanged += EditorApplication_playModeStateChanged;
}

private static void EditorApplication_playModeStateChanged(PlayModeStateChange state)
{
    if (AutoApply && state == PlayModeStateChange.ExitingEditMode)
    {
        ContextStore.Instance.Current.Apply();
    }
}

また、ビルド時はIPreprocessBuildWithReportインターフェースを実装したクラスを用意すると、ビルド直前に自動でインスタンス生成してメソッドを呼び出してくれるので、このタイミングでコンテキストを適用しています。

public sealed class AutoContextApplier : IPreprocessBuildWithReport
{
    public void OnPreprocessBuild(BuildReport report)
    {
        if (AutoApply)
        {
            ContextStore.Instance.Current.Apply();
        }
    }
}

以上

個人的には開発とリリースでどういうパラメータが変化するのかをUnity GUI上で一覧で見られるのと、コンテキストそのものや、コンテキストで管理する値の追加/削除が簡単にできるのが気に入っています。

仕組みを1回作っておくとだいたいどんなプロジェクトでも使いまわしが利くので便利です。もしあなたのプロジェクトが大規模化して、設定の管理が大変になってきたら、こういったものを作ってみては如何でしょうか?

プロジェクトをNetcode for GameObjectsのリリース版にアップデートする

はじめに

エンジニアの松原です。先日Unityがマルチプレイヤーのための新しいツール群の発表をUnityの公式ブログから発表がありました。

この中に含まれるNetcode for GameObjectsに関してはこのテックブログでもいくつかの記事(その1その2その3その4その5)で触れていましたが、正式リリース前のものでした。
この機会に知識のアップデートを行いたいと思い、以前作ったプロジェクトで使われていたNetcode for GameObjectsのバージョンをリリース版(2022年10月時点の最新は1.0.2)にアップデートする作業を記事にしました。 今回の内容について個人のGitHubリポジトリにアップロードしておりますので、もしよろしければ触ってみてください!

github.com

リポジトリを触る際の注意事項

今回の記事ではPhoton Realtimeに触れている部分があるため、前回の記事を参考にしつつ、 Assets/Photon/Resources/PhotonSettings.asset をインスペクタから設定し、App Id Realtimeにご自身で用意したPhotonのApp Idを追加しておきます。これをしておかないとネットワークが正常に動作しないと思います。

古いUnityのバージョンのプロジェクトでは再度パッケージを追加し直す必要がある

以前の記事ではプレリリース版のNetcode for GameObjectsを利用していたのですが、その時のUnityのバージョンは2021.2.3f1を利用していました。 この記事で利用したUnityプロジェクトからリリース版( 1.0.0 or 1.0.1 or 1.0.2 )が利用できるかPackage Managerからアップデートを検索したのですが、1.0.0-pre.10 (更新日は2022/6/22)のものまでしか出てきませんでした。 試しに2021.2系の最終バージョンである2021.2.19を入れてみても、同様に 1.0.0-pre.10 までしか認識していないようでした。

Unity公式のNetcode for GameObjectsのリリースノートを見てみましたが、この理由に関しては特に記載が見つかりませんでした。

ダメ元で再度パッケージを追加するためパッケージマネージャーの左上の「+」マークを押して、「Add package from git URL...」を押し、リポジトリ名である com.unity.netcode.gameobjects を入力しパッケージを追加し直したところ、リリース版( 1.0.2 )を読み込んでくれました。
この辺りの挙動の理由は分かりませんが、他のパッケージの場合でも、最新アップデートが候補に出てこないときは再度パッケージ自体を追加し直す作業で改善する可能性はありそうです。

公式では2020.3以降がサポート対象となっていますが、別の理由がない限りはこの機会に2021.3以降を使うのが良さそうです。 この問題が発生した直後に、試しに(2022/10/5)の2021.3系の最新版である2021.3.f11にプロジェクトバージョンを引き上げると上記対応をせずにアップデートできました。(GitHubのリポジトリは念のため、2021.2.3f1のままにしています)

Multiplayer Community Contributions(Photon)のバージョンをアップデートする

バージョン 1.0.0 以降では破壊的変更が含まれているため、前回の記事で取り上げたMultiplayer Community Contributionsの一つであるPhoton Realtime Transportのバージョンアップデート( 2.0.0 -> 2.0.1 )が必要です。
パッケージマネージャーの左上の「+」マークを押して、「Add package from git URL...」を押し、リポジトリ名を入れます。

コミットIDで導入するバージョンを固定しているため、URLが長くなっています。

https://github.com/Unity-Technologies/multiplayer-community-contributions.git?path=/Transports/com.community.netcode.transport.photon-realtime#9a480a58638b4bfbcc9fc6da4a5c39c216966aaf

2.0.1 のバージョンのパッケージが追加されました。

ClientNetworkTransformを更新する

バージョン 1.0.1 以降ではClientNetworkTransformに対しても変更が入っています。Unity公式ではPackageManagerから追加するようにコメントが書かれています。

The ClientNetworkTransform lives inside the Multiplayer Samples Utilities package. You can add this package via the Package Manager window in the Unity Editor by selecting add from Git URL and adding the following URL: https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git?path=/Packages/com.unity.multiplayer.samples.coop#main

つまりは、パッケージマネージャーで追加できるようにしており、パッケージマネージャーの左上の「+」マークを押して、「Add package from git URL...」を押し、リポジトリ名として以下のものを入れることでClientNetworkTransformを追加できるようになっているようです。

https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git?path=/Packages/com.unity.multiplayer.samples.coop#main

今回のプロジェクトファイルではAssetフォルダ以下でClientNetworkTransformのコードを管理していたため、こちらを直接書き換えて対応します。
ファイルは Assets/Samples/Netcode for GameObjects/1.0.0-pre.3/ClientNetworkTransform/ScriptsからAssets/Samples/Netcode for GameObjects/1.0.1/ClientNetworkTransform/Scriptsに移動し、ClientNetworkTransform.csを以下のリンクのコードに置き換えています。

github.com

using Unity.Netcode.Components;
using UnityEngine;

namespace Unity.Multiplayer.Samples.Utilities.ClientAuthority
{
    /// <summary>
    /// Used for syncing a transform with client side changes. This includes host. Pure server as owner isn't supported by this. Please use NetworkTransform
    /// for transforms that'll always be owned by the server.
    /// </summary>
    [DisallowMultipleComponent]
    public class ClientNetworkTransform : NetworkTransform
    {
        /// <summary>
        /// Used to determine who can write to this transform. Owner client only.
        /// This imposes state to the server. This is putting trust on your clients. Make sure no security-sensitive features use this transform.
        /// </summary>
        protected override bool OnIsServerAuthoritative()
        {
            return false;
        }
    }
}

破壊的変更のあったコードに対して修正を行う

リリース版以降で破壊的変更のあったコードを修正していきます。基本的にローレベルで処理を書いていたところでエラーが発生しており、ヘルパークラスやラッパークラスなどの変更による破壊的変更でエラーは発生していなかったので、プレリリース版から大きく仕様が変わっていないようでした。
ほとんど変更が無かったようで、あっさりと対応が済みました。

NetworkVariableのコンストラクタの引数を変更する

以前の記事で追加したコードのうち、PlayerSyncBehaviour.csで利用している、NetworkVariableのコンストラクタ周りに変更があったようで、第一引数に値の初期値を入れ、第二引数にNetworkVariableReadPermissionのタイプを指定する仕様に変わったようです。

    private readonly NetworkVariable<Vector3> networkMovingDirection = new NetworkVariable<Vector3>(
        Vector3.zero, // <- ここが増えた
        NetworkVariableReadPermission.Everyone
    );
    
    private readonly NetworkVariable<Vector3> networkCurrentPosition = new NetworkVariable<Vector3>(
        Vector3.zero, // <- ここが増えた
        NetworkVariableReadPermission.Everyone
    );

最後に

今回記事内容は短めでしたが、プレリリース版から破壊的変更が少なかっため、以前の記事で取り上げた知識やコードはほとんどそのまま利用できそうです。

前身であるUTPやMLAPIを含めると、Netcode for GameObjectsも例に漏れず、正式リリースまでに長い年月がかかっています。無事Netcode for GameObjectsがリリース版を迎え、安定軌道に乗った印象があります。
Unityの取り組んでいるプロジェクトの中では、長い間previewやexperimentalがついたままで、まだ正式リリースを迎えていないものもありますが、 Netcode for GameObjectsを含め、Unity Multiplayerは今回のリリースを踏まえ、Unityがかなり力を入れて取り組んでいるプロジェクトだと思います。
今後もUnity Multiplayerに関連するトピックは取り上げていきたいと思います。

【Golang】GinのログをカスタマイズしてリクエストIDを追加する

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

少し前にタイトルの件について調べていたのですが、あまりまとまった記事がなさそうだったのでまとめることにしました。
もし本記事の内容に間違っている記述や不適切な記述がある場合にはコメントやTwitterDMなどでお知らせいただけると幸いです。

Ginのデフォルトログ設定について

まず、カスタマイズする前にGinのデフォルトのログ設定がどうなっているかを確認します。

巷のGinの入門記事などを読むと、まずgin.Default()を呼ぶように書かれています。下記がよくある例かと思います。

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default() // これ
    r.GET("/hello", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "Hello World!"})
    })
    r.Run()
}

このgin.Default()関数の実装を確認するとgin.New()でインスタンスを生成した後、Logger()Recovery()というMiddlewareを使用していることがわかります。

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

というわけで、gin.Default()を使わず、gin.New()を使ってmiddewareをカスタマイズしてやればよさそうです。

ちなみにデフォルトだと下記のようなログになります。

[GIN] 2022/09/27 - 23:49:56 | 200 | 34.435µs | 172.23.0.1 | GET "/hello"

ロガーのカスタマイズについて

続いてロガーのカスタマイズです。

ロガーの実体はmiddlewareであることがわかりましたので、自分でカスタムミドルウェアを実装してもいいのですが、ginにはgin.LoggerWithConfig(conf LoggerConfig)というコンフィグ付きでロガーを生成できる関数が用意されています。

LoggerConfigという構造体を確認するとカスタマイズできる項目はフォーマット、ライター、ログ出力をスキップするパスの設定のようです。今回はフォーマットの修正で事足りそうなのでこちらを修正していきます。

// LoggerConfig defines the config for Logger middleware.
type LoggerConfig struct {
    // Optional. Default value is gin.defaultLogFormatter
    Formatter LogFormatter

    // Output is a writer where logs are written.
    // Optional. Default value is gin.DefaultWriter.
    Output io.Writer

    // SkipPaths is an url path array which logs are not written.
    // Optional.
    SkipPaths []string
}

まずはカスタマイズしたLoggerConfigを返す関数を実装します。一瞬です。

func customLogger() gin.LoggerConfig {
    conf := gin.LoggerConfig{}
    conf.Formatter = customLogFormatter // 次に実装します
    return conf
}

次にcustomLogFormatterを作っていきます。一旦タイムスタンプの後に適当な文字列を追加してみましょう。

デフォルトのログのフォーマットを参考に実装(ほぼ丸コピ)していきます。

var customLogFormatter = func(param gin.LogFormatterParams) string {
    var statusColor, methodColor, resetColor string
    if param.IsOutputColor() {
        statusColor = param.StatusCodeColor()
        methodColor = param.MethodColor()
        resetColor = param.ResetColor()
    }

    if param.Latency > time.Minute {
        param.Latency = param.Latency.Truncate(time.Second)
    }

        // ここでフォーマットを決めている
    return fmt.Sprintf("[GIN] %v| %s |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
        param.TimeStamp.Format("2006/01/02 - 15:04:05"),
        "ここに追加", // ここに追加
        statusColor,
        param.StatusCode,
        resetColor,
        param.Latency,
        param.ClientIP,
        methodColor,
        param.Method,
        resetColor,
        param.Path,
        param.ErrorMessage,
    )
}

最後に作成したgin.LoggerWithConfig(conf LoggerConfig)を使用するためにr.Use(gin.LoggerWithConfig(customLogger()))を追加します。

package main

import (
    "fmt"
    "time"
    
    "github.com/gin-gonic/gin"
)

// ログフォーマッタ
var customLogFormatter = func(param gin.LogFormatterParams) string {
    var statusColor, methodColor, resetColor string
    if param.IsOutputColor() {
        statusColor = param.StatusCodeColor()
        methodColor = param.MethodColor()
        resetColor = param.ResetColor()
    }

    if param.Latency > time.Minute {
        param.Latency = param.Latency.Truncate(time.Second)
    }

    // ここでフォーマットを決めている
    return fmt.Sprintf("[GIN] %v| %s |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
        param.TimeStamp.Format("2006/01/02 - 15:04:05"),
        "ここに追加", // ここに追加
        statusColor,
        param.StatusCode,
        resetColor,
        param.Latency,
        param.ClientIP,
        methodColor,
        param.Method,
        resetColor,
        param.Path,
        param.ErrorMessage,
    )
}

// カスタムログ
func customLogger() gin.LoggerConfig {
    conf := gin.LoggerConfig{}
    conf.Formatter = customLogFormatter
    return conf
}

func main() {
    r := gin.New()
    r.Use(gin.LoggerWithConfig(customLogger())) // ここ
    r.Use(gin.Recovery()) // お忘れなく

    r.GET("/hello", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "Hello World!"})
    })
    r.Run()
}

これで下記のようなログになります。

[GIN] 2022/09/27 - 23:52:26| ここに追加 | 200 | 50.529µs | 172.23.0.1 | GET "/hello"

リクエスト毎にUUIDを付与してみる

次にリクエストに対してUUID(リクエストID)を発行してログに追加していきます。

今回は下記のモジュールを使用します。

github.com

githubのREADMEのExampleの通りに実装してみます。こちらもmiddlewareとして設定するだけなので簡単ですね。

func main() {
    r := gin.New()
    r.Use(requestid.New()) // ここ
    r.Use(gin.LoggerWithConfig(customLogger()))
    r.Use(gin.Recovery())

    r.GET("/hello", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "Hello World!"})
    })
    r.Run()
}

上記実装でレスポンスヘッダにリクエストIDが付与されます。

あとはcustomLogFormatterの中でIDを取得すれば…と思いますが、gin.LogFormatterParamsは*gin.Context()をもっていないので取得できません。

 return fmt.Sprintf("[GIN] %v| %s |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
        param.TimeStamp.Format("2006/01/02 - 15:04:05"),
        requestId.Get(c), //こうすればできそうだけどできない
        statusColor,
        param.StatusCode,
        resetColor,
        param.Latency,
        param.ClientIP,
        methodColor,
        param.Method,
        resetColor,
        param.Path,
        param.ErrorMessage,
    )

仕方がないのでドキュメントを確認すると、カスタムハンドラ(呼び方はよくわからない)を実装できることがわかります。

それを利用して、下記のようにリクエストヘッダに追加するようにします。

package main

import (
    "fmt"
    "time"
    
    "github.com/gin-gonic/gin"
)

// ログフォーマッタ
var customLogFormatter = func(param gin.LogFormatterParams) string {
    var statusColor, methodColor, resetColor string
    if param.IsOutputColor() {
        statusColor = param.StatusCodeColor()
        methodColor = param.MethodColor()
        resetColor = param.ResetColor()
    }

    if param.Latency > time.Minute {
        param.Latency = param.Latency.Truncate(time.Second)
    }

    return fmt.Sprintf("[GIN] %v| %s |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
        param.TimeStamp.Format("2006/01/02 - 15:04:05"),
        param.Request.Header.Get("X-Request-Id"), // 修正
        statusColor,
        param.StatusCode,
        resetColor,
        param.Latency,
        param.ClientIP,
        methodColor,
        param.Method,
        resetColor,
        param.Path,
        param.ErrorMessage,
    )
}

// カスタムログ
func customLogger() gin.LoggerConfig {
    conf := gin.LoggerConfig{}
    conf.Formatter = customLogFormatter
    return conf
}

// カスタムハンドラを追加
func customRequestidHandler() requestid.Handler {
    return func(c *gin.Context, requestID string) {
        c.Request.Header.Set("X-Request-Id", requestID)
    }
}

func main() {
    r := gin.New()
    r.Use(requestid.New(customRequestidHandler())) // 修正
    r.Use(gin.LoggerWithConfig(customLogger()))
    r.Use(gin.Recovery())

    r.GET("/hello", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "Hello World!"})
    })
    r.Run()
}

こうすることで、無事ログにリクエストIDを残すことができました。下記のようにログに出力されます。

もちろん他の場所にリクエストID込みのログを仕込むことも可能です。これで大量のログの中からgrepなりなんなりで調査したいログを拾い上げることができますね。

[GIN] 2022/09/28 - 00:41:36| c6500909-bd3c-409e-8ff4-1236da298019 | 200 | 49.806µs | 172.23.0.1 | GET "/hello"

おまけ:特定のAPIのログを残さないようにする

同様のカスタマイズで特定のAPのログを残さないようにカスタマイズすることも可能です。
ALBを使っているとターゲットグループに対するヘルスチェックで特定のAPIを一定間隔で叩いたりするかと思います。死活監視のログで他のログが埋もれてしまうのは本末転倒ですが、この方法でスキップすることができます。

func customLogger() gin.LoggerConfig {
    conf := gin.LoggerConfig{
            SkipPaths: []string{"/healthcheck"} // 追加
        }
    conf.Formatter = customLogFormatter
    return conf
}

おわりに

今回はGinのログのカスタマイズについて調査した結果をまとめてみました。これでアプリケーション間で横断的なログ調査が出来るようになりますね。 何かの参考になれば幸いです。