【UE5】一枚でもいいのがあればさッイイよねッ!!~小さくて可愛いカードのデッキシステム構築~【59回目】
■今回やった事
・ホイール操作でデッキに追加・削除する機能を追加した。
・デッキに入ってるカードをソートする機能を追加した。
・カードを投入した際のアニメーションを追加した。
こんにちは、「まる。」です。
今回も、前回に引き続きデッキビルドシステムの
『カード追加・削除機能』を実装していきます🦍
■前回の記事
💡 ちょっと小話
最近、フォロワー様が開発された『ネタヴィライゼーション』というストラテジーゲームを遊んでみたのですが……
これがマジで面白かったです。📗
生成AIに物語を作ってもらいながら、
ゲームブックのようなテイストで自国の歴史を築いていくゲームなのですが😏
ちなみに、私『まる。』の国家では「幻覚の見える危ない果実やキノコ」をベースに宗教を設立。
それで飢餓問題を解決しようとした結果、最終的に国民全員で「解脱」みたいなことをして終わるという、
お茶目(?)なロールプレイをかましてきました😎
1. 本日の開発メニュー(積み残しの解消)
さて、今回は前回やり残してしまった以下の項目を一気に片付けていきます。
⑤ 自動ソートと総枚数カウント(メニュー側)
C++を組み込んで、表示されたカードをレアリティ・コスト順にビシッと整理。
さらに全体の枚数管理も自動化します。
⑥ ホイール操作での枚数操作(カード側)
クリック連打だけでなく、
マウスホイールでサクサク枚数を変えられる「地味だけど手放せない」快適機能を追加。
⑦ UIアニメーション(カードリスト側)
今回の目玉!
クリックしたカードがデッキへ吸い込まれるような、飛行演出を仕上げます。
実は、ゲームとして完成させるには他にも実装すべきことは山ほどあるのですが……。😏
「デッキを組む」という基本的な手触りについては、これでついに完成です。
2.ホイール操作での枚数操作(カード側)
順番が少し前後しますが、
まずは操作感に直結するマウスホイールでのカード枚数操作から実装していきます。
STEP 1: デッキ用カードWBPで「OnMouseWheel」をオーバーライド

まずは、右側のTileView(デッキリスト)に表示されている「デッキ内カード用のウィジェット(WBP)」のグラフを開きます。
左側の「マイブループリント」パネルにある 「関数」➔「オーバーライド」 のドロップダウンから 『On Mouse Wheel』 を選択して追加します。

STEP 2: ホイールの回転方向(上か下か)を判定する
ここでは、ホイールをどちらに回したかに応じて「増加」と「減少」の処理を仕分けていきます。
On Mouse Wheel ノードの Mouse Event ピンから、『Get Wheel Delta』 ノードを引き出します。
この「Delta」という値が、ホイールの回転方向と量を教えてくれます。
Delta値が 0以上(>= 0):奥に回した(上スクロール)➔ 枚数を増やす
Delta値が 0未満(< 0):手前に回した(下スクロール)➔ 枚数を減らす

取得した値を比較演算子(>=)に繋ぎ、その結果を 『Branch』(ブランチ)に放り込みます。
これで「Trueなら増加、Falseなら減少」という風に、ロジックを綺麗に分岐させることができます。
STEP 3: 親(メニュー画面)へ増減の処理を依頼する

ロジックの分岐ができたら、あとは親ウィジェット(メニュー画面)に
「このカードを増やして(または減らして)」と依頼するだけです。
【子ウィジェット(カード側)の処理】
前回のクリック処理と同様に、Cast を使って親のカスタムイベントを呼び出します。
上スクロール(増加)の場合:
すでに実装済みの追加処理(AddSpellCardToDeck など)をそのまま呼び出せばOK!下スクロール(減少)の場合: 新しく作成する減少用イベント(RemoveSpellCard)を呼び出します。
最後に、戻り値を 『Handled』(ハンドル済み)にして返すのを忘れずに。
これをしないと、ホイールを回した時にリスト全体まで一緒にスクロールしてしまいます。

【親ウィジェット(メニュー側)の減少処理】
カードを減らす時は、単に数字を減らすだけでなく、
「0枚になったらリストから消す」という制御が必要になります。
ここで、C++でClaudeさんが作ったノードが輝きます。
1.減少イベントの開始
C++の減少関数を呼び出し、結果が「成功」であれば処理を続けます。
2.現在の枚数を確認(C++ノード活用!)
『Get Spell Card Count In Deck』 ノードを配置します。
・Deckピン:編集中のデッキ(Editing Deck)を接続。
・Card IDピン:対象のカードIDを接続。
3.枚数に応じてUIの挙動を変える(Branch)
取得した枚数が 「> 0(0より大きい)」 かどうかで分岐させます。
True(1枚以上残っている場合):
まだデッキに存在するので、TileViewの 『Regenerate All Entries』 を呼び出して見た目の数字だけを更新します。False(0枚になった場合):
デッキから完全に消えたので、TileViewの 『Remove Item』 ノードを使い、リストからそのカードウィジェット自体を消去します。

3.デッキ内の自動ソート(C++実装)
次は、デッキにカードが追加・削除された際、リストを自動で整列させる機能を実装します。
今回は処理の正確さと今後の拡張性を考え、
ブループリントではなくC++でロジックを組むことにしました。
私がカードを並べる際にこだわった優先順位(ソート条件)は以下の通り。
第1優先:レアリティ順(希少なカードを一番後ろ)
第2優先:コスト順(同じレアリティなら、低コストから順に)
第3優先:ID順(上記が全て同じなら、内部IDのアルファベット順)
この「TCGとして譲れない並び順」を実現するため、
Gemini先生に相談しながらコードを構築していきました。
STEP 1: C++側にソート用の関数を追加する
まずは デッキ編成システム用のプログラム(DeckBuildLibrary) に、
BPから呼び出せるソート用の静的な関数(Static Function)を追加します。
【ヘッダー側 (DeckBuildLibrary.h)】
クラス内の public 領域に以下の関数宣言を追加します。
// -----------------------------------------------
// UIユーティリティ
// -----------------------------------------------
UFUNCTION(BlueprintCallable, Category = "DeckBuild|UI")
static TArray<UObject*> SortSpellCardsByRarityAndCost(const TArray<UObject*>& UnsortedItems);【実装側 (DeckBuildLibrary.cpp)】
// (例です)
// #include "CardData/SpellCardDataCollection.h"
TArray<UObject*> UDeckBuildLibrary::SortSpellCardsByRarityAndCost(const TArray<UObject*>& UnsortedItems)
{
// 元の配列をコピー
TArray<UObject*> SortedItems = UnsortedItems;
// ★修正ポイント1: (UObject* A, UObject* B) ではなく (const UObject& A, const UObject& B) で受け取る!
SortedItems.Sort([](const UObject& A, const UObject& B) {
// ★修正ポイント2: 参照で受け取ったものを「&」でポインタに戻してからCastする!
const USpellCardDataCollection* CardA = Cast<USpellCardDataCollection>(&A);
const USpellCardDataCollection* CardB = Cast<USpellCardDataCollection>(&B);
if (CardA && CardB)
{
// ① レアリティを取得
ERarityType RarityA = CardA->CardData.RarityID;
ERarityType RarityB = CardB->CardData.RarityID;
// ② コストを取得
int32 CostA = CardA->GetBaseCost();
int32 CostB = CardB->GetBaseCost();
// --- ソートの優先順位判定 ---
// 第1優先:レアリティ順
if (RarityA != RarityB)
{
return RarityA < RarityB;
}
// 第2優先:コスト順
if (CostA != CostB)
{
return CostA < CostB;
}
// 第3優先:ID順(アルファベット順)
return CardA->CardId.LexicalLess(CardB->CardId);
}
return false;
});
return SortedItems;
}ortedItems;
}
STEP 2: ブループリントでTileViewを更新する
C++のコンパイルが無事に通ったら、
あとはブループリントから呼び出すだけです。
カードを追加・削除したタイミング(枚数更新の後など)に、
以下の流れでノードを組んでいきます。
現在のアイテムを取得する
変数の 『TileView_Deck』(デッキ内容のタイルビュー) をグラフに出し、
そこから 『Get List Items』 を呼び出します。
これで、現在TileViewに並んでいる「全データオブジェクトの配列」が手に入ります。C++のソート関数を通す
取得した配列を、
先ほど自作した 『Sort Spell Cards By Rarity And Cost』 関数の Unsorted Items ピンに繋ぎます。
(※ここで、C++側で苦労して書いた「レアリティ ➔ コスト ➔ ID」の順序と整列されます)ソート済みのリストを再セットする
関数の戻り値(ソート済み配列)を、『Set List Items』 ノードの In List Items に繋いで実行します。
【ポイント】
Set List Items を実行すると、TileViewが中身を一度クリアして新しい順番で並べ直してくれるので、
これだけで「追加した瞬間に適切な位置へカードが移動する」という挙動が完成します!

4.UIアニメーション(飛行演出)の実装
機能が完成したら、あとは「見た目の気持ちよさ」を追求していきましょう!
カードがデッキリストへ吸い込まれるようなアニメーションを実装するため、
まずは「飛んでいく専用のカード(ダミー)」を準備します。
STEP 1: ダミーウィジェット(WBP_DummyCard)の階層を作る
このウィジェットは、クリックされた瞬間に生成され、目的地まで飛んで消えるだけの「使い捨て」の役者です。
本物のカードと同じ見た目にするため、以下のように階層を組みます。
・土台を作る
WBP_DummyCard のキャンバスに 『Overlay(オーバーレイ)』 を配置します。
・サイズを固定する(重要!)
Overlayの中に 『Size Box(サイズボックス)』 を配置し、詳細パネルの Width Override と Height Override にチェックを入れます。
(※実際のカードと同じサイズ、例えば横150、縦210などに固定します。これをしないと画像が巨大化してしまいます!)
・画像を重ねる
Size Boxの中に、イラスト用とフレーム用の 『Image』 を2つ重ねます。
下層: 名前を Image_Illustration に。(イラスト用)
上層: 名前を Image_Frame に。(枠用)
・配置設定
どちらの Image も、
詳細パネルの「スロット」設定で 『Fill(全体に引き伸ばす)』 を選択し、枠いっぱいに表示されるようにします。

STEP 2: スポーン時に画像を受け取る準備(超重要💡)
ダミーが生成された瞬間に「どのイラストとフレームを表示するか」を親から受け取れるようにします。
ここで、「画像が真っ白になる現象」を確実に防ぐためのテクニックも盛り込みます。
変数を作成し、外から見えるようにする
WBP_DummyCard の変数リストに以下の2つを追加します。Tex_Illustration(型:Texture 2D / ソフトオブジェクト参照)
Tex_Frame(型:Texture 2D / ソフトオブジェクト参照)
設定のキモ(ここを忘れると親から渡せません!)
各変数の詳細パネルで、以下の2箇所に必ずチェックを入れます。『インスタンス編集可能(Instance Editable)』
『スポーン時に公開(Expose on Spawn)』
これにチェックを入れることで、親ウィジェットで Create Widget ノードを置いたときに、入力ピンとして出現してくれます。
生成時に画像をセットする
Event Construct ノードから処理を伸ばします。
ここで使うのは Set Brush from Texture ……ではなく、
『Set Brush from Soft Texture』 ノードです。

【💡 なぜ「ソフト参照」と「Soft Texture」を使うのか?】
通常のオブジェクト参照だと、画像がメモリにロードされていない場合に「真っ白なカード」が飛んでいってしまうことがあります。
ソフト参照のノードを使うことで、「ロードが終わっていなければ、裏でロードしてから表示する」という賢い挙動になり、
表示バグを確実に防ぐことができます!

STEP 3:ダミーに「自律飛行ロジック」を書く
タイムラインを使わず、
あえてダミー自身に Event Tick を持たせることで、
「生成されたら目的地まで勝手に飛んで、着いたら自爆する」
という自律型のロジックを組んでいきます。
時間のカウント(寿命の管理)
Event Tick から出力される In Delta Time(前フレームからの経過時間)を変数 CurrentTime に加算し続けます。
これで「生まれてから何秒経ったか」がわかります。進行度(Alpha)の計算
CurrentTime を移動にかける秒数(今回の画像では 0.2秒)で割り算します。
その結果を Clamp (Float) ノードで 0.0 ~ 1.0 の間に収めることで、
正確なアニメーションの進行度(Alpha)が作れます。座標の更新(Lerp)
移動の要、『Lerp (Vector2D)』 ノードの出番です。A:出発地点(StartPos)
B:目的地(EndPos)
Alpha:
先ほど計算した進行度 この結果を 『Set Position In Viewport』 に繋げば、毎フレーム滑らかにカードが吸い込まれていく演出が完成します。
到着と自爆(Remove)
Alpha が 1.0(目的地に到着)になったら、『Remove from Parent』 を実行して自分自身を消去します。
これでメモリにも優しい設計になります。

STEP4 : 司令塔(親ウィジェット)での座標計算
「どこから、どこへ飛ばすか」の指示は、
すべてのウィジェットの座標を把握している親ウィジェット(WBP_Collection_View)の役目です。
① 目的地の取得(EndPos)
デッキリスト(TileView_Deck)を変数として取得し、
Get Cached Geometry ➔ Local to Viewport
のコンボで画面上の絶対座標(Pixel Position)を割り出します。

② ダミーの生成 算出した EndPos と、子から送られてきた StartPos を Create Widget ノードに放り込み、Add to Viewport で放流します!

STEP 5:子ウィジェットから座標を送信する
最後に、クリックされたカード(子ウィジェット)自身が、親ウィジェットに対して「今、私はここにいるよ!」と現在地を伝える処理を追加します。
これが飛行アニメーションのスタート地点になります。
親側に「受け皿」を作る
親ウィジェットの AddSpellCardToDeck イベントに、新しく StartPos(Vector 2D)の引数を追加しておきます。自分の位置を計算する
カード側のグラフ(OnMouseButtonUp など)で、『Get Cached Geometry』 ➔ 『Local to Viewport』 を呼び出します。
※ターゲットは Self(自分自身) にします。
これで「そのカード自身の座標」が取得できます。座標を添えて親を呼ぶ
計算して出てきた Pixel Position を、親のイベントの Start Pos ピンに繋いで実行します!

テスト実行
それでは、いよいよ実際に動かしてみます!
(……と言いつつ、実際は少し組んでは動かし、微調整を繰り返しているので、大丈夫なはず!🤔)
いざ、尋常に……勝負!!!
■デッキ追加・削除動画
結果は……完璧です😎👍
クリックやホイール操作でカードが吸い込まれるようにデッキへ飛び、C++のロジックによって瞬時にレアリティ順に並び変わります。
0枚になればリストから消える挙動もバッチリ。
これで、デッキビルドシステムの核となる「追加・削除」の基本処理はすべて完了しました!🎉
今後は、ここへ『HorizonNotes』固有の細かいルール(特定のカードの枚数制限など)を乗せたり、
投入不可なカードのグレーアウト処理といった微調整を加えていく予定です。
まとめ
今回は、一気に色んな機能を追加したので少しややこしい記事になってしまったかもですね🙏
ただ、機能作業しながら考えたのですがこうやって手順を明文化すれば
読者の中のUE使いの備忘録になるのはもちろんのこと
「この記事そのものを生成AIに読み込ませれば、開発状況の完璧な引き継ぎ資料になるのでは?🤔」
という考えに至りました。
実際、AIと対話しながら開発を進める現代において、
「何をしたか」の記録は未来の自分(とAI)への最高のアシストになります。
なので、これからも「ここは語る価値がある!」と思った部分については、詳しく掲載していくつもりです🦍🍀
(とはいえ、手順を文章化するのはなかなかにカロリーを使う作業なので……。
あくまで「余力がある時の努力目標」として、マイペースに続けていきます😅)
もしこの記事が「参考になった」「開発の苦労が伝わった!」「続きが気になる!」と思ったら、
記事の下にある「スキ(ハートマーク)」を押していただけると嬉しいです!
皆様の応援こそが、毎日の開発と更新の何よりの励みになります。
最後までお読みいただき、ありがとうございました!
また次回の記事でお会いしましょう!
#UE5 #UnrealEngine #ゲーム開発 #個人開発 #インディーゲーム #ゲームUI #UI実装 #ブループリント #UMG #TileView #デッキ構築 #自作TCG #HorizonNotes #開発日誌 #AI活用 #プログラミング #UE5C ++ #スキしてみて
いいなと思ったら応援しよう!
よろしければ応援お願いします!
いただいたチップはクリエイターとしてのゲーム開発に使わせていただきます!
※チップと共にやって欲しい事があったら言ってください!
可能な範囲であれば頑張ります