皆さんこんにちは。
最近ブログはサボりがちだったんですが、仕事も大分落ち着いてプライベートのやる気が再燃してきたのでブログを再開することにしたフーシャです。
最近UIの設計について考える機会があったので、今回はゲーム開発におけるUI設計について色々と書いていこうと思います。
ゲーム開発を行っていく上でUIの設計について考えなきゃいけないことはすごく多いです。
なんとなくUIを開発してくと、開発終盤や本番運用が始まってから困ったことになってしまうことがあります。
私自身これまでプロジェクトをいくつか経験してきましたが
- 「ちょっと機能追加するだけなのにめっちゃ苦労するな…」
- 「このUI無駄にオブジェクト読み込みすぎじゃない?」
- 「一部のUIだけ設定間違ってる!」
といったことに遭遇した経験があります。
もし本番運用が始まっているプロジェクトの場合、リリース済みのアプリが壊れないように後から直していくのはかなりの注意と労力が必要になります。
予め基盤となる実装から深く考慮をすることで後から困らないようにしていきたいものです。
じゃあ実際にUIの実装を最初からやってみようとなった時
一体何を考慮してどう実装すればいいんだ…?ってめちゃくちゃ悩むかと思います。
ということで
今回はスマホゲーム業界6年目のUnityエンジニアとしての経験を元に
自分自身のメモとしてもUIの実装設計について考えつつまとめてみようと思います!
もしこういう所も考慮した方がいいよ~とか、
ここはこういう方がいいんじゃない?とか
気になるところがあればぜひコメントで教えてください!
また、本記事は全3部作の第1部となります。
- 第1部(本記事):仕様に備えたシステム設計
- 第2部:処理効率の最適化
- 第3部:開発効率化のための工夫
それぞれの記事で異なる側面からUI設計にアプローチしていきますので、ぜひ続けてお読みいただければと思います。
本記事の目的
本記事は運用を想定したゲーム開発におけるUIの設計についてまとめることを目的としています。
それぞれの詳細を深堀りしていくのではなく、検討項目の洗い出しがメインとなります。
私はスマホゲーム業界の人間なので、考え方もスマホゲーム寄りになっているかと思いますが、PCゲームとかを作っている方にも通ずる話とかもあると思いますので、ぜひ参考に出来そうなところは参考にしていただければ幸いです。
またまだまだ勉強中の身なのでアドバイスも歓迎です!
色々アドバイス頂ければその内容も記事に含められたら嬉しいな~と思っています!
運用ゲームのUI開発について
運用を想定したゲームのUI開発においては次の3つの側面が大事だと考えています。
- 仕様に備えたシステム設計になっているか
- 処理効率は最適化されているか
- 開発の効率化が行えているか
UI開発においてゲーム自体の仕様に関わるUIの他にも、戻るボタン、セーフエリア、ローカライズなど満たさなければいけない要件が多く存在します。
また、処理効率の最適化が行えていないと、長期で運用を続けていくにつれてどんどんゲームが重くなってしまい、ゲームがカクついてユーザー体験を損なってしまったり、
開発の効率化が行えていないと「新しいものを作っていきたいのに時間が足りない…」といったことに陥ってしまうかもしれません。
なのでこれら3つの側面を元にそれぞれUI開発を行っていく際に考えた方がいいこと、気を付けた方がいいことなどを考えていきたいと思います。
全てを一度にまとめるとかなり長くなるため、先述した通りそれぞれ1つずつを1つの記事として全3部作で記事にしたいと思います。本記事では第1部として「仕様に備えたシステム設計」について解説していきます。
仕様に備えたUIのシステム設計
まずはUI実装設計の第1部として「仕様に備えたシステム設計」についてまとめていきたいと思います。
ゲーム開発においてUIが満たさないといけない仕様はかなり多いです。
- ゲーム情報の表示
- 表示優先度の管理
- インタラクティブ性
- SE、エフェクト、アニメーションなどの演出
- 複数解像度、SafeAreaなどのハード向け対応
- 海外向けのローカライズ対応
- 追加アセット切り替え
などなど色々あります。
後から実装を行いやすいものもあれば、基盤から直さないといけなくなってしまうものもあり、実装が本格化する前に十分な検討が必要です。
今回はそんな仕様に備えるためのシステム設計についてまとめてみましたので、ぜひ本記事を読んでUI設計を考える際の足しにしていただければ幸いです。
デザインパターンでコードを整理しよう
新規実装/改修をしやすくするため、デザインパターンを用いた実装は大切です。
UnityのUI実装でよく使われるのはMVC/MV(R)P/MVVMパターンかと思います。これらは「データ」「見た目」「ロジック」を分離して管理する設計手法で、修正時に影響範囲を限定できるメリットがあります。
最近ではUniRx/R3を使ったMV(R)P/MVVMパターンが人気ですが、チームの習熟度やプロジェクトの規模に合わせて設計手法は選びましょう。小規模なら単純な設計から始めるのもアリです。
☝ポイント
・データ層とビュー層を分離することで、仕様変更時の影響範囲を最小化
・チーム全体で統一したパターンを採用することで、コードの可読性と保守性が向上
・過度に複雑な設計は避け、プロジェクトの規模に見合った設計を選択
標準UIコンポーネントは拡張して使おう
Button、TextMeshProUGUI、Image、ScrollViewなど、Unityの標準UIコンポーネントをそのまま使っていませんか?
実はUnityの標準UIコンポーネントはそのまま使わず、独自の拡張クラスでラップして使うことを強くお勧めします。なぜなら、開発を進めていくと「ボタンに効果音を自動で付けたい」「テキストの表示アニメーションを全体で統一したい」といった標準のUIコンポーネントだけでは満たせない要望が必ず出てくるからです。
// 例:拡張ボタンクラス
public class CustomButton : Button
{
// 後から共通処理を追加できる
protected override void OnClick()
{
// 画像を変えたり、SEを鳴らしたり
base.OnClick();
}
}
特に追加したい機能が思いつかない場合はラップだけしたクラスを取り合えず作っておくだけでもいいでしょう。
予め独自のUIコンポーネントをオブジェクトにアタッチしておくことで、後から機能を追加したり、最適化することが容易になり開発の手戻りを減らすことが出来ます。
☝ポイント
・開発初期から拡張クラスを用意しておくことで、後からの仕様追加がスムーズ
・全てのUIで統一した挙動(SE、アニメーション等)を実現可能
・標準コンポーネントから拡張クラスへの置き換えは後からだと工数が膨大に
UIレイヤーを整理して表示順を管理
UIには用途ごとに表示レイヤーがあります
- Background:背景や演出
- Main:メインのゲーム画面UI
- Popup:ポップアップやダイアログ
- Overlay:常に最前面のローディング表示など
※プロジェクトによって定義はさまざまです
各レイヤーごとにCanvasを分けて、SortingOrderなどで表示順序を管理しましょう。
こうすることで「ポップアップがメニューの下に表示されてしまった!」といった事故を防げます。
動的にUIを生成する場合は、「どのレイヤーに」「どの優先度で」追加するか指定できる仕組みを作っておくと開発が楽になります。
もちろん動的生成はそれなりにコストがかかる処理なので、適宜処理の最適化を行う必要はあることは頭に入れておきましょう。
// 例:UI動的生成の実装イメージ
// UIレイヤーの定義
public enum UILayer
{
Background,
Main,
Popup,
Overlay
}
// UIレイヤー管理クラス
public class UILayerManager
{
[SerializeField]
private List<Canvas> layerCanvases;
// レイヤー指定でUIを生成
public T CreateUI<T>(T prefab, UILayer layer, int frontOrder = 0) where T : Component
{
var canvas = layerCanvases[(int)layer];
var instance = Instantiate(prefab, canvas.transform);
// frontOrder=手前から何番目に生成するか
var index = canvas.transform.childCount - frontOrder;
if (index < 0) index = 0;
instance.transform.SetSiblingIndex(index);
return instance;
}
}
// 使用例
UILayerManager.Instance.CreateUI(popup1, UILayer.Popup);
UILayerManager.Instance.CreateUI(popup2, UILayer.Popup, 2); // popup1より奥に生成
UILayerManager.Instance.CreateUI(loading, UILayer.Overlay);
また、各レイヤーごとにCanvasを分けることで、レイヤーごとに異なるスケーリング設定を適用できます。これにより後述するSafe Area対応も柔軟に行えます
(例:BackgroundはSafe Area外まで表示、MainはSafe Area内のみに表示など)
☝ポイント
・レイヤー構造を明確にすることで、表示順のトラブルを未然に防止
・動的生成時はレイヤー指定を必須にすることで、意図しない表示位置を防ぎ管理を楽に
・レイヤーごとにCanvasを分けることで、Safe Area対応や描画の最適化など、柔軟な設定が可能に
自由な画面遷移を想定した設計にしよう
ゲーム開発では「アイテム使用画面からキャラ強化画面を直接開きたい」など、開発当初では想定に無かった画面遷移が発生することがあります。
画面遷移を自由にできる設計にする際に困りやすいのがデータの受け渡しです。
クラスAで持ってるデータをクラスBに渡すためにはあそこを経由してこういってこう渡せば…
とやっているとメソッドの引数がかなり増えてしまったり、どこから何が渡されてきているのかが分からなくなくなり、プログラムの結合度も上がってしまいます。
画面間のデータ受け渡しを柔軟にするためには依存性注入(DI)の仕組みが便利です。ZenjectやVContainerといったDIライブラリを使うと、画面間の結合度を下げつつデータを渡せます。
// 例:DIを使ってキャラ強化画面に所持アイテムを渡す
public class CharaEnhanceScreen : MonoBehaviour
{
[Inject] private ItemData currentItem; // 他画面から注入される
void Start()
{
DisplayEnhance(currentItem); // 強化画面に所持アイテムを渡して表示
}
}
また、デバッグ機能として直接その画面を開いても動くような仕組みを作れていると開発効率が大幅に向上します。
毎回タイトル画面から移動して、ボタンを押してシーンを遷移して…という作業を繰り返すだけでもかなりの時間を取られてしまいます。
特によく開発の手が入る箇所(インゲームなど)については、ダミーデータを使って直接遷移できるような仕組みを用意しておくと良いでしょう。
☝ポイント
・DIコンテナを使うことで、画面間の密結合を回避
・画面遷移のルートが複雑化しても、データの受け渡しがシンプルに保てる
・エディタから直接画面を開ける仕組みがあると開発やデバッグの効率が大幅に向上
複数の画面サイズに対応させよう
スマホ向けゲームでは、iPhone、iPad、各種Androidなど様々な解像度への対応が必須です。それに加えてPC版も出す場合は、フルHDや4Kなど更に対応する解像度が増えてきます。
CanvasScalerの設定を適切に行い、AnchorとPivotを使ってUIの基準点を設定しましょう。例えば、画面右上に固定したいボタンは、Anchorを右上に設定することで、どの解像度でも右上に表示されます。
実装/テスト時に複数解像度でプレビューする習慣をつけると、リリース後のトラブルを減らせます。
また、高解像度で表示することを想定している場合、それに合わせて素材も高解像度で作る必要が出てきます。その際、低解像度端末ではどのように読み込むのかも併せて考える必要があります。
例えば、端末の解像度に応じて異なる解像度のアセットを切り替える仕組みや、ダウンスケーリングの品質設定などを検討しましょう。
☝ポイント
・CanvasScalerを活用して基準解像度を決めて、そこから相対的にスケールする設計に
・UnityエディタのGameビューで複数解像度確認を常にする習慣を
・素材の扱いを事前に設計し、負荷と見た目のバランスも考慮
Safe Areaと折りたたみスマホに備えよう
最近のスマホはノッチ(画面上部の切り欠き)があったり、折りたたみ式だったりします。
Safe Areaとは、これらの物理的制約を避けて安全に表示できる領域のこと。UnityではデフォルトでSafe Areaを考慮しないため、Screen.safeAreaを使って自分で対応する必要があります。
// 例:Safe Area対応
RectTransform rt = GetComponent<RectTransform>();
Rect safeArea = Screen.safeArea;
rt.anchorMin = safeArea.position / new Vector2(Screen.width, Screen.height);
rt.anchorMax = (safeArea.position + safeArea.size) / new Vector2(Screen.width, Screen.height);
Canvasの子オブジェクトでこの計算をさせるGameObjectを一つ追加し、その下にSafeArea対応したいUIを配置することで位置の調整を行うことが出来ます。
折りたたみスマホでは、ユーザーが開閉するたびに解像度とSafe Areaが変わります。
まだ既存のゲームでも対応されていないものも多くありますが、今後新しく開発をするものに関しては対策を考えておいたほうがいいでしょう。
折り畳みスマホはいつでも開閉が可能なため動的に変化を検知して再計算する仕組みを作る必要があるでしょう。
AndroidApplication.onConfigurationChangedを使用すればAndroidデバイスの設定変更を検出することが出来るのでこのコールバックに登録をしておけば良さそうです。
☝ポイント
・Safe Area対応はそれを想定したオブジェクト構造が重要
・端末の向き変更や折りたたみ操作時のonConfigurationChangedなどのイベントで再計算
・実機テストでノッチのある端末を必ず確認
戻るボタンの挙動を管理しよう
Androidには物理的な戻るボタンがありますが、ゲーム内にも戻るボタンを実装するのが今は一般的です。
戻るボタンを押したとき:
- 前の画面に戻る?
- ポップアップを閉じる?
- キャンセル扱いにする?
挙動は状況によって変わります。多くの場合戻るボタンは連続して押しても機能させたいため、スタック構造で戻る挙動を管理出来るように設計するといいでしょう。
// 例:戻るボタンのスタック管理
public class BackButtonManager
{
private Stack<Action> backActions = new Stack<Action>();
public void Push(Action action)
{
backActions.Push(action);
}
public Action Pop()
{
if (backActions.Count > 0)
{
return backActions.Pop();
}
else
{
return null;
}
}
public void OnBackButton()
{
Pop()?.Invoke();
}
public void Clear()
{
backActions.Cler();
}
}
画面を開くときにスタックに積んで、閉じるときに取り出す形にすれば、自然な戻る動作が実現できます。
よくあるゲームではホーム画面までいったらそれ以上戻れないことも多いと思うので、その際にはスタックの中身をクリアするのがよさそうです。
☝ポイント
・スタック構造により、複数階層のポップアップにも対応可能
・Android端末の物理戻るボタンとUI上の戻るボタンを統一した挙動に
・ダイアログではキャンセルを押した動作にするなど、特殊な挙動も想定する
多言語対応(ローカライズ)の準備をしよう
多言語対応の準備と書いていますが、海外展開を考えていなくてもテキストは必ず外部管理しましょう。
理由は2つ:
- 後から多言語対応が必要になるかもしれない
- 表記ゆれのチェックや一括修正が簡単になる
Unity公式のLocalizationパッケージを使えば、CSVインポートやGoogleスプレッドシート連携ができて便利です。
// 悪い例
text.text = "こんにちは"; // ハードコーディング
// 良い例
text.text = LocalizationManager.GetText("hello"); // キーで管理
日本語だけの場合でも外部管理しておけば、企画側で文言を直接修正できたり、検索しやすくなったりとメリットが大きいです。
もし海外展開をする場合は文字数制限に注意が必要です。日本語では問題なくても英語だと平気で表示領域を超えてしまうことは多いです。また、フォントがその言語に対応していない場合はフォント自体の切り替えも必要になるケースがあるので注意です。
ローカライズはテキストだけではなく画像などのアセットでも対応が必要になりますが、画像などのアセット切り替えもUnity Localizationで対応が可能です。
☝ポイント
・テキストの外部管理はやっておいて損はない
・企画やライターが直接編集/確認できる環境を整えることで、エンジニアの負担軽減
・文字数制限やフォント切り替えなど、多言語展開時の課題も事前に検討
運用中のアセット追加に備えよう
運営型ゲームでは、新キャラ追加やイベントのたびに新しい画像やデータが追加されます。
その際、可能な限りアプリ自体の更新は入れずにデータだけを追加したいことが多いです。
UIにおいてもアイテムが追加されればその画像が追加されるし、イベント期間中においてはボタン画像がそのイベント期間中だけの画像に差し変わるといったこともよくあります。
マスタデータとAssetBundleを組み合わせて、プログラムを変更せずにアセットを差し替えられる仕組みを作り、後から差し替えや追加が可能な仕組みを想定しておきましょう。
追加アセットのロード/ダウンロードでは独自のAssetBundle管理システムを作ってもいいですが、今ならUnityのAddressableを使ってもいいと思います。
// 例:マスタデータで画像パスを管理
[System.Serializable]
public class EventBannerData
{
public int eventId;
public string imagePath; // Addressableのパス
public DateTime startDate;
public DateTime endDate;
}
// 例:マスタデータの画像パスを使用してAddressableからロードして画像差し替え
public class EventBannerView
{
private Image bannerImage;
private EventBannerData data;
private void Start()
{
Load().Forget();
}
private async UniTask Load()
{
var handle = Addressables.LoadAssetAsync<Sprite>(data.imagePath);
await handle.ToUniTask();
if (handle.Status == AsyncOperationStatus.Succeeded) {
bannerImage.sprite = handle.Result;
}
}
}
また、アセットのロード/アンロードタイミングは重要です。
表示するタイミングでロードを開始すると表示できるまでに少し待ち時間が発生することがありユーザー体験を損ねることがあります。
事前に必要なものをロード出来るような作りにしておき、なるべくユーザーの手が止まる時間が少なくなる設計を目指しましょう。
☝ポイント
・動的にアセットロード可能な仕組みにすることでアプリ更新なしでコンテンツ追加を可能に
・ロードタイミングの設計は初期から考慮(プリロード、遅延ロードなど)
・メモリ管理とのバランスを取りながら、快適なUXを実現
まとめ
本記事では、長期運用を見据えたUnityのUI設計における「仕様に備えたシステム設計」について解説しました。
おさらいすると
☝ポイント
・デザインパターンでコードの保守性を高める
・標準UIコンポーネントの拡張で後からの機能追加に備える
・UIレイヤー管理で表示順のトラブルを防ぐ
・DIを活用して画面遷移を柔軟にする
・複数解像度/Safe Area/折り畳み想定で様々なデバイスに対応
・戻るボタンはスタック管理で直感的な操作を実現
・ローカライズの準備で将来の展開に備える
・アセットの動的管理で運用時のコンテンツ追加を容易にする
これらの設計を開発初期から取り入れることで、後々の仕様変更や機能追加に強いUI基盤を作ることができます。もちろん、全てを完璧に実装する必要はなく、プロジェクトの規模や要件に応じて優先順位をつけて取り組むことが重要です。
次回の第2部では「処理効率の最適化」について、パフォーマンスを意識したUI実装のテクニックを解説していく予定です。
最後までお読みいただきありがとうございました!

コメント