[Unity] スプライトエディタで自作画像からビットマップフォントを作る

# はじめに
この記事は、**複数の文字を一枚にまとめた画像からビットマップフォントファイル(.fnt)を作成する**Unity上のエディタ拡張を紹介するものです。Unityの**スプライトエディタ**を使って、文字ごとの調整を行うことが出来ます。

# 経緯

ビットマップフォントというのはスプライト画像を文字のように並べて表示するフォントです。
一般的なフォントに比べると以下のような違いがあります。

### 通常フォント(SDFなど)とビットマップフォントの違い・使い分け

| 項目 | 通常フォント | ビットマップフォント |
|------|----------------------|----------------------|
| **画質** | 拡大しても綺麗 | 解像度次第（拡大するとぼやける） |
| **装飾の自由度** | シェーダーで輪郭や陰影などつけるが、描画方法自体が特殊だとシェーダーも独自実装になる | 画像で自由にデザイン可能 |
| **パフォーマンス** | 装飾をつければつけるほど描画コストがかかる | ただのスプライトと同じなので軽い |
| **適した用途** | 統一感の欲しいUI、事前に内容確定しないテキスト | 内容が限定されてるが派手な演出が欲しいところ |
| **文字の追加** | 既存のフォントから作成、一応動的に追加も可能 | 事前に用意した画像のみ表示可能 |

本記事は、Unityで自作画像をフォントとして利用できるようにする自作ツールについて紹介します。

:::note info
Macには有名な Glyph Designer というソフトがあるらしいが、Windowsでは使えません。
他にないかとググると出てくるのは、既存のフォントからビットマップフォントを作るというものだったり、あるいはもっと高度なベクターデータでTTF(トゥルータイプフォント)を作成するものだったりで、大半はちょっと目的の物とは違いました。
これは良さそうと思って入手しに行くとサポート終了してたり・・・

Snoebox： AIRで動くが、AIR自体がサポート終了
Littera: Flashで動くが、Flash自体がサポート終了
Bitmap Font Generator: 既存のフォントからしか作れない
:::

自作画像からビットマップフォントを作れるオンラインツールはあります。

### オンラインツール
https://snowb.org/
https://hahahoho.studio/

ただ、どちらも**一文字ごとに別画像として読み込む必要がある**のがちょっと面倒くさいのですよね。
**「複数の文字を一枚にまとめた画像から作れるツール」が欲しかったのですが、なかったので自作しました**。


# コード：エディタ拡張
```csharp:BitmapFontConverter.cs
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Text;
using System.Linq;
using System.Text.RegularExpressions;
using System.Collections.Generic;

public class BitmapFontConverter : EditorWindow
{
    private Texture2D atlasTexture;
    private string fontName = "custom_font";
    private int fontSize = 42;
    private int lineHeight = 54;
    private int baseHeight = 37;
    private string outputPath = "Assets/custom_font.fnt";
    private bool exportAsXml = false;

    [MenuItem("Tools/Bitmap Font Converter")]
    public static void ShowWindow()
    {
        GetWindow<BitmapFontConverter>("Bitmap Font Converter");
    }

    private void OnGUI()
    {
        GUILayout.Label("Bitmap Font Converter", EditorStyles.boldLabel);
        EditorGUI.BeginChangeCheck();
        atlasTexture = (Texture2D)EditorGUILayout.ObjectField("Atlas Texture", atlasTexture, typeof(Texture2D), false);
        Sprite[] sprites = null;
        if (EditorGUI.EndChangeCheck())
        {
            var assetPath = AssetDatabase.GetAssetPath(atlasTexture);
            sprites = AssetDatabase.LoadAllAssetsAtPath(assetPath)
                .OfType<Sprite>().ToArray();
            var heights = sprites.Select(s => s.textureRect.height).ToArray();
            var pivots = sprites.Select(s => s.pivot.y).ToArray();
            fontName = atlasTexture.name;
            fontSize = Mathf.CeilToInt(heights.Average() + 2);
            lineHeight = Mathf.CeilToInt(heights.Max() + 2);
            baseHeight = Mathf.CeilToInt(pivots.Average());
            outputPath = $"{Path.GetDirectoryName(assetPath)}\\{fontName}.fnt";
        }
        fontName = EditorGUILayout.TextField("Font Name", fontName);
        fontSize = EditorGUILayout.IntField("Font Size", fontSize);
        lineHeight = EditorGUILayout.IntField("Line Height", lineHeight);
        baseHeight = EditorGUILayout.IntField("Base Height", baseHeight);
        outputPath = EditorGUILayout.TextField("Output Path", outputPath);
        exportAsXml = EditorGUILayout.Toggle("Export as XML", exportAsXml);

        if (GUILayout.Button("Export Font File"))
        {
            ExportFont(sprites);
        }
    }

    private void ExportFont(IEnumerable<Sprite> sprites = null)
    {
        if (atlasTexture == null)
        {
            Debug.LogError("Atlas Texture is not assigned!");
            return;
        }

        if (sprites == null)
        {
            sprites = AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(atlasTexture))
            .OfType<Sprite>().ToArray();
        }

        if (sprites.Count() == 0)
        {
            Debug.LogError("No sprites found in the selected texture!");
            return;
        }

        StringBuilder sb = new StringBuilder();
        if (exportAsXml)
        {
            sb.AppendLine("<font>");
            sb.AppendLine($"\t<info face=\"{fontName}\" size=\"{fontSize}\" bold=\"0\" italic=\"0\" charset=\"\" unicode=\"1\" stretchH=\"100\" smooth=\"1\" aa=\"1\" padding=\"1,1,1,1\" spacing=\"1,1\" />");
            sb.AppendLine($"\t<common lineHeight=\"{lineHeight}\" base=\"{baseHeight}\" scaleW=\"{atlasTexture.width}\" scaleH=\"{atlasTexture.height}\" pages=\"1\" />");
            sb.AppendLine("\t<pages>");
            sb.AppendLine($"\t\t<page id=\"0\" file=\"{atlasTexture.name}.png\" />");
            sb.AppendLine("\t</pages>");
            sb.AppendLine($"\t<chars count=\"{sprites.Count()}\">");
        }
        else
        {
            sb.AppendLine($"info face=\"{fontName}\" size={fontSize} bold=0 italic=0 charset=\"\" unicode=1 stretchH=100 smooth=1 aa=1 padding=1,1,1,1 spacing=1,1");
            sb.AppendLine($"common lineHeight={lineHeight} base={baseHeight} scaleW={atlasTexture.width} scaleH={atlasTexture.height} pages=1 packed=0");
            sb.AppendLine($"page id=0 file=\"{atlasTexture.name}.png\"");
            sb.AppendLine($"chars count={sprites.Count()}");
        }

        foreach (var sprite in sprites)
        {
            int charId;
            var match = Regex.Match(sprite.name, "u(\\d{4})");
            if (match.Success && int.TryParse(match.Groups[1].Value, System.Globalization.NumberStyles.HexNumber, null, out charId)) {
                Debug.Log($"Successfully parsed as unicode:{match.Groups[1].Value}");
            }
            else
            {
                charId = sprite.name.First();
            }

            Rect rect = sprite.rect;
            int xoffset = (int)(sprite.pivot.x - rect.width / 2);
            int yoffset = (int)(baseHeight - (rect.height - sprite.pivot.y));
            int flippedY = atlasTexture.height - (int)rect.y - (int)rect.height;
            if (exportAsXml)
            {
                sb.AppendLine($"\t\t<char id=\"{charId}\" x=\"{(int)rect.x}\" y=\"{flippedY}\" width=\"{(int)rect.width}\" height=\"{(int)rect.height}\" xoffset=\"{xoffset}\" yoffset=\"{yoffset}\" xadvance=\"{(int)rect.width}\" page=\"0\" chnl=\"15\" />");

            }
            else
            {
                sb.AppendLine($"char id={charId} x={(int)rect.x} y={flippedY} width={(int)rect.width} height={(int)rect.height} xoffset={xoffset} yoffset={yoffset} xadvance={(int)rect.width} page=0 chnl=15");
            }
        }

        if (exportAsXml)
        {
            sb.AppendLine("\t</chars>");
            sb.AppendLine("</font>");
        }

        File.WriteAllText(outputPath, sb.ToString(), Encoding.UTF8);
        AssetDatabase.Refresh();
        Debug.Log(".fnt file exported to " + outputPath);
    }
}
```

# 使い方

## Bitmap Font Imorter 導入
このツールは .fnt ファイルを書き出すだけのものです。
.fnt ファイルだけではUnityで使えるフォントとして認識されないので、 [Bitmap Font Importer](https://github.com/litefeel/Unity-BitmapFontImporter) を導入してください。

## スプライトエディタによる編集
Unityのスプライトエディタで画像のように一文字ずつスプライト切り出して、Nameに相当する文字を入れてください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/ff519e9b-cfcc-4d70-9f66-0047aab637b3.png)

必要な文字を切り出した状態：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/e64d2f90-172c-4ad9-8a16-8c82c06e00bd.png)

## テクスチャのバックアップ（推奨）
:::note warn
 一度フォント変換するとスプライトエディタの設定が消えてしまうので、将来再調整する可能性があるならテクスチャを複製してバックアップとして保存しておきましょう。
:::

## .fntファイル変換
メニューから `Tools` > `Bitmap Font Converter` を開く

Atlas Texture に先の画像をドロップして、Export Font Fileボタンを押すだけで .fnt ファイルが書き出されます。
（他の項目はいい感じに設定されるので、必要なければそのままでよいです）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/598a76b2-04ec-43e4-b741-b32c4050841f.png)

**Bitmap Font Importer が正しく動作していれば**、自動でフォントアセット (.fontsettings) が生成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/896d8503-4589-4303-b651-9e919f8a1f23.png)

## （オプション）文字の上下位置のカスタマイズ
**文字ごとの表示位置**調節は**スプライトエディタのピボット**で行えます。

こんな風にピボット ⭕を設定すると・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/c4e69501-26d1-49df-be4d-b2d7a562a624.png)

↓ こんな風に文字ごとの位置がずらせます！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/dc1521be-d260-4742-9b79-0b9add0eea99.png)


# 参考資料

https://github.com/litefeel/Unity-BitmapFontImporter
https://qiita.com/harayoki/items/36cfc6eae18d1cbe7c06