やあ
UE4 C++でインベントリシステムを作ろう!
Part.5では、装備UIと装備の着脱をやるよ。
前回
Part.5の結果
目次
概要
①プレイヤー : 装備をスロットにドロップ
②装備UIスロット : ドロップされた!イベント発生
③装備UI : ドロップされたスロットから、プレイヤーのどこに装備をアタッチするのかを決める
④プレイヤー : 装備をアタッチ
⑤終わり
装備UIの動作を考える
・スロットの情報に加え、どのスロットがプレイヤーのどのソケットかの情報の保持
これだけ!
装備UIの動作の実装
こんな感じにするよ。
InventoryWidgetBase
クラスを継承して、EquipmentWidget
クラスを作成
装備の情報を保持する
MyEnums.h
に追加
UENUM(Blueprintable) enum class EEquipmentType : uint8 { None = 0, Head = 1, Armor = 5, Shoulders = 6, Gloves = 8, Shoes = 9, WeaponLeft = 12, WeaponRight = 14, BothHandsWeapon = 15, Accessory = 16, }; ENUM_CLASS_FLAGS(EEquipmentType)
MyStructs.h
に追加
USTRUCT(BlueprintType) struct FEquipmentInfos { GENERATED_USTRUCT_BODY() public: UPROPERTY(EditAnywhere, Category = "Equip") EEquipmentType EquipmentType; UPROPERTY() int SlotIndex; FEquipmentInfos() : EquipmentType(), SlotIndex() {}; FEquipmentInfos(const EEquipmentType& InType) : EquipmentType(InType) {}; FEquipmentInfos(const int& Idx, const EEquipmentType& InType) : EquipmentType(InType), SlotIndex(Idx) {}; }; USTRUCT(Blueprintable) struct FItems { UPROPERTY(EditAnywhere, Category = "Item") bool bItemIsEquipment; FItems() : Name(), Description(), Image(), MaxStackSize(0), ItemClass(), bItemIsEquipment(false) {}; } USTRUCT(Blueprintable) struct FInventorySlots { UPROPERTY(EditAnywhere, Category = "Inventory") FEquipmentInfos EquipmentInfos; FInventorySlots(const FItems& InItemInfo, const int& InQ, const FEquipmentInfos& Info) : ItemInfo(InItemInfo), Quantity(InQ), EquipmentInfos(Info) { if (Info.SlotIndex != 0) { SlotIndex = Info.SlotIndex; } ItemInfo.bItemIsEquipment = true; }; void operator=(const FInventorySlots& In) { ItemInfo = In.ItemInfo; Quantity = In.Quantity; ItemIsFrom = In.ItemIsFrom; EquipmentInfos = In.EquipmentInfos; }; };
これで装備の情報を保持する。
装備UIの動作の実装
EquipmentWidget.h
に追加
// Copyright(C)write by pto8913. 2020. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "UIs/Bases/InventoryWidgetBase.h" #include "EquipmentWidget.generated.h" class AInventoryCharacterBase; UCLASS() class INVENTORYSYSTEM_API UEquipmentWidget : public UInventoryWidgetBase { GENERATED_BODY() public: UPROPERTY() AInventoryCharacterBase* OwnerChara; void OpenUI() override final; void CloseUI() override final; /* @ Idx : Is Actually Idx -> Inventory[Idx].SlotIndex */ FName GetSocketNameAt(const int& Idx) const; };
EquipmentWidget.cpp
に追加
// Copyright(C)write by pto8913. 2020. All Rights Reserved. #include "UIs/Widgets/EquipmentWidget.h" #include "UIs/Bases/InventorySlotBase.h" #include "Characters/Bases/InventoryCharacterBase.h" #include "Templates/ptoStructs.h" //#define TP TPair<FName, FEquipmentInfos> #define TP TPair<FName, FEquipmentInfos> const TArray<TP> EquipInfos = { TP("SocketHead", FEquipmentInfos(1, EEquipmentType::Head)), TP("SocketAccesory1",FEquipmentInfos(3, EEquipmentType::Accessory)), TP("SocketArmor", FEquipmentInfos(5, EEquipmentType::Armor)), TP("SocketShoulder",FEquipmentInfos(6, EEquipmentType::Shoulders)), TP("SocketAccesory2", FEquipmentInfos(7, EEquipmentType::Accessory)), TP("SocketGloves", FEquipmentInfos(8, EEquipmentType::Gloves)), TP("SocketShoes", FEquipmentInfos(9, EEquipmentType::Shoes)), TP("SocketAccesory3", FEquipmentInfos(11, EEquipmentType::Accessory)), TP("SocketWeaponLeft", FEquipmentInfos(12, EEquipmentType::WeaponLeft)), TP("SocketWeaponRight", FEquipmentInfos(14, EEquipmentType::WeaponRight)), TP("SocketAccesory4", FEquipmentInfos(15, EEquipmentType::Accessory)), }; //////////////////////////////////////// // Init void UEquipmentWidget::OpenUI() { for (int Idx = 0; Idx < EquipInfos.Num(); ++Idx) { /* SlotIdx : appearance index Idx : actually index */ UInventorySlotBase* NewSlot = CreateSlot(Inventory[Idx], Idx); if (IsValid(NewSlot) == true) { NewSlot->OwnerUI = this; NewSlot->SlotContents.EquipmentInfos.EquipmentType = EquipInfos[Idx].Value.EquipmentType; AddNewItem(NewSlot, EquipInfos[Idx].Value.SlotIndex); } } } //////////////////////////////////////// // Action FName UEquipmentWidget::GetSocketNameAt(const int& Idx) const { return EquipInfos[Idx].Key; } //////////////////////////////////////// // End void UEquipmentWidget::CloseUI() { OwnerChara->EquipmentInventory = MoveTemp(Inventory); }
InventoryCharacterBase.h
に追加
#include "Templates/ptoStructs.h" class UEquipmentWidget; UCLASS() class INVENTORYSYSTEM_API AInventoryCharacterBase : public AInventorySystemCharacter { public: void ToggleEquip(); UPROPERTY() TArray<FInventorySlots> EquipmentInventory; UPROPERTY() UEquipmentWidget* EquipmentUI; private: UPROPERTY(EditAnywhere) TSubclassOf<UEquipmentWidget> EquipmentUIClass;
InventoryCharacterBase.cpp
に追加
#include "UIs/Widgets/EquipmentWidget.h" void AInventoryCharacterBase::ToggleEquip() { if (IsValid(EquipmentUI) == true) { EquipmentUI->CloseUI(); EquipmentUI = nullptr; } else { if (EquipmentInventory.Num() == 0) { EquipmentInventory.Init(FInventorySlots(), 11); } EquipmentUI = CreateWidget<UEquipmentWidget>(GetWorld(), EquipmentUIClass); EquipmentUI->OwnerChara = this; EquipmentUI->Inventory = EquipmentInventory; EquipmentUI->OpenUI(); } }
PlayerBelongingsWidget.h
に追加
public:
UPROPERTY(meta = (BindWidget))
UBorder* EquipBorder;
PlayerBelongingsWidget.cpp
に追加
#include "UIs/Widgets/EquipmentWidget.h" ///////////////////////////////////////// // Init void UPlayerBelongingsWidget::OpenUI() { UWidgetBase::OpenUI(); _Owner->InventoryComp->ToggleInventory(); InventoryBorder->AddChild(_Owner->InventoryComp->InventoryUI); _Owner->ToggleEquip(); EquipBorder->AddChild(_Owner->EquipmentUI); } ///////////////////////////////////////// // End void UPlayerBelongingsWidget::CloseUI() { UWidgetBase::CloseUI(); _Owner->InventoryComp->ToggleInventory(); _Owner->ToggleEquip(); InventoryBorder->ClearChildren(); EquipBorder->ClearChildren(); }
とりあえずうまく表示できるかのテスト。
エディタに戻って、WBP_PlayerBelongingsUI
を変更
EquipmentWidget
クラスを継承して、WBP_EquipmentUI
を作成。
キャラクターにWBP_EquipmentUI
を設定して
見た目上はよくできました。
装備スロットの動作を考える
・ドロップされたイベントの受信
->装備のアタッチ
・スロットの基本的な動作
->Part.2参照
装備スロットの動作の実装
InventorySlotBase
クラスを継承して、EquipmentSlot
クラスを作成。
EquipmentSlot.h
に追加
// Copyright(C)write by pto8913. 2020. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "UIs/Bases/InventorySlotBase.h" #include "EquipmentSlot.generated.h" UCLASS() class INVENTORYSYSTEM_API UEquipmentSlot : public UInventorySlotBase { GENERATED_BODY() private: virtual bool Initialize() override; bool NativeOnDrop( const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) override final; };
EquipmentSlot.cpp
に追加
// Copyright(C)write by pto8913. 2020. All Rights Reserved. #include "UIs/Widgets/EquipmentSlot.h" #include "Operations/ItemDragDropOperation.h" ////////////////////////////////// // Init bool UEquipmentSlot::Initialize() { bool Res = UUserWidget::Initialize(); ItemIsFrom = EItemIsFrom::Equip; return Res; } ////////////////////////////////// // Action bool UEquipmentSlot::NativeOnDrop( const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) { /* DroppedItem info exists is in ItemOperation */ UItemDragDropOperation* ItemOperation = Cast<UItemDragDropOperation>(InOperation); if (IsValid(ItemOperation) == true) { EItemIsFrom _ItemIsFrom = ItemOperation->GetItemIsFrom(); if (_ItemIsFrom == ItemIsFrom || _ItemIsFrom == EItemIsFrom::HotBar) { /* From : Inventory, To : Inventory */ UInventorySlotBase::ExchangeSlotForUIToUI(ItemOperation->InventorySlotUI); } else if (_ItemIsFrom == EItemIsFrom::Inventory || _ItemIsFrom == EItemIsFrom::Chest) { UInventorySlotBase::ExchangeSlotForCompToUI(ItemOperation->InventorySlotUI); } InOperation = Cast<UDragDropOperation>(ItemOperation); } UInventorySlotBase::NativeOnDrop(InGeometry, InDragDropEvent, InOperation); return true; }
エディタに戻って、EquipmentSlot
クラスを継承して、WBP_EquipSlot
クラスを作成。
WBP_EquipmentUI
にWBP_EquipSlot
を設定。
おーけー。
アイテムがドロップされたらプレイヤーに装着
これもやるだけですが。
装着の動作を考える
①スロット : ドロップされた
②装備UI : ドロップされたスロットからソケット名を取得
③キャラクター : 装備をスポーンして装着
④終わり。
装備できるアイテムを作る
PickUpableItemBase
クラスを継承して、EquipmnetBase
クラスを作成。
どうやらメッシュコンポーネントでSimulatePhysics = true
になっているとソケットにアタッチできないようです。
EquipmentBase.h
に追加
// Copyright(C)write by pto8913. 2020. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Items/Bases/PickUpableItemBase.h" #include "EquipmentBase.generated.h" UCLASS() class INVENTORYSYSTEM_API AEquipmentBase : public APickUpableItemBase { GENERATED_BODY() private: AEquipmentBase(); };
EquipmentBase.cpp
に追加
// Copyright(C)write by pto8913. 2020. All Rights Reserved. #include "Items/Bases/EquipmentBase.h" #include "Components/SceneComponent.h" #include "Components/SkeletalMeshComponent.h" AEquipmentBase::AEquipmentBase() { MeshComp->SetSimulatePhysics(false); ItemInfo.bItemIsEquipment = true; }
装着の動作の実装
PickUpableItemBase.h
に追加
public: UPROPERTY() bool bIsIgnorePickUp = false;
PickUpableItemBase.cpp
に追加
bool APickUpableItemBase::PickUp(AInventoryCharacterBase* In) { if (bIsIgnorePickUp == true) return false; UActorComponent* _Comp = In->GetComponentByClass(UInventoryComponentBase::StaticClass()); if (IsValid(_Comp) == true) { UInventoryComponentBase* _InventoryComp = Cast<UInventoryComponentBase>(_Comp); if (IsValid(_InventoryComp) == true) { Quantity = FMath::Clamp(Quantity, 1, 9999); FInventorySlots NewSlotInfo = FInventorySlots(ItemInfo, Quantity); if (_InventoryComp->AddToInventory(NewSlotInfo) == true) { Destroy(); return true; } } } Destroy(); return false; }
InventoryCharacterBase.h
に追加
class UEquipmentSlot; class APickUpableItemBase; class INVENTORYSYSTEM_API AInventoryCharacterBase : public AInventorySystemCharacter { private: /* 装着された装備を保存する一時変数 */ UPROPERTY() TArray<APickUpableItemBase*> EquipmentActorTemp; public: /* Spawn equipment and attach to socket @ Class : Equipment Class @ SocketName : SocketName from Widget->Slot */ AActor* AttachEquipment( UClass* Class, const FName& SocketName ); /* Called when item was moved from EquipmentSlot to EquipmentSlot @ From : for SlotContents and SocketName @ To : for SlotContents and SocketName 装備の持ち替え、例えば左手の武器と右手の武器を持ち変える */ void AttachEquipment( UEquipmentSlot* From, UEquipmentSlot* To ); /* Called when item was moved from Inventory/HotBar...etc to EquipmentSlot @ In : for SlotContents and SocketName */ void AttachEquipment(UEquipmentSlot* In); /* Check if the target socket is used @ In : SlotContents @ Idx : SlotIndex @ SocketName : Attach socket name */ void AttachEquipment( const FInventorySlots& In, const int& Idx, const FName& SocketName ); /* Detach idx equipment */ void DetachEquipment(const int& Idx); // End Action
InventoryCharacterBase.cpp
に追加
void AInventoryCharacterBase::BeginPlay() { EquipmentActorTemp.Init(nullptr, 16); } AActor* AInventoryCharacterBase::AttachEquipment( UClass* Class, const FName& SocketName ) { if (Class == nullptr) return nullptr; if (SocketName.IsNone() == true) return nullptr; APickUpableItemBase* Equipment = Cast<APickUpableItemBase>(GetWorld()->SpawnActor(Class)); if (Equipment == nullptr) return nullptr; // 装備されたアイテムを拾えないようにする。 Equipment->bIsIgnorePickUp = true; Equipment->AttachToComponent( GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, SocketName ); return Equipment; } void AInventoryCharacterBase::AttachEquipment( UEquipmentSlot* From, UEquipmentSlot* To ) { int FromIdx = From->SlotContents.SlotIndex; int ToIdx = To->SlotContents.SlotIndex; if (EquipmentActorTemp[ToIdx] == nullptr) { AttachEquipment(To); DetachEquipment(FromIdx); } else { AttachEquipment(To); AttachEquipment(From); } } void AInventoryCharacterBase::AttachEquipment(UEquipmentSlot* In) { int Idx = In->SlotContents.SlotIndex; DetachEquipment(Idx); AttachEquipment( In->SlotContents, Idx, Cast<UEquipmentWidget>(In->OwnerUI)->GetSocketNameAt(Idx) ); } void AInventoryCharacterBase::AttachEquipment( const FInventorySlots& In, const int& Idx, const FName& SocketName ) { if (!EquipmentActorTemp.IsValidIndex(Idx)) return; EquipmentActorTemp[Idx] = Cast<APickUpableItemBase>( AttachEquipment(In.GetItemClass(), SocketName) ); } void AInventoryCharacterBase::DetachEquipment(const int& Idx) { if (!EquipmentActorTemp.IsValidIndex(Idx)) return; if (EquipmentActorTemp[Idx] != nullptr) { EquipmentActorTemp[Idx]->Destroy(); EquipmentActorTemp[Idx] = nullptr; } }
EquipmentSlot.cpp
に追加
#include "Characters/Bases/InventoryCharacterBase.h" bool UEquipmentSlot::NativeOnDrop( const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) { /* DroppedItem info exists is in ItemOperation */ UItemDragDropOperation* ItemOperation = Cast<UItemDragDropOperation>(InOperation); if (IsValid(ItemOperation) == true) { EItemIsFrom _ItemIsFrom = ItemOperation->GetItemIsFrom(); if (_ItemIsFrom == ItemIsFrom || _ItemIsFrom == EItemIsFrom::HotBar) { /* From : Inventory, To : Inventory */ UInventorySlotBase::ExchangeSlotForUIToUI(ItemOperation->InventorySlotUI); Cast<AInventoryCharacterBase>(GetPlayerCharacter())->AttachEquipment( Cast<UEquipmentSlot>(ItemOperation->InventorySlotUI), this ); } else if (_ItemIsFrom == EItemIsFrom::Inventory || _ItemIsFrom == EItemIsFrom::Chest) { UInventorySlotBase::ExchangeSlotForCompToUI(ItemOperation->InventorySlotUI); Cast<AInventoryCharacterBase>(GetPlayerCharacter())->AttachEquipment(this); } InOperation = Cast<UDragDropOperation>(ItemOperation); } UInventorySlotBase::NativeOnDrop(InGeometry, InDragDropEvent, InOperation); return true; }
これでおーけー。
エディタに戻って適当にソケットを作る。
EquipmentBase
クラスを継承して、BP_EquipTest
を作成。
できました。素敵な手袋です。
装備を外す
これは簡単です。
装備スロット以外のスロットが、装備スロットからのドロップイベントを受け取ったら、装備を外すだけです。
InventorySlotBase.cpp
に追加
bool UInventorySlotBase::NativeOnDrop( const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) { /* DroppedItem info exists is in ItemOperation */ UItemDragDropOperation* ItemOperation = Cast<UItemDragDropOperation>(InOperation); if (IsValid(ItemOperation) == true) { ItemOperation->InventorySlotUI->SetImageRenderOpacity(1.f); if (ItemOperation->GetItemIsFrom() == EItemIsFrom::Equip) { Cast<AInventoryCharacterBase>(GetPlayerCharacter())->DetachEquipment( ItemOperation->InventorySlotUI->SlotContents.SlotIndex ); } } UUserWidget::NativeOnDrop(InGeometry, InDragDropEvent, InOperation); return true; }
今回はここまで。