Unity WebGLの日本語入問題と対策
Unityはゲーム制作について、とても素晴らしいツールであり、更なるアップデートで進化を続けています。
しかし、そんな完璧に見えるツールでもまだ改善する所はあります。
今日の話は入力フィールドのUnicode入力です。
Unityで提供している入力フィールド(UnityEngine.UI.InputField)
プラットフォームによって、デバイスが提供しているIME(入力装置)を完全に対応する(モバイルなど)プラットフォームもありますが、Webプラットフォームについては、そうではありません。
WebGLの場合、基本提供しているInputFieldで日本語入力ができないことが分かります。
(英文字なら問題なく入力可能)
その理由は、「ブラウザIMEのアクセス権限をUnity側が持ってない」 からです。それで、特殊なIME(日本語のromaji入力や組み合わせ入力など)の入力が、思い通りに動かないです。
これはゲームの中でプレイヤーの名前を入れたり、チャット機能などを実装する時、ややこしい問題になります。
WebGL用入力問題を解決する方法について、色々検索してみましたところ、「WebGLNativeInputField」 というパッケージを見つけました。
こちらのデモで動作確認ができます。
このパッケージを使うと、WebGLでも日本語入力が問題なくできます。
(僕が調べた限り、これがUnityのWebGLで日本語入力できる唯一な対策ではないかと思われます。)
入力フィールドを触るとこのポップが出る
オーバーレイタイプも選択可能(こっちの方が少し自然に見えます。)
そもそも、WebGLNativeInputFieldで使われた方法は、「Unity上の入力フィールドを使うことではなく、ブラウザ上に仮想の入力フォームを作り、そこて入力された値をUnityに渡す」 ことです。確かに、この方法ならブラウザのIMEを自由に使用することができます。
でも入力の度にポップが飛び出るのも嫌だし、この入力フォームとゲーム画面との違和感が、どうしても気になりました。
それで、入力フォームをゲーム画面と一致させるよう、コードを修正してみました。
基本アイデアは、これです。
- 入力ObjectのTransform情報(座標や大きさなど)をブラウザーに渡す
- 指定された領域に入力フォームを生成
では、このパッケージに、少し修正をかけます。
SetupOverlayDialogHtml関数を修正します。引数に入力フィールドのRect情報と実際テキスト領域とのPadding情報、テキストのFontサイズを追加します。
private static extern string SetupOverlayDialogHtml(string title, string defaultValue, string okBtnText, string cancelBtnText);
修正前
private static extern bool SetupOverlayDialogHtml(string defaultValue, int x, int y, int width, int height, int paddingTop, int paddingRight, int paddingBottom, int paddingLeft, int fontSize);
修正後
(UnityEngine.UI.InputFieldを継承したコンポーネントです。)
ここで注意する点は 「Unityの座標情報はブラウザの座標と一致しないこと」 です。これはRetina画面を使ってるパソコンで発生している問題で、 「Unityは画面上のpixel情報に基づいた座標を使っている反面、ブラウザは目に見えるpointサイズに基づいた座標を使っている」 からです。(つまり、retina displayでウェブサイトを開いても、普通のディスプレイと同じ大きさで見えるようにpixel座標を補正しています。)
Unity2019.2まではエンジンで指定されたサイズに固定されましたが、2019.3以降からブラウザーのPixelサイズによってScreen sizeが変わることになりました。
正確な座標計算のために、Canvasのスケールモードを 「Scale With Screen Size」 にします。
(これによって、画面サイズが変わってもCanvas内部のUI座標は崩れません。)
Screnn sizeをInputFieldに渡し、それでpixel情報を計算し直して、その結果をブラウザに渡すように修正します。
public class WebGLNativeInputField : InputField
{
...
[SerializeField] Vector2 screenSize = Vector2.zero; // 追加
...
{
...
[SerializeField] Vector2 screenSize = Vector2.zero; // 追加
...
強調部分をWebGLNativeInputFieldクラスに追加
追加したPropertyがインスペクターに見えるように、WebGLNativeInputFieldEditorも修正します。
格納場所:WebGLNativeInputField/Editor/WebGLNativeInputFieldEditor.cs
public class WebGLNativeInputFieldEditor : InputFieldEditor
{
...
private SerializedProperty screenSize; // 追加
protected override void OnEnable()
{
base.OnEnable();
...
screenSize = serializedObject.FindProperty("screenSize"); //追加
}
public override void OnInspectorGUI()
{
...
serializedObject.ApplyModifiedProperties(); //追加
base.OnInspectorGUI();
}
}
{
...
private SerializedProperty screenSize; // 追加
protected override void OnEnable()
{
base.OnEnable();
...
screenSize = serializedObject.FindProperty("screenSize"); //追加
}
public override void OnInspectorGUI()
{
...
serializedObject.ApplyModifiedProperties(); //追加
base.OnInspectorGUI();
}
}
「Start()」 関数で、Transform情報を計算するコードを追加します。
private int _x, _y, _width, _height;
private int _paddingTop, _paddingRight, _paddingBottom, _paddingLeft;
private int _fontSize;
protected override void Start()
{
if (screenSize == Vector2.zero) screenSize = new Vector2(Screen.width, Screen.height);
RectTransform inputTransform = transform as RectTransform;
Vector3[] corners = new Vector3[4];
inputTransform.GetWorldCorners(corners);
Transform canvas = FindObjectOfType<Canvas>().transform;
Vector3 center = new Vector3(screenSize.x / 2f, screenSize.y / 2f, 0);
for (int i = 0; i < 4; i++)
{
corners[i] = corners[i] / canvas.localScale.x + center;
corners[i].y = screenSize.y - corners[i].y;
}
var pos = new Vector3(Mathf.Min(corners[0].x, corners[2].x), Mathf.Min(corners[0].y, corners[2].y), 0);
var size = new Vector3(Mathf.Abs(corners[0].x - corners[2].x), Mathf.Abs(corners[0].y - corners[2].y), 0);
RectTransform textTransform = textComponent.transform as RectTransform;
var offsetMin = textTransform.offsetMin;
var offsetMax = textTransform.offsetMax;
_x = Mathf.RoundToInt(pos.x);
_y = Mathf.RoundToInt(pos.y);
_paddingTop = Mathf.RoundToInt(-offsetMax.y);
_paddingRight = Mathf.RoundToInt(-offsetMax.x);
_paddingBottom = Mathf.RoundToInt(offsetMin.y);
_paddingLeft = Mathf.RoundToInt(offsetMin.x);
_width = Mathf.RoundToInt(size.x) - _paddingLeft - _paddingRight;
_height = Mathf.RoundToInt(size.y) - _paddingTop - _paddingBottom;
_fontSize = textComponent.fontSize;
}
private int _paddingTop, _paddingRight, _paddingBottom, _paddingLeft;
private int _fontSize;
protected override void Start()
{
if (screenSize == Vector2.zero) screenSize = new Vector2(Screen.width, Screen.height);
RectTransform inputTransform = transform as RectTransform;
Vector3[] corners = new Vector3[4];
inputTransform.GetWorldCorners(corners);
Transform canvas = FindObjectOfType<Canvas>().transform;
Vector3 center = new Vector3(screenSize.x / 2f, screenSize.y / 2f, 0);
for (int i = 0; i < 4; i++)
{
corners[i] = corners[i] / canvas.localScale.x + center;
corners[i].y = screenSize.y - corners[i].y;
}
var pos = new Vector3(Mathf.Min(corners[0].x, corners[2].x), Mathf.Min(corners[0].y, corners[2].y), 0);
var size = new Vector3(Mathf.Abs(corners[0].x - corners[2].x), Mathf.Abs(corners[0].y - corners[2].y), 0);
RectTransform textTransform = textComponent.transform as RectTransform;
var offsetMin = textTransform.offsetMin;
var offsetMax = textTransform.offsetMax;
_x = Mathf.RoundToInt(pos.x);
_y = Mathf.RoundToInt(pos.y);
_paddingTop = Mathf.RoundToInt(-offsetMax.y);
_paddingRight = Mathf.RoundToInt(-offsetMax.x);
_paddingBottom = Mathf.RoundToInt(offsetMin.y);
_paddingLeft = Mathf.RoundToInt(offsetMin.x);
_width = Mathf.RoundToInt(size.x) - _paddingLeft - _paddingRight;
_height = Mathf.RoundToInt(size.y) - _paddingTop - _paddingBottom;
_fontSize = textComponent.fontSize;
}
SetUpOverlayDialogを呼び出す部分を修正します。
public override void OnSelect(BaseEventData eventData)
{ ...
WebNativeDialog.SetUpOverlayDialog(m_DialogTitle, this.text , m_DialogOkBtn , m_DialogCancelBtn );
...
{ ...
WebNativeDialog.SetUpOverlayDialog(m_DialogTitle, this.text , m_DialogOkBtn , m_DialogCancelBtn );
...
修正前
public override void OnSelect(BaseEventData eventData)
{ ...
WebNativeDialog.SetUpOverlayDialog(this.text, _x, _y, _width, _height, _paddingTop, _paddingRight, _paddingBottom, _paddingLeft, _fontSize );
...
{ ...
WebNativeDialog.SetUpOverlayDialog(this.text, _x, _y, _width, _height, _paddingTop, _paddingRight, _paddingBottom, _paddingLeft, _fontSize );
...
修正後
(中身はjavascriptです。)
渡されたTransform情報を、使って入力フォームを配置するように、修正かけます。
// setup html
var html = '<div id="nativeInputDialog" style="background:#000000;opacity:0.9;width:100%;height:100%;position:fixed;top:0%;z-index:2147483647;">' +
' <div style="position:relative;top:30%;" align="center" vertical-align="middle">' +
' <div id="nativeInputDialogTitle" style="color:#ffffff;">Here is title</div>' +
' <div>' +
' <input id="nativeInputDialogInput" type="text" size="40" onsubmit="">' +
' </div>' +
' <div style="margin-top:10px">' +
' <input id="nativeInputDialogOkBtn" type="button" value="OK" onclick="" >' +
' <input id="nativeInputDialogCancelBtn" type="button" value="Cancel" onclick ="">' +
' <input id="nativeInputDialogCheck" type="checkBox" style="display:none;">' +
' </div>' +
' </div>' +
'</div>';
var html = '<div id="nativeInputDialog" style="background:#000000;opacity:0.9;width:100%;height:100%;position:fixed;top:0%;z-index:2147483647;">' +
' <div style="position:relative;top:30%;" align="center" vertical-align="middle">' +
' <div id="nativeInputDialogTitle" style="color:#ffffff;">Here is title</div>' +
' <div>' +
' <input id="nativeInputDialogInput" type="text" size="40" onsubmit="">' +
' </div>' +
' <div style="margin-top:10px">' +
' <input id="nativeInputDialogOkBtn" type="button" value="OK" onclick="" >' +
' <input id="nativeInputDialogCancelBtn" type="button" value="Cancel" onclick ="">' +
' <input id="nativeInputDialogCheck" type="checkBox" style="display:none;">' +
' </div>' +
' </div>' +
'</div>';
修正前
// setup html
var inputStyle = 'background:rgba(255, 255, 255, 0);' +
'font-size:' + fontSize.toString() + 'px;' +
'position:absolute;' +
'top:' + textY.toString() + 'px;left:' + textX.toString() + 'px;height:' + textH.toString() + 'px;width:' + textW.toString() + 'px;' +
'padding: ' + paddingT.toString() + 'px ' + paddingR.toString() + 'px ' + paddingB.toString() + 'px ' + paddingL.toString() + 'px;' +
'margin:0px;border:0px;';
var html = '<div id="nativeInputDialog" style="width:100%;height:100%;position:absolute;top:0px;left:0px;z-index:2147483647;">' +
' <input id="nativeInputDialogCheck" type="checkBox" style="display:none;">' +
' <div id="nativeInputBg" style="background:#000000;opacity:0;position:absolute;top:0px;left:0px;width:100%;height:100%;margin:0px"></div>' +
' <form id="nativeInputForm" onsubmit="">' +
' <input id="nativeInputDialogInput" type="text" style="' + inputStyle + '">' +
' <input type="submit" style="display:none;">' +
' </form>' +
'</div>';
var inputStyle = 'background:rgba(255, 255, 255, 0);' +
'font-size:' + fontSize.toString() + 'px;' +
'position:absolute;' +
'top:' + textY.toString() + 'px;left:' + textX.toString() + 'px;height:' + textH.toString() + 'px;width:' + textW.toString() + 'px;' +
'padding: ' + paddingT.toString() + 'px ' + paddingR.toString() + 'px ' + paddingB.toString() + 'px ' + paddingL.toString() + 'px;' +
'margin:0px;border:0px;';
var html = '<div id="nativeInputDialog" style="width:100%;height:100%;position:absolute;top:0px;left:0px;z-index:2147483647;">' +
' <input id="nativeInputDialogCheck" type="checkBox" style="display:none;">' +
' <div id="nativeInputBg" style="background:#000000;opacity:0;position:absolute;top:0px;left:0px;width:100%;height:100%;margin:0px"></div>' +
' <form id="nativeInputForm" onsubmit="">' +
' <input id="nativeInputDialogInput" type="text" style="' + inputStyle + '">' +
' <input type="submit" style="display:none;">' +
' </form>' +
'</div>';
修正後
入力Objectを生成するコードを見ると、親がRootになっています。そうするとフォームの位置がブラウザ基準になるので、親をゲーム画面に変えます。それで入力フォームの位置はゲーム画面にくっ付けられます。
...
// write to html
document.appendChild( element );
// write to html
document.appendChild( element );
修正前
...
// write to html
document.getElementById("unityContainer").appendChild( element );
// write to html
document.getElementById("unityContainer").appendChild( element );
修正後:Unity客体はunityContainerというidが付けられています。
これで必要なコード修正は終わりました。
※ 実際は他の部分も色々修正されましたが、ここでは見た目の修正分だけ説明し、他の部分は飛ばしました。
したの部分がWebGLNativeInputFieldです。
Screen Size欄に画面サイズを入れます。
Unity上では確認ができないので、WebGLでビルドして確認します。
いい感じで入力フィールドがゲームに溶け込んでいます!
まだ課題は幾つか残っていますが、(改行の対応、色情報の追加など…)これでゲームにチャット機能を入れることができて嬉しいです。
※ この入力フィールドはハバネロサイトに掲載されたゲームで使われています。入力フィールドの動作が気になる方は是非お試しください。
※ 元ブログから移転された記事です。
コメント
コメントを投稿