1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GameCanvas for Unity 独自フォントを使う方法(v7.0.2以前)

1
Last updated at Posted at 2026-07-02

GameCanvas for Unity は、慶應義塾大学『スマートデバイスプログラミング』にて教材として使われている 2Dゲームフレームワーク です。
引用元:https://github.com/sfc-sdp/GameCanvas-Unity

概要

GameCanvas for Unity v7.0.2以前では、Res フォルダに独自フォントを追加した状態でリソース定義を更新すると、InvalidCastException が発生する場合があります。
この問題が発生すると、リソース定義の更新が正常に完了せず、追加したフォントが GcFont.cs に反映されません。
この記事では、発生していた問題、原因、修正内容についてまとめます。
修正方法だけ知りたい方で、尚且つGameCanvas v7.0.2の方は、修正コード全文に飛んでください!

発生条件

Res フォルダに .ttf ファイルを追加した状態で、以下を実行します。

GameCanvas > リソース定義の強制更新

すると、次のようなエラーが発生していました。

InvalidCastException: Specified cast is not valid.
GameCanvas.Editor.GcEditorResourceBuilder.ValidateImages () (at ./Packages/jp.ac.keio.sfc.sdp/Editor/GcEditorResourceBuilder.cs:391)
GameCanvas.Editor.GcEditorResourceBuilder.Build () (at ./Packages/jp.ac.keio.sfc.sdp/Editor/GcEditorResourceBuilder.cs:54)
GameCanvas.Editor.GcEditorMenu.BuildResource () (at ./Packages/jp.ac.keio.sfc.sdp/Editor/GcEditorMenu.cs:55)

問題点

上記のエラーにより、リソース定義の更新処理が途中で止まってしまい、追加したフォントを GcFont.cs に反映できない状態になっていました。

原因は、画像の検証処理内で AssetImporter を直接 TextureImporter にキャストしていたことです。Package/GameCanvas/Editor/GcEditorResourceBuilder.csValidateImages()の以下の処理部分です。

TextureImporter importer = (TextureImporter)AssetImporter.GetAtPath(assetPath);

Res フォルダには画像だけでなく、フォントファイルなども配置できます。

.ttf ファイルは TextureImporter として扱うことができません。

そのため、.ttf ファイルに対して TextureImporter へのキャストを行うと、InvalidCastException が発生していました。

修正方法

AssetImporter を直接 TextureImporter にキャストしないようにします。
修正前は、次のように直接キャストしていました。

//元コード
TextureImporter importer = (TextureImporter)AssetImporter.GetAtPath(assetPath);

これを、as を使った安全なキャストに変更します。

//修正後
TextureImporter importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;

そして、TextureImporter として扱えないAssetは画像ではないため、画像検証処理をスキップします。

//完全版
TextureImporter importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
if (importer == null) continue;

これにより、.ttf ファイルなど画像以外のAssetが Res フォルダに含まれていても、画像検証処理で例外が発生しないようになります。

修正コード全文

以下は、GameCanvas for Unity v7.0.2 を前提にした修正版のコード全文です。
Package/GameCanvas/Editor/GcEditorResourceBuilder.csを以下に差し替えてください。
⚠︎環境によって内容が異なる可能性があります。

/*------------------------------------------------------------*/
// <summary>GameCanvas for Unity</summary>
// <author>Seibe TAKAHASHI</author>
// <remarks>
// (c) 2015-2026 Smart Device Programming.
// This software is released under the MIT License.
// http://opensource.org/licenses/mit-license.php
// </remarks>
/*------------------------------------------------------------*/
#nullable enable
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.U2D;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.Audio;
using UnityEngine.U2D;

namespace GameCanvas.Editor
{
    public sealed class GcEditorResourceBuilder
    {
        //----------------------------------------------------------
        #region 変数
        //----------------------------------------------------------

        const string k_GcAssetDir = "Assets/GameCanvas";
        const string k_GcAutoGeneratedScriptsDir = k_GcAssetDir + "/AutoGeneratedScripts";
        const string k_GcResourcesDir = k_GcAssetDir + "/Resources";
        const string k_GcSpriteAtlasPath = k_GcAssetDir + "/SpriteAtlas.spriteatlas";
        const string k_UserResourceDir = "Assets/Res";
        static readonly Regex k_RegBadChars = new(@"[^\w_]");
        static readonly Regex k_RegCloneSufix = new(@"\(Clone\)\z");
        static readonly Regex k_RegImg = new(@"^Assets/Res/(?<filename>.+\.(gif|png|jpg|jpeg|tga|tif|tiff|bmp|tiff|pict))$", RegexOptions.IgnoreCase);
        static readonly Regex k_RegSnd = new(@"^Assets/Res/(?<filename>.+\.(wav|mp3|ogg|aiff|aif))$", RegexOptions.IgnoreCase);
        static readonly Regex k_RegTopNumber = new(@"^\d");
        static readonly string[] k_SearchDir = new[] { k_UserResourceDir };
        #endregion

        //----------------------------------------------------------
        #region 公開関数
        //----------------------------------------------------------

        public static void Build()
        {
            using (GcEditorUtility.ProgressBarScope(nameof(GcEditorResourceBuilder), "ビルド中......"))
            {
                using (GcEditorUtility.AssetEditingScope())
                {
                    CreateMustAssets();
                    ValidateImages();
                    ValidateSounds();
                }
                using (GcEditorUtility.AssetEditingScope())
                {
                    BuildSprites();
                    BuildSounds();
                    BuildFonts();
                    BuildTexts();
                }
            }
        }

        internal static void OnLaunch()
        {
            CreateMustAssets();
        }

        internal static bool OnPostprocessAudio(in AudioImporter importer, in AudioClip clip)
        {
            const string k_AudioImporterLabel = "GameCanvas AudioImporter 1.2";

            if (importer == null) return false;
            if (!k_RegSnd.IsMatch(importer.assetPath)) return false;

            var sampleSettingSE = new AudioImporterSampleSettings()
            {
                compressionFormat = AudioCompressionFormat.ADPCM,
                loadType = AudioClipLoadType.DecompressOnLoad,
                sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate,
                preloadAudioData = true
            };
            var sampleSettingBGM = new AudioImporterSampleSettings()
            {
                compressionFormat = AudioCompressionFormat.Vorbis,
                loadType = AudioClipLoadType.Streaming,
                sampleRateSetting = AudioSampleRateSetting.OptimizeSampleRate,
                preloadAudioData = true
            };

            if (importer.userData != k_AudioImporterLabel)
            {
                var isSE = (clip.length < 3f);
                var sampleSetting = isSE ? sampleSettingSE : sampleSettingBGM;
                importer.loadInBackground = false;
                importer.forceToMono = isSE;
                importer.defaultSampleSettings = sampleSetting;
                importer.SetOverrideSampleSettings("Standalone", sampleSetting);
                importer.SetOverrideSampleSettings("iOS", sampleSetting);
                importer.SetOverrideSampleSettings("Android", sampleSetting);
                importer.userData = k_AudioImporterLabel;
                return true;
            }
            return false;
        }

        internal static bool OnPreprocessTexture(in TextureImporter importer)
        {
            const string k_TextureImporterLabel = "GameCanvas TextureImporter 2.1";

            if (importer == null) return false;
            if (!k_RegImg.IsMatch(importer.assetPath)) return false;

            if (importer.userData != k_TextureImporterLabel)
            {
                var settings = new TextureImporterSettings();
                importer.ReadTextureSettings(settings);
                {
                    settings.filterMode = FilterMode.Point;
                    settings.mipmapEnabled = false;
                    settings.readable = false;
                    settings.spriteAlignment = (int)SpriteAlignment.TopLeft;
                    settings.spriteMeshType = SpriteMeshType.Tight;
                    settings.spritePixelsPerUnit = 1f;
                    settings.textureType = TextureImporterType.Sprite;
                }
                importer.SetTextureSettings(settings);
                importer.maxTextureSize = 2048;
                importer.spriteImportMode = SpriteImportMode.Single;
                importer.textureCompression = TextureImporterCompression.Uncompressed; // note: 圧縮はスプライトアトラスで設定するため、ここでは非圧縮とすること
                importer.userData = k_TextureImporterLabel;
                return true;
            }
            return false;
        }
        #endregion

        //----------------------------------------------------------
        #region 内部関数
        //----------------------------------------------------------

        private static void BuildFonts()
        {
            const string k_DefaultFontPath = k_GcAssetDir + "/DefaultFont.ttf";

            var assetPaths = AssetDatabase.FindAssets($"t:{nameof(Font)}", k_SearchDir)
                .Select(AssetDatabase.GUIDToAssetPath)
                .Append(k_DefaultFontPath)
                .ToArray();

            var removeList = AssetDatabase.FindAssets($"t:{nameof(GcReferenceFont)}")
                .Select(AssetDatabase.GUIDToAssetPath)
                .ToList();
            var fontDict = new Dictionary<string, string>();

            foreach (var assetPath in assetPaths)
            {
                var font = AssetDatabase.LoadAssetAtPath<Font>(assetPath);
                if (font == null) continue;
                var name = GenerateFieldName(font.name);
                var path = k_GcResourcesDir + $"/GcFont{name}.asset";
                GcReferenceFont.Write(path, font);
                fontDict.Add(name, $"GcFont{name}");
                removeList.Remove(path);
            }
            GenerateResourceDefineScript("GcFont", fontDict);

            foreach (var path in removeList)
            {
                AssetDatabase.DeleteAsset(path);
            }
        }

        private static void BuildSounds()
        {
            const string k_AudioMixerPath = k_GcAssetDir + "/AudioMixer.mixer";
            const string k_DstPath = k_GcResourcesDir + "/GcAudioMixer.asset";

            var mixer = AssetDatabase.LoadAssetAtPath<AudioMixer>(k_AudioMixerPath);
            Assert.IsNotNull(mixer, $"[GameCanvas] {k_AudioMixerPath} is missing!");
            GcReferenceMixer.Write(k_DstPath, mixer);

            var assetPaths = AssetDatabase.FindAssets($"t:{nameof(AudioClip)}", k_SearchDir)
                .Select(AssetDatabase.GUIDToAssetPath)
                .ToArray();

            var removeList = AssetDatabase.FindAssets($"t:{nameof(GcReferenceSound)}")
                .Select(AssetDatabase.GUIDToAssetPath)
                .ToList();
            var soundDict = new Dictionary<string, string>();

            foreach (var assetPath in assetPaths)
            {
                var clip = AssetDatabase.LoadAssetAtPath<AudioClip>(assetPath);
                if (clip == null) continue;
                var name = GenerateFieldName(clip.name);
                var path = k_GcResourcesDir + $"/GcSound{name}.asset";
                GcReferenceSound.Write(path, clip);
                soundDict.Add(name, $"GcSound{name}");
                removeList.Remove(path);
            }
            GenerateResourceDefineScript("GcSound", soundDict);

            foreach (var path in removeList)
            {
                AssetDatabase.DeleteAsset(path);
            }
        }

        private static void BuildSprites()
        {
            const string k_DstPath = k_GcResourcesDir + "/GcAtlas.asset";

            var atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(k_GcSpriteAtlasPath);
            Assert.IsNotNull(atlas, $"[GameCanvas] {k_GcSpriteAtlasPath} is missing!");
            GcReferenceAtlas.Write(k_DstPath, atlas);

            SpriteAtlasUtility.PackAllAtlases(EditorUserBuildSettings.activeBuildTarget, true);
            var sprites = new Sprite[atlas.spriteCount];
            atlas.GetSprites(sprites);

            var spriteDict = sprites
                .Where(sprite => sprite != null)
                .ToDictionary(sprite => k_RegCloneSufix.Replace(sprite.name, ""), sprite => sprite.rect);
            GenerateResourceDefineScript("GcImage", spriteDict);
        }

        private static void BuildTexts()
        {
            var assetPaths = AssetDatabase.FindAssets($"t:{nameof(TextAsset)}", k_SearchDir)
                .Select(AssetDatabase.GUIDToAssetPath)
                .ToArray();

            var removeList = AssetDatabase.FindAssets($"t:{nameof(GcReferenceText)}")
                .Select(AssetDatabase.GUIDToAssetPath)
                .ToList();
            var fontDict = new Dictionary<string, string>();

            foreach (var assetPath in assetPaths)
            {
                var text = AssetDatabase.LoadAssetAtPath<TextAsset>(assetPath);
                if (text == null) continue;
                var name = GenerateFieldName(text.name);
                var path = k_GcResourcesDir + $"/Text{name}.asset";
                GcReferenceText.Write(path, text);
                fontDict.Add(name, $"Text{name}");
                removeList.Remove(path);
            }
            GenerateResourceDefineScript("GcText", fontDict);

            foreach (var path in removeList)
            {
                AssetDatabase.DeleteAsset(path);
            }
        }

        private static void CreateMustAssets()
        {
            const string k_AutoGeneratedScriptsAsmref = k_GcAutoGeneratedScriptsDir + "/GameCanvas.asmref";

            if (!AssetDatabase.IsValidFolder(k_UserResourceDir))
            {
                AssetDatabase.CreateFolder("Assets", "Res");
            }
            if (!AssetDatabase.IsValidFolder(k_GcAssetDir))
            {
                AssetDatabase.CreateFolder("Assets", "GameCanvas");
            }
            if (!AssetDatabase.IsValidFolder(k_GcResourcesDir))
            {
                AssetDatabase.CreateFolder(k_GcAssetDir, "Resources");
            }
            if (!AssetDatabase.IsValidFolder(k_GcAutoGeneratedScriptsDir))
            {
                AssetDatabase.CreateFolder(k_GcAssetDir, "AutoGeneratedScripts");
            }
            if (!File.Exists(k_AutoGeneratedScriptsAsmref))
            {
                File.WriteAllText(k_AutoGeneratedScriptsAsmref, @"{
    ""reference"": ""GameCanvas""
}");
                AssetDatabase.ImportAsset(k_AutoGeneratedScriptsAsmref, ImportAssetOptions.ForceSynchronousImport);
            }
            if (!File.Exists(k_GcSpriteAtlasPath))
            {
                var assetDir = AssetDatabase.LoadAssetAtPath<Object>(k_UserResourceDir);
                Assert.IsNotNull(assetDir, $"[GameCanvas] {k_UserResourceDir} is missing!");
                var atlas = new SpriteAtlas();
                atlas.Add(new[] { assetDir });
                AssetDatabase.CreateAsset(atlas, k_GcSpriteAtlasPath);
            }
        }

        private static string GenerateFieldName(in string path)
        {
            var fieldName = k_RegBadChars.Replace(ObjectNames.NicifyVariableName(path), "");
            if (k_RegTopNumber.IsMatch(fieldName))
            {
                return "__" + fieldName;
            }
            return fieldName;
        }

        private static void GenerateResourceDefineScript(string className, in Dictionary<string, string> resourcePaths)
        {
            const string k_FolderPath = "Assets/GameCanvas/AutoGeneratedScripts";

            var defines = resourcePaths
                .OrderBy(elem => elem.Key)
                .Select(path => $"        public static readonly {className} {path.Key} = new(\"{path.Value}\");")
                .ToArray();

            var filePath = k_FolderPath + $"/{className}.cs";
            var text = $@"/*------------------------------------------------------------*/
// <summary>GameCanvas for Unity</summary>
// <author>Seibe TAKAHASHI</author>
// <remarks>
// (c) 2015-2026 Smart Device Programming.
// This software is released under the MIT License.
// http://opensource.org/licenses/mit-license.php
// </remarks>
/*------------------------------------------------------------*/
#nullable enable

namespace GameCanvas
{{
    public readonly partial struct {className} : System.IEquatable<{className}>
    {{
        internal const int __Length__ = {defines.Length};
{string.Join(System.Environment.NewLine, defines)}
    }}
}}
";
            if (File.Exists(filePath))
            {
                var old = File.ReadAllText(filePath);
                if (text == old) return;
            }
            File.WriteAllText(filePath, text);
            AssetDatabase.ImportAsset(filePath);
        }

        private static void GenerateResourceDefineScript(string className, in Dictionary<string, Rect> resourcePaths)
        {
            var defines = resourcePaths
                .OrderBy(e => e.Key)
                .Select(e => $"        public static readonly {className} {GenerateFieldName(e.Key)} = new(\"{e.Key}\", {(int)e.Value.width}, {(int)e.Value.height});")
                .ToArray();

            var filePath = k_GcAutoGeneratedScriptsDir + $"/{className}.cs";
            var text = $@"/*------------------------------------------------------------*/
// <summary>GameCanvas for Unity</summary>
// <author>Seibe TAKAHASHI</author>
// <remarks>
// (c) 2015-2026 Smart Device Programming.
// This software is released under the MIT License.
// http://opensource.org/licenses/mit-license.php
// </remarks>
/*------------------------------------------------------------*/
#nullable enable

namespace GameCanvas
{{
    public readonly partial struct {className} : System.IEquatable<{className}>
    {{
        internal const int __Length__ = {defines.Length};
{string.Join(System.Environment.NewLine, defines)}
    }}
}}
";
            if (File.Exists(filePath))
            {
                var old = File.ReadAllText(filePath);
                if (text == old) return;
            }
            File.WriteAllText(filePath, text);
            AssetDatabase.ImportAsset(filePath);
        }

        private static void ValidateImages()
        {
            var assetPaths = AssetDatabase.FindAssets("t:Texture2D", k_SearchDir)
                .Select(AssetDatabase.GUIDToAssetPath)
                .ToArray();

            foreach (var assetPath in assetPaths)
            {
                var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
                if(importer == null) continue;
                
                if (OnPreprocessTexture(importer))
                {
                    importer.SaveAndReimport();
                }
                Resources.UnloadAsset(importer);
            }
        }

        private static void ValidateSounds()
        {
            var assetPaths = AssetDatabase.FindAssets("t:AudioClip", k_SearchDir)
                .Select(AssetDatabase.GUIDToAssetPath)
                .ToArray();

            foreach (var assetPath in assetPaths)
            {
                var clip = AssetDatabase.LoadAssetAtPath<AudioClip>(assetPath);
                var importer = (AudioImporter)AssetImporter.GetAtPath(assetPath);
                if (OnPostprocessAudio(importer, clip))
                {
                    importer.SaveAndReimport();
                }
                Resources.UnloadAsset(importer);
                Resources.UnloadAsset(clip);
            }
        }
        #endregion
    }
}

あとがき

現在これに関してPRを出しているので、次のバージョンなどでは治ってるかもしれません!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?