초보 코린이의 성장 일지

UE4 Bow Arrow 본문

언리얼

UE4 Bow Arrow

코오린이 2023. 7. 13. 17:18

화살을 만들어서 날린 후 충돌처리까지 구현해 볼 것이다.

 

1. 만들어 놓은 에임 오프셋인 AO_Aim을 최종 애니메이션 전인 블렌딩하고 나서 붙여준다.

2. 그래야만 몽타주까지 포함해서 AO가 진행된다.

1. 이제 조준 후 상하로 이동해도 에임한 자세로 자연스럽게 나오는걸 확인할 수 있다.


1. Actor를 상속받아 화살을 만들기 위해 생성해준다.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CArrow.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FProjectileHit, class AActor*, InCauser, class ACharacter*, InOtherCharacter);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FProjectileEndPlay, class ACArrow*, InDestroyer);

UCLASS()
class U2212_06_API ACArrow : public AActor
{
	GENERATED_BODY()

private:
	UPROPERTY(EditDefaultsOnly, Category = "LifeSpan")
		// 충돌한 후에 몇초 지난 후 사라질지
		float LifeSpanAfterCollision = 1;

private:
	UPROPERTY(VisibleDefaultsOnly)
		class UCapsuleComponent* Capsule;

	UPROPERTY(VisibleDefaultsOnly)
		class UProjectileMovementComponent* Projectile;

public:
	FORCEINLINE void AddIgnoreActor(AActor* InActor) { Ignores.Add(InActor); }

public:	
	ACArrow();

protected:
	virtual void BeginPlay() override;
	// 종료될때 부모에서 소유하고 있는 목록을 제거해주기 위해
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

public:
	void Shoot(const FVector& InForward);

private:
	UFUNCTION()
		void OnComponentHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

public:
	FProjectileHit OnHit; // Hit 이벤트
	FProjectileEndPlay OnEndPlay; // EndPlay에 대한 이벤트

private:
	TArray<AActor*> Ignores;

};
#include "Weapons/AddOns/CArrow.h"
#include "Global.h"
#include "GameFramework/Character.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Components/CapsuleComponent.h"

ACArrow::ACArrow()
{
	PrimaryActorTick.bCanEverTick = true;

	CHelpers::CreateComponent<UCapsuleComponent>(this, &Capsule, "Capsule");
	CHelpers::CreateActorComponent<UProjectileMovementComponent>(this, &Projectile, "Projectile");

	Projectile->ProjectileGravityScale = 0;
	Capsule->BodyInstance.bNotifyRigidBodyCollision = true; // c++에서는 반드시 켜줘야 충돌된다.(블록연산이 일어남)
	Capsule->SetCollisionProfileName("BlockAll"); // Block로 Collision을 처리하면 충돌시 바로 멈춘다.


}

void ACArrow::BeginPlay()
{
	Super::BeginPlay();

	// 시작시에는 충돌을 꺼준다. 화살이 붙어있어야 하므로,
	Capsule->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	Capsule->OnComponentHit.AddDynamic(this, &ACArrow::OnComponentHit); // Hit 이벤트 연결

	Projectile->Deactivate(); // 비활성화
}

void ACArrow::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);

	// 소유자 이벤트를 콜해서 자기자신을 소유자 목록에서 지워준다.
	if (OnEndPlay.IsBound())
		OnEndPlay.Broadcast(this);

}

void ACArrow::Shoot(const FVector& InForward)
{
	Projectile->Velocity = InForward * Projectile->InitialSpeed;
	Projectile->Activate();

	Capsule->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);

}

void ACArrow::OnComponentHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
	SetLifeSpan(LifeSpanAfterCollision);

	for (AActor* actor : Ignores)
		CheckTrue(actor == OtherActor);

	// 화살 충돌 시 충돌 off
	Capsule->SetCollisionEnabled(ECollisionEnabled::NoCollision);

	// hit 이벤트 연결된거 콜 해준다.
	ACharacter* character = Cast<ACharacter>(OtherActor);
	if (!!character && OnHit.IsBound())
		OnHit.Broadcast(this, character);

}

1. 충돌은 이번에 BlockAll로 설정해준다.

2. 화살 충돌시 Hit 이벤트 콜 해준다.

3. 충돌이 끝나면 꺼주고 반복.


#pragma once

#include "CoreMinimal.h"
#include "Weapons/CDoAction.h"
#include "CDoAction_Bow.generated.h"

UCLASS(Blueprintable)
class U2212_06_API UCDoAction_Bow : public UCDoAction
{
	GENERATED_BODY()

private:
    UPROPERTY(EditDefaultsOnly, Category = "Arrow")
        TSubclassOf<class ACArrow> ArrowClass;

public:
    UCDoAction_Bow();

    void BeginPlay
    (
        class ACAttachment* InAttachment,
        class UCEquipment* InEquipment,
        class ACharacter* InOwner,
        const TArray<FDoActionData>& InDoActionData,
        const TArray<FHitData>& InHitData
    ) override;

    void DoAction() override;
    void Begin_DoAction() override;
    void End_DoAction() override;

    void OnBeginEquip() override;
    void OnUnequip() override;

    void Tick(float InDeltaTime) override;

public:
    void End_BowString();

private:
    void CreateArrow(); // 화살 생성
    class ACArrow* GetAttachedArrow(); // 활에 붙어있는거 찾아오기

private:
    // 화살에 연결할 이벤트들,
    UFUNCTION()
        void OnArrowHit(class AActor* InCauser, class ACharacter* InOtherCharacter);

    UFUNCTION()
        void OnArrowEndPlay(class ACArrow* InDestroyer)
private:
    TArray<class ACArrow*> Arrows; // 화살에 대한 목록
};
#include "Weapons/DoActions/CDoAction_Bow.h"
#include "Global.h"
#include "Weapons/CEquipment.h"
#include "Weapons/Attachments/CAttachment_Bow.h"
#include "Weapons/AddOns/CArrow.h"
//#include "Weapons/AddOns/CProjectile.h"
#include "GameFramework/Character.h"
#include "Components/PoseableMeshComponent.h"
#include "Components/CStateComponent.h"
#include "Components/CMovementComponent.h"

UCDoAction_Bow::UCDoAction_Bow()
{


}

void UCDoAction_Bow::BeginPlay(ACAttachment* InAttachment, UCEquipment* InEquipment, ACharacter* InOwner, const TArray<FDoActionData>& InDoActionData, const TArray<FHitData>& InHitData)
{
	Super::BeginPlay(InAttachment, InEquipment, InOwner, InDoActionData, InHitData);

	SkeletalMesh = CHelpers::GetComponent<USkeletalMeshComponent>(InAttachment);
	PoseableMesh = CHelpers::GetComponent<UPoseableMeshComponent>(InAttachment);

	ACAttachment_Bow* bow = Cast<ACAttachment_Bow>(InAttachment);
	Bending = bow->GetBend(); // 활이 휘는 정도

	// 위치를 우선 저장
	OriginLocation = PoseableMesh->GetBoneLocationByName("bow_string_mid", EBoneSpaces::ComponentSpace);

	bEquipped = InEquipment->GetEquipped();

}

void UCDoAction_Bow::DoAction()
{
	CheckFalse(State->IsIdleMode());
	CheckFalse(State->IsSubActionMode());

	Super::DoAction();

	DoActionDatas[0].DoAction(OwnerCharacter);

}

void UCDoAction_Bow::Begin_DoAction()
{
	Super::Begin_DoAction();

	bAttachedString = false; // 발사하면 활줄에서 떨어져야하므로,

	*Bending = 0;
	// 활줄 다시 돌아오게,
	PoseableMesh->SetBoneLocationByName("bow_string_mid", OriginLocation, EBoneSpaces::ComponentSpace);

	CheckNull(ArrowClass);

	ACArrow* arrow = GetAttachedArrow(); // Attach된걸 먼저 가져온다.
	// 그다음 Detach 시켜준다, KeepWorld로 해줘야 World 공간인 손에서 화살이 떨어질 것이다.
	arrow->DetachFromActor(FDetachmentTransformRules(EDetachmentRule::KeepWorld, true));

	// 자신꺼 이벤트 연결
	arrow->OnHit.AddDynamic(this, &UCDoAction_Bow::OnArrowHit);
	arrow->OnEndPlay.AddDynamic(this, &UCDoAction_Bow::OnArrowEndPlay);

	// forward 방향으로 발사
	FVector forward = FQuat(OwnerCharacter->GetControlRotation()).GetForwardVector();
	arrow->Shoot(forward);

}

void UCDoAction_Bow::End_DoAction()
{
	Super::End_DoAction();

	CreateArrow(); // 쏘고나서 다시 생성
}

void UCDoAction_Bow::OnBeginEquip()
{
	Super::OnBeginEquip();

	// 충돌을 꺼준다.
	OwnerCharacter->GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);

	CreateArrow();
}

void UCDoAction_Bow::OnUnequip()
{
	Super::OnUnequip();

	// 값 0으로 돌려줘서 활을 제자리로 돌려놓는다.
	*Bending = 0;
	//  충돌체를 다시 켜준다.
	OwnerCharacter->GetMesh()->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
	// 원래 위치로 활줄을 다시 돌려준다.
	PoseableMesh->SetBoneLocationByName("bow_string_mid", OriginLocation, EBoneSpaces::ComponentSpace);

	// 화살 역으로 찾아서 붙어있는거 지워준다.
	for (int32 i = Arrows.Num() - 1; i >= 0; i--)
	{
		if (!!Arrows[i]->GetAttachParentActor())
			Arrows[i]->Destroy();
	}
}

void UCDoAction_Bow::Tick(float InDeltaTime)
{
	Super::Tick(InDeltaTime);

	// 항상 캡처
	PoseableMesh->CopyPoseFromSkeletalComponent(SkeletalMesh);

	// 활줄이 붙을 조건 체크
	bool bCheck = true;
	bCheck &= (*bEquipped == true);
	bCheck &= (bBeginAction == false); // 액션에 들어가면 붙으면 안된다.
	bCheck &= (bAttachedString == true);

	CheckFalse(bCheck); // 위에 조건이 성립하지 않으면 아래 코드 x

	// 손에다가 붙여주기.
	FVector handLocation = OwnerCharacter->GetMesh()->GetSocketLocation("Hand_Bow_Right");
	PoseableMesh->SetBoneLocationByName("bow_string_mid", handLocation, EBoneSpaces::WorldSpace);

}

void UCDoAction_Bow::End_BowString()
{
	*Bending = 100;
	bAttachedString = true; // 활 줄을 붙여준다.

}

void UCDoAction_Bow::CreateArrow()
{
	// true면 World가 종료되었다는 의미, 월드가 종료되는 조건은 게임이 끝나거나 맵 이동을 했을때,
	if (World->bIsTearingDown == true)
		return;

	FTransform transform;
	ACArrow* arrow = World->SpawnActorDeferred<ACArrow>(ArrowClass, transform, NULL, NULL, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
	CheckNull(arrow);

	arrow->AddIgnoreActor(OwnerCharacter);

	FAttachmentTransformRules rule = FAttachmentTransformRules(EAttachmentRule::KeepRelative, true);
	arrow->AttachToComponent(OwnerCharacter->GetMesh(), rule, "Hand_Bow_Right_Arrow");

	// 지연 시켜서 생성하기.
	Arrows.Add(arrow);
	UGameplayStatics::FinishSpawningActor(arrow, transform);

}

ACArrow* UCDoAction_Bow::GetAttachedArrow()
{
	// 붙어있는거 가져오기
	for (ACArrow* projectile : Arrows)
	{
		if (!!projectile->GetAttachParentActor())
			return projectile;
	}

	return nullptr;

}

void UCDoAction_Bow::OnArrowHit(AActor* InCauser, ACharacter* InOtherCharacter)
{
	CheckFalse(HitDatas.Num() > 0);

	// 데미지 주기
	HitDatas[0].SendDamage(OwnerCharacter, InCauser, InOtherCharacter);

}

void UCDoAction_Bow::OnArrowEndPlay(ACArrow* InDestroyer)
{
	// 다쐈으면 제거.
	Arrows.Remove(InDestroyer);

}

1. 화살을 찾아서 Player 전방방향으로 발사.

2. 연결된 이벤트를 콜해서 Hit 시켜준다.

3. 제거 후 다시 생성 반복.


1. 이제 화살을 BP로 추가해준다.

1. StaticMesh를 추가해주고, Mesh를 설정해준다.

2. 적당한 위치로 설정 후 충돌체와 맞춰준다. 충돌체 기준으로 화살이 움직여서 맞춰줘야 한다.

3. Projectile Speed도 1000으로 설정해줘야 정상적으로 나가게된다.

4. Static Mesh 콜리전에서 NoCollision으로 설정.

1. DoAction_Bow를 상속받아서 클래스를 생성해준다.

1. DoActionClass에 만들어준 Bow로 선택해준다.

1. Hit 몽타주도 설정해준다.


 

https://www.youtube.com/watch?v=do3-wJJFfE8 

 

'언리얼' 카테고리의 다른 글

UE4 Parkour Climb  (0) 2023.07.19
UE4 Parkour  (0) 2023.07.18
UE4 Bow Aim Bend  (0) 2023.07.10
UE4 Bow Aim (2)  (0) 2023.07.07
UE4 Bow Aim  (0) 2023.07.06
Comments