[UE5 reprint] Use the Interface interface to open the box

  1. Create boxes and UI classes
    In this section, you need to realize that the character presses E on the keyboard to open the item box. First, create a SurGameplayInterface class in UE, which inherits from the Unreal interface class. You will find that two classes are generated in the .h file: USurGameplayInterface and ISurGameplayInterface. According to the code comments, the first class should not be modified and the relevant functionality needs to be added to the second class. The role of this class is to serve as a shared public interface, and the specific implementation requires other classes to override it (it feels similar to a virtual function).

     The specific implementation method is to use the [1]UFUNCTION macro to modify the Interact function we wrote ourselves so that it can be used and edited in the UE blueprint. At the same time, setting the input of this function, you can pass in different APawn objects (the main body of calling this function) to facilitate us to control the display of related animations. Related UFUNCTION usages are:
    
     BlueprintCallable: Can be called in a blueprint
     BlueprintImplementableEvent: can be implemented in blueprints
     BlueprintNativeEvent: Blueprint is callable and implementable; needs to be overridden, but also has a default
    
// SurGameplayInterface.h
class SURKEAUE_API ISurGameplayInterface
{<!-- -->
public:
// Pass in the caller. In order to enable characters that cannot walk on two legs to be called correctly, define them as Pawn instead of Character.
UFUNCTION(BlueprintNativeEvent)
void Interact(APawn* InstigatorPawn);
};

Then, derive a SurItemChest box class from AActor and ISurGameplayInterface, and add two Mesh controls to represent the base and lid of the box. Because UFUNCTION (BlueprintNativeEvent) is set for Interact(), it is stipulated in UE that the following syntax needs to be used to implement (rewrite?) this function. According to the official documentation, this usage is very similar to the implementation of polymorphism in C++.

// SurItemChest.h
class SURKEAUE_API ASurItemChest : public AActor, public ISurGameplayInterface
{<!-- -->
public:
    //_Implementation must be added after modification of UFUNCTION(BlueprintNativeEvent)
    void Interact_Implementation(APawn* InstigatorPawn);

protected:
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* BaseMesh;
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* LidMesh;
};
// SurItemChest.cpp
ASurItemChest::ASurItemChest()
{<!-- -->
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>("BaseMesh");
RootComponent = BaseMesh;

LidMesh = CreateDefaultSubobject<UStaticMeshComponent>("LidMesh");
LidMesh->SetupAttachment(BaseMesh);
}
  1. Create a blueprint class
    Create a SurItemChest blueprint class box in UE and name it TreasureChest. There are meshes of boxes in the ExampleContent folder provided by the course project. Just set them to the Base and Lid of TreasureChest respectively.

    Then use the “Transform” attribute to adjust the position of the lid so that it just fits on top of the base.
    Adjust mesh position
  2. Control box opens
    We can try it in the viewport. Drag and adjust the angle of the lid to achieve the opening and closing effect of the box. When adjusting, you can pay attention to the changes in the “Transform” -> “Rotation” attribute in the details panel and find that the Pitch is changing.

Therefore, the opening and closing animation of the box can be achieved by changing the Pitch variable. In addition, in order to more conveniently control the opening angle, the floating-point TargetPitch is declared in .h and modified with the UPROPEERTY (EditAnywhere) macro, and then assigned an initial value in the .cpp constructor.

void ASurItemChest::Interact_Implementation(APawn* InstigatorPawn)
{<!-- -->
// Rotate relative to base, parameters (pitch, yaw, roll)
LidMesh->SetRelativeRotation(FRotator(TargetPitch, 0, 0));
}

It was also mentioned in the course that the animation achieved by this method is relatively stiff. But the current focus is not on animation production, and the Tick function will be used in the future to achieve more precise and silky animation control.
4. Control animation
To control the unboxing animation, you first need to bind a key event, and execute a function after pressing it. This function can determine whether there is a box within a certain distance of the line of sight, and if so, open the box.
According to the relevant theories of design patterns, the coupling of each functional module should be reduced as much as possible during development, so as to avoid bloated and redundant code in the later stage. Therefore, when implementing this function, we do not continue to write specific code in the SurCharacter class, but create a class specifically responsible for implementing this part of the logic, and then combine it with the SurCharacter class. At the same time, it is also mentioned in the course that because all characters can carry out attacks, it is best to encapsulate the relevant code of the previously implemented attacks separately and provide calls, which will be optimized in the future.
To implement this function, you can use the [2] ActorComponent class in UE. As the name suggests, this class can be attached to an Actor like a normal Component. Therefore, the SurInteractionComponent class is derived, in which we need to check which objects around us can interact, that is, collision query, so we declare PrimaryInteract() in .h to implement this functional requirement.

// SurInteractionComponent.h
class SURKEAUE_API USurInteractionComponent : public UActorComponent
{<!-- -->public:
void PrimaryInteract();
};

Then declare and create instances of SurInteractionComponent in the two files of SurCharacter, and declare the key operation PrimaryInteract to be bound.

// SurCharacter.h
UCLASS()
class SURKEAUE_API ASurCharacter : public ACharacter
{<!-- -->
protected:
\t// interface
UPROPERTY(VisibleAnywhere)
USurInteractionComponent* InteractionComp;

void PrimaryInteract();
}
// SurCharacter.cpp
ASurCharacter::ASurCharacter()
{<!-- -->
InteractionComp = CreateDefaultSubobject<USurInteractionComponent>("InteractionComp");
}
void ASurCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{<!-- -->
Super::SetupPlayerInputComponent(PlayerInputComponent);
//interaction
PlayerInputComponent->BindAction("PrimaryInteract", IE_Pressed, this, & amp;ASurCharacter::PrimaryInteract);
}
void ASurCharacter::PrimaryInteract() {<!-- -->

InteractionComp->PrimaryInteract();
}

To implement collision detection, the method of emitting rays is commonly used in game development, that is, a ray of a certain length is emitted from our character’s glasses, and when the ray collides with the first object, the object is returned in the function. The LineTraceSingleByObjectType() function in UE can implement this function. Its four parameters are: detection result, ray starting point, ray end point, and detection parameters. For relevant content about collision, you can refer to the [3] official document:

void USurInteractionComponent::PrimaryInteract()
{<!-- -->
FHitResult Hit; // Detection result

FVector EyeLocation; // character eye position
FRotator EyeRotation; // Character's gaze direction
AActor* MyOwner = GetOwner(); // Get the control role
// Output the position and direction of the player's sight to EyeLocation and EyeRotation
MyOwner->GetActorEyesViewPoint(EyeLocation, EyeRotation);
// Along the line of sight, the point 1000cm away from the model's eye position is the end point
FVector End = EyeLocation + (EyeRotation.Vector() * 1000);

FCollisionObjectQueryParams ObjectQueryParams; // Query parameters
ObjectQueryParams.AddObjectTypesToQuery(ECC_WorldDynamic); //Select query scene dynamic objects

GetWorld()->LineTraceSingleByObjectType(Hit, EyeLocation, End, ObjectQueryParams);
}

Finally, the function to open the box is called based on the collision result. The details have been explained in the comments:

// Get the detected Actor from the judgment result. If not detected, it will be empty.
AActor* HitActor = Hit.GetActor();
if (HitActor) {<!-- -->
        // If it is detected that the actor is not empty, then determine whether the actor implements the SurGameplayInterface class
if (HitActor->Implements<USurGameplayInterface>()) {<!-- -->
            // The Interact() we defined is passed in as Pawn type, so type conversion is done
APawn* MyPawn = Cast<APawn>(MyOwner);
//Polymorphism, call the corresponding function according to the incoming HitActor
// The first parameter cannot be empty, so the outer layer has been judged to be empty; the second parameter is customized by us and has no effect for the time being. It does not need to be judged to be empty.
ISurGameplayInterface::Execute_Interact(HitActor, MyPawn);
// Used for debugging, draw this collision detection line, green
DrawDebugLine(GetWorld(), EyeLocation, End, FColor::Green, false, 3);
}
}
else{<!-- --> DrawDebugLine(GetWorld(), EyeLocation, End, FColor::Red, false, 3); }

Finally, I bound the keyboard operation in UE, and then tested the code effect and found that the character could successfully open the box.
At the end of this course, the author also proposes optimization of collision queries. There is nothing wrong with using ray detection for objects with a certain volume such as boxes, but if you want to pick up small items such as coins, this method has too strict requirements on the viewing angle. Therefore, there are various detection methods in UE, such as scanning, sphere detection, etc. Developers should choose the optimal detection method based on the actual situation.