薬草を拾うと9個まで増え、10個目は持てない。カバン全体が満杯になっても、薬草を使えば、そのぶん別のアイテムを入れられる。今回は、そんな持ち物の仕組みを作ります。
インベントリ は、「何を、いくつ持っているか」を管理する仕組みです。アイテムの説明、個数の増減、画面表示を一度に作ろうとすると、どこを直せばよいか見えにくくなります。まずは、薬草の性質と、自分が持つ薬草の数を分けるところから始めましょう。
この記事でわかること
- アイテムの定義と、プレイヤーごとの所持数を分ける
- 種類ごとの上限と、カバン全体の容量を守って増減する
- 薬草を1個使い、CharacterのHPを回復する
- 所持数が変わったときだけ、簡単な持ち物表示を更新する
Blueprintの変数・関数・Branchを使える人向けの実践です。Data Asset、Map、Actor Componentを組み合わせます。Third Personテンプレートの1人プレイで、キー入力を「拾う」操作の代わりに使います。
薬草の性質と、持っている数を分ける
薬草を3個持つ人も、9個持つ人も、1個使ったときの回復量は同じです。この「薬草とは何か」という共通の情報を 定義 と呼び、Data Assetへ置きます。
一方、「自分はいま3個持っている」は、そのプレイヤーだけの 所持状態 です。こちらはカバン側で管理します。
| 分けるもの | 薬草の例 | 置き場所 |
|---|---|---|
| 定義 | 表示名は薬草、回復量20、上限9個 | DA_Herb |
| 所持状態 | DA_Herbを3個持っている | Inventory ComponentのItems |

分けておくと、回復量を20から25へ調整するときは、定義を1か所直せます。所持品ごとに回復量のコピーを持たせていると、そちらも更新する必要があります。今回の所持データには、どの定義を使うかという参照と、個数 を入れます。
今回は、1種類につき1つの個数を持つ
Map は、「調べるためのキー」と「対応する値」を組にして持つ入れ物です。今回は、アイテム定義をキー、所持数を値にします。DA_Herbを渡せば、薬草の個数を取り出せます。

| 欲しいカバン | 扱いやすい形 |
|---|---|
| 薬草9個、鍵1個のように、種類ごとの合計だけ分かればよい | 今回のMap |
| 薬草を「9個の枠」と「1個の枠」に分けたい | スロットの配列 |
| 同じ剣でも耐久値72と13を区別したい | 個体の情報を持つ構造体の配列など |
スタック は、同じ種類をまとめて持つことです。今回はMapなので、薬草は合計9個で上限とし、10個目を別枠へ回しません。また、容量15は「15種類」「15枠」ではなく、全種類を合わせて15個 です 。このルールを先に決めてから作ります。
準備:4種類の定義とカバンを作る
1. アイテム定義を作る
Third PersonのBlueprintプロジェクトを使います。Variantを選べる版ではNoneを選び、標準のBP_ThirdPersonCharacterで進めます。
コンテンツブラウザで右クリックし、「Blueprint Class → All Classes」からPrimary Data Assetを親にして、BPDA_ItemDefinition を作ります。次の変数を追加し、すべてInstance Editableを有効、Privateを無効にして、コンパイル・保存します。
| 変数名 | 型 | 初期値 |
|---|---|---|
| DisplayName | Text | 空欄 |
| MaxStack | Integer | 1 |
| HealAmount | Float | 0.0 |
続いて「Miscellaneous → Data Asset」でBPDA_ItemDefinitionを選び、4枚作ります。Blueprintのクラスを複製する操作ではなく、値を入れるデータを作る操作です。
| アセット名 | DisplayName | MaxStack | HealAmount |
|---|---|---|---|
| DA_Herb | 薬草 | 9 | 20.0 |
| DA_Potion | ポーション | 5 | 50.0 |
| DA_Key | 鍵 | 1 | 0.0 |
| DA_Bomb | 爆弾 | 3 | 0.0 |

今回は名前と個数を表示するので、アイコン画像の準備は不要です。データの型と実データの違いは、Data Assetの記事でも説明しています。
2. 所持数を管理するComponentを作る
Actor Componentを親に BP_InventoryComponent を作ります。ComponentはActorへ付け足せる機能の部品です。カバン自体の位置は必要ないので、Scene ComponentではなくActor Componentを使います。
| 用意するもの | 名前 | 設定 |
|---|---|---|
| 変数 | Items | キーがBPDA_ItemDefinitionのObject Reference、値がIntegerのMap。初期値は空。Privateを有効 |
| 変数 | MaxCapacity | Integer、初期値15。Instance Editableを有効 |
| Event Dispatcher | OnInventoryChanged | 入力なし |
Itemsの型をBPDA_ItemDefinitionの Object Reference にし、コンテナをMapへ切り替え、右側の値の型をIntegerにします。Class Referenceを選ばないようにしてください。

Event Dispatcher は、登録した相手へ「変わりました」と知らせる仕組みです。「My Blueprint」のEvent DispatchersからOnInventoryChangedを追加します。通知を受け取る表示側は後で作ります。
Itemsは外から直接書き換えず、これから作るAddItemとRemoveItemを通して変更します。上限の確認や通知を、この2つの入口へまとめるためです。
現在の個数を調べる
先に、Componentの中へ3つの関数を作ります。後の追加処理とUIが、この関数を使って現在値を調べます。
GetCount:1種類の個数
関数 GetCount に、入力Item(BPDA_ItemDefinitionのObject Reference)、出力Count(Integer)を追加し、Pureを有効にします。
ItemsのGetからMap用の Find を作り、Keyへ入力のItem、ValueからReturn NodeのCountへつなぎます。FindのBooleanのReturn Valueは今回は使いません。指定したキーがなければ、IntegerのValueは既定値の0になります。

GetCountは値を調べるだけの Pure(純粋)関数 なので、呼び出す側に白い実行ピンはありません。Findも同じです。
GetKindCount:持っている種類数
関数 GetKindCount は入力なし、出力Kinds(Integer)で、Pureを有効にします。ItemsのGetからMap用の Length を作り、その出力をReturn NodeのKindsへつなぎます。
これは個数の合計ではありません。薬草9個、ポーション5個、鍵1個なら、Mapの項目は3つなので、種類数は3です。
GetTotalCount:カバン全体の個数
関数 GetTotalCount は入力なし、出力Total(Integer)で、Pureは無効にします。関数内だけで使う ローカル変数 として、Sum(Integer)を作ります。
白い線を、関数入口→Set Sum=0→Map用のValues→For Each Loopへつなぎます。ValuesのTarget MapはItems、配列出力ValuesはFor Each LoopのArrayへ渡します。

Loop BodyからSet Sumへつなぎ、「SumのGet+Array Element」を代入します。CompletedからReturn Nodeへつなぎ、TotalへSumを渡します。
ValuesはMapの値を配列へ取り出すノードで、白い実行ピンがあります。9、5、1を順番に足し、ループが終わってから15を返します。毎回最初にSumを0へ戻すこと、ReturnをLoop Bodyへつながないことを確認してください。
コンパイルして保存します。これで、所持数を外へ見せる入口ができました。
追加する:入る分を確かめてから書き換える
関数 AddItem を作ります。入力はItem(BPDA_ItemDefinitionのObject Reference)とAmount(Integer)、出力はSuccess(Boolean)です。Pureは無効にし、ローカル変数CurrentCount(Integer)を追加します。
Amountは「今回入れたい個数」です。今回は、全部入るなら追加し、入らないなら1個も追加しないルールにします。
1. アイテムと個数を確かめる
関数入口から、実行ピンのある Is Valid へつなぎ、Input ObjectへItemを渡します。Is Not Validは「アイテムが未指定です」とPrint Textで表示し、Return Nodeへ進めてSuccess=falseにします。
Is Valid側からBranchへ進み、Conditionを Amount <= 0 にします。Trueは「個数は1以上にしてください」と表示してSuccess=falseで返します。FalseからSet CurrentCountへ進み、GetCount(Item)のCountを代入します。

Is Valid は、指定した相手を使えるか調べる入口です。ここを通ってから定義のMaxStackを読みます。0個や負数は追加・削除として受け付けず、所持数を変えません。
以後の失敗経路も、Print Text→Return Node(Success=false)で終えます。Return Nodeは関数の結果を返して終了するノードです。失敗箇所ごとに置いて構いません。
この記事で使うPrint Textは、Print to ScreenとPrint to Logを有効、Duration=5、Key=Noneにそろえます。失敗した理由を画面とログの両方で確認するためです。
2. 種類ごとの上限と、全体の容量を調べる
Set CurrentCountの後にBranchを置き、Conditionを Amount > ItemのMaxStack - CurrentCount にします。Trueは「これ以上重ねられません」と表示してfalseで返します。
Itemの青いピンからドラッグしてGet Max Stackを作ると、その定義の上限を読めます。そこからCurrentCountを引き、Amountとの比較結果をBranchへ渡します。
FalseからGetTotalCount(Target=Self)を呼び、その白い出力から次のBranchへ進みます。今度のConditionは Amount > MaxCapacity - Total です。Trueは「カバンがいっぱいです」と表示してfalseで返します。

「上限から現在数を引く」と、あと何個入るかが分かります。薬草8個なら残りは 9 - 8 = 1。1個の追加は通りますが、2個の追加は断ります。カバン全体も、15個入っていれば残り0です。
比較は > です。残り1個に1個入れる操作まで断る >= にしないようにします。
3. 現在数に足して、Mapへ入れ直す
2つ目のBranchのFalseから、Map用のAdd→Call OnInventoryChanged→Return Nodeの順につなぎ、最後のSuccessをtrueにします。
Addの入力はTarget Map=Items、Key=Item、Value=CurrentCount + Amount です。

Mapの Addは、同じキーがあれば値を上書きする操作 です。1を渡せば常に1個になってしまいます。現在数を調べ、足した結果を書き戻すことで、1→2→3と増やせます。
減らす:0個になった種類は消す
関数 RemoveItem も、入力Item・Amount、出力Successで作り、Pureは無効にします。ローカル変数CurrentCountとNewCountをIntegerで作ります。
最初のIs ValidとAmount<=0の確認はAddItemと同じです。失敗時のメッセージとfalseのReturnも同じにします。通ったら、GetCount(Item)の結果をCurrentCountへ代入します。
次のBranchで CurrentCount < Amount を調べます。Trueは「持っていません」と表示してfalseで返します。Falseから NewCount = CurrentCount - Amount をSetし、もう一つのBranchで NewCount == 0 を調べます。

| 分岐 | Mapの操作 | その後 |
|---|---|---|
| True:0個になった | Remove。Target Map=Items、Key=Item | Call OnInventoryChanged→Return(Success=true) |
| False:まだ残っている | Add。Target Map=Items、Key=Item、Value=NewCount | Call OnInventoryChanged→Return(Success=true) |
通知とReturnは、それぞれの枝に置いて構いません。RemoveのBooleanの戻り値は使いません。
0個のキーを消すと、GetKindCountが「実際に持っている種類数」を返せます。薬草を使い切れば、薬草・ポーション・鍵の3種類から、ポーション・鍵の2種類になります。
実践:拾って、薬草を使う
1. Characterへカバンを付ける
BP_ThirdPersonCharacterを開き、ComponentsのAddからBP_InventoryComponentを追加し、名前を Inventory にします。選択したComponentのMaxCapacityが15になっていることを確認します。
CharacterにはCurrentHealth(Float、初期値50.0)とMaxHealth(Float、100.0)を追加します。さらにHerbDefinition(BPDA_ItemDefinitionのObject Reference)を作り、コンパイル後、既定値へDA_Herbを指定します。
2. キー入力で拾う
イベントグラフへキーボードの 1 を追加します。Components欄のInventoryをグラフへドラッグして参照を出し、そこからAddItemを作ります。Pressedを白い実行入力へつなぎ、Item=DA_Herb、Amount=1にします。

同じ形で、2・3・4キーも作ります。Itemの欄では、文字を打つ代わりに対象アセットを選びます。Successはこの段階では接続不要です。失敗時はComponent側が理由を表示します。
| キー | Item | Amount |
|---|---|---|
| 1 | DA_Herb | 1 |
| 2 | DA_Potion | 1 |
| 3 | DA_Key | 1 |
| 4 | DA_Bomb | 1 |
キー入力は拾う処理を確かめるための仮操作です。床のアイテムへ触れた場合も、同じAddItemを呼び、Success=trueのときだけ床のActorを消す形へつなげられます。
3. 1個減らせたときだけ、HPを回復する
Characterへキーボードの Q を追加し、PressedからInventoryのRemoveItemを呼びます。ItemへHerbDefinitionのGet、Amountへ1を渡します。
白い出力からBranchへ進み、ConditionへSuccessをつなぎます。Falseは終了。TrueだけSet CurrentHealthへ進めます。

Setする値は、CurrentHealth + HerbDefinitionのHealAmount を Clamp (Float) へ通した結果です。Min=0、MaxへMaxHealthを渡します。Clampは、計算結果を指定範囲に収める処理です。90に20を足しても、上限100で止まります。

Setの後にFormat Textで HP: {HP} を作り、HPへCurrentHealthを入れ、Print Textへ渡します。
今回は HPが満タンでも薬草を消費する ルールで、0個になるまでの動きを確認します。満タン時に使わせたくなったら、RemoveItemの前にCurrentHealth<MaxHealthを調べる分岐を追加します。
回復をCharacterへ置いたことで、Inventory Componentは特定のCharacterへCastする必要がありません。宝箱へ付けた場合も、同じ追加・削除の処理を使えます。
変更があったときだけ表示を更新する
最後に、名前と個数だけの小さなUIを付けま す。4種類を固定順で表示し、持っていない種類は0と表示します。アイコンを並べる前に、所持数と表示が一致すること を確かめるための画面です。
今回は表示名をUIへ直接書くので、定義のDisplayNameを変えても、この画面の名前は変わりません。
1. 表示用Widgetを用意する
User Widgetを親に WBP_InventorySummary を作ります。DesignerにCanvas Panelを置き、その子へTextを追加し、名前を SummaryText、Is Variableを有効にします。左上基準でPosition=(40,40)、Size=(600,220)、Font Size=22を目安にします。TextのBindは設定しません。
WidgetへInventoryRef(BP_InventoryComponentのObject Reference)を作り、Instance EditableとExpose on Spawnを有効にします。Expose on Spawn は、Widgetを作るノードに、この値を渡す入力欄を出す設定です。

2. RefreshInventoryで現在値を表示する
Widgetに関数 RefreshInventory を作ります。入力・出力なし、Pureは無効です。入口から実行ピン付きIs ValidでInventoryRefを確認し、Is Valid側だけGetTotalCountへ進めます。TargetはInventoryRefです。
Format Textへ次の文字を入力します。改行は入力欄で Shift + Enter を使えます。
薬草: {Herb} / ポーション: {Potion}
鍵: {Key} / 爆弾: {Bomb}
合計: {Total} / 種類: {Kinds}
Herb・Potion・Key・Bombには、InventoryRefをTargetにしたGetCountをそれぞれつなぎ、Itemへ対応するDA_Herbなどを指定します。TotalはGetTotalCountの出力、KindsはGetKindCount(Target=InventoryRef)の出力です。
GetTotalCountの白い出力からSetText (Text)へつなぎます。TargetへSummaryTextのGet、In TextへFormat TextのResultを渡します。

3. 変更通知を受け取り、初回も表示する
コンパイルしたら、イベントグラフのConstructからIs Valid(InventoryRef)へ進み、有効側からBind Event to OnInventoryChangedを呼びます。TargetはInventoryRefです。
赤いEvent入力から Create Event を作り、Object=Self、関数の選択欄をRefreshInventoryにします。これで、通知が届いたときに呼ぶ処理を指定できます。Bindの白い出力から、RefreshInventory(Target=Self)を1回呼びます。

Bind は、通知先を登録する操作です。登録しただけでは過去の変更は届かないので、初回の表示は自分で呼びます。その後はAddItem・RemoveItemが通知するたびに更新されます。
DestructからもIs Valid(InventoryRef)を通し、有効ならUnbind Event from OnInventoryChangedを呼びます。同じInventoryRefと、同じSelf/RefreshInventoryを指定したCreate Eventを渡してください。表示を外した後の不要な購読を終えるためで、ほかの購読まで外すUnbind Allは使いません。
4. Characterから画面へ追加する
BP_ThirdPersonCharacterのBeginPlayへCreate Widgetをつなぎ、Class=WBP_InventorySummary、Owning Player=Get Player Controller(Player Index=0)、Inventory RefへComponents欄から出したInventoryの参照を渡します。既存のBeginPlay処理がある場合は残し、その後へつなぎます。
Create Widgetの白い出力からAdd to Viewportへつなぎ、TargetへReturn Valueを渡します。これで、キャラクターが使っているカバンと、画面が読むカバンが同じになります。
UIのTickで作り直す処理はありません。表示を詳しく作るときも、データはComponentが持ち、UIは現在値を読む関係を保てます。Event Dispatcherの記事とUMGの記事も参考になります。
確認:満杯のカバンに、また入る
すべてコンパイル・保存し、Playします。画面をクリックしてキー入力を受け取れる状態にし、次の順で試してください。
| 操作 | 確認する結果 |
|---|---|
| 開始直後 | 4種類とも0、合計0、種類0 |
| 1を9回 | 薬草9、合計9、種類1 |
| 1をもう1回 | 「これ以上重ねられません」。数は変わらない |
| 2を5回、3を1回 | 薬草9・ポーション5・鍵1、合計15、種類3 |
| 4 | 「カバンがいっぱいです」。爆弾0のまま |
| Q | 薬草8、合計14、種類3。HPは50→70 |
| Qをさらに2回 | 薬草6、 合計12。HPは90→100 |
| Qをさらに6回 | 薬草0、合計6、種類2。HPは100のまま |
| Qをもう1回 | 「持っていません」。数もHPも変わらない |
| 4 | 今度は爆弾1を持てる。合計7、種類3 |

満杯だったカバンが、使ったぶんだけまた入るようになる。 ここまで通れば、上限の判定・削除・表示更新が、同じ現在値を使ってつながっています。
境界も少し試してみる
Playを止め、1キーのAmountを2へ変えて再開します。4回押すと薬草8個になり、5回目は断られて8個のままです。一部だけ入れるルールではないので、9個にはなりません。確認後はAmount=1へ戻します。
Amount=0や-1でも数が変わらず、個数の確認メッセージが出ることを試せます。最後に1へ戻してください。
うまく動かないとき
| 症状 | 確認するところ |
|---|---|
| 薬草が常に1個 | AddへCurrentCount+Amountを渡したか |
| 9個目が入らない | 残り数との比較が > になっているか |
| 合計数がおかしい | Valuesの各値を足したか。Sumを毎回0へ戻したか。Completedから返しているか |
| 薬草を使い切っても種類3 | 0の枝でMapのRemoveを呼んだか |
| 回復しない | HerbDefinitionがDA_Herbか。RemoveItemのSuccessで分岐 しているか |
| UIが最初から空欄 | Create WidgetのInventory Ref、Construct後の初回Refresh、SummaryTextのIs Variable |
| UIだけ数が古い | 変更後のCall OnInventoryChanged、BindのTargetとEventを確認 |
| キーが反応しない | 標準Characterが操作対象か、ゲーム画面に入力フォーカスがあるか |
おまけ:自分のゲームへ広げる
同じ名前でも、別のアセットは別種類
Mapのキーは表示名ではなく、DA_Herbというアセットへの参照です。複製したDA_Herb2は、画面に同じ「薬草」と表示されても別のキーになります。試すなら、カバンが空の状態で両方を1個ずつ追加すると、種類数は2になります。
Map自体の並び順も表示順として当てにしません。今回のUIは薬草・ポーション・鍵・爆弾の順を明示しています。種類が増えたら、表示順を決める定義の配列を別に持つ方法があります。
個体差やスロットが必要になったら
剣1本ずつの耐久値や、アイテムを置いた枠の位置も残すなら、スロットを表す構造体の配列へ広げます。コピーとして取り出した構造体を変更する場合は、元の配列への書き戻しも必要です。構造体の記事で確認できます。
重量制にする場合も、追加前に空きを調べる考え方は同じです。ただし、定義へWeightを足し、合計を「個数×重さ」で求め、上限や比較も重さの単位へそろえる必要があります。
レベル移動と、次回の起動へ残す
今回のInventoryはCharacterに付いているので、Characterが作り直されれば中身も初期状態になります。レベル移動で持ち越すには、GameInstanceへ必要な所持データを預けます。
次回の起動にも残すなら、アイテムIDと個数をSave Gameへ書き出し、ロード時にIDから定義を引き直す形にできます。Data Assetへの参照を記録することと、アセットの内容そのものをセーブへ複製することは別です。IDを使う場合も、IDと定義の対応を用意する処理が必要になります。
今回できたのは、出し入れのルールをComponentへまとめたカバンです。拾うきっかけやUIを変えるときも、AddItemとRemoveItemを通して所持数を変えることで、上限確認と変更通知を共通に使えます。
まとめ
- アイテムの「定義」と、プレイヤーの「所持数」は別に持つ
- 増やす前に、種類ごとの上限とカバンの容量を確かめる
- 0個になった種類は、Mapから消して残さない
- 表示の更新は、数が変わったときだけ呼ぶ
作るときの問いかけは、「これは全員で同じ値か、その人だけの値か」 です。同じならData Asset、その人だけならComponentへ持たせます。
データの持ち方は Array・Map・Set、定義側は Data Asset で扱っています。