【Unity】uGUIで入力できない文字をはじくまでの長き道のり - Unicodeの誕生からサロゲートペア、バリデーション用ソースコードまで

UnityのuGUIで入力できない文字をはじくために必要な知識をまとめました。

はじめに

UnityのuGUIは、実はほとんど絵文字を表示することができません。
また、囲み文字なども一部正確に表示できないものがあります。

そのため、ユーザが自由に入力できる部分ではこれらの文字をバリデーションする必要があります。
この記事ではこのようにuGUIで表示できない文字をはじく方法をまとめます。

また、この実装を理解するための前提知識は意外と幅広いので、そのあたりも踏まえてまとめてみます。

文字コードの誕生と分岐

まず、そもそも文字コードとは?というお話からです。

コンピュータはビットしか取り扱えないので、文字も二進数で表す必要があります。
例えば「01」は「あ」を表して「10」は「い」を表すというルールにすれば、文字を二進数で表せます。
このようなルールを文字コードと呼びます。

最初に生まれた文字コードはASCIIという、アルファベットと数字と基本的な記号だけを収録したものでした。
しかしその後、もっと日本語とかも入れたいよねって話になり、各国語の文字コードがそれぞれ勝手に作られていきました。

その結果、「あ」を表すのにある文字コードでは「0001」、またある文字コードでは「1000」みたいな感じのカオスな状況が生まれました。
(上記の文字コードの値はあくまで例です)

Unicodeの誕生

これではまずいので、同じ文字は同じコードを対応させよう、として生まれたのがUnicodeです。
Unicodeによって、「この文字はこのコード」とちゃんと一意に対応付けられるようになりました。

このUnicode自体は文字コードとはまた違うもので、U+の後に16ビットをくっつけた「U+FFFF」みたいな形式で表されます。
これをコードポイントと呼びます。

サロゲートペア

さてこのようにしてUnicodeは16ビットの情報量を持つコードポイントに文字を割り当ててていくわけですが、
途中で全世界の文字列を表すには16ビットだときついよねって話になりました。漢字とかめっちゃ多いし。

そこで、まだ使っていなかったUnicodeのある一定の範囲を「上位サロゲート」、
また異なる一定の範囲を「下位サロゲート」として定め、上位サロゲートと下位サロゲートの組み合わせで1文字を表現することにしました。
これにより、より多くの文字を表せるようになりました。

UTF-8UTF-16Unicode文字コードに変換

さて前述の通りUnicode文字コードではないので、実際に使う際にはコードポイントをコンピュータが理解できるバイト列に変換する必要があります。
このルール(=文字コード)が色々あって、例えばUTF-8とかUTF-16とかがそれにあたります。

UnicodeUTF-8の関係

さてUnityの文字コードUTF-8なのでこれとUnicodeの関係についてもう少し深掘ります。
UTF-8は、それがUnicodeのどの範囲を表すかによって、一文字を1バイト~4バイトの可変長で表します。

UTF-8で3バイト以内で表せる範囲は、Unicodeでいうところのサロゲートペアを使ってない範囲のコードポイントです。
漢字などは大体この3バイトの範囲に入っています。
そしてUTF-8において4バイトで表す範囲はUnicodeでいうとサロゲートペアを使って表した範囲になります。
大半の絵文字などがこの範囲に入っています。

uGUIとサロゲートペア

さてここまでが非常に長い前置きです。
なぜこんな前置きをしたかというと、ずばりuGUIがサロゲートペアに対応していないためです。
サロゲートペアを使った文字を入力されてもuGUIでは表示することができません。
したがってuGUIではサロゲートペアを使う文字は入力できないようにする必要があります。

uGUIと結合文字

それとは別に、Unicodeには結合文字というものが存在します。
結合文字は、「ある文字」と「それを修飾する文字」の二文字で一つの文字を表します。
例えば実は「①」は、「1」という文字と「対象を〇で囲んで修飾する文字」という二文字からできています。

発音記号の上についている点(ダイアクリティカルマーク)なども結合文字の一部です。
これらの結合文字は一部だけuGUIで表示できないものがあります。
結構表示できるものもあるようですが、はじいてしまったほうが無難なので今回はこれもはじきます。

ソースコード

さて大変長くなりましたが、いよいよソースコードです。
今回はInputFieldへの入力制御コンポーネントを作成しました。

[RequireComponent(typeof(InputField))]
public class Example : MonoBehaviour
{
    private InputField _inputField;

    private void Start()
    {
        _inputField = GetComponent<InputField>();
        _inputField.onValidateInput += ValidateInput;
    }

    private void OnDestroy() => _inputField.onValidateInput -= ValidateInput;

    private char ValidateInput(string text, int index, char addedChar) => IsDisplayable(addedChar) ? addedChar : '\0';

    /// <summary>
    /// 表示できる文字か
    /// </summary>
    public bool IsDisplayable(char character)
    {
        var unicodeCategory = char.GetUnicodeCategory(character);

        // サロゲートペアは表示できない
        if (unicodeCategory == UnicodeCategory.Surrogate)
        {
            return false;
        }

        // 結合文字も一部表示できない
        // 今回は全部はじいてしまう
        if (unicodeCategory == UnicodeCategory.NonSpacingMark
            || unicodeCategory == UnicodeCategory.SpacingCombiningMark
            || unicodeCategory == UnicodeCategory.EnclosingMark)
        {
            return false;
        }

        return true;
    }
}

結合文字については3種類あるようなので、これらをすべてはじきました。

ja.wikipedia.org

参考

codezine.jp

qiita.com

tech.sanwasystem.com

note.mu

https://pc-pier.com/blog/2016/01/29/character-code/

orange-factory.com

orange-factory.com

qiita.com

www.weblio.jp