
【UE5】同じ時を刻んで同じ未来信じてるふたり...『プログレス』バーでマナカーブを作ってみた。【60回目】
★今回やったこと
・マナカーブ実装した!!!
こんばんは、「まる。」です!
カードゲームのデッキ構築において、
カードのコストバランスは生命線ですよね。
ということで今回は、前回作成したデッキ編集機能を発展させて、
『入れたカードのコスト帯が一目で確認できるマナカーブ』
を実装しました!📉
▼前回の開発記事はこちら
今回はUE使い向けに解説を入れてみました!(ややこしかったらごめんなさい🙇♂️)
TCG(トレーディングカードゲーム)らしさがグッと増した画面をサクッと紹介させていただきます😎
1.マナカーブって何?
簡単に言うと、デッキに入れたカードの「消費コスト(マナ)」が、
どのような割合で分布しているかを一目で確認できるグラフのことです。
MTGやデュエル・マスターズなど、
多くのカードゲームを遊んだことがある方ならご存知の通り、
強力なカードほど発動に必要な燃料(マナ)が多くなります。

もちろん、『HorizonNotes』も基本的にはそのルールに則っています。
さらに本作では、「変換したエーテル(マナ)をそのまま供給できる」という独自のシステムがあるため、
他のTCG以上にマナのやり繰り・管理が超重要になってきます。
デッキを組むときに、
「変換用に重いコストのカードをこれくらい入れて、あとは小回りが利くように軽いカードを多めにして…」
といった戦略的なバランス調整を直感的に行えるようにするために、
このマナカーブ画面が絶対に必要だったわけです!
2.C++でデッキ内の各コストの分布を取得する
まずは、編集中のデッキ内に「どのコストのカードが何枚あるか」
を計算して取得する関数を作成します。
💡どうして、C++で作るの?
UEを使っている方の中には、
「なんでわざわざBP(ブループリント)じゃなくて一からC++で作ってるの?」
と疑問に思う方もいらっしゃるかもしれません。
私は基本的に、
「取り扱うデータの種類」や「処理の複雑さ」によってC++とBPを使い分けるようにしています。
今回集計する「スペルカードの情報」は、
もともとC++で定義した構造体やPDA(プライマリデータアセット)の中に格納されています。
これをわざわざBP側に一つずつ引っ張り出して処理するよりも、
データの大元があるC++側で一気に計算してしまった方が、
ロジックとして圧倒的にスマートですし、
処理スピードも段違いに速くなるという寸法です。

ステップ1:集計用構造体(Struct)の定義
まずは、C++で集計したデータをブループリント(UI)へ渡すための「箱」を作ります。
(今回もC++のコード作成自体は、
Gemini先生やClaudeさんにお願いしています。
私はあくまで中身をチェックするディレクター係です😎)
ここで大事なポイントが「配列の要素数」です。
今回のUIでは、「0, 1, 2, 3, 4, 5, 6以上」とそれぞれのコスト帯の枚数を個別に計算してグラフ化するのが目的です。
全部で7つの段階に分かれているため、
配列の数は「7」で固定してあげます。
これを、デッキシステムを司るC++のヘッダーファイルに追加します。
C++
// DeckBuildLibrary.h に追加
USTRUCT(BlueprintType)
struct FDeckAnalytics
{
GENERATED_BODY()
// コスト帯ごとの枚数(Index 0 = 0コスト, Index 6 = 6コスト以上)
UPROPERTY(BlueprintReadWrite, Category = "Analytics")
TArray<int32> ManaCurve;
// クラス(タグ)ごとの枚数
UPROPERTY(BlueprintReadWrite, Category = "Analytics")
TMap<FGameplayTag, int32> ClassDistribution;
FDeckAnalytics()
{
// 0コスト〜6コスト以上の「7段階」として初期化
ManaCurve.Init(0, 7);
}
};
ステップ2:集計関数の定義と実装
次に、デッキ情報を受け取って集計結果を返す関数 CalculateDeckAnalytics を作ります。
この関数は、先ほど作った FDeckAnalytics 型のデータとして結果を返してくれます。
これも、先ほど同様ヘッダーファイル(入れ物)に追加します。
💡 ちょっと解説:型(Type)って何?
プログラムの世界では、データの種類(型)をきっちり決める必要があります。
・整数(Int):
数字として扱うので、足し算や引き算などの計算ができる!
・文字列(String):
文字として扱うので、画面に表示したり文章を繋げたりできる!
文字列で「1」と入力しても計算はできませんし、
整数の箱に「あ」という文字を入れるとエラーになってしまいます。
今回のポイントは、先ほど自分で作った DeckAnalytics という構造体を「オリジナルの型」として使っているところです!
(※UEのC++ルールに従って、頭に『F』をつけて FDeckAnalytics と呼んでいます)

ポラちゃん、可愛い😊
C++
/** デッキ内のマナカーブとクラス分布を集計して返します */
UFUNCTION(BlueprintCallable, Category = "DeckBuild|Analytics")
static FDeckAnalytics CalculateDeckAnalytics(const FPlayerDeck& Deck);
STEP 3 : ソースファイル (DeckBuildLibrary.cpp) の実装
入れ物が出来たので、次はいよいよ実際に動かす部分(ソースファイル)を作成します。
細かい部分をすべて解説すると大変なので…
今回は一番のポイントは
FMath::Clamp を使って「コスト6以上」をひとまとめにしている点です。
💡そもそも、FMath::Clampって何?
数値を「指定した範囲内」に強制的に収めてくれる便利な機能(関数)です。
今回は、コストが6以上のカードは一律で同じ「6+」の枠として計算するようにしています。
(コスト7、8、9…とグラフの枠を作り続けると、
画面がいくらあっても足りないですし、プレイヤーも直感的に見づらくなってしまいます😅)
そのため、ここで予め「最大でも6として扱う」と制限をかけておきます。
こうすれば実際に取り扱うデータがぴったり7種類(0〜6)に限定されるので、
後からUIに表示する処理が格段に楽になります。

C++
#include "Utility/HorizonGameplayTags.h"
FDeckAnalytics UDeckBuildLibrary::CalculateDeckAnalytics(const FPlayerDeck& Deck)
{
FDeckAnalytics Analytics;
UAssetManager* AssetManager = UAssetManager::GetIfInitialized();
if (!AssetManager)
{
return Analytics;
}
for (const auto& Pair : Deck.SpellCardList)
{
FName CardID = Pair.Key;
int32 CardCount = Pair.Value;
FPrimaryAssetId AssetId(FPrimaryAssetType(TEXT("SpellCard")), CardID);
// 事前ロード済みなので TryLoad で高速に取得
if (UObject* LoadedObj = AssetManager->GetPrimaryAssetPath(AssetId).TryLoad())
{
if (USpellCardDataAsset* PDA = Cast<USpellCardDataAsset>(LoadedObj))
{
// ---------------------------------------------
// 1. マナカーブの集計
// ---------------------------------------------
int32 BaseCost = 0;
for (const FDataTableRowHandle& Handle : PDA->Data.CostTypeHandles)
{
if (Handle.IsNull()) continue;
FCardCost* Row = Handle.GetRow<FCardCost>(TEXT("AnalyticsCostLookup"));
if (Row && Row->ResourceTag == HorizonTags::Stat_Cost)
{
BaseCost = Row->Amount;
break;
}
}
// コストが6以上の場合はすべてIndex 6(上限)にまとめる
int32 CostIndex = FMath::Clamp(BaseCost, 0, 6);
Analytics.ManaCurve[CostIndex] += CardCount;
// ---------------------------------------------
// 2. クラス分布の集計
// ---------------------------------------------
if (PDA->Data.RequiredClasses.IsEmpty())
{
Analytics.ClassDistribution.FindOrAdd(HorizonTags::Class_Neutral) += CardCount;
}
else
{
for (const FGameplayTag& ClassTag : PDA->Data.RequiredClasses)
{
Analytics.ClassDistribution.FindOrAdd(ClassTag) += CardCount;
}
}
}
}
}
return Analytics;
}
これでC++側の準備は完了です。
3.BP側で取得したマナカーブをプログレスバーに反映させる
C++のコンパイルが成功したら、
デッキ編成画面のウィジェットを開きます。
ここで、今回の主役になる今回の主役になる「プログレスバー」というUIパーツについて、ちょっとした補足をします。

💡プログレスバーって何?
ざっくり言うと、中身が上下する「棒グラフ(ゲージ)」です。
UEでは、このゲージの伸び具合を
「Float(浮動小数点数=小数のデータ)」を使って制御します。
具体的には、
最小値を 0.0 (0%)、最大値を 1.0 (100%) として値を入力します。
■値が0.0の場合(空っぽ)

■値が0.5の場合(ちょうど半分)

STEP1 : プログレスバーや枚数表示のテキストを配列としてセットする
まずは、画面に配置した7つのプログレスバーと、
枚数を表示するテキストを、それぞれ「配列変数」にまとめて格納する準備をします。
(これは、後の処理を効率的に行う為です。)
①.配列変数を用意する
変数名: ManaCurveBars (型: ProgressBar のオブジェクト参照 / 配列にする)
変数名: ManaCurveTexts (型: テキスト のオブジェクト参照 / 配列にする)

②.Event Construct(初期化)での登録
Event Construct(ウィジェットが画面に表示された時に最初に呼ばれる処理)に Make Array(配列の作成) ノードを繋ぎます。
ここで先ほど配置したUIパーツを順番(0コスト〜6コスト以上)にピンに繋ぎ、作成した配列変数にセットしておきます。

STEP2 : 更新用関数(UpdateManaCurveUI)の実装
次に、C++から構造体(FDeckAnalytics)を受け取ったあとの処理をBPに書いていきます。
全体像はざっくりこんな感じです。(ちょっとむずいかも…)
構造体をBreak(分解)し、
ManaCurve配列から Max (Int Array) ノードを繋いで、
一番枚数が多いコスト帯の「最大値(Max)」を取得します。Branchで「Max > 0」かを判定します
(デッキが0枚のときの「エラー」を回避するためです)。Trueなら、事前に用意した ManaCurveBars(プログレスバーの配列) に対して For Each Loop を回します。
【ループ内で行う更新処理】
プログレスバーの更新:
ManaCurve 配列から現在の Index で枚数を取得してFloatに変換。
それを最大値(Max)で割り算し、
その結果を ProgressBar の Set Percent に繋ぎます。テキストの更新:
ManaCurveTexts 配列から対象のテキストを引っ張り出し、
Set Text に枚数を渡して表示を更新します。

ここでの一番の工夫は「割合(Percent)の計算」です。
まず、ManaCurve配列の中から「一番枚数が多いコスト帯(Max)」を見つけます。
そして、各コストの枚数をこのMaxで割り算します。
こうすることで、
「一番枚数が多いコストのゲージがぴったり1.0(100%)」になり、
他のコスト帯もそれに合わせて綺麗な山なりのグラフになってくれます。
💡今日のハイライト!プログレスバーの操作には「Set Percent」!
プログレスバーのゲージを動かす時は、Set Percent ノードを使います。
操作したいプログレスバーをターゲットに繋ぎ、
In Percent のピンに Float(0.0〜1.0の小数)を渡してあげることで、
ゲージの長さを自由に操作することができます!
STEP3:更新用カスタムイベントを作って完成!
いよいよ最後の仕上げです。
これまで作った機能をガッチャンコします。

冒頭で作成したC++の集計ノードを呼び出して、
その結果を先ほどのUI更新ノードに繋ぐだけ。
あとは、このイベントを「デッキにカードを追加・削除した時」に呼ばれるように間に挟んであげれば……
カードを出し入れするたびにぬるぬる動く、マナカーブUIの完成です👍
テスト実行
それでは、早速実行してみます。
■結果は…
うん、上手く行きましたね😎
枚数に応じてテキストやバーが変わってくれていることが確認できました。
これで、マナカーブの実装完了です🎉🎉
あとは「クラスごとの枚数」も表示させるようにしたいところですが……
実装手順は今回のマナカーブとほぼ同じになるはずなので、
そちらは裏でサクッとやっておきます!
まとめ
今回はマナカーブの実装をお届けしましたが、いかがでしたか?
グラフが動くと、デッキ編成画面も一気に本格的になってきましたね。
まだまだ細かい調整は必要ですが、
個人的にも順調な手応えを感じています👍
次回はいよいよ「デッキの保存・ロード処理」!
……と行きたいところなのですが、
まずは今の画面の操作感などを裏でじっくり仕上げようと思います。
そのため、システム開発のログは少しお休みして、
次からは例の
「noteの皆さんと一緒にゲームを作っていく企画」
の記事をメインにお届けしていく予定です!
皆さんとワイワイ進める企画、ぜひご期待ください!
もしこの記事が「参考になった」「開発の苦労が伝わった!」「続きが気になる!」と思ったら、
記事の下にある「スキ(ハートマーク)」を押していただけると嬉しいです!
皆様の応援こそが、毎日の開発と更新の何よりの励みになります。
最後までお読みいただき、ありがとうございました!
また次回の記事でお会いしましょう!
#プログレスバー #UIデザイン #データ駆動 #カードゲーム #デッキ構築型 #HorizonNotes