Creating an InventoryComponent for an RPG

An InventoryComponent enables its containing Actor to store InventoryActors in its inventory, and place them back into the game world.

Getting ready

Make sure you've followed the Axis Mappings – keyboard, mouse and gamepad directional input for an FPS character recipe in Chapter 6, Input and Collision, before continuing with this recipe, as it shows you how to create a simple character.

Also, the recipe Instantiating an Actor using SpawnActor in this chapter shows you how to create a custom GameMode.

How to do it...

  1. Create an ActorComponent subclass using the engine called InventoryComponent, then add the following code to it:
    UPROPERTY()
    TArray<AInventoryActor*> CurrentInventory;
    UFUNCTION()
    int32 AddToInventory(AInventoryActor* ActorToAdd);
    
    UFUNCTION()
    void RemoveFromInventory(AInventoryActor* ActorToRemove);
  2. Add the following function implementation to the source file:
    int32 UInventoryComponent::AddToInventory(AInventoryActor* ActorToAdd)
    {
      return CurrentInventory.Add(ActorToAdd);
    }
    
    void UInventoryComponent::RemoveFromInventory(AInventoryActor* ActorToRemove)
    {
      CurrentInventory.Remove(ActorToRemove);
    }
  3. Next, create a new StaticMeshActor subclass called InventoryActor. Add the following to its declaration:
    virtual void PickUp();
    virtual void PutDown(FTransform TargetLocation);
  4. Implement the new functions in the implementation file:
    void AInventoryActor::PickUp()
    {
      SetActorTickEnabled(false);
      SetActorHiddenInGame(true);
      SetActorEnableCollision(false);
    }
    
    void AInventoryActor::PutDown(FTransform TargetLocation)
    {
      SetActorTickEnabled(true);
      SetActorHiddenInGame(false);
      SetActorEnableCollision(true);
      SetActorLocation(TargetLocation.GetLocation());
    }
  5. Also, change the constructor to look like the following:
    AInventoryActor::AInventoryActor()
    :Super()
    {
      PrimaryActorTick.bCanEverTick = true;
      auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
      if (MeshAsset.Object != nullptr)
      {
        GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
        GetStaticMeshComponent()->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
      }
      GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
      SetActorEnableCollision(true);
    }
  6. We need to add an InventoryComponent to our character so that we have an inventory that we can store items in. Create a new SimpleCharacter subclass using the editor, and add the following to its declaration:
    UPROPERTY()
    UInventoryComponent* MyInventory;
    
    UFUNCTION()
    virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;
    
    UFUNCTION()
    void DropItem();
    UFUNCTION()
    void TakeItem(AInventoryActor* InventoryItem);
    
    UFUNCTION()
    virtual void NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit) override;
  7. Add this line to the character's constructor implementation:
    MyInventory = CreateDefaultSubobject<UInventoryComponent>("MyInventory");
  8. Add this code to the overriden SetupPlayerInputComponent:
    void AInventoryCharacter::SetupPlayerInputComponent(class UInputComponent* InputComponent)
    {
      Super::SetupPlayerInputComponent(InputComponent);
      InputComponent->BindAction("DropItem", EInputEvent::IE_Pressed, this, &AInventoryCharacter::DropItem);
    }
  9. Finally, add the following function implementations:
    void AInventoryCharacter::DropItem()
    {
      if (MyInventory->CurrentInventory.Num() == 0)
      {
        return;
      }
    
      AInventoryActor* Item = MyInventory->CurrentInventory.Last();
      MyInventory->RemoveFromInventory(Item);
      FVector ItemOrigin;
      FVector ItemBounds;
      Item->GetActorBounds(false, ItemOrigin, ItemBounds);
      FTransform PutDownLocation = GetTransform() + FTransform(RootComponent->GetForwardVector() * ItemBounds.GetMax());
      Item->PutDown(PutDownLocation);
    }
    
    void AInventoryCharacter::NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit)
    {
      AInventoryActor* InventoryItem = Cast<AInventoryActor>(Other);
      if (InventoryItem != nullptr)
      {
        TakeItem(InventoryItem);
      }
    }
    
    void AInventoryCharacter::TakeItem(AInventoryActor* InventoryItem)
    {
      InventoryItem->PickUp();
      MyInventory->AddToInventory(InventoryItem);
    }
  10. Compile your code and test it in the Editor. Create a new level and drag a few instances of InventoryActor out into your scene.
  11. Refer to the Instantiating an Actor using SpawnActor recipe if you need a reminder of how to override the current game mode. Add the following line to the constructor of your Game Mode from that recipe, then set your level's GameMode to the one you created in that recipe:
    DefaultPawnClass = AInventoryCharacter::StaticClass();
  12. Verify your code against the listing here before compiling and launching your project.
    #pragma once
    
    #include "GameFramework/Character.h"
    #include "InventoryComponent.h"
    #include "InventoryCharacter.generated.h"
    
    UCLASS()
    class UE4COOKBOOK_API AInventoryCharacter : public ACharacter
    {
      GENERATED_BODY()
    
      public:
      AInventoryCharacter();
      virtual void BeginPlay() override;
      virtual void Tick( float DeltaSeconds ) override;
      virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;
    
      UPROPERTY()
      UInventoryComponent* MyInventory;
      UPROPERTY()
      UCameraComponent* MainCamera;
      UFUNCTION()
      void TakeItem(AInventoryActor* InventoryItem);
      UFUNCTION()
      void DropItem();
      void MoveForward(float AxisValue);
      void MoveRight(float AxisValue);
      void PitchCamera(float AxisValue);
      void YawCamera(float AxisValue);
    
      UFUNCTION()
      virtual void NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit) override;
      private:
      FVector MovementInput;
      FVector CameraInput;
    };
    
    #include "UE4Cookbook.h"
    #include "InventoryCharacter.h"
    
    AInventoryCharacter::AInventoryCharacter()
    :Super()
    {
      PrimaryActorTick.bCanEverTick = true;
      MyInventory = CreateDefaultSubobject<UInventoryComponent>("MyInventory");
      MainCamera = CreateDefaultSubobject<UCameraComponent>("MainCamera");
      MainCamera->bUsePawnControlRotation = 0;
    }
    
    void AInventoryCharacter::BeginPlay()
    {
      Super::BeginPlay();
      MainCamera->AttachTo(RootComponent);
    }
    
    void AInventoryCharacter::Tick( float DeltaTime )
    {
      Super::Tick( DeltaTime );
      if (!MovementInput.IsZero())
      {
        MovementInput *= 100;
        FVector InputVector = FVector(0,0,0);
        InputVector += GetActorForwardVector()* MovementInput.X * DeltaTime;
        InputVector += GetActorRightVector()* MovementInput.Y * DeltaTime;
        GetCharacterMovement()->AddInputVector(InputVector);
        GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, FString::Printf(TEXT("x- %f, y - %f, z - %f"),InputVector.X, InputVector.Y, InputVector.Z));
      }
    
      if (!CameraInput.IsNearlyZero())
      {
        FRotator NewRotation = GetActorRotation();
        NewRotation.Pitch += CameraInput.Y;
        NewRotation.Yaw += CameraInput.X;
        APlayerController* MyPlayerController =Cast<APlayerController>(GetController());
        if (MyPlayerController != nullptr)
        {
          MyPlayerController->AddYawInput(CameraInput.X);
          MyPlayerController->AddPitchInput(CameraInput.Y);
        }
        SetActorRotation(NewRotation);
      }
    }
    void AInventoryCharacter::SetupPlayerInputComponent(class UInputComponent* InputComponent)
    {
      Super::SetupPlayerInputComponent(InputComponent);
      InputComponent->BindAxis("MoveForward", this, &AInventoryCharacter::MoveForward);
      InputComponent->BindAxis("MoveRight", this, &AInventoryCharacter::MoveRight);
      InputComponent->BindAxis("CameraPitch", this, &AInventoryCharacter::PitchCamera);
      InputComponent->BindAxis("CameraYaw", this, &AInventoryCharacter::YawCamera);
      InputComponent->BindAction("DropItem", EInputEvent::IE_Pressed, this, &AInventoryCharacter::DropItem);
    }
    void AInventoryCharacter::DropItem()
    {
      if (MyInventory->CurrentInventory.Num() == 0)
      {
        return;
      }
      AInventoryActor* Item = MyInventory->CurrentInventory.Last();
      MyInventory->RemoveFromInventory(Item);
      FVector ItemOrigin;
      FVector ItemBounds;
      Item->GetActorBounds(false, ItemOrigin, ItemBounds);
      FTransform PutDownLocation = GetTransform() + FTransform(RootComponent->GetForwardVector() * ItemBounds.GetMax());
      Item->PutDown(PutDownLocation);
    }
    
    void AInventoryCharacter::MoveForward(float AxisValue)
    {
      MovementInput.X = FMath::Clamp<float>(AxisValue, -1.0f, 1.0f);
    }
    
    void AInventoryCharacter::MoveRight(float AxisValue)
    {
      MovementInput.Y = FMath::Clamp<float>(AxisValue, -1.0f, 1.0f);
    }
    
    void AInventoryCharacter::PitchCamera(float AxisValue)
    {
      CameraInput.Y = AxisValue;
    }
    void AInventoryCharacter::YawCamera(float AxisValue)
    {
      CameraInput.X = AxisValue;
    }
    void AInventoryCharacter::NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit)
    {
      AInventoryActor* InventoryItem = Cast<AInventoryActor>(Other);
      if (InventoryItem != nullptr)
      {
        TakeItem(InventoryItem);
      }
    }
    void AInventoryCharacter::TakeItem(AInventoryActor* InventoryItem)
    {
      InventoryItem->PickUp();
      MyInventory->AddToInventory(InventoryItem);
    }
    
    #pragma once
    
    #include "Components/ActorComponent.h"
    #include "InventoryActor.h"
    #include "InventoryComponent.generated.h"
    
    UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
    class UE4COOKBOOK_API UInventoryComponent : public UActorComponent
    {
      GENERATED_BODY()
    
      public:
      UInventoryComponent();
      virtual void TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) override;
    
      UPROPERTY()
      TArray<AInventoryActor*> CurrentInventory;
      UFUNCTION()
      int32 AddToInventory(AInventoryActor* ActorToAdd);
    
      UFUNCTION()
      void RemoveFromInventory(AInventoryActor* ActorToRemove);
    };
    #include "UE4Cookbook.h"
    #include "InventoryComponent.h"
    
    UInventoryComponent::UInventoryComponent()
    {
      bWantsBeginPlay = true;
      PrimaryComponentTick.bCanEverTick = true;
    }
    void UInventoryComponent::TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction )
    {
      Super::TickComponent( DeltaTime, TickType, ThisTickFunction );
    }
    
    int32 UInventoryComponent::AddToInventory(AInventoryActor* ActorToAdd)
    {
      return CurrentInventory.Add(ActorToAdd);
    }
    
    void UInventoryComponent::RemoveFromInventory(AInventoryActor* ActorToRemove)
    {
      CurrentInventory.Remove(ActorToRemove);
    }
    
    #pragma once
    
    #include "GameFramework/GameMode.h"
    #include "UE4CookbookGameMode.generated.h"
    
    UCLASS()
    class UE4COOKBOOK_API AUE4CookbookGameMode : public AGameMode
    {
      GENERATED_BODY()
    
      public:
      AUE4CookbookGameMode();
      };
    
    #include "UE4Cookbook.h"
    #include "MyGameState.h"
    #include "InventoryCharacter.h"
    #include "UE4CookbookGameMode.h"
    
    AUE4CookbookGameMode::AUE4CookbookGameMode()
    {
      DefaultPawnClass = AInventoryCharacter::StaticClass();
      GameStateClass = AMyGameState::StaticClass();
    }
  13. Lastly, we need to add our InputAction to the bindings in the editor. To do this, bring up the Project Settings... window by selecting Edit | Project Settings...:
    How to do it...

    Then, select Input on the left-hand side. Select the plus symbol beside Action Mappings, and type DropItem into the text box that appears. Underneath it is a list of all the potential keys you can bind to this action. Select the one labelled E. Your settings should now look like the following:

    How to do it...
  14. Then we can hit play, walk over to our inventory actor, and it will be picked up. Press E to place the actor in a new location! Test this with multiple inventory actors to see that they all get collected and placed correctly.

How it works...

  1. Our new component contains an array of actors, storing them by pointer as well as declaring functions that add or remove items to the array. These functions are simple wrappers around the TArray add/remove functionality, but allow us to optionally do things such as checking if the array is within a specified size limit before going ahead with storing the item.
  2. InventoryActor is a base class that can be used for all items that can be taken by a player.
  3. In the PickUp function, we need to disable the actor when it is picked up. To do that, we have to do the following:
    • Disable actor ticking
    • Hide the actor
    • Disable collision
  4. We do this with the functions SetActorTickEnabled, SetActorHiddenInGame, and SetActorEnableCollision.
  5. The PutDown function is the reverse. We enable actor ticking, unhide the actor, and then turn its collision back on, and we transport the actor to the desired location.
  6. We add an InventoryComponent to our new character as well as a function to take items.
  7. In the constructor for our character, we create a default subobject for our InventoryComponent.
  8. We also add a NotifyHit override so that we are notified when the character hits other Actors.
  9. Inside this function, we cast the other actor to an InventoryActor. If the cast is successful, then we know our Actor was an InventoryActor, and so we can call the TakeItem function to take it.
  10. In the TakeItem function, we notify the Inventory item actor that we want to pick it up, then we add it to our inventory.
  11. The last piece of functionality in the InventoryCharacter is the DropItem function. This function checks if we have any items in our inventory. If it has any items, we remove it from our inventory, then we calculate a safe distance in front of our player character to drop the item using the Item Bounds to get its maximum bounding box dimension.
  12. We then inform the item that we are placing it in the world at the desired location.

See also

  • Chapter 5, Handling Events and Delegates, has a detailed explanation of how events and input handling work together within the Engine, as well as a recipe for the SimpleCharacter class mentioned in this recipe
  • Chapter 6, Input and Collision, also has recipes concerning the binding of input actions and axes
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.160.154