「Unity ゲーム一覧」で検索すると、有名タイトルの羅列がたくさん出てきます。
この記事は“それ”ではなく、自分のプロジェクトに「ゲーム一覧(ゲーム選択・カタログ)画面を作る」ための実装ノウハウをまとめました。
スマホでもPCでも動くUI、Steam向けビルドでの見え方、無料アセット活用、パフォーマンスの注意点まで、わたしが実際にやった手順+比較で紹介します。
前提:Unity 2022 LTS 以降(UI Toolkitの安定化を重視)。
uGUIでもいけますが、この記事の本線は UI Toolkit です1。
対象読者:
- Unityでランチャー/タイトル画面に「ゲーム一覧」を置きたい人
- ミニゲーム集、DLC一覧、イベント一覧など“増えるリスト”を運用したい人
- スマホ(iOS/Android)とPC/Steamの両対応を視野に入れている人
完成イメージ:フィルター付きのスクロール一覧(スマホ/PC/Steamで共通)
やりたいことはシンプルです。
カード(サムネイル+タイトル+タグ+プラットフォームアイコン)を縦スクロールで並べ、上部にフィルター(「すべて / スマホ / PC / Steam / 無料だけ」)を置く。
カードを押すと詳細モーダルが開き、「プレイ」「ダウンロード」「Steamを開く」等のアクションが出る。
UIは1つのUXML/USSで作り、端末差はスタイルとコードの分岐で吸収します。
“データ起点”にするのがコツ。
カードはScriptableObject(SO)またはJSONから生成し、アプリの外に置きたい場合はAddressablesやRemote Configで差し替え可能にしておくと運用が楽です。
以下、わたしの最小構成。
- データ:GameMeta(SO/JSON両対応)
- UI:UI Toolkit(ScrollView + ListView風の仮想化)
- アセット:SpriteAtlas でサムネイル集約、Addressablesで配信
- 分岐:モバイルはSafeArea考慮、PC/Steamはマウスフォーカスとホバー演出
再現手順:ゼロから“ゲーム一覧”が動くまで(UI Toolkit版)
長く見えますが、やることは一本道です。
サンプルコードはそのまま貼って動かせます。
1. データモデルを決める(ScriptableObject + JSON互換)
SOはエディタで編集しやすく、JSONは外部配信しやすい。
両立のため、シリアライズ構造を同じにします。
using System;
using UnityEngine;
[CreateAssetMenu(fileName = "GameMeta", menuName = "Catalog/GameMeta")]
public class GameMeta : ScriptableObject
{
public string id; // 一意ID
public string title; // 表示名
[TextArea] public string summary; // 説明
public string[] tags; // 例: "無料","アクション"
public PlatformFlags platforms; // ビルド対象
public Sprite thumbnail; // サムネ
public string storeUrl; // Steam/モバイル用
public bool free; // 無料か
}
[Flags]
public enum PlatformFlags
{
None = 0,
Mobile = 1 << 0, // iOS/Android
PC = 1 << 1, // Windows/Mac/Linux
Steam = 1 << 2, // Steam配信
}
JSON側のDTOを用意して、SO⇄DTO変換を書いておくと後で楽です。
using System;
using UnityEngine;
[Serializable]
public class GameMetaDto
{
public string id;
public string title;
public string summary;
public string[] tags;
public int platforms;
public string thumbnailAddress; // Addressablesキー
public string storeUrl;
public bool free;
}
public static class GameMetaMapper
{
public static GameMetaDto ToDto(GameMeta so) => new GameMetaDto
{
id = so.id,
title = so.title,
summary = so.summary,
tags = so.tags,
platforms = (int)so.platforms,
thumbnailAddress = so.thumbnail ? so.thumbnail.name : null,
storeUrl = so.storeUrl,
free = so.free
};
}
2. データを束ねるカタログを作る(Addressables前提)
一覧に出すタイトル群をCatalogとして1つにまとめ、エディタ配列/JSONリモートどちらからでも読み込めるようにします。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
[CreateAssetMenu(fileName="GameCatalog", menuName="Catalog/GameCatalog")]
public class GameCatalog : ScriptableObject
{
public List<GameMeta> localEntries; // エディタ直編集用
public AssetReferenceTextAsset remoteJson; // JSONを配信したい場合
}
ロードマネージャを用意。
JSONがあればそれを優先、なければSO配列を使う。
サムネはAddressablesのラベルでまとめておくと高速です。
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class CatalogLoader : MonoBehaviour
{
[SerializeField] private GameCatalog catalog;
public async Task<List<GameMetaDto>> LoadAsync()
{
if (catalog.remoteJson != null)
{
var handle = catalog.remoteJson.LoadAssetAsync<TextAsset>();
await handle.Task;
var json = handle.Result.text;
var list = JsonUtility.FromJson<GameMetaListWrapper>(json);
return new List<GameMetaDto>(list.items);
}
else
{
var list = new List<GameMetaDto>();
foreach (var so in catalog.localEntries)
list.Add(GameMetaMapper.ToDto(so));
return list;
}
}
[System.Serializable]
private class GameMetaListWrapper { public GameMetaDto[] items; }
}
3. UI ToolkitでListView風の仮想化スクロール
UI Toolkit の ScrollView とテンプレート要素(UXML)を組み合わせ、プールして再利用します。
要素ごとにBind(GameMetaDto)を用意してデータを差し替えるだけにしておくと、数百件でもサクサクです。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
public class GameListView : MonoBehaviour
{
[SerializeField] private VisualTreeAsset cardTemplate; // UXML
[SerializeField] private UIDocument uiDocument;
private ScrollView _scroll;
private readonly List<VisualElement> _pool = new();
private List<GameMetaDto> _data = new();
void Awake()
{
var root = uiDocument.rootVisualElement;
_scroll = root.Q<ScrollView>("GameScroll");
}
public void SetData(List<GameMetaDto> data)
{
_data = data;
Rebuild();
}
void Rebuild()
{
// プール回収
foreach (var ve in _pool) ve.RemoveFromHierarchy();
_pool.Clear();
_scroll.Clear();
foreach (var item in _data)
{
var card = cardTemplate.Instantiate();
Bind(card, item);
_scroll.Add(card);
_pool.Add(card);
}
}
void Bind(TemplateContainer card, GameMetaDto data)
{
card.Q<Label>("Title").text = data.title;
card.Q<Label>("Tags").text = string.Join(" / ", data.tags ?? new string[0]);
card.Q<Label>("Summary").text = data.summary;
var btn = card.Q<Button>("Action");
btn.clicked += () => OnClick(data);
}
void OnClick(GameMetaDto data)
{
// SteamならURLを開く、スマホならDeepLink/ストアへ、ローカルならシーン遷移など
Application.OpenURL(data.storeUrl);
}
}
4. フィルター(スマホ/PC/Steam/無料)を実装
トグル(またはToolbar)でフラグを持ち、SetData()に渡す直前でフィルタリングします。
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
public class GameListPresenter : MonoBehaviour
{
[SerializeField] private CatalogLoader loader;
[SerializeField] private GameListView listView;
[SerializeField] private UIDocument ui;
private List<GameMetaDto> _all = new();
async void Start()
{
_all = await loader.LoadAsync();
ApplyFilter();
SetupFilters();
}
void SetupFilters()
{
var root = ui.rootVisualElement;
root.Q<Toggle>("FilterMobile").RegisterValueChangedCallback(_ => ApplyFilter());
root.Q<Toggle>("FilterPC").RegisterValueChangedCallback(_ => ApplyFilter());
root.Q<Toggle>("FilterSteam").RegisterValueChangedCallback(_ => ApplyFilter());
root.Q<Toggle>("FilterFree").RegisterValueChangedCallback(_ => ApplyFilter());
}
void ApplyFilter()
{
var root = ui.rootVisualElement;
var m = root.Q<Toggle>("FilterMobile").value;
var p = root.Q<Toggle>("FilterPC").value;
var s = root.Q<Toggle>("FilterSteam").value;
var f = root.Q<Toggle>("FilterFree").value;
var filtered = _all.Where(x =>
{
bool platformOk =
(!m && !p && !s) ||
((m && ((x.platforms & (int)PlatformFlags.Mobile) != 0)) ||
(p && ((x.platforms & (int)PlatformFlags.PC) != 0)) ||
(s && ((x.platforms & (int)PlatformFlags.Steam) != 0)));
bool freeOk = !f || x.free;
return platformOk && freeOk;
}).ToList();
listView.SetData(filtered);
}
}
5. サムネ最適化(SpriteAtlas+Addressables)
スマホで“一覧スクロールがカクつく”大半の原因は画像読み込みとバッチ割れ。
Sprite Atlasでまとめ、同一サイズを徹底し、FormatはASTC(Android)/PVRTC(iOS)等のプラットフォーム別設定で容量を抑えます。
Addressablesなら初回にCatalogだけ落とし、サムネは見えた時点で順次ロード+キャッシュ。
// 疑似コード:可視時に非同期ロード
async void BindThumbnail(VisualElement root, string addressKey)
{
var img = root.Q<UnityEngine.UIElements.Image>("Thumb");
img.image = placeholderTexture;
var handle = UnityEngine.AddressableAssets.Addressables.LoadAssetAsync<Texture2D>(addressKey);
await handle.Task;
img.image = handle.Result;
}
スマホ/PC/Steamでの“注意点”まとめ(ここでハマりました)
SafeAreaとレイアウト崩れ(スマホ)
iPhoneのDynamic Islandやホームバーで上端/下端が欠けることがあるので、UI Toolkitならpadding-top/bottomをSafeAreaに合わせてコードで注入します。
特に横向きでScrollViewの最後が隠れがち。
using UnityEngine;
using UnityEngine.UIElements;
public class SafeAreaPadding : MonoBehaviour
{
[SerializeField] UIDocument doc;
void OnEnable()
{
var root = doc.rootVisualElement;
var area = Screen.safeArea;
float top = Screen.height - (area.y + area.height);
float bottom = area.y;
root.style.paddingTop = top;
root.style.paddingBottom = bottom;
}
}
フォント&日本語折り返し(スマホ/PC共通)
日本語は等幅じゃないので、カード幅が変わると高さもバラつきます。
UI Toolkitのwhite-space: normal;で折り返し許可+2行上限をCSSで指定、残りは「…」で省略。
複数端末で幅を変えたくないならベースフォントを埋め込みましょう。
ListViewの仮想化(PC/Steam)
大量データは仮想化しないとGPU/CPUともに悲鳴。
UI ToolkitならListViewコンポーネントを使うと仮想化が手取り早く、安全です(上の手書き実装より推奨)。
var listView = new ListView(items,
itemHeight: 96,
makeItem: () => cardTemplate.Instantiate(),
bindItem: (ve, i) => Bind((TemplateContainer)ve, items[i])
);
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
Steam連携(URL/オーバーレイ)
「Steamで開く」ボタンはsteam://store/APPIDでOK。
オーバーレイを使うならSteamworks.NETを導入し、SteamFriends.ActivateGameOverlayToWebPage(url)。
レビュー誘導の位置はEU/日本の表示ガイドラインに触れない程度に控えめに。
メモリとGC(モバイル)
スクロール中にGCのスパイク→カクつき。
テクスチャのDecode/Destroyが原因になりやすい。
サムネはプールして差し替え、Destroyはシーン終了時にまとめて。
Resources.UnloadUnusedAssets()の乱用は厳禁。
uGUI と UI Toolkit の比較(わたしはこう選んだ)
結論:新規ならUI Toolkit、既存uGUIプロジェクトなら無理に移行しない。
わたしは新規だったのでUI Toolkitを採用。
理由と妥協点は下。
UI Toolkitを選ぶ理由
- USS/UXMLで見た目と構造が分離し、チームでの衝突が減る
- ListViewの仮想化が素直に使える(大量リストと相性◎)
- 解像度対応がCSS的で楽。スキンの差し替えも容易
妥協した点:TMPとの連携・リッチテキストがuGUIほど成熟していない箇所がある。
TMP派生の演出はuGUIが速いケースも。
カードのテキスト演出を抑えてUI Toolkitに寄せました。
uGUIを選ぶ理由(既存資産がある場合)
- TMP/Animator/レイアウトグループ等の資産をそのまま活用できる
- アセットストアのUIキットが豊富(即戦力のテンプレ多数)
- 3DワールドとUIを混在させる演出がやや作りやすい
妥協点:大量リストの仮想化は自前実装かアセット依存。
Canvas再計算の“沼”にハマるとつらい。
遅延ロード/プールをきっちりやれば十分いけます。
無料アセット&運用の小ワザ(コストをかけずに仕上げる)
SpriteAtlasは標準でOK。
サムネ生成はTextureImporterのスクリプトで一括設定すると事故が減ります。
アイコン類はMaterial Icons等の商用可セットを活用。
Addressablesの“Remote Build & Load”を使うと、アプリ更新なしで新作カードを追加できます(ただし初回カタログURLをどこかに埋める必要あり)。
タグ機能は文字列マッチから始めて、DL数やプレイ時間などテレメトリを集め出したらサーバー側でおすすめ順を計算して並べ替えると、体感が一気に良くなります。
“無料/スマホ/Steam/PC”の観点での比較(UIとデータの持ち方)
同じカードでも、アクションボタンはプラットフォームで変えた方がUXが良いです。
わたしの最低限ルール:
- スマホ:「今すぐプレイ」→シーン遷移/未インストールはストアへ
- PC(ローカルランチャー):「インストール/アンインストール」「実行」
- Steam:「Steamで開く」「ウィッシュリストに追加」
- 無料フラグ:価格表記を消し、“無料”タグを左上に固定表示
データ的には、アクション種別を列挙にしておき、プラットフォームごとにボタンマップを切り替えるのが堅実。
ビルド時に“このプラットフォームで無効なボタン”は非表示にします。
public enum ActionKind { PlayLocal, Install, Uninstall, OpenStore, Wishlist }
[Serializable] public class GameAction { public ActionKind kind; public string arg; }
ミニ実例:ローカル“無料”ゲームを同梱して一覧から起動する
たとえば2Dミニゲームを複数シーンで同梱し、「無料」タグを付けて一覧から起動。
アプリ内完結なのでスマホの審査やネットワークに左右されにくい。
using UnityEngine;
using UnityEngine.SceneManagement;
public class LocalLauncher
{
public static void Play(GameMetaDto data)
{
// data.arg にシーン名が入っている想定
SceneManager.LoadScene(data.storeUrl); // 例としてstoreUrlをシーン名に流用
}
}
※実運用ではstoreUrlを使い回さず、sceneNameフィールドを別で持たせる方が安全です。
品質を一段上げるチェックリスト(出して学んだ)
最初に出した版は「なんとなく動く」けれど、リリース後に刺さる“細かい粗”が多い。
下のチェックはすべて実害があったやつです。
- スクロール終端のスナップ感(指を離したときの減速/反発)
- サムネのシャープネス(輸出時のmipmap/フィルタ設定)
- “…続きを読む”モーダルの戻る操作(Androidの戻るキー対応)
- オフライン時の挙動(JSON配信が落ちたときのローカルフォールバック)
- メトリクス:クリック率/滞在時間/検索語(後で改善の根拠になります)
よくある質問(Q&A風に短く)
Q. uGUIで作った方が早い?
A. 既存UIやTMP演出が豊富ならuGUIで即戦が早いです。
大量リストで詰まったら「仮想化リスト」のアセットを検討。
新規&長く運用するならUI Toolkitを推します。
Q. リモートJSONはどこで配信?
A. 最初はGitHub Pages / Cloud Storageで十分。
将来はCDNを挟み、E-Tag等で差分配信。
アプリ側は**Addressables.LoadAssetAsync<TextAsset>**でOK。
Q. Steamとスマホで同じデータを使える?
A. はい。
プラットフォームフラグをデータに持たせ、ビルド時に「置換 or 非表示」で調整すれば1ソースで回ります。
(おまけ)カテゴリ別の“Unity製の代表例”を見る目線
「Unityゲームの一覧」を作るとき、ただ名前を並べるより、なぜそのタイトルを置くかを一言添えると価値が出ます。
たとえば――
- モバイル発:位置情報やカメラ連携など端末機能フル活用の好例
- PC/Steam:インディーでも運営が継続している例(パッチ頻度/コミュニティ運用)
- 無料:収益設計が健全(広告/課金の見せ方がクリーン)
- クロスプレイ:アカウント連携/セーブ同期のUXが洗練
技術ブログとしての“一覧”は、技術と運用の学びポイントを添えるだけで、ただの羅列から一気に実務の資料になります。
まとめ:小さく作って運用で磨く、“一覧”はプロダクトの顔
ゲーム一覧は“ただのリスト”に見えて、最初に触れるUXの核です。
UI Toolkitなら少ないコードで仮想化・レスポンシブ・テーマ切替まで揃います。
データはSOとJSONの両輪、配信はAddressablesで小さく始める。
画像はAtlas、フィルターは単純なところから。
まず出して、メトリクスで磨く――これがわたしの解でした。
もしアセットや教材を探すときは、開発のついでにUnity入門の森ショップものぞいてみると良いかも。
ちょうど必要な“足りない部品”が見つかること、けっこうあります。
付録:最小JSONの例(リモート配信用)
{
"items": [
{
"id": "mini-001",
"title": "はじめての2Dラン",
"summary": "短いコースを走ってゴール。チュートリアル兼デモ。",
"tags": ["無料", "2D", "ラン"],
"platforms": 3, // Mobile|PC
"thumbnailAddress": "thumb_mini_001",
"storeUrl": "MiniRunScene", // ローカル再生用ならシーン名
"free": true
},
{
"id": "steam-hero",
"title": "Hero on Steam",
"summary": "ボスラッシュ。ゲームパッド推奨。",
"tags": ["アクション", "PC"],
"platforms": 6, // PC|Steam
"thumbnailAddress": "thumb_hero_steam",
"storeUrl": "steam://store/123456",
"free": false
}
]
}
付録:UXMLの骨格(概念図)
<!-- GameList.uxml -->
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement name="Root" class="root">
<ui:VisualElement class="toolbar">
<ui:Toggle name="FilterMobile" text="スマホ"/>
<ui:Toggle name="FilterPC" text="PC"/>
<ui:Toggle name="FilterSteam" text="Steam"/>
<ui:Toggle name="FilterFree" text="無料のみ"/>
</ui:VisualElement>
<ui:ScrollView name="GameScroll" class="list" />
</ui:VisualElement>
</ui:UXML>
付録:USSの一部(行高と省略)
.root { padding: 12px; }
.toolbar { flex-direction: row; gap: 8px; }
.list { height: 100%; }
.card { padding: 10px; border-radius: 8px; margin-bottom: 8px; }
#Title { unity-font-style: bold; font-size: 16px; }
#Summary { white-space: normal; -unity-text-overflow-position: end; }
以上、お役に立てばうれしいです。
質問があればコメントへどうぞ。
-
UI Toolkitは2021以降で実戦投入しやすくなりました。
古い環境ではuGUIの方が安定する場合があります。 ↩