うどんてっくメモ

技術的なメモをまったりと

【Unity】TextMeshProのアウトラインの太さをランタイムで変更する

はじめに

本記事で紹介および検証を行なっているツール、ライブラリのバージョンは次の通りです。

  • Unity 2022.3.16f1
  • com.unity.textmeshpro@3.0.6

バージョンによっては挙動に差異がある場合もありますので、ご了承ください。

TextMeshProのアウトライン

TexeMeshProでアウトラインを表現する際にはマテリアルを調整することになります。 詳しくは以前記事を書いてるのでそちらを参照してください。

myudon.hatenablog.com

マテリアルのアウトラインの太さを制御しているのが「Thickness」と「Dilate」になります。 これはシェーダーのプロパティとしては「_OutlineWidth」と「_FaceDilate」として用意されています。

下記がシェーダーの引用です。

Shader "TextMeshPro/Distance Field" {

Properties {
    _FaceTex            ("Face Texture", 2D) = "white" {}
    _FaceUVSpeedX       ("Face UV Speed X", Range(-5, 5)) = 0.0
    _FaceUVSpeedY       ("Face UV Speed Y", Range(-5, 5)) = 0.0
    [HDR]_FaceColor     ("Face Color", Color) = (1,1,1,1)
    _FaceDilate         ("Face Dilate", Range(-1,1)) = 0

    [HDR]_OutlineColor  ("Outline Color", Color) = (0,0,0,1)
    _OutlineTex         ("Outline Texture", 2D) = "white" {}
    _OutlineUVSpeedX    ("Outline UV Speed X", Range(-5, 5)) = 0.0
    _OutlineUVSpeedY    ("Outline UV Speed Y", Range(-5, 5)) = 0.0
    _OutlineWidth       ("Outline Thickness", Range(0, 1)) = 0
    _OutlineSoftness    ("Outline Softness", Range(0,1)) = 0
//...(以下省略

アウトラインの大きさを変える

マテリアルの「_OutlineWidth」と「_FaceDilate」をランタイムで更新することで、太さを調整することが可能です。

private void SetOutlineMaterialPropertySample(Material material, float outlineWidth, float dilate)
{
    material.SetFloat("_OutlineWidth", outlineWidth);
    material.SetFloat("_FaceDilate", dilate);
}

TextMeshProUGUIから装飾に該当するマテリアルを参照する際は、fontSharedMaterialを参照します。

private void SetOutlineMaterialPropertySample(TextMeshProUGUI text, float outlineWidth, float dilate)
{
    var material = text.fontSharedMaterial;
    material.SetFloat("_OutlineWidth", outlineWidth);
    material.SetFloat("_FaceDilate", dilate);
}

アウトラインの検証機能を実装してみる

マテリアルを動的に変更するアプローチをもとに、アウトラインの検証を行う機能を作ってみます。 先にスクリプトの全容だけ乗せます。

using TMPro;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 適当なアウトラインの太さ調整を行なってみるサンプルUI
/// </summary>
public class FontOutlineController : MonoBehaviour
{
    [SerializeField] 
    private Slider _fontSlider;
    
    [SerializeField] 
    private TextMeshProUGUI[] _sampleTexts;

    private void Awake()
    {
        _fontSlider.onValueChanged.AddListener(SetSampleTextsOutlineMaterialProperty);
    }

    /// <summary>
    /// 適当に指定したサンプルのテキストにアウトライン設定を行う
    /// </summary>
    private void SetSampleTextsOutlineMaterialProperty(float outlinePixel)
    {
        foreach (var text in _sampleTexts)
        {
            SetOutlineMaterialProperty(text, outlinePixel);
        }
    }
    
    /// <summary>
    /// アウトラインをピクセル単位で指定
    /// </summary>
    private void SetOutlineMaterialProperty(TextMeshProUGUI text, float outlinePixel)
    {
        var fontSize = text.fontSize;
        var offset = GetOutlineThicknessOffset(text, outlinePixel, fontSize);
        var material = text.fontSharedMaterial;
        material.SetFloat("_OutlineWidth", offset);
        material.SetFloat("_FaceDilate", offset);
    }

    /// <summary>
    /// TMPのアウトラインに基づく指定ピクセルサイズの太さのための設定値計算
    /// </summary>
    /// <returns></returns>
    private float GetOutlineThicknessOffset(TextMeshProUGUI text, float pixelSize, float fontSize)
    {
        var fontAsset = text.font;
        var padding = fontAsset.creationSettings.padding;
        var samplingSize = fontAsset.creationSettings.pointSize;
        // マテリアルの値(px) = 実現したいアウトラインの太さ / 文字のフォントサイズ * SamplingSize / Padding / 2
        return pixelSize / fontSize * samplingSize / padding / 2;
    }
}

これを適当なUIに当てはめて実行できるようにしてみます。 今回は検証として異なるマテリアルを設定した文字をいくつか並べ、SerializeFieldからアタッチし、適用させます。

このように、アウトラインの太さを実行中にピクセル単位で調整できる機能は、一番見栄えのいい状態や表示崩れが起きてしまう値の検証することができます。

まとめ

アウトラインは実際にランタイムで確認してみないとデザインの調整がしづらいものですが、マテリアルを動的に調整する機能を用意することでそういった調整のサポートをすることが可能です。

今回はアウトラインに関係する値だけ調整しましたが、他の要素の調整にも活用可能なのでぜひご活用ください。

2023年の振り返り

2023年やったこと

個人ブログ投稿 10本

この技術ブログである「うどんてっくメモ」、今年は10個の記事を投稿しました。 毎月1個を目標にしていますが、仕事が色々とばたついてきており後半はなかなか時間がとれずでした。

myudon.hatenablog.com

仕事で学んだ知見が中心で、Unityの細かい困りごととか新しい機能をちょこちょこと文章化していました。

また、後述もするCEDEC2023でも触れた自動テスト周りの技術に業務的に大きく携わるタイミングがあり、色々とUnityのモバイルゲームに対する自動テストのアプローチと向き合っていたのも印象に残っています。

myudon.hatenablog.com

自動テスト自体は2019年ぐらいからAirtestを個人的に色々仕組み化して遊んでいたのですが、まさかここまで自動テストに業界が注目するようになるとは思っていなかったです。あの頃の知識には今年とてもお世話になりました。ありがとう昔の自分。

会社ブログ投稿 1本

所属している会社のエンジニアブログに1本記事を投稿しました。アドベントカレンダーの運営を技術広報として行なっているため、今年も会社の仕事や技術を紹介するお話になりました。

technote.qualiarts.jp

このエンジニアブログも結構年数も経つのですがかなり記事も蓄積されてきました。SNSでも反応をもらうことが増えて嬉しい限りです。

ちなみに2023年はエンジニアブログからは20個の記事を公開しました。どの記事にも実は裏で記事の推敲や公開作業に携わっております。

技術広報というものが難しすぎて色々試行錯誤しながらですが、QualiArtsの技術は業界に色々と知見を届けられそうだなと内部のエンジニアとして思っているので、今後もアウトプットを出し続けていきたいな〜と感じています。

CEDEC2023登壇

業界に入って6年目、いつか登壇とかできるかなあと思ってたのですが、ついにCEDECに登壇することができました。

モバイルゲームのQA効率化という観点で、リモート操作可能なDevice Farmを社内に構築したり、それで自動テストしてみたりするニッチな話にはなってしまいました。

しかし、昨今のモバイルゲーム開発の大規模化や自動テストのトレンドを見ると、これらの技術はちゃんと向き合うべきと考えて色々と試行錯誤した結果、ちゃんとCEDECという形で伝えられる内容になったかなと思います。

資料も公開していますので、ぜひぜひUnityとかモバイルゲームのテスト周りに興味がある方はご覧ください。

speakerdeck.com

また、会社の技術広報としても公募に力を入れて、社内のネタ探しや内容の推敲、広報活動を行なっておりました。 今年は3つのセッションをQualiArtsから届けることができました。

technote.qualiarts.jp

今後も自分の登壇、会社の登壇を双方伸ばしていきたい所存です。プレイングマネージャー的な。

技術書典の出典

技術書典で「UniTips」というUnityの技術書を定期的に会社で出しているのですが、2023年も執筆に参加しました。

creator.game.cyberagent.co.jp

実は直近あった技術書典15にはUniTipsの新刊を届けられずでした(自分含めた運営チームがバタバタしていた)。2024年はUniTipsを再開していきたい...

個人でも1冊Godotのゲーム開発のサンプル本を出す予定だったのですが、念の後半からバタついてかなわずでした。来年こそ形にできるのだろうか。

とはいえ、技術書という形で知見を文章化するのは引き続き自分の研鑽にも役立つので、積極的に取り組んでいきたいところではあります。

商業誌執筆

2023年もご縁がありまして、2022年の技術書典で出していた個人執筆の本を商業誌として刊行していただきました。

nextpublishing.jp

今年は個人で本を執筆できていないため、商業誌を新しくは出せてないです。時間を見つけてこの辺も再開したいところです。

CA.unityの継続

CA.unityを2021年にはじめ、2023年はその継続回となるCA.unity #6~7を開催いたしました。

meetup.unity3d.jp

今年も株式会社コロプラ様、株式会社ディー・エヌ・エー様にご協力いただきまして、開催いたしました。本当にありがとうございました。開催したアーカイブについてもすべてUnity Learning Materialsに掲載していただいております。

learning.unity3d.jp

今後も多くの企業の知見を届けられるよう運用を続けていく予定です。

FP2級取得

2022年にFP3級を取ったのですが、ついでだと思って勉強してFP2級もとりました。

3級は正直ちょっと勉強したらとれたんですが、2級は問われる知識が細部のものになったりなかなか難しく、通勤の合間だけではなく休日とかにも作業しながら勉強をする習慣をつけていました。

家のローン周りや親の資産整理、金融資産周りのマネジメントに大変役立っており、FPの勉強してよかったなと思う毎日です。 社会人のみなさん、FPはいいぞ

Qiitaアドベントカレンダー 2つ参加

毎年恒例アドベントカレンダー、2023年も会社のものとUnityのものに参加させていただきました。 会社のものは前述した記事で、UnityのものではUGSの紹介をしました。

myudon.hatenablog.com

今年も会社のアドベントカレンダーを運用して広報していました。Unityしかりインフラしかり、結構流行りの技術も取り入れた身のある内容ばかりとなっています。協力してくださっている方には頭が上がりません。 こちらもぜひぜひご覧ください。

qiita.com

余談 : 趣味の活動

趣味として家でゲームや映画などを楽しむほかに、各所のテーマパークやコンテンツにいそしんだり、旅行に行ったりしています。 今年はそういった活動が多かった年でした。

  • 1月 : ディズニーシー
  • 2月 : ライブ、熱海旅行
  • 4月 : ライブ、宮城旅行
  • 5月 : ライブ、鎌倉旅行、富山旅行、福岡旅行
  • 7月 : ディズニーランド、伊勢志摩旅行
  • 8月 : ライブ、ポケモンWCS巡り、伊勢志摩旅行
  • 9月 : 千葉旅行
  • 10月 : 江ノ島旅行、ユニバ、ハリーポッター・スタジオツアー
  • 11月 : ライブ、鳥取・島根旅行

ゲームクリエイターだし趣味で色んなゲームするのはまぁ当然なんですが、個人的にこういった世の中のコンテンツに色々触れるのが結構学びがあったりしています。技術的にも色んなアプローチがあるので、技術的な目線でもとても面白いです。

あと、お酒とご飯にお金使うのも趣味なので、各地でご馳走食べたり、お土産買いまくるのもいい栄養になっています。

最後に

2023年は仕事の忙しさが去年より増しながらも、技術広報として自分も登壇しつつ会社の知見を押し出したり、ゲーム外の知識にも色々向き合ったり、並列にタスクに取り組む日々でした。

2024年以降はこの忙しさで得た知識をもっと業界に出したり、業界で話題の技術に触れていきたいな〜とふんわり思っています。

Godotとか、AIとか、色々興味は尽きないですね。

【Unity】サーバーレスで定数管理!サンプルで学ぶRemote Config【Unity Gaming Service】

はじめに

本記事は、Unity Advent Calendar 2023 20日目の記事になります。 検証環境は以下になります。バージョンが違う場合には動作しない場合がありますので、ご留意ください。

  • Unity2022.3.11f1
  • Unity Gaming Services Use Cases 1.10.0

Unity Gaming Service

Unity Gaming Service(以下、UGS)はゲームでよく必要となる機能を使いやすい形でUnityが提供しているサービスです。自前でサーバーを設けたりせず、さまざまな機能を実現することが可能です。

unity.com

2023年の頭にもUGSについては紹介記事を出しています。こちらでは「Cloud Code」や「Cloud Save」といったサービスについて触れていますので、興味のある方はそちらもご覧ください。

myudon.hatenablog.com

Remote Config

Remote Configは必要な定数をゲーム中のコードではなく、UGS上で管理することのできるサービスです。 ゲームをビルドして更新せずとも、ゲーム中の定数を調整してリリースすることが可能です。

unity.com

stringのkeyと保存する値をペアにして、任意の定数をリアルタイムで設定することができます。

バトル中のダメージ係数だったり、何かしらのアイテムの消費数だったり、さまざまな定数が存在しますが、変更するたびにビルドするのは面倒です。そこで、定数をゲームのバイナリとは別のサーバーなどで保管するのはひとつの手段です。

一定の規模感のある開発であれば自前で運用することも検討できますが、小規模な開発ではこれを運用をするのはハードルが高いです。 そこで、サーバーレスでこういった機能を使えるRemote Configが活躍します。

A/B Test for Level Difficulty

今回はA/B Test for Level Difficultyを見ていきます。 これはゲーム開発におけるプレイ継続率や課金率といった指標に対する有効なパラメータを探るための仕組みのサンプルです。どのパラメータが有効かを探るために、ゲーム上でパラメータを色々と試せる仕組みをUGSを用いて実現しています。

github.com

サンプルの実行やダッシュボードの確認を行うまでには、クローンして起動、そして各種サービスのセットアップ処理が存在します。こちらの説明は署略しますので、上記リンクの公式手順に従ってください。

プロジェクトを起動し、A/B Test for Level Difficultyのシーンを実行します。 プレイヤーの経験値ゲージとボタンが表示され、ボタンを押すと経験値の上昇とレベルアップが確認できます。

UGSのサンプルということで、以下の機能がUGSによって実現されています。

UGS 機能
Analytics プレイヤーの行動解析
Authentication プレイヤー認証
Cloud Code プレイヤーの経験値上昇
Cloud Save プレイヤーの経験値やレベルなどの情報の保存
Economy ゲーム内通貨の管理
Game Overrides プレイヤーの経験値テーブルの反映
Remote Config プレイヤーの経験値処理に関する定数管理

今回注目するのがRemote Configです。 このサンプルでは以下の定数を定義しています。前述したセットアップのタイミングでUGSのダッシュボードから設定します。

Key Type 内容
AB_TEST_GROUP string プレイヤーのグループ
AB_TEST_ID string テストのID
AB_TEST_LEVEL_UP_XP_NEEDED int レベルアップに必要な経験値
AB_TEST_XP_INCREASE int ボタン押したときの経験値獲得量
CURRENCIES json 通貨ごとのアイコンのパス指定

ちなみにダッシュボードにサクッとUnity上から行きたい時は、Servicesのところから「Go to Dashboard」から飛ぶのが簡単です。

Remote Configの設定

Remote Configの定数は前述の通りブラウザのダッシュボードから設定可能ですが、Unityのエディタ上でもAPIを介して専用のWindowで設定できるようになっています。

Unity上で設定を変更して反映を行うには「Push」を、逆にダッシュボード上での変更を取り込みたい時には「Pull」のボタンを押します。Unity上で変更した際には「Push」を忘れると変更が反映されないので注意してください。

型はintやstringから選択することが可能です。ちょっと複雑なパラメータを定義するときなどはjson形式で設定することも可能です。

各種UGSのセットアップが完了していれば、AB_TEST_LEVEL_UP_XP_NEEDEDとAB_TEST_XP_INCREASEを好きな数字に変えて試してみてください。 ボタンを押した時の経験値量やレベルアップの閾値が変わることが確認できるかと思います。

スクリプト上からRemote Configへの参照

定数を設定した後は、スクリプト上から参照する必要があります。サンプルがどのようにRemote Configの定数を取り扱っているかみてみます。 RemoteConfigManagerというクラスで、RemoteConfigServiceを経由してkeyをもとにC#上のDictionaryとして管理しています。RemoteConfigServiceから直感的にGetHogeHogeというメソッドで呼び出すだけでシンプルです。

また、通貨ごとのアイコンのパスが4種類あるのをCURRENCIESというjsonにまとめて管理しています。json形式の設定を活用することで、特定のデータ型に対応した定数を定義することも可能になっています(直感的な調整はやりづらくなるので、使う場面の注意は必要そうです)。

void GetConfigValues()
{
    levelUpXPNeeded = RemoteConfigService.Instance.appConfig.GetInt("AB_TEST_LEVEL_UP_XP_NEEDED");
    abGroupName = RemoteConfigService.Instance.appConfig.GetString("AB_TEST_GROUP");
    abTestID = RemoteConfigService.Instance.appConfig.GetString("AB_TEST_ID");
    var json = RemoteConfigService.Instance.appConfig.GetJson("CURRENCIES");
    currencyDataDictionary = CreateCurrencyDictionary(json);
}

Dictionary<string, CurrencySpec> CreateCurrencyDictionary(string json)
{
    var dictionary = new Dictionary<string, CurrencySpec>();

    var currencyDataHolder = JsonUtility.FromJson<CurrencyDataHolder>(json);

    foreach (var currencyData in currencyDataHolder.currencyData)
    {
        dictionary[currencyData.currencyId] = currencyData.currencySpec;
    }

    return dictionary;
}

また、このサンプルのレベルアップの実処理については、Unity上のC#ではなく、Cloud Code上で実行されています。 Cloud Code自体の実装はjavascriptで、プロジェクト配下のABTestLevelDifficulty_GainXPAndLevelIfReady.jsに実装されています。その中でもRemote Configへの参照が確認できます。

async function getRemoteConfigData(remoteConfig, projectId, environmentId) {
    const getRemoteConfigSettingsResponse = await remoteConfig.assignSettingsGet(
        projectId,
        environmentId,
        'settings',
        ["AB_TEST_XP_INCREASE", "AB_TEST_LEVEL_UP_XP_NEEDED"]
    );

    if (getRemoteConfigSettingsResponse.data.configs &&
        getRemoteConfigSettingsResponse.data.configs.settings &&
        getRemoteConfigSettingsResponse.data.configs.settings.AB_TEST_XP_INCREASE &&
        getRemoteConfigSettingsResponse.data.configs.settings.AB_TEST_LEVEL_UP_XP_NEEDED) {

        return {
            xpIncreaseAmount: getRemoteConfigSettingsResponse.data.configs.settings.AB_TEST_XP_INCREASE,
            xpNeededForNextLevel: getRemoteConfigSettingsResponse.data.configs.settings.AB_TEST_LEVEL_UP_XP_NEEDED
        };
    }

    throw new CloudCodeCustomError("Failed to get AB_TEST_XP_INCREASE or AB_TEST_LEVEL_UP_XP_NEEDED data from Remote Config.");
}

このCloud CodeとRemote Configの連携からもわかるように、UGS同士の連携が容易なのも便利な点です。

Game Overrideによる調整

このサンプルでは、Remote Configの定数群に対してテンプレートを用意して一括で反映する手段も説明しています。 それがGame Overrideです。

docs.unity.com

サンプルのREADMEでは、AB_TEST_GROUP、AB_TEST_ID、AB_TEST_LEVEL_UP_XP_NEEDEDをパターン分けして反映可能にしています。 セットアップの手順については公式のガイドを参考にしてください。 ダッシュボード上から、Remote Configの定数のパターンの定義と時間を設定することが可能です。

これによって、特定の定数の値をスケジュールに沿って反映することができます。機能を時限式で提供したり、イベントの運用をしたりすることが可能です。

サンプルではA/Bテストということで、パラメータのパターンによるユーザーの反応をテストする機能として活用されています。 ゲームの指標を測るためのオプションもいろいろと揃っているので、ストアでリリースしているゲームのパラメータを指標に沿って調整したい方はぜひ触ってみてください。

まとめ

サンプルから、Remote Configの使い方や他のUGSとの連携した使い方を学ぶことができました。ゲーム内の定数管理を柔軟にしたいけどどうしようかなと悩んでいる方は一考の余地はあるのではないでしょうか。

本記事はサンプルの全容は説明しておらず、つまんで紹介しています。詳細が気になる方は、ぜひともサンプルを手元で実行してみることをおすすめします。

【Unity】インストールされているプラットフォームのモジュールの分だけmetaに差分が出てしまう

最近、仕事のチーム内でUnityのバージョンを更新したところ、テクスチャのmetaに差分が意図せず生まれてしまうようになりました。

この差分はテクスチャのプラットフォームごとのimport設定に基づくものです。たとえばAndroidiOSWebGLとモジュールを入れていれば、その分の設定がmetaとして自動で生成されました。

従来だとプラットフォームのタブを選択し、特定の設定を保存しない限りは生まれていなかったのですが、どうやらimportした時に自動でデフォルト設定を行うようになったっぽいです。

プラットフォームのモジュール

Unityのインスペクタ上の設定表示

チーム内ではgitによるバージョン管理を行なっています。誰かが画像を新規にコミットするたびに、インストールしているモジュールの差によって作業者のローカルにmetaの差分が出てしまうのはいささか面倒でした。

また、チームではコードの整理を定期的に行い、プルリクエストを出す仕組みを運用しています。 そのコードの整理を行うCI環境で、特定のプラットフォームのモジュールが都合上必要でした。そのため、そのmetaの差分が毎度関係ないコードの整理のプルリクエストに紛れるようになってしまいました。

検証してみると、以下のバージョンでは確実に起きていることを確認しました。
2022については最新のLTSで、2021以前のバージョンではどこかのパッチバージョンから適用されてそうでした。(2021.3.0f1ではそういった現象は起きなかった)

  • Unity 2021.3.31f
  • Unity 2022.3.0f1

チームとして、以下の取り組みでなんとか誤魔化しています。

  • Unityを開いて差分をコミットしてプルリクエストを出す仕組みを定期的に稼働
  • チームメンバーはプロジェクトで使うバージョンのUnityについて、指定のプラットフォームのモジュールをインストール
  • チームメンバーは既存で入っている指定外のプラットフォームのモジュールを削除

モジュールの削除については、Unity Hubにメニューがなかったため直接Applications配下にあるモジュールを物理的に削除することでなんとかなりました。(良い方法あれば教えてください)

個人で開発する分には問題ないのですが、チーム開発だとmetaの差分はなかなかに困りものです。 Unityさんがいい感じにしてくれることを祈っています。

【Unity】タップやドラッグなどのUIの入力処理をスクリプトから実行する【EventSystems】

はじめに

本記事の検証環境は以下になります。バージョンが違う場合には動作しない場合がありますので、ご留意ください。

  • Unity 2022.3.0f1
  • Unity UI 1.0.0

ExecuteEventsによる入力処理

ユーザーのここを押下した、長押しした、ドラッグした、といった入力処理は「UnityEngine.EventSystems.ExecuteEvents」を活用することで実現できます。

// どこかのタイミングでUIのカメラを渡す
private Camera _camera;

// クリック位置をPointerEventDataとして定義
var eventData = new PointerEventData(EventSystem.current)
{
    // UI空間の座標から、実際の画面の入力座標に変換する
    position = RectTransformUtility.WorldToScreenPoint(_camera, target.position)
};

// EventSystems.ExecuteEventsによるシミュレート
ExecuteEvents.Execute(target.gameObject, eventData, ExecuteEvents.pointerClickHandler);

必要な情報は、

  • 対象のGameObject
  • 入力情報の定義となるPointerEventData
  • 対象の入力を発火させるEventFunctor

になります。

3つ目のEventFunctorはPointerEventとしての種類を定義するための引数です。EventFunctor自体はExecuteEventsにpublic staticなメンバとして用意されています。基本的には、特定のイベントに合わせてこれらを呼び出します。

// ExecuteEvents内のEventFunctorの抜粋
public static EventFunction<IPointerMoveHandler> pointerMoveHandler
{
    get { return s_PointerMoveHandler; }
}

public static EventFunction<IPointerEnterHandler> pointerEnterHandler
{
    get { return s_PointerEnterHandler; }
}

public static EventFunction<IPointerExitHandler> pointerExitHandler
{
    get { return s_PointerExitHandler; }
}

public static EventFunction<IPointerDownHandler> pointerDownHandler
{
    get { return s_PointerDownHandler; }
}

public static EventFunction<IPointerUpHandler> pointerUpHandler
{
    get { return s_PointerUpHandler; }
}

クリック、押下の開始と終了、ドラッグなどなど、適宜UI操作に対応するIEventSystemHandlerに沿って、EventFunctorを選定します。

そして肝心なのがPointerEventDataです。入力する座標を始めとして多くの情報を定義できます。

docs.unity3d.com

最初のサンプルではRectTransformUtility.WorldToScreenPointによって変換した座標を渡しています。基本的にExecuteEventsの座標情報は、ワールド空間の座標をもとに、スクリーン空間の座標にしてやる必要があります。

たとえば、ドラッグ処理などを行う際には移動量の計算に必要となるpressPositionやdeltaを適宜設定する必要があります。

// ドラッグの始点と終点の定義
var startOffset = 0f;
var endOffset = 100f;
var targetPosition = target.transform.position;
var startPositionWorld = targetPosition + startOffset;
var endPositionWorld = targetPosition + endOffset;
var startPosition = RectTransformUtility.WorldToScreenPoint(_camera, startPositionWorld);
var endPosition = RectTransformUtility.WorldToScreenPoint(_camera, endPositionWorld);

// 始点からドラッグ開始
ExecuteEvents.Execute(target.gameObject, eventData, ExecuteEvents.pointerDownHandler);
ExecuteEvents.Execute(target.gameObject, eventData, ExecuteEvents.beginDragHandler);

// durationにそって移動
var elapsedTime = 0d;
while (elapsedTime < duration)
{
    // ドラッグしたまま位置を調整
    eventData.position = Vector2.Lerp(startPosition, endPosition, (float)(elapsedTime / duration));
    ExecuteEvents.Execute(target.gameObject, eventData, ExecuteEvents.dragHandler);
    elapsedTime += Time.deltaTime;
}

// 終点でドラッグ終了
ExecuteEvents.Execute(target.gameObject, eventData, ExecuteEvents.endDragHandler);
ExecuteEvents.Execute(target.gameObject, eventData, ExecuteEvents.pointerUpHandler);

選択操作やリストのスクロール操作なども適宜PointerEventDataとEventFunctorを渡せば実現できます。

おわりに

EventSystems.ExecuteEventsによってスクリプトから適宜UIの操作を実行することが可能でした。自動でUI操作を行いたい時など、場面はニッチですが活躍するのではないでしょうか。筆者は自動テストに用いています。

ここでまとめたExecuteEventsによる実装は、「【Unity】Anjinを使ってシーン中のuGUIを自動で操作する仕組み作り - うどんてっくメモ」という記事でも活用しています。 興味のある方は見てみてください。

【Unity】Unity Recorderをスクリプトから実行する

はじめに

本記事の検証環境は以下になります。バージョンが違う場合には挙動やAPIが違う場合がありますので、ご留意ください。

  • Unity 2022.3.0f1
  • Unity Recorder 4.0.1

Unity Recorderとは

Unity RecorderはUnity Editor上でのシーン描画の動画や連番画像での記録や、オブジェクトの動きを記録する公式が提供する機能です。

assetstore.unity.com

利用する際にはPackage Managerから導入します。 基本的な使い方は以下の記事が丁寧に解説してくれていますので、参照してみてください。

qiita.com

Unity Recorderをスクリプトから呼び出す

本記事では基本的な使い方であるGUIからの利用ではなく、スクリプトから呼び出す方法を紹介します。

スクリプトから呼び出す際には、大まかに次の流れで実行できます。

  • RecorderControllerSettingsの用意
  • 各種RecorderSettingsの設定
  • RecorderControllerSettingsへのRecorderSettingsの設定
  • RecorderControllerからPrepareRecordingの呼び出し(録画準備)
  • RecorderControllerからStartRecordingの呼び出し(録画開始)
  • RecorderControllerからStopRecordingの呼び出し(録画終了)

RecorderControllerSettingsはScriptableObject形式の大元の設定です。RecorderWindowで操作する際も裏ではこれを用いて録画形式の設定を行なっています。 RecorderWindowが使っている設定はLibraryで管理されていて、GetRecorderControllerSettings GetGlobalSettingsで取得できます。 自前でスクリプト上からも普通のScriptableObjectの容量で生成可能です。

// 設定の生成
var setting = ScriptableObject.CreateInstance<RecorderControllerSettings>();

// グローバル設定
var settingGlobal = RecorderControllerSettings.GetGlobalSettings();

RecorderSettingsは動画や音声といった項目ごとの設定を行います。継承したクラスがいくつか用意されていて、それらを適宜設定します。

RecorderSettings 役割
AnimationRecorderSettings AnimationClipの記録
ImageRecorderSettings 連番画像の記録
MovieRecorderSettings 動画の記録
AudioRecorderSettings 音声の記録

本記事では動画の記録のためのMovieRecorderSettingsと音声の記録ためのAudioRecorderSettingsの設定の実装をサンプルとして示します。

// ScriptableObjectの生成
var movieRecorderSettings = ScriptableObject.CreateInstance<MovieRecorderSettings>();
var audioRecorderSettings = ScriptableObject.CreateInstance<AudioRecorderSettings>();

// 動画の記録設定
movieRecorderSettings.FrameRate = 30f;
movieRecorderSettings.ImageInputSettings = new GameViewInputSettings
{
    OutputWidth = 1080, 
    OutputHeight = 1920,
};
// エンコード周りはIEncoderSettingというinterfaceで管理されていて、特定の設定のテンプレートが派生クラスとして用意されている
movieRecorderSettings.EncoderSettings = new CoreEncoderSettings
{
     Codec = CoreEncoderSettings.OutputCodec.MP4,
     EncodingQuality = CoreEncoderSettings.VideoEncodingQuality.Medium,
};
movieRecorderSettings.OutputFile = "file_name";
movieRecorderSettings.Enabled = true;

// 音声の記録設定
audioRecorderSettings.OutputFile = Application.temporaryCachePath;
audioRecorderSettings.Enabled = true;

AudioRecorderSettingsなどは元々internalだったのですが、最近になってpublicになったりと結構変更が激しい部分なのでバージョンには注意してください。 そして、RecorderSettingsはAddRecorderSettingsでRecorderControllerSettingsに設定を行います。

setting.AddRecorderSettings(movieRecorderSettings);
setting.AddRecorderSettings(audioRecorderSettings);

ここまで完了したら録画処理を呼び出すだけです。RecorderControllerのインスタンスを生成し、準備したRecorderControllerSettingsを渡します。 後はStartRecordingで開始し、StopRecordingで終了です。StartRecordingを呼び出す前には一回PrepareRecordingを呼び出す必要があります。

var recorderController = new RecorderController(setting);
recorderController.PrepareRecording();
recorderController.StartRecording();

// なんやかんや記録したいことをする

recorderController.StopRecording();

以上がUnity Recorderの機能をスクリプトから呼び出す大まかな手順になります。 最後にまとめた最低限のコードを貼っておきます。

var setting = ScriptableObject.CreateInstance<RecorderControllerSettings>();

// ScriptableObjectの生成
var movieRecorderSettings = ScriptableObject.CreateInstance<MovieRecorderSettings>();
var audioRecorderSettings = ScriptableObject.CreateInstance<AudioRecorderSettings>();

// 動画の記録設定
movieRecorderSettings.FrameRate = 30f;
movieRecorderSettings.ImageInputSettings = new GameViewInputSettings
{
    OutputWidth = 1080, 
    OutputHeight = 1920,
};
// エンコード周りはIEncoderSettingというinterfaceで管理されていて、特定の設定のテンプレートが派生クラスとして用意されている
movieRecorderSettings.EncoderSettings = new CoreEncoderSettings
{
    Codec = CoreEncoderSettings.OutputCodec.MP4,
    EncodingQuality = CoreEncoderSettings.VideoEncodingQuality.Medium,
};
movieRecorderSettings.OutputFile = "file_name";
movieRecorderSettings.Enabled = true;

// 音声の記録設定
audioRecorderSettings.OutputFile = Application.temporaryCachePath;
audioRecorderSettings.Enabled = true;

setting.AddRecorderSettings(movieRecorderSettings);
setting.AddRecorderSettings(audioRecorderSettings);

var recorderController = new RecorderController(setting);
recorderController.PrepareRecording();
recorderController.StartRecording();

// なんやかんや記録したいことをする

recorderController.StopRecording();

おわりに

Unity Recorderはスクリプトからも簡単に使えて便利です。Device Simulatorだと使えなかったりするのでめんどくさい部分もまだまだありますが、今後も改善されていきそうで少し期待しています。

筆者の職場ではこれを用いてUnityエディタ上でのテストの録画だったり、3D描画のチェックの自動化に用いたりしています。 やや使い道はトリッキーですが、ぜひ興味のある方は使ってみてください。

【Rider】プロジェクト内で使っている造語にtypoの警告を出さないようにする

はじめに

本記事は下記のRiderのバージョンで検証しています。バージョンによっては設定項目や機能が違う可能性がありますので、ご留意ください。

  • Rider 2023.1

ReSpellerの設定

typo判定を調整するには、Preferencesを開き、Editor -> Spelling -> ReSpellerから設定を行います。

pleiades.io

Generalでtypo判定を行うケースの調整、Visibility Optionsで対象メンバの設定、UserWordsで無視する単語の指定が行えます。 プロジェクト内の造語を無視したいので、UserWordsに追加して保存します。

ソリューションレイヤーでの保存

ReSpellerはソリューションレイヤーでの設定の保存が可能です。

pleiades.io

保存する際に、共有レイヤーベースでの保存を選択します。

後は、[ソリューション名].sln.DotSettingsをgitなどのバージョン管理システムで共有すればプロジェクト内のメンバーで設定が利用できます。