UnityのUI Elements (UI Toolkit)を触ってみた

概要

UnityでEditor WindowをUI Elementsで試してみました。 従来のIMGUIだとUIの配置などもスクリプトでやっていると地味に時間が取られてしまうものでした。 EditorのUIはある程度楽に実装したいですよね。

EditorWindowのUI Elementsのセットアップ

  • EditorWindowの場合、一式をメニューから生成します
  • 名称を設定しConfirm
  • 生成されるファイルは以下の通り
  • 「Window」→「UI Toolkit」→「名称」でサンプルが表示されます。

    • 楽です。

UI Elements一連ファイルについて

スクリプト(cs)

uxmlのロードと、各VisualElement(要素)のイベント処理を実装します。
最低限以下を記述すれば画面が生成されます。

    public void CreateGUI()
    {
        // Each editor window contains a root VisualElement object
        VisualElement root = rootVisualElement;

        // Import UXML
        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/UIElements/Blog/Blog.uxml");
        root.Add(visualTree.Instantiate());
    }

対象の要素をQuery(Q)で名前を指定して取得。各プロパティ・イベントを設定する流れが基本となりそうです。

var textField = root.Q<TextField>("name"); // 要素取得
textField.value = "value"; // 値変更
textField.AddToClassList("class"); // スタイルのクラス追加
textField.RegisterValueChangedCallback(evt => {~~~}); // value change イベントアクション
textField.RegisterCallback<MouseOverEvent>(evt => {~~~}); // mouse over イベントアクション

UI Document(uxml)

xmlで要素を定義していきます。

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi=~~~ >
    <Style src="project://database/Assets/UIElements/Blog/Blog.uss?fileID=7433441132597879392&amp;guid=20c827d3be187c146b576d843f7d1a60&amp;type=3#Blog" />
    <ui:GroupBox name="Toolbar" ~~~>
        <uie:ToolbarMenu display-tooltip-when-elided="true" text="メニューアクション" name="ToolbarMenu" />
        <uie:ToolbarToggle focusable="false" label="トグル" name="ToolbarToggle" text="無効" />
    </ui:GroupBox></ui:UXML>

がGUIのデザインツールのUI Builderがあるのでこちらが楽なので基本となりそうです。

スタイルシート(uss)

webのcssとほぼほぼ同じように記述できます。
セレクタに対してプロパティを定義していきます。

Label.pagetitle {
   color: red;
}

セレクタや、プロパティについては公式参照です docs.unity3d.com
また、ussより直接設定したプロパティ値の方が優先されます。これもhtmlと同様ですね。
楽ですね。

各種Component

LabelやTextField等シンプルなものはスキップして個人的に使いそうなものをピックアップしました。

ToolbarMenu


こちらはEditor専用のコンポーネントでUI Builderで有効にすることで利用可能になります。
HierarchyのTopのinspecterから設定します。

スクリプトでアイテムを追加し、クリックイベントを実装します

        // Menu
        var menu = root.Q<ToolbarMenu>("ToolbarMenu");
        menu.menu.AppendAction("アクション", action =>
        {
            if (EditorUtility.DisplayDialog("メニューアクション", action.name, "OK"))
            {
                Debug.Log("Click");
            }
        });
        menu.menu.AppendSeparator("------------");
        menu.menu.AppendAction("アクション2", action => {});

ToolbarToggle


こちらもEditor専用でメニューのトグルになります。

        var toggle = root.Q<ToolbarToggle>("ToolbarToggle");
        toggle.RegisterValueChangedCallback(evt =>
        {
            toggle.text = (evt.newValue) ? "有効" : "無効";
        });

ScrollView


こちらはスクロール付きのリストビューとなります。
シーンのオブジェクトを取得しLabelとして追加、表示してみました。
また、列をマウスオーバー時にクラスを切り替えることで背景色を変化させたりします。

        var scene = EditorSceneManager.GetActiveScene();
        var objects = scene.GetRootGameObjects();
        var menu = root.Q<ScrollView>("SceneList");

        foreach (var obj in objects)
        {
            var label = new Label(obj.name);
            label.RegisterCallback<MouseOverEvent>(evt =>
            {
                label.AddToClassList("MouseOver");
            });
            label.RegisterCallback<MouseLeaveEvent>(evt =>
            {
                label.RemoveFromClassList("MouseOver");
            });
            menu.Add(label);
        }

css

.MouseOver {
    background-color: darkblue;
}

Foldout


TreeViewを実現したく、Foldoutというコンポーネントが近かったのでこれで再現してみました。
合わせてDragAndDropも試してみました。

        // left
        var leftTree = root.Q<Foldout>("LeftFoldout");
        leftTree.text = "root";

        var child = new Foldout();
        child.AddToClassList("TreeFirst");
        child.text = "child";
        leftTree.Add(child);

        var child2 = new Foldout();
        child2.AddToClassList("Tree");
        child2.text = "child2";
        child.Add(child2);

        var child3 = new Foldout();
        child3.AddToClassList("Tree");
        child3.text = "child3";
        child.Add(child3);

        var child3a = new Label();
        child3a.AddToClassList("Tree");
        child3a.text = "childA";
        child3.Add(child3a);

        child3.RegisterCallback<MouseDownEvent>(evt =>
        {
            DragAndDrop.PrepareStartDrag();
            DragAndDrop.StartDrag("Dragging");
            DragAndDrop.SetGenericData("treedata", child3);
        });

        // right
        var rightTree = root.Q<Foldout>("RightFoldout");
        rightTree.text = "root";

        var rightChild = new Foldout();
        rightChild.AddToClassList("TreeFirst");
        rightChild.text = "ここにドロップ";
        rightTree.Add(rightChild);
        rightChild.RegisterCallback<DragUpdatedEvent>(evt =>
        {
            object draggedLabel = DragAndDrop.GetGenericData("treedata");
            if (draggedLabel != null)
            {
                DragAndDrop.visualMode = DragAndDropVisualMode.Move;
            }

        });
        rightChild.RegisterCallback<DragExitedEvent>(evt =>
        {
            var draggedLabel = (Label) DragAndDrop.GetGenericData("treedata");
            if (draggedLabel == null)
            {
                return;
            }

            var newchild = new Label();
            newchild.AddToClassList("Tree");
            newchild.text = draggedLabel.text;
            rightChild.Add(newchild);
        });

css

.Tree {
    padding-left: 0px;
}
.TreeFirst {
    padding-left: 15px;
}

Foldout要素に追加していくことで階層構造が構築できます。
見栄え的には縦に並んでいくだけだったので、スタイルシートでpaddingでそれっぽく調整しています。

ドラッグアンドドロップですが、Editorの場合UnityEditor.DragAndDropが利用できるので簡単なものならこれで実現できそうです。
ただ凝ったものや、ランタイムの場合は別の方法で実現するようです。
docs.unity.cn

また、Foldout要素自体にMouseDownEventイベントを登録してもうまく動作しませんでした。ツリーごとドラッグしたい場合は、別の手段で実現する必要がありそうです。

ちなみに要素の「userData」プロパティに任意のobjectを入れておくことでDrop時のカスタマイズも色々行えそうです。
また、DragAndDrop.objectReferencesにオブジェクトをセットしてHierarchyにドラッグすると、オブジェクトを配置
DragAndDrop.pathsにパスを入れてProjectにドラッグするとpathsのアセットをコピーしたりできます。


こちらはシンプルにDropdownメニューです。
スクリプトからアイテムを追加して選択時のアクションを設定します。

        var dropdown = root.Q<DropdownField>("Dropdown");
        dropdown.choices.Clear();
        dropdown.choices.Add("アイテム1");
        dropdown.choices.Add("アイテム2");
        dropdown.choices.Add("アイテム3");
        dropdown.choices.Add("アイテム4");

        dropdown.index = 0;

        dropdown.RegisterValueChangedCallback(evt =>
        {
            if (EditorUtility.DisplayDialog("ドロップダウン", evt.newValue, "OK"))
            {
                Debug.Log("Click");
            }
        });

RadioButtonGroup


こちらもシンプルにRadioボタンです。
スクリプトから追加します。

        List<string> radioButtons = new List<string>();
        radioButtons.Add("radio1");
        radioButtons.Add("radio2");
        var radiogroup = root.Q<RadioButtonGroup>("RadioGroup");
        radiogroup.choices = radioButtons;
        radiogroup.RegisterValueChangedCallback(evt =>
        {
            Debug.Log(evt.newValue);
        });

なおアイテムはstringだけ指定可能なようなので、文字以外で表現する場合は通常のRadio Button等でカスタマイズが必要そうです。

Progress bar


プログレスバー(とついでにスライダ)です。
値を設定するだけです。シンプルです。
なお下限値・上限値はプロパティから設定することができます

        var progress = root.Q<ProgressBar>("Progress");
        var slider = root.Q<Slider>("ProgressSlider");

        progress.value = slider.value;
        progress.title = $"{progress.value}%";
        slider.RegisterValueChangedCallback(evt =>
        {
            progress.value = evt.newValue;
            progress.title = $"{progress.value}%";
        });

レイアウトについて少し

基本的にはwindowのサイズに応じてフレキシブルに配置したく試していたのですが、
特に横並びの均等配置がcssになれてないとわかりづらかったので残しておきます。

GroupBox


要素をグループ化します。VisualElement等でも良いのですが、これを例に横並び配置します。
Positionはデフォルトのまま相対配置の自動サイズ

FlexのDirectionを横並びを指定。

alignでクロス軸方向の揃え位置を指定します。(MDN クロス軸)


次に並べる子要素側のFlexのShink/Growに1を指定します。
これで要素の表示領域の割合を決めます。

flex shink/growについてはcssの説明がわかりやすいです。MDN flex-grow
また要素が増減しない単純な場合はwidthで「%」指定でも良いです。
こういう設定をスタイルシートにまとめておくと良さそうですね。

最後に

やはりViewの分離とスタイルシートでの制御は簡潔で楽になったと思います。
ツール作成する際はわかりやすさも大事かと思うので、積極的に利用していきたいです。

またScreenSpaceのみですが、ランタイムでも利用できるそうです。
ただレスポンシブやゲームUI等細かい表現を実現しようとすると標準機能だけだとまだまだ難しそうかな(?)という印象です。
今回はEditorWindowを試しましたがもちろんカスタムインスペクターもUI Elementで対応できます。
カスタムインスペクターの作成

Amazon CodeCatalystを触ってみる

こんにちは、エンジニアの黒岩(@kro96_xr)です。

本記事は Synamon Advent Calendar 2022 の7日目の記事になります。

adventar.org

はじめに

今年もAWS re:Invent 2022で様々な新機能の発表やアップデートがありました。
今回はそれらの中で気になったAmazon CodeCatalyst(Preview)を触ってみたいと思います。

aws.amazon.com

Amazon CodeCatalystとは

一言で言うと「統合ソフトウェア開発サービス」とのことで、開発ライフサイクルの効率化を提供するサービスのようです。

提供される機能は以下のようなものになっています。

  • ソースコード管理
  • Blueprint
  • Issue管理
  • CI/CDパイプライン
  • ユーザー管理
  • プロジェクトのダッシュボード表示

なお、現在はプレビュー版ということでus-east2(オレゴン)のみで使用可能です。

東京リージョンから飛ぶと別のリージョンを選ぶように促されます。

また、価格については無料枠もあるようなので気軽に試すことが出来ます。
※デプロイしたリソースは別途料金がかかるかと思うのでご注意ください。

実際に触ってみる

それではここから実際にプロジェクトを作成してみます。

登録からスペースの作成まで

まずCodeCatalystのページに移動してGet started for Freeをクリックします。
なんかスタイリッシュなデザインですね。

次にBuilder IDを作成します。
メールアドレスと名前を入力すると認証コードを受け取れるので、それを入力していきます。


画面に沿って登録を進めてログインします。

その後、自分のエイリアスの作成を求められます。これは説明文を読む限りユーザー名のようなものでしょうか。

Choose an alias as your unique user identifier in Amazon CodeCatalyst. Enter a shortened version of your full name, without spaces. Others will use your alias to @mention you in comments and pull requests. You can't change your alias after creation.

次にスペースの作成を行います。スペース名とCodeCatalystを使用するAWSアカウントを入力します。
入力するとトークンの有効化が求められるので、Verify in the AWS Consoleをクリックして有効化します。


作成直前まで進んだ状態

スペースが作成され、ダッシュボードが表示されます。

プロジェクトの作成

スペースのダッシュボードからCreateProjectをクリックするとblueprintの選択画面になります。
事前に用意されたblueprintを使うことも、自分で作成することも出来るようです。

blueprintを選択すると右側にペインが表示され、blueprintの詳細が表示されます。
技術スタックなどやインフラ構成が書かれていてわかりやすいですね。

今回はなんとなくイメージしやすかったのでTo Do web applicationを選択してみます。
プロジェクト名やIAMロールの作成が求められるので、画面に従って設定していきます。

※後から気付きましたが公式のチュートリアルがあり、そこではModern three-tier web applicationを選択していました。

作成中

あっという間にプロジェクトが完成し、プロジェクトのダッシュボードが表示されました。

ダッシュボードを触ってみる

それでは実際に作成されたプロジェクトのダッシュボードを確認してみます。

Repositories

ここからはGitを使ったソースコード管理を行うことが出来ます。ブランチやプルリクエスト等見慣れた文字が並んでいますね。

右上のClone repositoryからGithub等と同様にローカルに取り込むことが出来ます。

Create Dev Environmentからはクラウド上に開発環境を構築することが出来るようです。

Issues

ここからはIssueの管理が行えます。こちらもGithubでIssueを立てるのと同じような感覚で使えそうです。

作成画面は以下のようになっていて、特に違和感なく登録できました。
Issueのテンプレートの作成は出来なさそう?今のところは見当たりませんでした。

一覧はGithubでいうプロジェクトのような見た目になっています。

Settingはこのようになっています。Boardへのステータス追加やラベルの追加なども出来そうですね。

CI/CD

ここからはコードのデプロイなどのワークフローの管理が行えます。
今回はblueprintから作成したため、既にワークフローが用意されていました。

ワークフロー定義は./codecatalyst/workflows内にyaml形式で作成されていますが、下のスクショのようにフローで見ることも出来ます。

直近の実行結果も同様です。

Latest stateのFrontendCDKDeployを選択し、Variablesを見るとAppURLが表示されるのでアクセスしてみます。

無事に表示されました!

リソースの削除

最後に、自動で作成されたリソースを削除しておきます。

AWS CloudFormationにアクセスするとスタックが表示されるので全て削除しておきます。

おわりに

本記事では新しくプレビューとして公開されたCodeCatalystについて、プロジェクトの作成と機能について簡単に見てみました。
これまでの私の経験だと、インフラ構築はterraformで、アプリケーションコードのデプロイはGithub Actionsで、Issue管理はGithub Projectもしくはその他管理ツールでというようなケースが多かったので、これらを一括で管理できるのはたしかに効率化につながるのかもしれないと思いました。

今回は既存のBlueprintを使いましたが、どのくらい柔軟にそして簡潔に自作できるのかといったところも触ってみたいなと思います!

参考リンク

統合ソフトウェア開発サービスである Amazon CodeCatalyst (プレビュー) の発表 | Amazon Web Services ブログ

AWS re:Invent 2022で発表された新サービス/アップデートまとめ - Qiita

[速報] シームレスにプロジェクトの状態を一元管理するCI/CD の統合サービスAmazon CodeCatalyst (preview) が発表されました!#reInvent | DevelopersIO

開発チームが必要とするすべてのツールとインフラが5分で手に入る?期待の統合DevOpsサービス「Amazon CodeCatalyst」をご紹介します (DOP206-R1) #reinvent2022 | DevelopersIO

【完全に】統合型DevOpsサービス「Amazon CodeCatalyst」【理解した】 - Qiita
⇒前日に素晴らしい記事が公開されておりました。参考になると思いますので是非ご覧ください!

SYNMNのマルチプレイの仕組みについてちょっと紹介

本記事は Synamon Advent Calendar 2022 の5日目の記事になります。

adventar.org


エンジニアの岡村です。

Synamonが現在ベータ版として提供している新しいアプリ「SYNMN」。皆さん触ってみましたか?

今回は、そんなSYNMNのマルチプレイ処理をちょっと紹介しようと思います。

SYNMNの同期概要

SYNMNのマルチプレイは主に2種類の仕組みを使って実現しています。1つが状態ベースの同期処理、もう一つはトリガーです。

状態の同期

ルーム内にある1個の同期オブジェクトがあったとして、そのオブジェクトにはかならず持ち主となるプレイヤーがいます。持ち主の世界での状態がそのオブジェクトの正しい状態として定義されており、同期システムによって監視されています。同期システムは一定間隔でオブジェクトの状態をチェックし、更新があれば新しい値を他のプレイヤーの世界のオブジェクトに送信します。

状態の監視と送信は自動で行われており、単純にオブジェクトの状態が変化したときだけでなく、プレイヤーの入室時の同期も自動的に行われます。状態にはラベルを付けて分類することができ、座標だけの同期やそれ以外の細かい同期を分けて行うことも出来ます。

また、オブジェクト自体の状態が変化したタイミングでイベントが発火するようになっており、自分のオブジェクトかそうでないかに関わらず(判別することも可能)、イベントを読むことでビューへの反映が出来るようになっています。

トリガーの送信

状態だけでは持ち主以外のプレイヤーがオブジェクトに対して状態の変更をさせたい時に不便です。なので、そんな時の為に、トリガーを持ち主側に送る処理もあります。 トリガーは持ち主以外のプレイヤーから呼び出され、持ち主の世界のオブジェクトに届けられる経路を主とします。

トリガーは特定のプレイヤーに送ることは出来ず、必ず特定の属性(オブジェクトの持ち主やそれ以外など)を指定して送る必要があります。PhotonにおけるRPCのRPCTargetのような形ですね。PhotonではPlayerを直接指定してRPCを送信できますが、SYNMNではできません。 ここは後述の仕組みを実現する為にあえて制約を設けている部分でもあります。

意図と強み

以前までのSynamon製のアプリでは、同期システムはPUN2をベースにした同期システムを使っていました。これは自由度が高く、凝った同期ロジックを組むことが出来たのですが、SYNMNではオブジェクト側の同期ロジックの自由度をあえて削り、不便な状態にしています。これは、以下のようなメリットがあると判断したためです。

同期処理をシステム側で制御できる

SYNMNで採用している同期システムは、同期されるオブジェクトそのものに同期のタイミングや相手を制御する権限がありません。一応トリガーはありますが、他のプレイヤーの入室をオブジェクト側で検出できないので、どのタイミングで送ればトリガーで同期できるのかわかりません。

これは裏を返せば、同期のタイミングや相手の制御はすべて同期システム側が持っているということになります。こうすることで、負荷が上がった時や同期ロジックに大幅な変化があったとしても、各オブジェクトに対する改修が最小限で済みます。

オブジェクト側で同期用のコードを書く量が最小限になる

オブジェクト側でハンドリングできることは少ないので、必然的にオブジェクト側で書くことになる同期コードの量も減ります。今までの同期システムでは、各オブジェクトのロジック中に他のプレイヤーが入室したときの同期処理を書かなければなりませんでした。これはコードの肥大を招くと共に、オブジェクト自身のロジックが同期ロジックに隠されて読みづらくなる原因となっていました。SYNMNでは状態の同期は同期システムが勝手に行うので、オブジェクト内に書く同期ロジックは最小限になりました。同期したい状態を定義すること、状態を変えること、状態が変わった時に正しくビューなどに反映することがオブジェクト側に残った責務になります。

同期用のコードがマルチプレイ以外にも使える

同期ロジックから他のプレイヤーの存在などを排除し抽象化したことで、同期ロジックをマルチプレイ以外にも使うことが出来るようになりました。例えばローカルでオブジェクトを2つ用意してのデバッグや、リプレイ機能、動きをマルチプレイのルームの外へ配信することなどが、オブジェクト内のコードを弄ることなく実現できます。

弱み

SYNMNの大人数まで対応できるユースケースに合致しているため、今回はこのような仕組みを採用しました。当然これはすべてのプロダクトに適用できるものではなく、ユースケースによって採用する同期の仕組みは異なってくるはずです。今回の仕組みはそれっぽく同期してみんなと繋がる、コミュニケーション特化のメタバース向けの仕組みと言えるでしょう。なので、例えば格闘ゲームのような細かい同期ズレが致命的なユースケースではこの仕組みは使えません。

技術的な弱みもあります。オブジェクト同士が相互作用を行うとき、SYNMNの同期システムでは他のオブジェクトが同期されているかどうかが保証されていないので、最悪の場合、特定のプレイヤーの世界では存在しないオブジェクトに依存するオブジェクトが生成されることがあります。その場合の辻褄の合わせかたはオブジェクト自体のロジックに依存しています。 他にはやはり同期システムが一元管理しているため、オブジェクト内で特殊な同期処理を行うのが難しいです。例えばNEUTRANSのAirCanvas(空中に絵を描くことのできるペン)はかなり特殊な同期処理をし、描画のほぼリアルタイムな同期を実現していました。そのようなことはSYNMNの同期システムでは実現できないので、代替手段を考える必要があります。

おわりに

以上、SYNMNで採用している同期の仕組みをざっくりと紹介しました。この仕組みはUNetやNetCode for GameObject, Photon Fusion等でも似た仕組みが採用されており、今後のメタバースにおける同期システムのトレンドになりそうな気がしています。SYNMNもまだそのポテンシャルを100%引き出し切れていないので、引き続き開発を頑張っていきます。

ここまで読んでいただきありがとうございました。良いクリスマスを!

KafkaとKafkaJSのHelloWorldを試してみた

はじめに

エンジニアの松原です。普段の開発ではHTTPサーバーを扱うことが多いのですが、負荷軽減のための様々なサーバー側のギミックについて調べていく中で、メッセージキューの仕組みについて目に留まりました。

今回はKafkaについて紹介します。元々メッセージキューに利用できることは知っていたのですが、バックグラウンドでどのような振る舞いをしているかに関しては、あまり詳しく調べられていませんでした。

社内でアドベントカレンダーを今年もやることになったので、この機会にしっかりと勉強しようと思い、この記事ではまずはHelloWorld的に、単純にメッセージキューとしてHelloWorldを試してみました。今回の記事で登場したコードはGitHubにリンクを置いていますので、参考になれば幸いです。

github.com

手っ取り早くKafkaを始める

Kafka自体はかなり複雑な仕組みを持っており、書籍を読んでもしっくりこなかったため、ちょうどセールス中だったUdemyの「Apache Kafka Series - Learn Apache Kafka for Beginners v3」を見て学んでようやくどういった仕組みを持っているのか理解できました。(ブログで説明しようとするとかなり記事が長くなってしまうので、今回はKafkaそのものの解説は割愛します。)

さて、ここから実際に触るとなった際に、ネット上の記事をかじりつつ試そうとしたら大いにハマりました。

私は開発用のPCにWindowsを使っているのですが、Unityの開発以外は仮想環境を扱っていることが多く、Kafkaも簡単に仮想環境で扱えると思っていましたが、環境変数などの設定が問題なのか、立ち上がっているはずのKafkaのサーバーにローカルから接続できないという問題に大いにハマり、ひたすらトライアル&エラーを繰り返して試すという状態に陥りました。

結論として、Udemyで取り上げられていた、 conduktor/kafka-stack-docker-composezk-single-kafka-single.yml を使ってdocker-composeから立ち上げて使う事が、Windows環境で素早く試せました。

github.com

Docker Desktop が Windows に入っていれば、以下の docker-compose コマンドから仮想環境上に最低限の環境であるKafkaのBrokerとZookeeperが立ち上がります。

 docker-compose -f zk-single-kafka-single.yml up

ProducerとConsumerをNode.jsから用意する

Kafkaそのものはメッセージキューのサービスですが、メッセージ自体を生成するにはProducer、メッセージを読み取るにはConsumerがそれぞれ必要になります。

これらは複数の言語でクライアントが提供されていますので、Java、Ruby、Node.js、Golangなど、様々な言語を使う事ができます。

今回は開発速度を最優先してNode.jsで動作するProducerとConsumerを用意しました。

クライアントにはKafkaJSを利用しました。

kafka.js.org

KafkaJSのサンプルコードを流用して、ProducerとConsumerを作ります。

Producerのサンプルコード

const { Kafka } = require('kafkajs')
const { setTimeout } = require('timers/promises')

const kafka = new Kafka({
  clientId: 'my-app',
  brokers: ['localhost:9092'],
})

const producer = kafka.producer()

async function main() {
  await producer.connect()

  let count = 0
  while(true) {
    console.log('send message from producer.')
    await producer.send({
      topic: 'test-topic',
      messages: [
        { value: `Hello KafkaJS user!:${count}` },
      ],
    })

    count++
    if (count > 99) {
      break;
    }
    await setTimeout(5000)
  }

  await producer.disconnect()
}

main()

Consumerのサンプルコード

const { Kafka } = require('kafkajs')

const kafka = new Kafka({
  clientId: 'my-app',
  brokers: ['localhost:9092'],
})

const consumer = kafka.consumer({ groupId: 'test-group' })

async function main() {
  await consumer.connect()
  await consumer.subscribe({ topic: 'test-topic', fromBeginning: true })
  
  await consumer.run({
    eachMessage: async ({ topic, partition, message }) => {
      console.log(`received from consumer: ${message.value.toString()}`)
    },
  })
}

main()

サンプルコードを実行する

docker-compose から KafkaのBrokerとZookeeperを立ち上げ後、それぞれProducerとConsumerをNode.jsのアプリをローカル上で実行したログが以下の画像になります。

おわりに

Kafkaそのものは大規模サービスで利用される分散処理の処理基盤であり、クラウドとも相性が良いので、しっかりと勉強して使いこなしていけるようになりたいと考えています。

今回はKafka Streamsまで紹介できませんでしたが、Kafka Streamsは非常に強力な仕組みを作れるため、実サービスにも応用ができそうです。

今後の記事で取り上げることができるかもしれません。

Synamonアドベントカレンダー2022を開催します!!

本記事は Synamon Advent Calendar 2022 の1日目の記事になります。

adventar.org


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

もう今年も終わりに近づいてきましたね。

年末といえば、アドベントカレンダーの季節ですね!

昨年の話

Synamonは昨年、初めてアドベントカレンダーに参加しました。

synamon.hatenablog.com

qiita.com

昨年8月頃からテックブログの発信を活発化したこともあり、エンジニア、デザイナーを中心に25記事の発信をし、最後まで完走することができました🎉

最近の話

毎週の記事投稿という運用はなんとか維持できていますので、今年もアドベントカレンダーやりたいよねという声がテックブログ運営メンバーから挙がりました。

実は最近のSynamonはテックブログに限らず、noteでのデザイン、コーポレートの発信も盛んに行われています。

note.synamon.jp

デザイン関連はこちらの Designer's Magazine。

note.synamon.jp

コーポレート関連はこちらのSynamonの組織づくり。

note.synamon.jp

全社で情報発信が活発な良い流れがあるので、エンジニアだけでアドベントカレンダーをやるのはもったいない!という話になりました。

というわけで

前置きが長くなりましたが、今年はSynamon全社を巻き込んでアドベントカレンダーを開催することになりました!!🎉🎉

adventar.org

ジャンルも特に縛りはなく、Synamonの技術、デザイン、ビジネス、組織、プロダクトの話からメンバーの趣味や私生活の話まで幅広いテーマでお送りする予定です。

今年ベータ版を公開した SYNMN は「ファン」や「推し活」を応援するプロダクトですので、それにまつわるテーマの記事も出てくるかもしれません。

synmn.app

12/1から12/25まで毎日一記事ずつ公開していきますので、メンバーのTwitterでの記事投稿のTweet、Synamon公式Twitterのシェアをお見逃しなく!

Twitter等で反応をいただけると記事を書いたメンバーも喜びますので、もしいいなと思った記事がありましたらシェアや感想のTweetいただけると嬉しいです!

それでは、Synamon Advent Calendaer 2022 をお楽しみください。

adventar.org

Addressablesの意図しない依存関係をチェックしたい

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

Synamonの新プロダクト SYNMN (読みはシナモン)

synmn.app

のベータ版のリリースが始まり、8月頃に記事を書いた Unity の Addressables 周りの仕組みが運用に乗り始めました。

synamon.hatenablog.com

synamon.hatenablog.com

今回はそんな Addressables の運用時に遭遇した、Addressables 内の AssetBundle の依存関係の課題とその解決をするために作った依存関係チェックの仕組みに関する記事になります。

要約すると、Addressables が AssetBundle 間の依存関係を勝手に良い感じに解決をしてくれるのですが、そのせいで意図しない AssetBundle をロードしてしまうのを避けるために、依存関係をチェックするツールを作ったという話です。

もし同じように Addressables の依存関係で悩んでいる方の参考になれば幸いです。

Addressables における依存関係解決とそれによる副作用

Addressables でアセットをロードする際には、そのアセットが他の AssetBundle に含まれているアセットに依存している場合にはそれらも自動的にロードをしてくれます。

依存関係の情報は、Catalog の解説記事

synamon.hatenablog.com

でもコメントしているように、Catalog に明示的に含まれていますし、AssetBundle の内部にもそれらしき情報が埋め込まれています。

つまり Addressables はビルド時にアセット間の依存関係を適切に処理し、ロード時にもAssetBundle 間の依存関係を自動的に解決をしてくれます。

ですがそれは逆に言えば意図しない依存関係が入ってしまっている場合には、それも勝手に解決してしまうということです。

実際に起きた例をご紹介しましょう。

とある SceneA があり、それを Addressables で管理をしていました。

その SceneA をその内部で使用している3Dモデルや Material などのアセットごとコピーし、SceneA' としました。

フォルダ構成は以下のような形で、細かいアセットはフォルダごとまるっと Addressable に設定しています。

- SceneA (Addressable)
    - SceneA.unity (Addressable)
    - SceneA で使用しているアセット
- SceneA' (Addressable)
    - SceneA'.unity (Addressable)
    - SceneA' で使用しているアセット

ただコピーしただけでは SceneA'.unity 内の参照先のアセットは SceneA で使用しているアセット のままのため、これをコピーした後の SceneA' で使用しているアセット に差し替えます。

これで SceneA と SceneA' は互いに独立した依存関係を持たない Scene とすることができるように思えますが、実際には差し替え忘れの SceneA のアセットへの参照が残っていたり、FBXなどのインポートしたアセットのメタデータにかなりわかりにくい形で参照が残ってしまうケースがあります。

その場合には SceneA' だけを使用したいと思って Scene をロードしたのに、裏では SceneA のアセットを含んでいる AssetBundle までロードしてしまう、といったことが起きます。(実際に起きました)

このような意図しない依存関係を外すためには、アセット間の依存関係を解析して問題点を修正することが必要です。

依存関係の調べ方

Addressables における依存関係はどうやって調べれば良いのでしょうか。

Catalog の解説記事

synamon.hatenablog.com

でもコメントしているように、Catalog には Dependencies というプロパティでIResourceLocation の単位で依存関係の情報を持っていました。

ですがそもそも Catalog の内部情報を覗くのは頑張ってツールを作れば可能なものの(実際作ったのですが)容易ではないですし、覗けたとしても IResourceLocation の指しているアセットに具体的にどのような依存関係を持っているのかの詳細までは分かりません。

また、Unity公式が提供している AssetBundles-Browser

github.com

を使ってビルド済みの AssetBundle の中に埋め込まれている依存関係を覗くこともできます。

ですがこちらもおそらく AssetBundle 単位でしか情報がなく、しかもその ID がパッと見てもどの AssetBundle を指しているのか分かりづらいものでした。

ではどうすれば良いのかと悩みましたが、Catalog に依存関係の情報が含まれているということは、Addressables のビルド時には依存関係の完全な情報を持っているだろうという推測ができます。

Addressables の Build Script を調べてみると、BuildScriptPackedMode

docs.unity3d.com

の実装内部に CalculateAssetDependencyData

docs.unity3d.com

というそれらしきクラスを使用していて、その内部に持っている IDependencyData が利用できそうだと調べがつきます。

docs.unity3d.com

そのプロパティの AssetInfoSceneInfo がそれぞれ GUID などの情報を持っている AssetLoadInfoSceneDependencyInfo の Map (C#の Dictionary) となっているため、これらの Values が全てのアセット、Sceneの依存関係として利用することができそうです。

AssetLoadInfo.referencedObjects を見ればそのアセットがどのアセットを参照しているかの情報が書かれていますし、GUID が分かれば AssetDatabase を使用してアセットの Path に変換できます。

docs.unity3d.com

あとは IDependencyData をどうやって取得するかですが、ExtractDataTask という IBuildTask を利用するとビルド処理内部で保持している情報を外から利用できます。

docs.unity3d.com

ですのでこれを Build Script 内の Build Task の処理に追加してあげればよいとなります。

var extractDataTask = new ExtractDataTask();
var buildTasks = new List<IBuildTask>
{
         new CalculateAssetDependencyData(),
         new GenerateBundlePacking(),
         new GenerateBundleCommands(),
         new WriteSerializedFiles(),
         new ArchiveAndCompressBundles(),
         extractDataTask // <- Insert
};
var dependencyData = extractDataTask.DependencyData;

このような形で、Addressables の Build Script を少しカスタマイズしたものを自作することで、Addressables 内のビルド処理内で保持している依存関係の情報を直接利用することができます。

あとは得られた依存関係の情報を解析して、そのプロジェクトで持たせたくない依存関係を検知する仕組みを作ってあげれば良いです。

例えば先に挙げた Scene のコピーの例では、SceneASceneA' のフォルダを跨るような依存関係が存在してしまうと不要なアセットをロードしてしまうため、AssetLoadInfo のファイルの Path と AssetLoadInfo.referencedObjects のファイルの Path を比較して同一フォルダに含まれるかチェックする、といった形で実装できます。

URP を採用しているプロジェクトではなるべく Builtin の Shader は使わずに URP Shader を使用したいと思いますが、URP 対応していないアセットを手動で変換した時に差し替え忘れたものを検知することもできます。

FBX ファイルを Unity 上でコピーするとインポート設定に含まれている Material の参照が Editor では見えない形で残ってしまったりするのですが、そういった分かりづらい依存関係も全て検知できます。

アセットの運用面で気をつけるべきことではありますが、人間のチェックなのでどうしても限界はありますし、こういったことは仕組みで解決できるように整えてあげると運用が少し楽になると思います。

FileType に関する注意点

上記の方法で依存関係を全て拾った場合には、スクリプト系や Unity の Builtin アセット、Addressablesで管理されていないアセットなどへの参照も含まれます。

依存先のアセットの情報は ObjectIdentifier という struct で参照できるのですが、そのプロパティに FileType を持っています。

docs.unity3d.com

少し分かりづらいため、簡単な説明と具体例を挙げておきます。

  • SerializedAssetType
    • Object is contained in a standard asset file type located in the Assets folder.
      • Assets フォルダに配置されている標準的なアセットファイルに含まれるオブジェクト
    • 具体例
      • SerializedObject
      • Material
      • Scene
      • Animation
      • AudioMixer
      • etc...
  • MetaAssetType
    • Object is contained in the imported asset meta data located in the Library folder.
      • Library フォルダに配置されている、インポートされたアセットのメタデータのオブジェクト
    • 具体例
      • Script
      • Shader
      • Prefab
      • JPGやPNG、FBXなどのインポートして使用している外部データ
  • NonAssetType
    • Object is contained in file not currently tracked by the AssetDatabase.
      • ファイル内に含まれている、AssetDatabase で管理されていないオブジェクト
    • 具体例
      • Library/unity default resources (Unity の builtin の Mesh、GUI のアセットなど)
      • Resources/unity_builtin_extra (Unity の Builtin Shader、デフォルトの Material、uGUI の Spriteなど)
  • DeprecatedCachedAssetType
    • Object is contained in a very old format. Currently unused.
      • とても古いフォーマットのオブジェクト、現在は使用されていない
    • 具体例
      • 不明(Unity2021.3 のプロジェクトでは見つかりませんでした)

これら全ての依存関係が含まれるため、実際のプロジェクトで持ちたくない依存関係に合わせて無視したいものをフィルタしてあげないと不要な依存関係までチェックしてしまいます。

おわりに

今回のまとめになります。

  • Addressables はアセット、AssetBundle の依存関係を自動的に解決してくれる仕組みを持っている
  • ただし意図しない依存関係が含まれてしまうと、それも勝手に解決してしまい、本来不要なアセットをロードしてしまう場合がある
  • 意図しない依存関係を取り除くためには、依存関係の情報をチェックする仕組みを作れば良い
  • 依存関係の情報は、Addressables の Build Script を少しカスタマイズすると拾うことができる
  • その中にはスクリプトやUnity の Builtin なアセットなどの参照も全て含まれるため、FileType などを見ながら適切にフィルタしてあげる必要がある

Addressables はとても便利なので積極的に利用したいものですが、仕組みをよくわかっていないと今回の依存関係のケースのように運用時に意図しない挙動をしてしまう場合もあります。

ですが Addressables に関する情報は特に日本語ではまだまだ少ない印象なので、馴れていない方だと苦労する場面も多いと思います。

自分もまだまだ分かっていない部分もあります。(ContentState の使い道など...)

テーマとしてはかなりピンポイントな内容になりますが、もし Addressables の運用面で同じようなことに困っている方の参考になれば幸いです。

GitHub ActionsのキャッシュをS3に移行した

こんにちは、エンジニアの岡村です。

このブログで何度か紹介してきましたように、弊社ではGitHub ActionsとAWSでUnityのCIを回しています。

synamon.hatenablog.com

Unityもコマンドで動かす事が出来るのでCIへの組み込み自体は可能なのですが、Unityは一般的なプログラムと異なり多くのグラフィックアセットを扱うのでリポジトリサイズや処理負荷がどうしても高く、CIの時間も伸びがちです。その為、UnityのCIではキャッシュが重要です。

一般的なUnityを使った開発において、Unityをローカルマシン上で動かしてプロジェクトフォルダを使いまわしている場合はLibraryフォルダ内に様々なキャッシュが入っている為に高速で起動・ビルドが可能です。しかし、弊社のCI環境ではUnityをEC2のスポットインスタンス上で動かしている為、プロジェクトフォルダは毎回削除されてしまいます。これは環境依存要素を排除する観点では利点なのですが、代償としてビルド時間が延びる問題、GitHub LFS帯域の問題が発生していました。

ビルド時間に関してはマシンスペックを上げることである程度解決しています。GitHub ActionsにCI環境を移行してからはUnity Acceleratorも併用しており、30GBを超えるプロジェクトではありますが、一番長いAndroidビルドでも1時間以内には終わる様になっています。並列化も併用している為現時点ではビルドが詰まる状態にはなっていません。

Unityプロジェクトでは大量の画像や音声、3Dモデル等のデータを扱っている為、それらはLFSで管理しているのですが、GitHubではLFSデータのダウンロードの際、転送するファイルサイズに応じて課金されます。金額自体はそれほど高いわけではないのですが、CIが活発化すると実行の度にチェックアウトが走るために一気に利用量が増え、制限に引っ掛かってしまい、その度にデータパック(GitHub LFSの課金単位)を購入する必要が出てしまいます。繁盛期にはキャッシュを使わない状態だと2~3日で追加購入が必要になるという事態になっていました。

  • ビルド時間の問題(軽微)
  • GitHubのLFSストレージ帯域がもりもり消費される問題

この帯域の問題の解決の為に、まずはGitHubがデフォルトで持っているキャッシュの仕組みを利用する事にしました。

GitHub公式のキャッシュ機能

GitHubにはキャッシュの仕組みがあり、各リポジトリに10GB(2022年11月17日時点)のキャッシュ専用ストレージが付与されています。このストレージからの読み出しには課金されず、用量上限に達したり一定期間利用されなかったキャッシュは自動で削除されます。

弊社環境では本体リポジトリのLFSに加えて、upmパッケージ化した外部アセットや汎用アセットでもLFSを活用していたため、LFSフォルダ及びupmパッケージのローカルキャッシュをGitHub上でキャッシュする様に設定しました。

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          lfs: false
      - name: Get LFS file catalog
        run: "git lfs ls-files -l | cut -d' ' -f1 | sort | uniq > .lfs-files"
        shell: bash
      - name: Cache LFS files
        uses: actions/cache@v3
        with:
          path: .git/lfs
          key: lfs-files-${{ hashFiles('.lfs-files') }}
          restore-keys: |
            lfs-files-
        timeout-minutes: 10
        continue-on-error: true
      - name: LFS pull
        run: git lfs pull

      - name: Cache packages
        uses: actions/cache@v3
        with:
          path: Library/PackageCache/
          key: packages-${{ hashFiles('Packages/packages-lock.json') }}
          restore-keys: |
            packages-
        timeout-minutes: 10
        continue-on-error: true

      # 以降にUnityのビルド処理を書く

GitHub公式のキャッシュ機能の問題点

まず、10GBのキャッシュストレージの容量制限が全然足りませんでした。前述の例ではキャッシュ内容をLFS及びパッケージに限定していましたが、それでも弊社プロジェクトにおいてはそれぞれ1~3GB(圧縮済)の大きさになっていました。そこに他のブランチでのキャッシュ等を含めると容易に制限を超えてしまい、すぐにキャッシュミスが発生するようになってしまいました。このキャッシュストレージはお金を払って拡張することが出来ません。

また、大容量のキャッシュをダウンロードする際に、ダウンロードが途中停止し、そのままタイムアウトになってしまう現象も発生していました。

  • キャッシュストレージが1リポジトリ当たり10GBしかなく、拡張もできない問題
  • 大容量のキャッシュをダウンロードする際、途中でダウンロード速度が0になり、そのままタイムアウトする問題

このような問題が発生していた為、キャッシュを別の方式に変更することにしました。

tespkg/actions-cacheの採用

github.com

兎にも角にもキャッシュストレージが足りないので、Amazon AWS S3ストレージにキャッシュを保存することにしました。S3にキャッシュを保存するActionはMarketplaceにいくつかあるのですが、その中でも新が割と頻繁に行われていて、動作が安定しており、使い方がGitHub公式のアクションとほぼ同じであるこちらのアクションを採用しました。

      - name: Cache packages
        uses: tespkg/actions-cache@v1
        with:
          region: ap-northeast-1
          bucket: cicd-caches
          accessKey: ${{ secrets.AWS_CACHE_ID }}
          secretKey: ${{ secrets.AWS_CACHE_KEY }}
          path: Library/PackageCache/
          key: packages-${{ hashFiles('Packages/packages-lock.json') }}
          restore-keys: |
            packages-
        timeout-minutes: 10
        continue-on-error: true

tespkg/actions-cacheを動かすには、キャッシュの保存先となるバケット(例ではcicd-caches)と、保存及び取り出しの権限を持ったIAMユーザーを作成し、以下のようなポリシーをアタッチしてやる必要があります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:ListBucketMultipartUploads",
                "s3:ListBucket",
                "s3:DeleteObject",
                "s3:GetBucketLocation",
                "s3:ListMultipartUploadParts"
            ],
            "Resource": [
                "arn:aws:s3:::cicd-caches/*",
                "arn:aws:s3:::cicd-caches"
            ]
        }
    ]
}

また、キャッシュはライフサイクルルールを設定して一定期間で削除するようにすると、ストレージの容量が際限なく消費されることを防げます。

結果

今回紹介したActionに乗り換えた結果、キャッシュ容量が足りずにLFSからダウンロードし直しになる事がなくなりました。また、同じリージョン内のEC2を使ってビルドしているおかげか、キャッシュのダウンロード/アップロード速度も上がった印象です。おまけに公式のActionでは発生していた、ダウンロードが途中で止まってタイムアウトする現象も発生しなくなったので、動作を安定させることが出来ました。

  • キャッシュストレージ上限が無くなった
  • ダウンロード/アップロードが150%くらい早くなった
  • キャッシュダウンロード中に速度が0になってそのままタイムアウトする現象が無くなった

諸々の心配事が解決したので、今後はキャッシュで保存する範囲を広げる事を視野に入れてもいいかもしれません。ビルド時間は今の所それ程問題にはなっていませんが、それでも短くなるに越したことはないです。最初に話した環境依存要素という観点もあるので少しずつ試す形にはなりますが。

という所で今回の記事は終わりです。読んでいただきありがとうございました!