Picking up an item in The Last of Us is very special, character will put his hand on the item and then get his hand back, picking triggers when character’s hand touches the item.
This post focuses on how to create a simple TLoU-styled picking system in Unreal Engine 5, first of all, make sure you have a basic knowledge of Advanced Locomotion System V4 or Advanced Locomotion System Refator which is as folloed:
Note that you should be familiar with the IK system and the usage of curves in ALS, otherwise it may be hard to understand.
I Wrote some posts about ALS Refactor before, all of these posts are in Chinese, benifits for Chinese readers:
This is not a project, it is just a idea, so please forgive me for the bad encapsulation and programming specification.
1. Classes Abstract
For those pickable items, I created a component which inherits from UShapeComponent called UPickableComponent. However, after I finishing this system, I find that it is better to create a base class called UInteractiveComponent, for buttons and many other things could reuse this system.
UPickableComponent inherits from UShapeComponent for sphere overlay check, which allows UI system to display interactive tips and if the item is in range for character’s picking. There is no other data members here, but you can add them for demands in your project.
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent), Blueprintable, BlueprintType, ShowCategories=(Mobility)) class ADVANCEDPICK_API UPickableItemComponent : public USphereComponent { GENERATED_BODY() public: // Sets default values for this component's properties UPickableItemComponent(); protected: // Called when the game starts virtual void BeginPlay() override; public: UFUNCTION(BlueprintImplementableEvent) void OnPickup(); };
It has a virtual method: OnPick, which implemented by blueprints, you can add a OnAbort() method in your project. The reason why is that if player is running fast enough that the hand of the character cannot touch the item, it should call OnAbort(). And it is better to be a virtual method, for an item to pick, it can be implemented as pick up the item, but for a button which costs a lot of time to trigger for player, it is better to be treated as an invalid operation.
What’s more, create a character class called APickableCharacter, this is only for tutorial, you can encapsule the picking ability to a class using GAS or some other game ability system created by yourself, but it is enough here:
class UInputAction; UCLASS(AutoExpandCategories = ("Settings|Als Character Example", "State|Als Character Example")) class ADVANCEDPICK_API APickableCharacter : public AAlsCharacterExample { GENERATED_BODY() public: UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Pickable Settings") TObjectPtr<UInputAction> PickupAction; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Pickable Settings", meta=(Units = "cm")) float SearchPickableItemRadius = 50.0f; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Pickable Settings") TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypeQuery; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Pickable Settings", Instanced) TObjectPtr<UPickupItemRequest> PickupItemAbility; // Sets default values for this actor's properties APickableCharacter(); protected: // Called when the game starts or when spawned virtual void BeginPlay() override; virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override; private: void PickupItem(const FInputActionValue& InputActionValue); void PickupItemInterval(); public: // Called every frame virtual void Tick(float DeltaTime) override; };
which is implemented as:
APickableCharacter::APickableCharacter() : Super() { } // Called when the game starts or when spawned void APickableCharacter::BeginPlay() { Super::BeginPlay(); } void APickableCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { Super::SetupPlayerInputComponent(PlayerInputComponent); if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) { EnhancedInputComponent->BindAction(PickupAction, ETriggerEvent::Triggered, this, &APickableCharacter::PickupItem); } } void APickableCharacter::PickupItem(const FInputActionValue& InputActionValue) { if (InputActionValue.Get<bool>()) { PickupItemInterval(); } } void APickableCharacter::PickupItemInterval() { const FVector SphereOverlapCheckBeginLocation = GetActorLocation(); TArray<UPrimitiveComponent*> PickableItemComponents; DrawDebugSphere(GetWorld(), SphereOverlapCheckBeginLocation, SearchPickableItemRadius, 0, FColor::Green, false, 5.0f); if (UKismetSystemLibrary::SphereOverlapComponents( GetWorld(), SphereOverlapCheckBeginLocation, SearchPickableItemRadius, ObjectTypeQuery, UPickableItemComponent::StaticClass(), {this}, PickableItemComponents)) { if (PickableItemComponents.Num() > 0 && PickableItemComponents[0]->GetOwner()) { PickupItemAbility->Init(this, PickableItemComponents[0]->GetOwner()); return; } } UE_LOG(LogTemp, Log, TEXT("Abort for no pickable item.")) } // Called every frame void APickableCharacter::Tick(float DeltaTime) { Super::Tick(DeltaTime); }
You may find that there is a class called UPickupItemRequest, why an object? that is a traditional idea of gameplay ability system, encapsule an ability to a object so that you can give to your character when he unlocks this ability. And what’s more, picking needs tick event to observe when should the character change the other arm and when should the character abort picking.
The UPickupItemRequest is as followed:
enum class EPickupItemState { Default, AbortForCannotReachForRotationWithoutBeginning, AbortForCannotReachForDistanceWithoutBeginning, CanBegin, OnGoingWithLeft, OnGoingWithRight, AbortForCannotReachForDistanceAfterBeginning, FinishedSucceed }; UCLASS(EditInlineNew) class ADVANCEDPICK_API UPickupItemRequest : public UObject, public FTickableGameObject { GENERATED_BODY() private: inline static float TickIntervalTime = 0.2f; float IntervalTimer = 0.0f; TObjectPtr<APickableCharacter> PickableCharacter; TObjectPtr<AActor> PickableItem; EPickupItemState CurrentPickupItemState = EPickupItemState::Default; private: bool BeginAtRight = false; public: UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Animation") TObjectPtr<UAnimMontage> PickWithLeftHandMontage; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Animation") TObjectPtr<UAnimMontage> PickWithRightHandMontage; UPickupItemRequest(); void Init(APickableCharacter* InPickableCharacter, AActor* InPickableItem); protected: virtual void Tick(float DeltaTime) override; virtual void TickInterval(); virtual ETickableTickType GetTickableTickType() const override; virtual UWorld* GetTickableGameObjectWorld() const override; virtual bool IsTickableWhenPaused() const override; virtual TStatId GetStatId() const override; virtual bool IsTickable() const override; private: EPickupItemState CheckRequestBeginState(); void TryBeginPickup(); void UpdatePickOnGoingState(); void SetIkBoneTarget() const; };
2. Basic Thought
the basic thought is as followed:
Use IK
We should add an IK system for this, just play an animation montage of picking sth and use IK system to put the hands of the character on it.
A good idea is that using curve to control the weight of IK, so it would cost a lot for coding, and it is more flexiable.
Abort if it is too far away
Character can run very fast when picking sth, if it is too far for the character for touching, it should trigger abort event, stop the animation montage and calls OnAbort() method of the component.
Change arm if it is more convinient for character
If the character begin to pick an item when he is on the right of the item, but he is moving too fast that he cannot touch with his left arm but can still get it with his right arm, he should change his arm to pick.
If begins, don’t change arm as far as possible
If the character is a little bit right before start picking, then he should use his left arm, but if a character is a little bit right after he start picking with his right arm, a natural human would not change arm, so he should continue picking with his right arm.
3. Ik
Add two curves: GameplayWorldIkTargetLeft and GameplayWorldIkTargetRight:
inline const FName& UAlsConstants::GameplayIkTargetLeftCurve() { static const FName Name{TEXT("GameplayIkTargetLeft")}; return Name; } inline const FName& UAlsConstants::GameplayIkTargetRightCurve() { static const FName Name{TEXT("GameplayIkTargetRight")}; return Name; }
Add two variables corresponding to these curves in AnimInstance and update them by the curve every frame, I add them in PostState:
USTRUCT(BlueprintType) struct ALS_API FAlsPoseState { GENERATED_BODY() ...... UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS") FVector GameplayWorldIkLocationLeft; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ALS") FVector GameplayWorldIkLocationRight; };
void UAlsAnimationInstance::RefreshPose() { ... PoseState.GameplayWorldIkTargetLeftAmount = GetCurveValueClamped01(UAlsConstants::GameplayIkTargetLeftCurve()); PoseState.GameplayWorldIkTargetRightAmount = GetCurveValueClamped01(UAlsConstants::GameplayIkTargetRightCurve()); ... }
Remember to do IK in Control Rig:
I am not a anim artist, so acutally I use a mantle animation sequence here, which makes the result not as good as in the one in TLoU, but it doesn’t matter, it is enough for our tutorial. Add a Ik Curve and modifer it in animation sequencer, and make an animation montage for it.
Note that this animation only plays for the left arm of the character, so the montage is:
There is a additional slot called curve, it maybe confuse if you are not familar about ALS Refactor, in fact, this is used to get the original curve value of the animation sequence which doesn’t be mixed in animation blends. For more info, you can see Layering method of AB_ALS.
4. Picking ability
Picking ability is encapsuled as a object, and it is a tickable object, so it inherits the FTickableGameObject interface.
It has a state enum for checking the state of picking.
To start a pick action, here are some important work to do:
- Check the angle between the character and if is a devious degree, it should be abort.
- Check the distance between the character and the pickable item, abort if is too far away.
- Check if the item is on the right or left of the character, and choose a convinience arm.
- It is a project, check if the hand is occupied by weapon or sth.
All these are implemented as:
EPickupItemState UPickupItemRequest::CheckRequestBeginState() { const FTransform CharacterPelvisTransform = PickableCharacter->GetMesh()->GetSocketTransform(UAlsConstants::PelvisBone()); const FTransform ActorToPickTransform = PickableItem->GetActorTransform(); const FVector PelvisForward = PickableCharacter->GetActorForwardVector().GetSafeNormal(); const FVector CharacterToPickableActor = ActorToPickTransform.GetLocation() - CharacterPelvisTransform.GetLocation(); #if ENABLE_ANIM_DEBUG DrawDebugLine(GetWorld(), PickableCharacter->GetActorLocation(), PickableCharacter->GetActorLocation() + PelvisForward * 100.0f, FColor::Red ,false, 5.0f); DrawDebugLine(GetWorld(), PickableCharacter->GetActorLocation(), ActorToPickTransform.GetLocation(), FColor::Blue, false, 5.0f); #endif const float Distance = CharacterToPickableActor.Length(); const float Angle = FMath::RadiansToDegrees(FMath::Acos(PelvisForward.Dot(CharacterToPickableActor.GetSafeNormal()))); UE_LOG(LogTemp, Log, TEXT("Distance = %f, Angle = %f"), Distance, Angle) if (Angle > 145) { return EPickupItemState::AbortForCannotReachForRotationWithoutBeginning; } if (Distance > 100.0f) { return EPickupItemState::AbortForCannotReachForDistanceWithoutBeginning; } if (PelvisForward.Cross(CharacterToPickableActor).Z > 0.0f) { BeginAtRight = true; } else { BeginAtRight = false; } CurrentPickupItemState = EPickupItemState::CanBegin; TryBeginPickup(); return EPickupItemState::CanBegin; }
This method returns a EPickupItemState enumeration so that you can use a UI animation or SFX to nofity why cannot pick the item up.
TryBeginPickup just begin the picking ability and play animation:
void UPickupItemRequest::TryBeginPickup() { if (CurrentPickupItemState != EPickupItemState::CanBegin) { return; } const USkeletalMeshComponent* SkeletalMeshComponent = PickableCharacter->GetMesh(); UAnimInstance* AnimInstance = SkeletalMeshComponent->GetAnimInstance(); BeginAtRight ? AnimInstance->Montage_Play(PickWithRightHandMontage, 1.0f) : AnimInstance->Montage_Play(PickWithLeftHandMontage, 1.0f); CurrentPickupItemState = BeginAtRight ? EPickupItemState::OnGoingWithRight : EPickupItemState::OnGoingWithLeft; }
The picking ability should be observed after beginning for every possibility we have mentioned before:
void UPickupItemRequest::UpdatePickOnGoingState() { if (CurrentPickupItemState != EPickupItemState::OnGoingWithLeft && CurrentPickupItemState != EPickupItemState::OnGoingWithRight) { return; } SetIkBoneTarget(); const FTransform CharacterPelvisTransform = PickableCharacter->GetMesh()->GetSocketTransform(UAlsConstants::PelvisBone()); const FTransform ActorToPickTransform = PickableItem->GetActorTransform(); const FVector PelvisForward = CharacterPelvisTransform.TransformVector(FVector::ForwardVector).GetSafeNormal(); const FVector CharacterToPickableActor = ActorToPickTransform.GetLocation() - CharacterPelvisTransform.GetLocation(); const float Distance = CharacterToPickableActor.Length(); const float Angle = FMath::Acos(PelvisForward.Dot(CharacterToPickableActor.GetSafeNormal())); const USkeletalMeshComponent* SkeletalMeshComponent = PickableCharacter->GetMesh(); UAnimInstance* AnimInstance = SkeletalMeshComponent->GetAnimInstance(); UE_LOG(LogTemp, Log, TEXT("distance = %f"), Distance) if (Distance > 100.0f) { UE_LOG(LogTemp, Log, TEXT("Abort for distance")) AnimInstance->Montage_Stop(0.25, PickWithRightHandMontage); AnimInstance->Montage_Stop(0.25, PickWithLeftHandMontage); CurrentPickupItemState = EPickupItemState::AbortForCannotReachForDistanceAfterBeginning; return; } const bool ItemOnRight = PelvisForward.Cross(CharacterToPickableActor).Z > 0.0f; if (CurrentPickupItemState == EPickupItemState::OnGoingWithRight) { if (ItemOnRight && Angle < 145 || !ItemOnRight && Angle < 45) { return; } UE_LOG(LogTemp, Log, TEXT("Abort ArmRight Anim")) CurrentPickupItemState = EPickupItemState::CanBegin; AnimInstance->Montage_Stop(0.25, PickWithRightHandMontage); TryBeginPickup(); } if (CurrentPickupItemState == EPickupItemState::OnGoingWithLeft) { if (!ItemOnRight && Angle < 145 || ItemOnRight && Angle < 45) { return; } UE_LOG(LogTemp, Log, TEXT("Abort ArmRight Left")) CurrentPickupItemState = EPickupItemState::CanBegin; AnimInstance->Montage_Stop(0.25, PickWithLeftHandMontage); TryBeginPickup(); } }
This method is too expensive for calling every frame, so I add a interval for it:
void UPickupItemRequest::Tick(float DeltaTime) { IntervalTimer += DeltaTime; if (IntervalTimer > TickIntervalTime) { TickInterval(); IntervalTimer = 0.0f; } } void UPickupItemRequest::TickInterval() { UpdatePickOnGoingState(); }
5. Preview of the result
I use a mantle animation as a picking animation, so the rotation of the result seems weird. And what’s more, you can use different picking animation just like TLoU, some items are put at pocket, some are packet etc.
I didn’t implement the trigger of picking, but you can see the IK, picking while moving and abort for distance in this video etc.