【UE5】ソートのフリーレン。散らかったカードを「整列する魔法(C++)」で片付ける(準備編)【開発37日目】
こんにちは、「まる。」です。
さて、突然ですが皆さま
もし一つだけ『魔法』が使えるならどんな魔法が欲しいですか?
空を自由に飛び回る魔法や、物を黄金に変える魔法……。
いろいろありますが、今この瞬間の私が喉から手が出るほど欲しいのは、「お片付けしてくれる魔法」です!
……実は私、子供の頃から整理整頓が壊滅的に苦手でして。
もうすぐ年越しなので、
どうにかして大掃除をしたいと思ってはいるのですが、なかなか体が動きません(笑)。
というわけで!
「現実世界で出来ないなら、せめてコレクションメニュー内だけでも整理整頓したい!」 そんな切実な願いを込めて、
今回は「ソート機能」の実装に挑戦します!
▼前回の記事
1.フィルタ機能のUI実装(前回の続き)
さて、まずは前回の続きから。
前回、チェックボックスやボタンを使ったフィルタ機能のロジック(頭脳)をC++で制作しました。
今回は、それをUI側と連携させ、
フィルタ機能を完全に動作する状態へと仕上げます!
STEP 1:所持フィルターの改良
まずは、画面右上にある「所持済みのみ」というチェックボックスの改良から。

現在は単純なチェックボックス式のため、「全て表示」か「所持のみ表示」の2パターンしか切り替えられません。
これだと、「持っていないカードだけを確認したい(図鑑の空きを確認したい)」という時に不便ですよね。
そこで、これをクリックするたびに状態が切り替わるボタンに変更し、
「全て」→「所持」→「未所持」 の3パターンを自在に切り替えられるように進化させます!
手順1:ラベルの変更
「所持済みのみ」だと機能と合わなくなるので、
ラベルのテキストを「表示切り替え」に変更します。

手順2 : ボタンの配置
既存のチェックボックスを削除し、代わりに「ボタン(Button)」を配置します。

手順3 : テキストの追加
ボタンの子要素として「テキストブロック(TextBlock)」を追加し、
初期値として「全て」と入力しておきます。
これで、「今は全てのカードを表示していますよ」という状態を作ります。

手順4 : 変数として公開
ここが重要です! ボタンの中にあるテキストブロックの名前を 「TextBlock_AS_View」 に変更し、
詳細パネルの「Is Variable(変数を公開)」にチェックを入れます。
こうすることで、ブループリント側から「ボタンの文字」を書き換えられるようになります。
(クリックするたびに「全て」→「所持」→「未所持」と文字を変えるためですね!)

STEP2 : 変数の追加
続けて、UIのロジックを作成するための変数を予め用意します。
手順1:変数を2つ用意する

手順2 : それぞれの変数に名前と型を指定する。
今回、用意したい変数は以下の2つです。
🛠️ 作成すべき変数リスト
1. 所持フィルタ用変数
ボタンを押すたびに切り替わる「現在の状態(0, 1, 2)」を管理します。
変数名: CurrentOwnedFilterIndex
変数の型: Integer (整数)
2. クラスフィルタ用変数
チェックボックスで選択されたクラスをまとめて保存しておくリストです。
変数名: SelectedClassFilters
変数の型: ECharacterClass (プロジェクトで使用しているクラスのEnum)
⚠️重要: 変数の横のアイコンをクリックして、「配列(Array)」に変更します。

STEP3 :ボタンのクリック処理
先ほど作成した所持フィルタ用のボタンをクリックした時の処理を追加します。
ノード(命令ブロック)を繋いで、以下のようなロジックを組みました。


……BPを見慣れていないと、線が絡まっていて少し分かりづらいですよね?
そこで、この処理の中で行われている「会話」をイメージしてみました。
🔘表示切替ボタン 「スイッチオン! クリックされました!」
===コレクションメニューの内部===
👩OnClickedイベント (司令塔) 「ボタンが押されましたね! 皆さん、仕事してください!」
⚙️ CurrentOwnedFilterIndex変数 (記録係) 「了解です! 次の画面を出すために、今の番号にプラス1しますよー!(例:0→1)」
😎 Branch (門番) 「待ちたまえ。その番号は正しいのかね? 画面切り替えは3種類(0,1,2)しかない。
もし『3』になっていたら……『0』に戻して最初からだ!」
👷♂️ Switch on Int (案内人) 「おや、今回は『0番』の画面ですね。
なら、ボタンの表示は『全て』と書き換えて、プレイヤーさんにお伝えしましょう!」
👨🏫 FilterCardsUnified関数 (最強の検索係) 「OK。
あとは私に任せたまえ。
指定された条件(所持・未所持)を元に、『表示すべきカードのリスト』を抽出して渡そう!」
こんな感じのことが中で行われている訳です。
実行確認とトラブルシューティング
所持フィルタの実装が完了したので、動作確認を行ってみます。


所持フラグは立てたはずなのに、なぜでしょうか?
原因の推測
おそらく原因は、「所持フラグ(Is Owned)」と「所持数(OwnedCount)」のデータの不整合にあります。
昨日作成した FilterCardsUnified 関数は、仕様上「所持数が1以上か」で判定を行っています。
一方で、今回のテストデータは無理やり「所持フラグ」だけをONにした状態で、肝心の「所持数」が「0」のまま更新されていませんでした。
そのため、ロジック側からは「所持数0 = 未所持」と判定されてしまったようです。
所持判定をプロパティで管理する
Gemini先生に相談したところ、根本解決として「所持フラグ」という変数を実体として持たず
所持数から自動計算(Getter)させる方法を提案されました。
これなら「フラグはONだけど数は0」といった矛盾が起きなくなります。
エディタ上で枚数を変更するだけで即座に正しいテストができるので、管理もシンプルになりそうです。
テストを楽にするための「裏口」を作る
まずは、カードの所持枚数を自由に操作できるようにコードを修正します。
編集するファイル:ヘッダーファイル(AlcaStellaCardDataCollection.h)
修正前(Before): BlueprintReadOnly(読み取り専用):
UPROPERTY(BlueprintReadOnly, Category = "Card Data")
int32 OwnedCount = 0;これだと、プログラムから数字を見ることはできますが
私たちが後から数字をいじることができません。
修正後(After): EditAnywhere & BlueprintReadWrite(編集可能)
// EditAnywhere を追加し、BlueprintReadWrite に変更します
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Card Data")
int32 OwnedCount = 0;
この魔法の言葉を書き足すことで、入力欄のロックが解除されます!
これで準備完了です。
あとはこの権限を使って、ブループリント(BP)上で「ポラリスなら数字を1にする(所持扱い)」という命令を書いてあげれば、
テストができるようになるはずです。
再確認
それでは、先ほど開通させた「裏口(編集権限)」を使って、テスト用のBPを組み込んでみます。 (※細かいノードの繋ぎ方は割愛します)
意図的に「ポラリス」だけを所持状態にして……いざ、実行!
フィルタ機能が正しく働いているか、運命の点呼確認です。



完璧です。どうやら成功のようです。
これで、つまずいていた「所持フィルタ」の実装と動作確認が無事に完了しました。
STEP4 :クラス別フィルタの実装
さて、思いのほか記事が長くなってきました。
ここからはテンポよく、「クラス別フィルタ」についても実装していきます!
手順1:リセットボタンを作成
フィルタ条件をあれこれ触ったあと、一発で初期状態に戻せるように「リセット」ボタンを画面に追加しました。
これがないと、チェックボックスを一つずつ外す作業が発生してしまいますからね。

手順2:リセットボタンのクリック処理
ボタンを押した瞬間に、現在選択されているクラスのチェックマークが「全て外れる」ようにブループリント(BP)でロジックを組んでいきます。
ここで一つ工夫をしました。
ボタンを押すたびに画面上のチェックボックスを探しに行くと処理が重くなったり、予期せぬエラーの元になったりします。
そこで、画面を作った段階で「操作対象のチェックボックス」をあらかじめ配列(リスト)として変数にまとめておきました。

リセットボタンが押されたら、この用意しておいたリストを順番に読み込み、チェックを「False(外す)」に書き換えていきます。

仕上げに、クラスフィルタの選択状況を管理している変数(SelectedClassFilters)を空っぽにして、
最後に更新用関数(BP_Filter Cards Unified)を呼び出せば完了です。
これで、見た目も内部データもクリーンな状態に戻ります!
実行結果

手順3 :更新用関数(UpdateClassFilter)を作る
チェックボックスはたくさんあるので、個別にロジックを書くのは大変です。
そこで、「クラス名(Enum)」と「チェック状態(Bool)」さえ渡せば、あとはよしなにやってくれる共通関数を作ります。

中身はシンプルです。
チェックがONならフィルタリストに追加(Add Unique)し、 OFFならリストから削除(Remove)する。
これを一つの関数にまとめておくことで、
今後チェックボックスがいくら増えても呼び出すだけで済むようになります。
手順4:各チェックボックスへの紐づけ
便利な関数が完成したので、あとは各チェックボックスからそれを呼び出すだけです。
ここからは少し地道な作業になりますが、一つずつ確実に繋いでいきます。
デザイナー画面で、クラスのチェックボックス(例:CheckBox_Aura)を選択し、
詳細パネルの一番下にあるイベント On Check State Changed の「+」を押します。

生成されたイベントノードから、
さっき作った UpdateClassFilter を呼び出します。
引数の設定はとても簡単です。
ClassName: ドロップダウンから、そのチェックボックスに対応するクラス名(「アウラ・コンボルタ」など)を選ぶだけ!
IsChecked: イベントから渡される赤色のピン(Is Checked)をそのまま繋ぐ。

型をEnum(列挙型)に合わせておいたおかげで、クラス名の指定もリストから選ぶだけで済み入力ミスをする心配もありません。
この作業を全てのクラス分繰り返せば、クラス別フィルタの実装は完了です!

最終テスト実行
さて、いよいよ実装した「クラス別フィルタ」を動かしてみます。
チェックボックスをポチポチ押してみると……。

よし!完璧!
指定したクラスのカードだけがしっかり絞り込まれて表示されています。
複数選んでも問題ないですし、先ほど作った「リセットボタン」もいい仕事をしています
これで、一連のフィルタの作成は以上です!
2.ソート機能の実装方法を考える(ようやく本題)
さて、前置きが長くなりましたが、ここからがようやく本題です。
UIの便利機能の仕上げである「ソート(並び替え)」をどう実装するか、設計を検討します。

まず、私が考えるアプローチについてお話します。
📗a.今回のプログラムには何が必要なのか?(機能要件)
・単一選択の並び替え:
「ID順」「名前順」「好感度順」「ATK順」「DEF順」の5項目を用意。
これらはラジオボタンのように「常にどれか一つだけ」が選ばれている状態にする(デフォルトはID順)。
・リストの再取得とソート:
選択された並び替え項目に応じて、
リストの中身を適切な順番に並び替える。
【追加要件】昇順・降順:
同じ項目でも「高い順(降順)」「低い順(昇順)」を切り替えられるようにする。
⚙b. 技術的課題(どう実現するか?)
実現にあたってクリアすべき技術的な壁を整理します。
Q1. チェックボックスを一つだけ選ばせる方法は?
⇒ UI的にはチェックボックスですが、
機能としては「ラジオボタン」の挙動が必要です。
先ほど作った「クラスフィルタの全解除(リセット)」のロジックを応用すれば、擬似的に実装できるはず。
Q2. リストの中身をソートするタイミングは?
⇒ これが一番の悩み所です。
「一度View(表示)を作ってから、中身をチェックして並び替える」という方法だと、
「表示処理→並び替え→再表示処理」という二度手間が発生し、
ロード時間が倍増する恐れがあります。
Q3. 昇順・降順の切り替えは?
⇒ ボタンで制御します。
ただ、これも切り替えるたびに全件読み込み直しが発生すると動作が重くなりそうです。
🤔c. 検討結果
ブループリントだけでUI要素を並び替えようとすると、処理負荷が高くなりそうです。
「View(見た目)」をいじるのではなく、「渡すデータそのもの」をC++側で高速に並び替えてから渡すのが、
最も安全かつメモリに優しいアプローチだと判断しました。
こんな感じでどうでしょうか?
Gemini先生。
👨🏫 Gemini先生からの技術アドバイス
記事内の「検討結果」で導き出した答え、大正解です。
1.ソートのタイミング:
懸念されていた通り、「Viewを作ってから並び替える」のはNGです。 **「データ配列(TArray)の段階で並び替え(Sort)て、その綺麗なデータをViewに渡す」**のが鉄則です。これならロード回数は1回で済みます。
2.C++ vs BP:
ソート処理は、数千枚のカードがあっても C++ の Algo::Sort や TArray::Sort を使えば一瞬(ミリ秒単位)で終わります。一方、BPでソートロジックを組むと桁違いに遅くなります。ここをC++にする判断は完璧です。
3.UIについて(補足):
image_0bf04e.jpg を拝見すると、チェックボックスで実装されていますね。 もしUIスペースが許すなら、
**「コンボボックス(ドロップダウンリスト)」**を使うと、プログラムを書かなくても「どれか一つしか選べない」という挙動が勝手についてくるので実装が楽になります。 (もちろん、現在のデザインに合わせてチェックボックス制御で作るのも勉強になるのでOKです!)
【補足】UIデザインの方針について
Gemini先生からは「コンボボックス(プルダウン)を使えば、一つしか選べない機能が簡単に作れますよ」と提案がありましたが……。
今回はあえて却下します。
理由はシンプル。
以前試した際に挙動が不安定で、私の求める操作感(使い勝手)と合致しなかったからです。
標準機能に頼って妥協するよりも、多少手間がかかっても「自分で制御するチェックボックス」で実装する方を選びます。
やるなら徹底的に。
既存のチェックボックスをロジックで制御して、「疑似的なラジオボタン」の挙動を再現していきます!
3.実装(チェックボックスのラジオボタン化)
それでは、早速実装していきます。
※記事の文字数が多くなってきたので今回はC++実装に向けた仕込みだけ済ませます。
STEP1 : チェックボックスを配列にまとめる
クラスフィルタの時と同様、画面を作った最初(Construct)のタイミングでソート用の5つのチェックボックスを一つの配列変数にまとめておきます。
変数名: CheckBoxList_Sort (チェックボックスの配列)
中身: ID順、名前順、好感度順、ATK順、DEF順 の5つを格納

STEP2 : 切り替え用関数を作る(排他制御)
ここが今回の肝です。
「新しく押されたチェックボックス」を受け取って、それ以外を強制的にOFFにする関数を作ります。
関数名: UpdateSortRadioButton
引数: TargetCheckBox (押されたチェックボックス)

ロジックはシンプルです。 登録しておいたチェックボックスを順番に確認し、 「今回押されたボタン」ならON(Set Checked = True)、
「それ以外のボタン」ならOFF(Set Checked = False)に書き換えます。
これで、どれか一つしか選べない「ラジオボタン」の挙動が再現できました!
STEP3 : 各ボタンから呼び出す & UI更新
仕上げに、5つのチェックボックスそれぞれの On Check State Changed イベントから、この関数を呼び出します。
ついでに、画面上の「並び替え:〇〇」というテキストも、
現在選んでいる項目名に合わせて更新してあげましょう。
ID順のチェックボックスの場合:
切り替え関数を呼び出す(引数には自分自身を渡す)
表示テキストを「ID順」に変更する

これを5つのボタン全てに設定すれば、UI側の準備は完了です。
実行確認
それでは、早速実行してみます。

上手く行きました!成功です!
これで、C++でロジックを作った後でも連携出来るように準備が整いました。
4.昇順・降順トグルボタン実装
本日のUI作業の仕上げとして、並び替えの「昇順・降順」をいつでも切り替えられるトグルボタンを用意します。
まずは画面右上に、ボタンとテキストを配置しました。

STEP1 : 管理用変数の作成
「今、昇順なのか降順なのか」を記憶しておくための変数を作ります。
変数名: IsAscending (Boolean / ブール型)
初期値: True(昇順) ※お好みで

STEP2 : ボタンクリック時の処理(トグル)
ボタンを押すたびに、TrueとFalseが反転(スイッチ)するようにロジックを組みます。
ロジックはシンプルです。
On Clicked イベントで、現在の Is Ascending の値に NOT(反転) をかけたものをセットし直すだけ。
これでボタンを押すたびに True → False → True ... と交互に切り替わるようになります。

STEP3 :見た目の更新
中身の値が変わったら、ユーザーにも分かるようにボタンの文字を書き換えます。
Is Ascending が True なら「昇順(▲)」、False なら「降順(▼)」と表示するように分岐させます。

STEP 4:リスト更新
最後に、いつもの更新用関数 BP_Filter Cards Unified を呼び出して完了です。
(※まだC++側のソート機能を作っていないので、今の段階では「押しても並び順は変わらないけど、文字だけ変わる」状態でOKです!)

実行確認
さて、では早速ボタンが切り替わるか確認します。

OKです!完璧に切り替わりました!
これでUI側の準備はすべて完了です。
まとめ
今回は思いのほかボリューミーな記事になりました。
難関だった「フィルタ機能」の実装が無事に終わってホッとしています。
次は「ソート機能」ですが、正直なところ上手く実装できるか不安はあります。
ただ、これまでのC++実装の経験から「こう書けば動きそうだな」というイメージは湧くようになってきたので、
その勘を信じて試行錯誤してみます!
【明日のお知らせ】
明日はお昼から終日予定があるため、
もしかすると当記事初のお休みを頂くかもしれません。
早起きできれば何かしら書きたいのですが……
開発日記の連続記録を途絶えさせたくないので、軽めの更新でも顔を出せればなと思っています🤔
今日のポラリスちゃん

ポラちゃんの圧が強い…
以上!
もしこの記事が「参考になった」「開発の苦労が伝わった!」「続きが気になる!」と思ったら、
記事の下にある「スキ(ハートマーク)」を押していただけると嬉しいです!
皆様の応援こそが、毎日の開発と更新の何よりの励みになります。
最後までお読みいただき、ありがとうございました!
また次回の記事でお会いしましょう!
いいなと思ったら応援しよう!
よろしければ応援お願いします!
いただいたチップはクリエイターとしてのゲーム開発に使わせていただきます!
※チップと共にやって欲しい事があったら言ってください!
可能な範囲であれば頑張ります