Unreal5 무기 Collision C++

Unreal5 무기 Collision C++

Notify 추가.

캐릭터 공격 montage에 Notify를 추가해서 무기의 Collision을 공격중일때 활성, 공격중이 아닐 때 비활성으로 만들어주겠습니다.

애니메이션의 공격 모션이 때리는 순간일 때 Collision을 활성화 할 수 있도록 EnableBoxCollision이라는 Notify를 배치하고 때리는 모션이 끝나면 DisableBoxCollision Notify를 배치했습니다.

코드 수정.

Weapon.h 에서 때린 액터를 또 때리지 않도록 IgnoreActors를 추가하고, 외부에서 WeaponBox에 접근할 수 있도록 함수를 추가했습니다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Item/Item.h"
#include "Weapon.generated.h"

/**
 * 
 */
class UBoxComponent;
UCLASS()
class SLAYER_API AWeapon : public AItem
{
	GENERATED_BODY()
public:
	AWeapon();//생성자.
	void Equip(USceneComponent* InParent,const FName& InSocketName);
	void AttachMeshToSocket(USceneComponent* InParent, const FName& InSocketName);
	TArray<AActor*> IgnoreActors;
protected:
	virtual void BeginPlay() override;
	UFUNCTION()
	void OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
	
private:
	/*Attack*/
	UPROPERTY(VisibleAnywhere, Category = "Weapon Properties")
	UBoxComponent* WeaponBox;
	UPROPERTY(VisibleAnywhere)
	USceneComponent* BoxTraceStart;
	UPROPERTY(VisibleAnywhere)
	USceneComponent* BoxTraceEnd;
public:
	FORCEINLINE UBoxComponent* GetWeaponBox() const { return WeaponBox; }
};

Weapon.cpp에서는 OnBoxOverlap이벤트에 때린것을 또 때리지 않도록 for문을 통해 이전에 Hit가 되었던 것들을 ActorToIgnore에 넣어주었습니다.

void AWeapon::OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	const FVector Start = BoxTraceStart->GetComponentLocation();
	const FVector End = BoxTraceEnd->GetComponentLocation();
	TArray<AActor*> ActorsToIgnore;
	ActorsToIgnore.Add(this);
	//#include "Kismet/KismetSystemLibrary.h"
	FHitResult BoxHit;
	//이전 충돌했던 것 빼기.
	for (AActor* a : IgnoreActors) {
		ActorsToIgnore.AddUnique(a);
	}
	UKismetSystemLibrary::BoxTraceSingle(this,Start,End, FVector(5.f, 5.f, 5.f),BoxTraceStart->GetComponentRotation(), ETraceTypeQuery::TraceTypeQuery1,
		false,ActorsToIgnore,EDrawDebugTrace::None, BoxHit,true);
	IgnoreActors.AddUnique(BoxHit.GetActor());
	
	if (BoxHit.GetActor()) {
		/*Actor를 때렸을 때*/
		IHitInterface* HitInterface = Cast<IHitInterface>(BoxHit.GetActor());
		if (HitInterface) {
			HitInterface->GetHit(BoxHit.ImpactPoint);
		}
	}
}

SlayerCharacter.h 에서는 Enable, Disable어쩌고 Notify가 들어왔을 때 Collision을 설정할 함수를 선언하고, 저번에 실수로 빼먹은 것으로 생각되는 Weapon변수를 선언합니다.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "CharacterTypes.h"
#include "SlayerCharacter.generated.h"
/*input*/
class UInputMappingContext;
class UInputAction;
/*camera, spring arm*/
class USpringArmComponent;
class UCameraComponent;
/*Equip*/
class AItem;
class AWeapon;
/*Attack*/
class UAnimMontage;
UCLASS()
class SLAYER_API ASlayerCharacter : public ACharacter
{
	GENERATED_BODY()
public:
	ASlayerCharacter();
	virtual void Tick(float DeltaTime) override;
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
	//UFUNCTION(BlueprintCallable)
	void SetWeaponCollision(ECollisionEnabled::Type CollisionEnabled);
protected:
	virtual void BeginPlay() override;
	/*Enhanced input*/ 
	//#include "InputActionValue.h"
	UPROPERTY(EditAnywhere, Category=Input)
	UInputMappingContext* SlayerContext;
	UPROPERTY(EditAnywhere, Category = Input)
	UInputAction* MovementAction;
	UPROPERTY(EditAnywhere, Category = Input)
	UInputAction* JumpAction;
	UPROPERTY(EditAnywhere, Category = Input)
	UInputAction* LookAction;
	void Look(const FInputActionValue& value);
	void Move(const FInputActionValue& value);
	void Jump() override;

	//Equip
	UPROPERTY(VisibleAnywhere, Category = Weapon)
	AWeapon* EquippedWeapon;
	UPROPERTY(EditAnywhere, Category = Input)
	UInputAction* EkeyAction;
	void EkeyPress();
	//Attack
	UPROPERTY(EditAnywhere, Category = Input)
	UInputAction* AttackAction;
	void Attack();
	void PlayAttackMontage();
private:
	/*camera,spring arm*/
	UPROPERTY(VisibleAnywhere)
	USpringArmComponent* CameraBoom;
	UPROPERTY(VisibleAnywhere)
	UCameraComponent* ViewCamera;
	//Equip
	UPROPERTY(VisibleInstanceOnly)
	AItem* OverlappingItem;
	//Attack
	/*Animation montages*/
	UPROPERTY(EditDefaultsOnly, Category = Montages)
	UAnimMontage* AttackMontage;
	//Attack State
	//#include "CharacterTypes.h"
	ECharacterState CharacterState = ECharacterState::ECS_Unequipped;
	//UPROPERTY(BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
	EActionState ActionState = EActionState::EAS_Unoccupied;
public:
	FORCEINLINE void SetOverlappingItem(AItem* Item) { OverlappingItem = Item; }
	FORCEINLINE EActionState SetAttackEndState(){ return ActionState = EActionState::EAS_Unoccupied; }
};

SlayerCharacter.cpp 의 EKeyPress에서  오른손에 장착한 Weapon도 EquippedWeapon에 넣어주고, SetWeaponCollision함수도 정의합니다.

// Fill out your copyright notice in the Description page of Project Settings.
#include "Character/SlayerCharacter.h"
/*input*/
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
/*Rotate*/
#include "GameFramework/CharacterMovementComponent.h"
/*camera, spring arm*/
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
/*Equip*/
#include "Item/Weapons/Weapon.h"
#include "Components/BoxComponent.h"
/*Attack*/
//#include "Animation/AnimMontage.h"
#include "Animation/AnimInstance.h"
// Sets default values
ASlayerCharacter::ASlayerCharacter()
{
 	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	/*spring arm*/
	//#include "GameFramework/SpringArmComponent.h"필요
	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
	CameraBoom->SetupAttachment(GetRootComponent());
	CameraBoom->TargetArmLength = 300.f;
	CameraBoom->bUsePawnControlRotation = true;
	/*CharacterRotate*/
	bUseControllerRotationPitch = false;
	bUseControllerRotationRoll = false;
	bUseControllerRotationYaw = false;
	//#include "GameFramework/CharacterMovementComponent.h" 추가
	GetCharacterMovement()->bOrientRotationToMovement = true;
	/*Camera*/
	ViewCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ViewCamera"));
	ViewCamera->SetupAttachment(CameraBoom);
}

// Called when the game starts or when spawned
void ASlayerCharacter::BeginPlay()
{
	Super::BeginPlay();
	//#include "EnhancedInputSubsystems.h" 추가 필요.
	if (APlayerController* PlayerController = Cast<APlayerController>(GetController())) {
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer())) {
			Subsystem->AddMappingContext(SlayerContext, 0);
		}
	}
}

void ASlayerCharacter::Look(const FInputActionValue& value)
{
	const FVector2D LookVector = value.Get<FVector2D>();
	AddControllerYawInput(LookVector.X);//도리도리
	AddControllerPitchInput(-LookVector.Y);//끄덕끄덕
}

void ASlayerCharacter::Move(const FInputActionValue& value)
{
	if (ActionState != EActionState::EAS_Unoccupied) return;
	const FVector2D MovementVector = value.Get<FVector2D>();
	const FRotator Rotation = Controller->GetControlRotation();
	const FRotator YawRotation(0.f, Rotation.Yaw, 0.f);
	const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
	AddMovementInput(ForwardDirection, MovementVector.Y);//이동
	const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
	AddMovementInput(RightDirection, MovementVector.X);//이동
}

void ASlayerCharacter::Jump()
{
	Super::Jump(); //Character.h에 있는 Jump사용.
}

void ASlayerCharacter::SetWeaponCollision(ECollisionEnabled::Type CollisionEnabled)
{
	if (EquippedWeapon && EquippedWeapon->GetWeaponBox()) {
		EquippedWeapon->GetWeaponBox()->SetCollisionEnabled(CollisionEnabled);
		UE_LOG(LogTemp, Log, TEXT("SetWeaponCollision"));
		EquippedWeapon->IgnoreActors.Empty();
	}
}

void ASlayerCharacter::EkeyPress()
{
	/*Equip*/
	//#include "Item/Weapons/Weapon.h"
	if (AWeapon* Weapon = Cast<AWeapon>(OverlappingItem)) {
		Weapon->Equip(GetMesh(),FName("RightHandSocket"));
		OverlappingItem = nullptr;
		//무기장착.
		CharacterState = ECharacterState::ECS_EquippedOneHandedWeapon;
		EquippedWeapon = Weapon;
	}
}

/*Attack*/
void ASlayerCharacter::Attack()
{
	//공격중이 아니고, 무기 장착중.
	if (ActionState == EActionState::EAS_Unoccupied && CharacterState != ECharacterState::ECS_Unequipped) {
		PlayAttackMontage();
		ActionState = EActionState::EAS_Attacking;
	}
}

void ASlayerCharacter::PlayAttackMontage()
{
	//#include "Animation/AnimInstance.h"
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && AttackMontage) {
		AnimInstance->Montage_Play(AttackMontage);
		const int32 Selection = FMath::RandRange(0,AttackMontage->GetNumSections()-1);
		GEngine->AddOnScreenDebugMessage(1, 2.f, FColor::Cyan, FString::Printf(TEXT("GetSection: %d"), Selection));
		FName SectionName = FName();
		switch (Selection) {
		case 0:
			SectionName = FName("Attack1");
			break;
		case 1:
			SectionName = FName("Attack2");
			break;
		case 2:
			SectionName = FName("Attack3");
			break;
		case 3:
			SectionName = FName("Attack4");
			break;
		case 4:
			SectionName = FName("Attack5");
			break;
		case 5:
			SectionName = FName("Attack6");
			break;
		case 6:
			SectionName = FName("Attack7");
			break;
		case 7:
			SectionName = FName("Attack8");
			break;
		//case 8:
		//	SectionName = FName("Attack9");
		//	break;
		default:
			break;
		}
		//GEngine->AddOnScreenDebugMessage(2, 2.f, FColor::Red, SectionName.ToString());
		AnimInstance->Montage_JumpToSection(SectionName, AttackMontage);
	}
}

// Called every frame
void ASlayerCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

// Called to bind functionality to input
void ASlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	//#include "EnhancedInputComponent.h"추가.
	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent)) {
		EnhancedInputComponent->BindAction(MovementAction, ETriggerEvent::Triggered, this, &ASlayerCharacter::Move);
		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ASlayerCharacter::Jump);
		EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ASlayerCharacter::Look);
		EnhancedInputComponent->BindAction(EkeyAction, ETriggerEvent::Triggered, this, &ASlayerCharacter::EkeyPress);
		EnhancedInputComponent->BindAction(AttackAction, ETriggerEvent::Triggered, this, &ASlayerCharacter::Attack);
	}
}

SlayerAnimInstance.h에서 Notify이벤트를 선언합니다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "SlayerAnimInstance.generated.h"

/**
 * 
 */
UCLASS()
class SLAYER_API USlayerAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
public:
	virtual void NativeInitializeAnimation() override;
	virtual void NativeUpdateAnimation(float DeltaTime) override;
	UPROPERTY(BlueprintReadOnly)
	class ASlayerCharacter* SlayerCharacter;
	UPROPERTY(BlueprintReadOnly, Category = Movement)
	class UCharacterMovementComponent* SlayerCharacterMovement;
	UPROPERTY(BlueprintReadOnly, Category = Movement)
	float GroundSpeed;
	UPROPERTY(BlueprintReadOnly, Category = Movement)
	bool isFalling;
	UFUNCTION()
	void AnimNotify_AttackEnd();
	UFUNCTION()
	void AnimNotify_EnableBoxCollision();
	UFUNCTION()
	void AnimNotify_DisableBoxCollision();
	
};

SlayerAnimInstace.cpp에서 EnableBoxCollision Notify가 발생하면 Collision이 QueryOnly가 되도록 하고, 공격이 끝나면(DisalbeBoxCollision) Collision을 NoCollision이 되도록 정의하였습니다.

// Fill out your copyright notice in the Description page of Project Settings.
#include "Character/SlayerAnimInstance.h"
#include "Character/SlayerCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Kismet/KismetMathLibrary.h"
void USlayerAnimInstance::NativeInitializeAnimation()
{
	Super::NativeInitializeAnimation();
	SlayerCharacter = Cast<ASlayerCharacter>(TryGetPawnOwner());
	if(SlayerCharacter){
		//#include "GameFramework/CharacterMovementComponent.h" 필요.
		SlayerCharacterMovement = SlayerCharacter->GetCharacterMovement();
	}
}
void USlayerAnimInstance::NativeUpdateAnimation(float DeltaTime)
{
	Super::NativeUpdateAnimation(DeltaTime);
	if (SlayerCharacterMovement) {
		//#include "Kismet/KismetMathLibrary.h"
		GroundSpeed = UKismetMathLibrary::VSizeXY(SlayerCharacterMovement->Velocity);
		isFalling = SlayerCharacterMovement->IsFalling();
	}
}
void USlayerAnimInstance::AnimNotify_AttackEnd()
{
	SlayerCharacter->SetAttackEndState();
}
void USlayerAnimInstance::AnimNotify_EnableBoxCollision()
{
	SlayerCharacter->SetWeaponCollision(ECollisionEnabled::QueryOnly);
}
void USlayerAnimInstance::AnimNotify_DisableBoxCollision()
{
	SlayerCharacter->SetWeaponCollision(ECollisionEnabled::NoCollision);
}

결과.

이렇게 수정하니 한번 공격에 여러 번 맞는 문제나 공격도 안 했는데 Hit Reaction애니메이션이 나가는 문제는 사라졌습니다.

근데 이거 계속 작성하면 코드 수정이랑 가독성이 좀 아쉽기도 하고, 어느 정도 힌트는 될 수 있다고 생각하지만 코드 자체는 저만 알아볼 수 있는 코드가 되어버려서...

블로그에 계속 이런 식으로 쓰는 것은 생각 좀 다시 해봐야 될 것 같아요.

Designed by JB FACTORY