0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-03-14

はじめに

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

経緯

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

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

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

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

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

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

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

オンラインツール

https://snowb.org/
https://hahahoho.studio/

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

コード:エディタ拡張

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 を導入してください。

スプライトエディタによる編集

Unityのスプライトエディタで画像のように一文字ずつスプライト切り出して、Nameに相当する文字を入れてください
image.png

必要な文字を切り出した状態:
image.png

テクスチャのバックアップ(推奨)

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

.fntファイル変換

メニューから Tools > Bitmap Font Converter を開く

Atlas Texture に先の画像をドロップして、Export Font Fileボタンを押すだけで .fnt ファイルが書き出されます。
(他の項目はいい感じに設定されるので、必要なければそのままでよいです)
image.png

Bitmap Font Importer が正しく動作していれば、自動でフォントアセット (.fontsettings) が生成されます。
image.png

(オプション)文字の上下位置のカスタマイズ

文字ごとの表示位置調節はスプライトエディタのピボットで行えます。

こんな風にピボット ⭕を設定すると・・・
image.png

↓ こんな風に文字ごとの位置がずらせます!
image.png

参考資料

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

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?