效果展示
按E拾取之後,物品會出現在背包中,我們可以按I進行背包的開關。
對點選Use會提示你使用了該物品。
點選Drop之後可以将物品丢棄
左鍵可以移動物品
移動到另一個物品的時候會融合成一個新的物體:
然後丢掉~
(直接把我的方塊彈飛了)
實作的整體思路
- 将可拾取物體的一些基本功能用C++實作出來,然後添加一個識别物體的UI,提示用E拾取物品。
- 拾取之後要存儲到背包中,将背包的UI實作,背包的UI分為兩個部分,一個是大架構,另一個是小結構,可以通過函數調用,将物品按照小結構的形式放到大架構中。
- 實作小結構的功能,可以拖拽、融合、丢棄、使用。
建立可互動物體
建立一個C++第三人稱項目,打開之後建立繼承Actor的C++類,命名為:
Interactable
(為了友善後面了解,這裡就給出一個示例名字。)
Interactable
Interactable
.h 檔案
- 聲明他的名稱和動作行為變量。
UPROPERTY(EditDefaultsOnly)
FString Name;
UPROPERTY(EditDefaultsOnly)
FString _Action;
- 封裝物品資訊
UFUNCTION(BlueprintCallable, Category = "Pickup")
FString GetUseText() const { return FString::Printf(TEXT("%s : Press E to %s"), *Name, *_Action);}
.cpp檔案
- 構造函數
初始化變量資訊
Name = "Name not set";
_Action = "Interact";
pickup
回到編譯器,右鍵我們剛剛建立的那個類,進行繼承,建立C++,命名為:
pickup
(這裡建立的時候忘記把P大寫了····)
.h檔案
- 聲明靜态網格體,并放在保護類型中。
寫代碼要謹慎一點,萬一被别人發現了這個變量是公開可修改的,一個外挂殺過來直接GG。
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UStaticMeshComponent* PickupMesh;
.cpp檔案
- 構造函數
建立靜态網格體執行個體,賦予他實體屬性,這個有利于我們後面進行丢棄的真實感。
PickupMesh = CreateDefaultSubobject<UStaticMeshComponent>("PickupMesh");
PickupMesh->SetSimulatePhysics(true);
回到編譯器在Content 中建立檔案夾Blueprint,右鍵pickup,繼承它建立藍圖。命名為:
BP_pickup
, 我們需要建立三個執行個體,右擊該藍圖,繼承它建立藍圖,命名為:
BP_Item_Cube
, 靜态網格體給個方塊。
同理建立
BP_Item_chair
,給他一個椅子,建立
BP_Item_sphere
,給它一個球。
建立自定義玩家控制器
為什麼要建立自定義玩家控制器?
因為我們的UI是通過玩家控制器來展現在使用者面前的。
建立繼承
PlayerController
類的C++檔案,命名為:
GameplayController
。
.h檔案
- 聲明公開變量,儲存的是控制目前所指的可互動對象。
UPROPERTY(BlueprintReadWrite, VisibleAnywhere)
class AInteractable* CurrentInteractable;
回到編輯器,右擊他建立藍圖。命名為:
BP_GameplayController
為角色增添功能
.h檔案
- 在保護類型中聲明射線識别函數聲明
protected:
void CheckForInteractables();
- 每一幀都要對物體進行射線檢測,是以要重寫Tick函數。
public:
virtual void Tick(float DeltaTime) override;
.cpp 檔案
- CheckForInteractables()
添加頭檔案:
#include "GameplayController.h"
#include "Interactable.h"
這個函數的整體思路就是通過調用
LineTraceSingleByChannel
,從StartTrace點開始到EndTrace點形成的一個射線中檢測是否有接觸到設定為可見的、符合QueryParams碰撞資訊的對象,對象資訊儲存在HitResult中。如果有,通過這個碰撞結果去擷取對象,判斷是否為可互動的對象,如果是的,那麼就将玩家控制器中的待拾取物體設定為目前這個物體。
// 射線拾取
FHitResult HitResult; // 存儲碰撞的結果的變量
FVector StartTrace = FollowCamera->GetComponentLocation(); // 射線起始點
FVector EndTrace = (FollowCamera->GetForwardVector() * 300) + StartTrace; // 射線終止點。
FCollisionQueryParams QueryParams; // 儲存了碰撞相關的資訊
QueryParams.AddIgnoredActor(this); // 将我們角色自身忽略掉,減少性能開銷
AGameplayController* controller = Cast<AGameplayController>(GetController()); // 玩家控制器
if (GetWorld()->LineTraceSingleByChannel(HitResult, StartTrace, EndTrace, ECC_Visibility, QueryParams) && controller) {
//檢查我們點選的項目是否是一個可互動的項目
if (AInteractable* Interactable = Cast<AInteractable>(HitResult.GetActor())) {
controller->CurrentInteractable = Interactable;
return;
}
}
//如果我們沒有擊中任何東西,或者我們擊中的東西不是一個可互動的,設定currentinteractable為nullptr
controller->CurrentInteractable = nullptr;
建立UI
在content中建立檔案夾,命名為:
UI
, 右擊空白處,建立藍圖控件。
命名為:
WB_Ingame
,這個UI用來識别物體。
在common中找到Text,拖到圖形中去。
一個放在正中間,text内容為一個點,主要作用是友善我們鏡頭移動去識别。另一個放在旁邊,表示我們識别物體的資訊。
點選展示物體資訊的文本,細節面闆中找到:
然後建立綁定:
按照這個藍圖去是實作
具體思路就是先擷取我們玩家的控制器,看能不能成功轉化為我們自定義的控制器類型,通過自定義的玩家控制器去擷取我們現在的所指的可互動對象,調用可互動對象中的傳回物體資訊的字元串,然後輸送到UI的展示界面。
接下來我們需要将界面展現到我們的遊戲中:
點選打開關卡藍圖:
然後藍圖這麼畫:
思路:建立元件,然後展示到界面中。
建立自定義遊戲模式
因為隻有我們自定義的遊戲模式才可以修改屬性,把我們的玩家控制器放進去。
建立繼承
GameMode
的C++檔案,命名為:
GameplayGameMode
。
然後右擊它,建立藍圖。命名為:
BP_GameplayGameMode
打開我們的項目設定
第一部分完成。
實作背包界面
建立大架構UI
在UI中建立UI控件,命名為:
WB_Inventory
。
建立内容,設計UI:
圖檔顔色設定為黑色,稍微透明一點,然後在加上一個Wrap Box。
建立小元件UI
在UI檔案夾中建立控件:
C_InventorySlot
。
建立資料類型
我們可拾取的物品有很多,如果一個一個建立小元件UI會很繁瑣,這就與計算機友善性相違背。
這時候我們就需要采用資料表來輔助我們,但是在建立資料表之前,先要定義資料類型。
資料類型定義在第三人稱角色中(具體原因不知道為什麼。)
// 資料表中的類型定義,資料表如果采用了下面結構體的類型,資料表中就會顯示他的所有資料,就有點類似于繼承。
USTRUCT(BlueprintType) // 聲明為藍圖類型
struct FInventoryItem : public FTableRowBase {
GENERATED_BODY();
public:
FInventoryItem() { // 構造函數 變量進行初始化。
Name = FText::FromString("Item");
Action = FText::FromString("Use");
Description = FText::FromString("Please enter a description for this item");
Value = 10;
}
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName ItemID; // 物品的ID
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSubclassOf<class Apickup> Itempickup; // 拾取類型對象
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FText Name; // 對象名字
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FText Action; // 對象作用
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Value; // 對象的值
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UTexture2D* Thumbnail; // 儲存對象圖檔資訊
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FText Description; // 對該資料的描述
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FCraftingInfo> CraftCombinations; // 儲存可以互相融合的物品資訊
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bCanBeUsed; // 是否可以被使用
bool operator == (const FInventoryItem& Item) const { // 重載等于号,如果他們ID相同,就說明他們兩個是屬于同一種類型。
if (ItemID == Item.ItemID) return true;
else return false;
}
};
接下來我們完善一下融合類型的聲明,這個類型寫在FInventoryItem 類型的前面。
// 融合類型的定義
USTRUCT(BlueprintType)
struct FCraftingInfo : public FTableRowBase {
GENERATED_BODY();
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName ComponentID; // 可以融合的物品ID
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName ProductID; // 融合之後的物品ID
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bDestroyItemA; // 是否銷毀物品A 物品A就是ComponentID所代表的物品
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bDestroyItemB; // 是否銷毀物品B
};
建立資料表
在此之前,我們需要導入幾張圖檔,做為他們他們标志圖檔。
圖檔從哪裡來的呢?
輕按兩下進去:
取消Grid之後,移動位置進行截圖儲存在桌面,然後導入即可。
建立檔案夾data, 然後再建立一個檔案夾Thumbnail, 在這個檔案夾中導入圖檔。
在data中建立我們的資料表,右擊
進入之後點選加号:
按照這個形式進行填寫:
在這個裡面還需要加一個東西,代表他可以和其他東西融合。
這樣就解決了之前的麻煩了。
接下來實作按下E鍵之後,我們的物品到我們的背包中。
拾取操作
打開
GameplayGameMode
檔案
添加:
public:
class UDataTable* GetItemDB() const { return ItemDB; }
protected:
UPROPERTY(EditDefaultsOnly)
class UDataTable* ItemDB;
編譯之後,打開
BP_GameplayGameMode
,在細節面闆中添加我們的資料表:
打開
C_InventorySlot
:
建立變量,命名為:
Item
設定類型為資料表中的類型
這個眼睛要處于打開狀态,這樣其他地方就能夠通路到他
點選圖檔,綁定圖檔,
點選文本,綁定名字:
打開
pickup
檔案:
頭檔案添加:
protected:
// 儲存的是我們物品的ID,我們是通過物品ID去添加我到我們的背包
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName ItemID;
在構造函數中初始化他
ItemID = FName("Please enter an ID");
打開我們的
Interactable
檔案, 添加一個公開函數:
UFUNCTION(BlueprintImplementableEvent)
void Interact(APlayerController* controller);
代表的是互動操作,這個函數到藍圖中實作,為什麼要這麼寫?
因為互動操作有很多,你可能在這個項目中看不到他的優勢,當他不僅僅是按E鍵拾取物品這個功能的時候,他在藍圖中實作的優勢就展現出來了。不直接在C++中寫死,讓我們的代碼更加有拓展性。
那它在我們這個項目中功能的實作在哪裡呢?
打開
BP_pickup
, 在藍圖中,重新實作它。
思路:先拿到控制器,控制器中有一個函數就是通過ID去添加這個物品到背包中(這個待會就講),添加完成之後就把自己删除了。
去綁定按鍵,項目設定Input中。
然後就到了這部分的重頭戲:
GameplayController函數添加
- 在頭檔案中添加:
需要添加頭檔案,第三人稱角色的頭檔案,類型在第三人稱頭檔案中。
public:
// 通過資料ID查找并添加到我們的資料數組中
UFUNCTION(BlueprintCallable, Category = "Utils")
void AddItemToInventoryByID(FName ID);
// 儲存背包中的内容
UPROPERTY(BlueprintReadWrite, VisibleAnywhere)
TArray<FInventoryItem> Inventory;
protected:
// 物品互動操作
void Interact();
virtual void SetupInputComponent() override;
- 在CPP檔案中添加
添加角色頭檔案、
#include "GameplayGameMode.h"
#include "Interactable.h"
void AGameplayController::AddItemToInventoryByID(FName ID)
{
// 從世界中擷取真實的遊戲模式
AGameplayGameMode* GameMode = Cast<AGameplayGameMode>(GetWorld()->GetAuthGameMode());
// 将遊戲模式裡面定義的資料表拿出來
UDataTable* ItemTable = GameMode->GetItemDB();
// 從資料表中查找資料
FInventoryItem* ItemToAdd = ItemTable->FindRow<FInventoryItem>(ID, "");
// 如果找到了,那麼就添加到我們UI展示的清單中
if (ItemToAdd) {
Inventory.Add(*ItemToAdd);
}
}
void AGameplayController::Interact()
{
// 按鍵一按下,如果我們目前識别到了物體,那麼就調用物體的互動函數。
if (CurrentInteractable) {
CurrentInteractable->Interact(this);
}
}
void AGameplayController::SetupInputComponent()
{
Super::SetupInputComponent();
//将按鍵與互動函數綁定
InputComponent->BindAction("Use", IE_Pressed, this, &AGameplayController::Interact);
}
這個時候我們任務移動到物體位置,按下E鍵,物體會消失,物體到哪裡去了呢,背包裡啊!背包怎麼打開啊,你快告訴我啊。
打開背包
綁定按鍵:
然後在
BP_GameplayController
藍圖中修改添加:
思路:按一次走A,建立背包控件,按第二次的時候就走B,銷毀背包。
但是這個時候打開并沒有什麼東西出現,這是因為大架構裡面的東西都還沒有綁定。
接下來完善背包:
打開
WB_Inventory
建立新函數:
然後在事件圖中實作下面的操作:
思路:當我們的背包打開的時候,将滑鼠顯示出來,然後加載背包内容,背包關閉的時候把滑鼠取消顯示。
這樣寫會有一點小瑕疵,可以在打開背包的時候隻認UI輸入,關閉背包的時候隻認遊戲模式輸入,然後需要加一個按鈕(或者在其他地方加個條件打開背包的時候不能使用按鍵,是不是太麻煩了,不知道那些遊戲裡面是怎麼實作的)。
實作背包内的操作
實作使用功能
打開
C_InventorySlot
,點選
Use
按鈕,
設定為變量,然後拉到最下面,點選加号:
然後按照下面這個藍圖進行實作:
思路:點選使用之後,我們要把背包裡面的我們點選的這個元素給删除掉(就是FIND那一塊的操作),然後把目前顯示到背包中的卡片給删除掉,你會發現他生成了一個Actor然後又把他删掉,主要的作用在于調用OnUsed。
OnUsed 這個函數的意思就是用來實作使用這個Actor之後的效果,比如說,我們人物用了血包,人物要加血,加血這個動作,就是在這個函數中實作。
我們先在pickup.h中給他聲明:
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "Pickup")
void OnUsed();
然後我們就可以在它的藍圖中實作效果,比如說:可以在
BP_Item_Cube
中添加:
那麼我們使用的時候就會列印這個字元。
實作丢棄功能
這個和之前那個差不多,隻是沒有把我們生成的Actor删除而已。
實作拖拽功能
先在我們的Blueprint檔案夾中建立一個藍圖:
繼承DragDropOperation, 它是實作拖拽基礎功能的類。
命名為:
InventoryDragDropOperation
。
建立變量:
這個變量是儲存我們拖拽物體的資訊,用于物體于物體之間的融合。
在UI檔案夾中建立一個UI控件,命名為:
C_InventorySlot_Drag
,
sizeBox需要設定:
這裡是設定拖拽的時候的圖示,然後給圖檔綁定圖檔變量,
建立變量, Thumbnail,表示拖拽時候的圖檔:
點選圖檔,檢視細節面闆,綁定變量:
打開
C_InventorySlot
:
打開藍圖,重載滑鼠左鍵按鍵反應:
重寫
OnDragDetected
:
思路:将我們設計好的拖拽UI,放到拖拽藍圖中,還要記得把拖拽物體的資訊也傳輸進去。
重寫
Drop
:
思路:實作當拖拽停止的時候調用融合函數。
融合函數實作:
打開C++檔案:
GameplayController
頭檔案:
public:
//重新加載玩家背包-當你對玩家庫存做了更改時調用這個, 并且在藍圖可以直接實作他的邏輯。
UFUNCTION(BlueprintImplementableEvent)
void ReloadInventory();
// 融合A、B元素 物品A代表正在拖拽的物品, 物品B表示物品B拖到的函數位置所代表的物品
UFUNCTION(BlueprintCallable, Category = "Utils")
void CraftItem(FInventoryItem ItemA, FInventoryItem ItemB, AGameplayController* controller);
CPP檔案中添加:
void AGameplayController::CraftItem(FInventoryItem ItemA, FInventoryItem ItemB, AGameplayController* controller)
{
//檢查我們是否做了一個操作,或者如果物品不正确,什麼都沒有做
// 先在我們待融合的物品B的融合條件中尋找是否有物體A
for (auto Craft : ItemB.CraftCombinations) {
// 如果兩個可以融合,那麼就将融合之後的物品加入到我們的背包中,根據設定判斷是否要把融合的兩個物體删除。
if (Craft.ComponentID == ItemA.ItemID) {
if (Craft.bDestroyItemA) {
Inventory.RemoveSingle(ItemA);
}
if (Craft.bDestroyItemB) {
Inventory.RemoveSingle(ItemB);
}
AddItemToInventoryByID(Craft.ProductID);
ReloadInventory(); // 更新背包
}
}
}
出現了一個藍圖中實作的函數,接下來實作一下:
打開
BP_GameplayController
添加:
完結撒花!!!