うにてぃブログ

主にUnityとC#に関する記事を書いていきます

まとめ

ライブラリ

github.com

デバッグツール
Easing関数
Missing Script 検索ツール
モデル撮影ツール
Module化ライブラリ
Unity上で使える Git Tool
単純なメッシュを作成するツール
カメラ一覧を表示
アセットのディレクトリを移動する
テクスチャ生成ツール
対象のコンポーネントを利用している Prefab や Scene を探すツール
CustomInspector の テンプレートコードを出力するツール
アセットの参照を確認するツール
Hierarchy に存在するオブジェクトの参照を調べるツール
Texture を加工するツール
ProjectWindow を拡張するツール
AnimationClip の Path を一括で置換するツール

アドベントカレンダー

hacchi-man.hatenablog.com

【Unity】Addressable Asset Systemを活用したアセットの動的なカタログ登録方法

UnityのAddressable Asset Systemは、アプリケーションのアセットを効率的に管理し、ダイナミックなロードやアンロードを可能にする強力なツールです。このシステムを使用することで、ゲームやアプリの開発者は、リソースの管理を容易にし、ユーザー体験を向上させることができます。

しかし、Addressable Asset Systemのカタログに登録されていないアセットをロードしようとすると、通常はエラーが発生します。この問題を解決するために、UnityのEditor上で動作する便利な処理を開発しました。

こちらの処理は Unity Editor上でのみ動作します。そのため、開発中にアセットをダイナミックにロードしたい場合に有用です。

この機能は、指定されたアドレスのアセットを非同期でロードすることです。しかし、そのアドレスがAddressableのカタログに登録されていない場合、自動的にカタログに登録する処理が行われます。これにより、アプリケーションの実行時にエラーが発生することなく、必要なアセットを動的にロードすることが可能になります。

ただし、TryGetPathToAddressの処理は Addressable パスの付け方によって異なると思うので、環境に合った変換処理が必要になります

using System.IO;
using System.Linq;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;

public static class AddressableUtility
{
    private static readonly string LOCATION_ID = "Temp";
    
    /// <summary>
    /// カタログにアドレスが登録されていないのであれば登録する
    /// </summary>
    public static async Task<T> LoadAssetAsync<T>(string address)
    {
        var handle = Addressables.LoadResourceLocationsAsync(address);

        await handle.Task;

        // ロード成功した
        if (handle.Status == AsyncOperationStatus.Succeeded
            && handle.Result != null
            && handle.Result.Count > 0)
        {
            return (T) handle.Result.ElementAt(0);
        }
        
        // アドレスに対応したファイルを探す
        if (!TryGetPathToAddress(address, out var path))
        {
            Debug.LogError("Not Found");
            return default;
        }
        
        var resourceProviders = Addressables.ResourceManager.ResourceProviders;
        if (resourceProviders.All(v => v.GetType() != typeof(AssetDatabaseProvider)))
        {
            resourceProviders.Add(new AssetDatabaseProvider());
        }

        var resourceLocationMap = Addressables.ResourceLocators
            .Where(v => v.GetType() == typeof(ResourceLocationMap))
            .FirstOrDefault(v => v.LocatorId == LOCATION_ID);
        if (resourceLocationMap == null)
        {
            resourceLocationMap = new ResourceLocationMap(LOCATION_ID);
            Addressables.AddResourceLocator(resourceLocationMap);
        }
        
        var location = new ResourceLocationBase(
            address, 
            path, 
            typeof(AssetDatabaseProvider).FullName,
            AssetDatabase.GetMainAssetTypeAtPath(path)
        );
        ((ResourceLocationMap)resourceLocationMap).Add(address, location);
        
        var reloadHandle = Addressables.LoadAssetAsync<T>(address);
        await reloadHandle.Task;
        return reloadHandle.Result;
    }
    
    /// <summary>
    /// 対象のアドレスからパスを検索する
    /// </summary>
    private static bool TryGetPathToAddress(string address, out string path)
    {
        var addressDirName = Path.GetDirectoryName(address);
        var addressFileName = Path.GetFileNameWithoutExtension(address);
        var guids = AssetDatabase.FindAssets(addressFileName, new[] {addressDirName});
        foreach (var guid in guids)
        {
            var assetPath = AssetDatabase.GUIDToAssetPath(guid);
            var fileName = Path.GetFileNameWithoutExtension(assetPath);
            if (fileName != addressFileName)
                continue;

            path = assetPath;
            return true;
        }

        path = null;
        return false;
    }
}

呼び出し処理は以下のようにします

ただし今回の処理では Handle を返してないので内部でリークしてしまうのでよしなに調整する必要があります

public class Sample : MonoBehaviour
{
    public string Address;
    public string Address2;
    
    private async void Start()
    {
        var obj = await AddressableUtility.LoadAssetAsync<GameObject>(Address);
        Instantiate(obj);
        obj = await AddressableUtility.LoadAssetAsync<GameObject>(Address2);
        Instantiate(obj);
    }
}

Unity のあまり記事を見かけない便利そうな機能 ~PrefabUtility~

UnityのPrefabUtilityは、プレハブに関連するさまざまな操作を行うための強力なツールです。この記事では、PrefabUtilityの主要な機能を紹介し、Unity開発者がプレハブをより効果的に扱えるようにする方法を説明します。

PrefabUtilityの概要

PrefabUtilityはUnityエディターの一部で、プログラム的にプレハブを作成、変更、情報取得するためのAPIを提供します。これにより、開発者はスクリプトを介してプレハブに対する複雑な操作を実行できます。

主要な機能

プレハブの作成(CreatePrefab)

プレハブを作成する基本的な方法です。

    public GameObject objectToPrefab;

    void Start()
    {
        string prefabPath = "Assets/MyPrefab.prefab";
        PrefabUtility.SaveAsPrefabAsset(objectToPrefab, prefabPath);
        Debug.Log("Prefab created at " + prefabPath);
    }

このコードは、objectToPrefabに設定されたGameObjectをプレハブとして保存します。

プレハブのインスタンス化(InstantiatePrefab)

既存のプレハブから新しいインスタンスを生成します。

    public GameObject prefab;

    void Start()
    {
        GameObject prefabInstance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
        prefabInstance.transform.position = new Vector3(0, 0, 0);
    }

InstantiatePrefabメソッドは、選択されたプレハブの新しいインスタンスをシーンに追加します。

プレハブの更新(ApplyPrefabInstance)

シーン内のプレハブインスタンスに加えた変更をプレハブアセットに適用します。

    public GameObject prefabInstance;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            PrefabUtility.ApplyPrefabInstance(prefabInstance, InteractionMode.UserAction);
            Debug.Log("Prefab updated");
        }
    }

この例では、スペースキーが押された時にプレハブインスタンスの変更をプレハブに適用します。

プレハブの状態の確認(GetPrefabInstanceStatus)

ゲームオブジェクトのプレハブの状態を確認します。

    public GameObject objectToCheck;

    void Start()
    {
        PrefabInstanceStatus status = PrefabUtility.GetPrefabInstanceStatus(objectToCheck);
        Debug.Log("Prefab status: " + status);
    }

このコードは、指定されたオブジェクトがプレハブインスタンスであるか、接続が切断されているかなどの情報を提供します。

PrefabUtilityはこれらの機能だけでなく、さまざまな便利なメソッドを提供します。上記の例を基に、さまざまなプレハブ操作をカスタマイズしてみてください。

もちろん、UnityのPrefabUtilityには他にも多くの便利な機能があります。以下にさらに詳しくいくつかの機能とサンプルコードを追加します。

プレハブへの変更の反映(ApplyChangesToPrefab)

シーン内のプレハブインスタンスで行った変更をプレハブに反映します。

    public GameObject prefabInstance;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.U))
        {
            GameObject root = PrefabUtility.GetOutermostPrefabInstanceRoot(prefabInstance);
            if (root != null)
            {
                PrefabUtility.ApplyPrefabInstance(root, InteractionMode.UserAction);
            }
        }
    }

このスクリプトでは、「U」キーを押すことで、指定されたプレハブインスタンスに加えられた変更を、そのプレハブのアセットに反映します。

プレハブの置換(ReplacePrefab)

シーン内のゲームオブジェクトを既存のプレハブに置き換えます。

    public GameObject originalObject;
    public GameObject prefab;

    void Start()
    {
        if (PrefabUtility.IsPartOfPrefabAsset(originalObject))
        {
            PrefabUtility.SaveAsPrefabAsset(originalObject, AssetDatabase.GetAssetPath(prefab));
            Debug.Log("Prefab replaced");
        }
    }

このコードは、originalObjectの内容をprefabに置き換えます。

ネストされたプレハブの取得(GetCorrespondingObjectFromSource)

ネストされたプレハブのルートを取得します。

    public GameObject nestedPrefabInstance;

    void Start()
    {
        GameObject sourcePrefab = PrefabUtility.GetCorrespondingObjectFromSource(nestedPrefabInstance) as GameObject;
        if (sourcePrefab != null)
        {
            Debug.Log("Found nested prefab: " + sourcePrefab.name);
        }
    }

このスクリプトは、ネストされたプレハブインスタンスに対応する元のプレハブアセットを見つけます。

プレハブの解除(UnpackPrefabInstance)

プレハブインスタンスを通常のゲームオブジェクトに変換します。

    public GameObject prefabInstance;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.P))
        {
            PrefabUtility.UnpackPrefabInstance(prefabInstance, PrefabUnpackMode.OutermostRoot, InteractionMode.UserAction);
            Debug.Log("Prefab unpacked");
        }
    }

「P」キーを押すと、選択されたプレハブインスタンスが通常のゲームオブジェクトに変換されます。

これらの例はPrefabUtilityクラスの一部の機能を示しています。Unityのドキュメントやコミュニティで共有されている情報をもとに、さらに深く探究することをお勧めします。プレハブシステムはUnity開発の強力なツールであり、その全機能を理解することで、より効率的かつ効果的にゲーム開発を進めることができます。

もちろんです、UnityのPrefabUtilityにはさらにいくつかの興味深い機能があります。それらについても見ていきましょう。

プレハブのオーバーライドの検出(HasPrefabInstanceAnyOverrides)

プレハブインスタンスがオーバーライドされているかどうかを確認します。

    public GameObject prefabInstance;

    void Start()
    {
        bool hasOverrides = PrefabUtility.HasPrefabInstanceAnyOverrides(prefabInstance, false);
        Debug.Log("Prefab has overrides: " + hasOverrides);
    }

このスクリプトは、指定されたプレハブインスタンスが元のプレハブから変更されているかどうかを検出します。

プレハブの検証(IsPartOfPrefabAsset)

オブジェクトがプレハブの一部かどうかを確認します。

    public GameObject gameObjectToCheck;

    void Start()
    {
        bool isPartOfPrefab = PrefabUtility.IsPartOfPrefabAsset(gameObjectToCheck);
        Debug.Log("Is part of a prefab: " + isPartOfPrefab);
    }

このメソッドは、指定されたGameObjectがプレハブアセットの一部かどうかを判断します。

プレハブのルートを取得(GetOutermostPrefabInstanceRoot)

プレハブの最外部のルートを取得します。

    public GameObject prefabInstance;

    void Start()
    {
        GameObject outermostRoot = PrefabUtility.GetOutermostPrefabInstanceRoot(prefabInstance);
        if (outermostRoot != null)
        {
            Debug.Log("Outermost Prefab Root: " + outermostRoot.name);
        }
    }

このスクリプトは、指定されたプレハブインスタンスの最外部のルートオブジェクトを取得します。

プレハブモードでの編集(EnterPrefabMode)

プレハブモードでプレハブを編集するために使用します。

    public GameObject prefabAsset;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.E))
        {
            AssetDatabase.OpenAsset(prefabAsset);
            PrefabUtility.EnterPrefabMode(prefabAsset);
            Debug.Log("Entered Prefab Mode: " + prefabAsset.name);
        }
    }

このコードは、「E」キーを押すことで指定されたプレハブアセットの編集をプレハブモードで開始します。


この記事はUnity開発者向けにPrefabUtilityの主要機能と実用例を簡潔にまとめたものです。Unityのドキュメントやオンラインコミュニティからさらに詳しい情報を得ることをお勧めします。

【Unity】TextMeshPro DynamicFontを使用する際のgit管理の課題を解決するスクリプト

TextMeshPro DynamicFontを使用する際のgit管理の課題を解決するスクリプト

Unityのテキスト表示において、TextMeshProは広く使用されています。その中でも、DynamicFontを利用する場合、再生中にFontAtlasが変更されると、gitの履歴管理が困難になるという問題があります。この記事では、そんな課題を解決するためのスクリプトを紹介します。

DynamicFontを利用すると、再生中にテキストが動的に変化するため、それに伴いFontAtlasも変更されます。この結果、gitのコミット履歴にはフォント関連の変更が頻繁に含まれることになり、差分の管理が複雑化します。

そこで、以下のスクリプトを利用することで、再生中にFontAtlasをクリアすることができます。これにより、gitの履歴管理がより正確になり、変更の追跡が容易になります。

#if UNITY_EDITOR

using TMPro;
using UnityEditor;
using UnityEngine;

public static class TextMeshProAtlasClear
{
    [InitializeOnLoadMethod]
    private static void Test()
    {
        EditorApplication.playModeStateChanged -= PlayModeStateChanged;
        EditorApplication.playModeStateChanged += PlayModeStateChanged;
    }

    private static void PlayModeStateChanged(PlayModeStateChange state)
    {
        if (state != PlayModeStateChange.ExitingPlayMode)
            return;
        
        var assets = Resources.FindObjectsOfTypeAll<TMP_FontAsset>();
        // Dynamicなアセットのアトラスをクリア
        foreach (var asset in assets)
        {
            if (asset.atlasPopulationMode != AtlasPopulationMode.Dynamic)
                continue;

            asset.ClearFontAssetData(setAtlasSizeToZero: true);
        }
    }
}

#endif

このスクリプトは、Unityのエディタモードでプレイモードが終了する際に実行され、DynamicFontに関連するFontAtlasをクリアします。これにより、再生中のFontAtlasの変更がリセットされ、gitの差分管理が容易になります。

このスクリプトをプロジェクトに組み込むことで、TextMeshPro DynamicFontを使用する際のgit管理の課題を解決し、スムーズな開発作業をサポートします。

【Unity】親 RectTransform 内でドラッグ可能な UI 要素を実装する方法

このスクリプトをUI要素にアタッチすることで、そのUI要素がドラッグ可能になり、親要素の範囲内で自由に移動できるようになります。

using UnityEngine;
using UnityEngine.EventSystems;
 
public class UIRectDraggable : UIBehaviour, IBeginDragHandler, IDragHandler
{
    private Vector2 _pointerPosition;
    private Vector3 _dragPosition;
    private RectTransform _rect;
    private RectTransform _parentRect;
 
    protected override void Awake()
    {
        _rect = transform as RectTransform;
        _parentRect = _rect.parent as RectTransform;
    }
 
    void IBeginDragHandler.OnBeginDrag(PointerEventData data)
    {
        _dragPosition = _rect.localPosition;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(_parentRect, data.position, data.pressEventCamera, out _pointerPosition);
    }
 
    void IDragHandler.OnDrag(PointerEventData data)
    {
        Vector2 localPointerPosition;
        if (RectTransformUtility.ScreenPointToLocalPointInRectangle(_parentRect, data.position, data.pressEventCamera, out localPointerPosition))
        {
            Vector3 offsetToOriginal = localPointerPosition - _pointerPosition;
            _rect.localPosition = _dragPosition + offsetToOriginal;
        }
        
        var localPosition = _rect.localPosition;
 
        var minPosition = _parentRect.rect.min - _rect.rect.min;
        var maxPosition = _parentRect.rect.max - _rect.rect.max;
 
        localPosition.x = Mathf.Clamp(_rect.localPosition.x, minPosition.x, maxPosition.x);
        localPosition.y = Mathf.Clamp(_rect.localPosition.y, minPosition.y, maxPosition.y);
 
        _rect.localPosition = localPosition;
    }
}

【Unity】UnityEngine.Object.InstantiateAsync

Unity 2022.3.20f1から、「Object.InstantiateAsync」が新たに導入されました。これにより、プレハブの非同期インスタンス化が可能になりました。

従来のUnityでは、プレハブのインスタンス化は同期的に行われていました。これにより、複数のプレハブを同時に生成しようとすると、ゲームの進行が一時停止したり、フレームレートが低下したりする問題がありました。

しかし、「Object.InstantiateAsync」を使用すると、プレハブのインスタンス化をゲームの他のプロセスと並行して行うことができ、ゲームの流れを中断せずにフレームレートを安定させながらプレハブを生成できます。この機能は、特に大量のプレハブが必要なシーンや、重いコンテンツのロードが必要な場面で、ゲームパフォーマンスの最適化とプレイの滑らかさを向上させます。

Unity 2022.3.20f1では、以下の「InstantiateAsync」メソッドが追加されました。

InstantiateAsync<T>(T original)
InstantiateAsync<T>(T original, Transform parent)
InstantiateAsync<T>(T original, Vector3 position, Quaternion rotation)
InstantiateAsync<T>(T original, Transform parent, Vector3 position, Quaternion rotation)
InstantiateAsync<T>(T original, int count)
InstantiateAsync<T>(T original, int count, Transform parent)
InstantiateAsync<T>(T original, int count, Vector3 position, Quaternion rotation)
InstantiateAsync<T>(T original, int count, ReadOnlySpan<Vector3> positions, ReadOnlySpan<Quaternion> rotations)
InstantiateAsync<T>(T original, int count, Transform parent, Vector3 position, Quaternion rotation)
InstantiateAsync<T>(T original, int count, Transform parent, ReadOnlySpan<Vector3> positions, ReadOnlySpan<Quaternion> rotations)

サンプル

以下は、Unity 2022.3.20f1における「InstantiateAsync」メソッドの一つである「InstantiateAsync(T original, int count, ReadOnlySpan positions, ReadOnlySpan rotations)」を使用したサンプルコードです。このメソッドは、指定されたプレハブを複数の位置と回転で非同期にインスタンス化する際に使用します。

private IEnumerator Start()
{
    int count = 5; // インスタンス化する数

    // 位置と回転を設定
    Vector3[] positions = new Vector3[count];
    Quaternion[] rotations = new Quaternion[count];

    for (int i = 0; i < count; i++)
    {
        positions[i] = new Vector3(i * 2.0f, 0, 0); // 位置をずらして設定
        var rotation = Quaternion.Euler(Random.Range(0, 180f), Random.Range(0, 180f), Random.Range(0, 180f)); // 回転を設定
        rotations[i] = rotation;
    }

    // 位置と回転データをReadOnlySpanに変換
    ReadOnlySpan<Vector3> positionSpan = new ReadOnlySpan<Vector3>(positions.ToArray());
    ReadOnlySpan<Quaternion> rotationSpan = new ReadOnlySpan<Quaternion>(rotations.ToArray());

    // 非同期インスタンス化
    yield return InstantiateAsync(prefab, count, positionSpan, rotationSpan);
}

【Unity】ScrollRect を使用して特定の要素までスクロールさせる方法

このクラスは、指定したインデックスの子オブジェクトを表示エリア内にスクロールする「ScrollTo」メソッドと、指定したRectTransformが表示エリア内に収まるようにスクロールする「ScrollTo」メソッドが実装されています。

これを利用することで、以下のように一部隠れているオブジェクトを全部表示させることができます

public static class ScrollRectExtension
{
    public static void ScrollTo(this ScrollRect self, int index)
    {
        var count = self.content.childCount;
        if (index < 0 || index >= count)
            return;

        var rect = self.content.GetChild(index).GetComponent<RectTransform>();
        if (rect == null)
            return;
        
        self.ScrollTo(rect);
    }
    
    public static void ScrollTo(this ScrollRect self, RectTransform child)
    {
        var vectorIndex = self.horizontal ? 0 : 1;
        var rect1 = self.viewport.rect;
        var rect2 = self.content.rect;
        
        var viewSize = new Vector2(rect1.width, rect1.height)[vectorIndex];
        var contentSize = new Vector2(rect2.width, rect2.height)[vectorIndex];
        var anchoredPosition = Mathf.Abs(self.content.anchoredPosition[vectorIndex]);
        var max = contentSize - viewSize;
        
        var childPosition = Mathf.Abs(child.anchoredPosition[vectorIndex]);
        var childSizeHalf = (new Vector2(child.rect.width, child.rect.height) / 2f)[vectorIndex];
        
        if (childPosition - childSizeHalf < anchoredPosition)
        {
            var value = childPosition - childSizeHalf - viewSize / 2f + childSizeHalf;
            ApplyAnchoredPosition(value);
        }
        else if (childPosition + childSizeHalf > anchoredPosition + viewSize)
        {
            var value = childPosition + childSizeHalf - viewSize + (viewSize - childSizeHalf * 2) / 2f;
            ApplyAnchoredPosition(value);
        }
        
        void ApplyAnchoredPosition(float value)
        {
            var pos = self.content.anchoredPosition;
            pos[vectorIndex] = Mathf.Clamp(value, 0, max) * (vectorIndex == 1 ? 1 : -1);
            self.content.anchoredPosition = pos;
        }        
    }
}

【Unity】Inspectorで簡単にカスタムメニュー項目を追加する方法

UnityのInspector上で特定のフィールド名を右クリックした際に表示されるメニューに、カスタムな動作や処理を追加したいことはよくあります。

そんなときに便利なのが、ContextMenuItem属性です。この属性を使うと、簡単にカスタムなメニュー項目を追加できます。具体的な使い方を見てみましょう。

using UnityEngine;

public class ExampleScript : MonoBehaviour
{
    [ContextMenuItem("Log Speed", nameof(LogSpeed))]
    [SerializeField]
    private float _speed = 1f;
    
    private void LogSpeed()
    {
        Debug.Log(_speed);
    }
}

上記の例では、"speed"というフィールドにContextMenuItem属性を追加し、"Log Speed"というメニュー項目を追加しています。このメニュー項目をクリックすると、LogSpeedメソッドが呼び出され、"speed"の値がログに出力されます。

これにより、Inspector上で簡単にカスタムメニュー項目を追加し、開発効率を向上させることができます。