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で“ゲーム一覧”画面を作る:PC/スマホ/Steam対応の実装手順とハマりどころ、そしてわたしの比較メモ

Posted at

「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から生成し、アプリの外に置きたい場合はAddressablesRemote 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/bottomSafeAreaに合わせてコードで注入します。
特に横向きで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; }

以上、お役に立てばうれしいです。
質問があればコメントへどうぞ。

  1. UI Toolkitは2021以降で実戦投入しやすくなりました。
    古い環境ではuGUIの方が安定する場合があります。

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?