필요한 개념
Aim을 담당하는 클래스를 일반 클래스로 만들어 Aim의 기능을 모아둘 것임
사실 1. CAimComponent를 만들어서 Aim을 사용할 애들한테 줘도 된다.
그렇지만 이번엔 안써본 방식인 2. 일반 클래스 방식으로 한다.
플레이어는 오른쪽 마우스를 클릭하여 Aim을 갖지만, 적은 Aim을 가지지 않는다. 플레이어와 적의 구성을 생각해봐야 함
* CDoAction_Throw 클래스 생성
* BP_CAttachemnt_IceBall 생성
BP_CAttachemnt_IceBall는 존재만하도록 함(따로 뭐 하진 않음)
손에다가 얼음 덩이 안붙이는게 더 자연스러움
* DA_IceBall 생성 및 값 세팅
- PawnControl true로 정면 바라보게
* IceBall_Montage 몽타주 세팅
- 달리면서 할거니까 UpperBody
- 공격 시 BeginAction()
- 끝날시에 EndAction() 추가
* 프로젝트 세팅 -> 액션 매핑 입력 추가
IceBall, 4번 키로
* CActionComponent에 IceBall 모드 추가
UENUM(BlueprintType)
enum class EActionType: uint8
{
Unarmed, Fist, OneHand, TwoHand, Warp, FireStorm, IceBall, Max,
};
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsIceBallMode() { return Type == EActionType::IceBall; }
void UCActionComponent::SetIceBallMode()
{
SetMode(EActionType::IceBall);
}
* CPlayer 입력 키 추가
PlayerInputComponent->BindAction("IceBall", EInputEvent::IE_Pressed, this, &ACPlayer::OnIceBall);
void ACPlayer::OnIceBall()
{
CheckFalse(State->IsIdleMode());
Action->SetIceBallMode();
}
* BP_CPlayer에 Action에 DA_IceBall 지정
* ABP_CPlayer에서 Wizard 애니메이션 설정해서, IceBall 모드때 세팅
<Aim 작업 세팅>
* 프로젝트 세팅 -> 입력 -> 액션매핑 Aim 추가
마우스 오른쪽 버튼
일반 클래스 Aim으로 만듬
Aim은 UObject(최상위)로 부터 상속받아 구현함
컴포넌트로 처리해도 무방하나 UObject로 하는 방법을 다뤄봄
* UObejct를 상속받는 CAim 클래스 생성
Aiming을 담당하는 작업을 수행
UCAim();
// 얘는 AActor로 부터 상속받는 클래스가 아니어서 BeginPlay()가 없음
void BeginPlay(class ACharacter* InCharacter);
void OnZoom();
void OffZoom();
// Tick()도 없음
void Tick(float DeltaTime);
private:
class ACharacter* OwnerCharacter;
class UStateComponent* State;
class USpringArmComponent* SpringArm;
// FOV 조정을 위해
class UCameraComponent* Camera;
Throw 뿐만 아닌 어떤 무기도 Aiming을 가진다는 전제로 작업을 진행
* CDoAction에 추가
// Aiming이 필요한 애들은 이것을 재정의
virtual void OnAim() {}
virtual void OffAim() {}
* CDoAction_Throw에 추가
// 재정의
virtual void OnAim() override;
virtual void OffAim() override;
private:
class UCAim* Aim;
// BeginPlay()
// UObject로 상속 받지 않는 일반애들은 이렇게 가능하지만..
// Aim = new UCAim();
// NewObject : BeginPlay 이후에 동적할당을 수행하며
// 가비지 컬렉션에 의해 자동으로 제거되도록 해주는 함수(즉 관리받는 애)
Aim = NewObject<UCAim>();
Aim->BeginPlay(OwnerCharacter);
// 가비지 컬렉터에 별로 의존하고 싶지 않고, 가비지 컬렉션은 일정 모아서
// 지우기 때문에, 지우는 속도가 느림
// 지우는 부분만 스레드로 처리하는 경우도 있지만
// 바로 지우는 명령어도 있다.
// ConditionalBeginDestroy() : 가비지 컬렉터에 등재, 가비지 컬렉터 이후의 삭제가 됨
// ConditionalFinishDestroy() : 가비지 컬렉터에 등재, 가비지 컬렉터를 동작시켜서 바로 지움, 얘는 언제든지 바로 삭제 가능함
// Aim->ConditionalFinishDestroy();
void ACDoAction_Throw::OnAim()
{
Aim->OnZoom();
}
void ACDoAction_Throw::OffAim()
{
Aim->OffZoom();
}
void ACDoAction_Throw::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
Aim->Tick(DeltaTime);
}
* CPlayer에 Aim 액션매핑 입력 추가
Tip) OnAim(), OffAim()은 CActionComponent가 가지고 있는 ActionData의 DoAction에 있다.
* CActionComponent에 추가
void DoAim(); // 에임 실행
void UndoAim(); // 에임 해제
private:
// 에임 수행여부를 bool 타입으로
// 내부함수
void SetAim(bool InAim);
void UCActionComponent::DoAim()
{
SetAim(true);
}
void UCActionComponent::UndoAim()
{
SetAim(false);
}
void UCActionComponent::SetAim(bool InAim)
{
// 액션이 있는 상태에서만 수행
CheckTrue(IsUnarmedMode());
if (!!Datas[(int32)Type])
{
ACDoAction* action = Datas[(int32)Type]->GetDoAction();
if (!!action)
InAim ? action->OnAim() : action->OffAim(); // Aim이 없는 애들은 넘어가고, Aim이 있는 즉, 재정의 된 애들만 수행됨
}
}
Tip) Aim은 모드가 아니다.
* CPlayer에 Aim 입력 처리
PlayerInputComponent->BindAction("Aim", EInputEvent::IE_Pressed, this, &ACPlayer::OnAim); // 눌렀을 때
PlayerInputComponent->BindAction("Aim", EInputEvent::IE_Released, this, &ACPlayer::OffAim); // 키를 떼었을 때
void ACPlayer::OnAim()
{
Action->DoAim();
}
void ACPlayer::OffAim()
{
Action->UndoAim();
}
이번에는 Timeline도 C에서 처리한다.
CAim에 추가
bool bInZoom; // 현재 줌이 되고 있는지
FORCEINLINE bool IsAvaliable() { return SpringArm != NULL && Camera != NULL; }
FORCEINLINE bool IsZoom() { return bInZoom; }
void UCAim::BeginPlay(ACharacter* InCharacter)
{
OwnerCharacter = InCharacter;
// 스프링암이 있는 여부에 따라 에임이 가능한 캐릭터 인지 판단할 것이다.
// 적 같은 경우에는 Aiming을 할 필요가 없기 때문에
SpringArm = CHelpers::GetComponent<USpringArmComponent>(OwnerCharacter);
Camera = CHelpers::GetComponent<UCameraComponent>(OwnerCharacter);
State = CHelpers::GetComponent<UCStateComponent>(OwnerCharacter);
}
void UCAim::OnZoom()
{
// Aim이 가능한(SpringArm과 Camera 컴포넌트를 가지고 있는) 상황에서만 수행
CheckFalse(IsAvaliable());
CheckTrue(bInZoom); // 현재 줌이 되고 있는 상황이면 실행하면 안됨
bInZoom = true;
SpringArm->TargetArmLength = 100.0f;
SpringArm->SocketOffset = FVector(0, 30, 10);
// bEnableCameraLag : 카메라를 부드럽게 이동하는 처리
// 이거 키면 시야를 땡겨놓으면 좀 뒤에 나타남, 그래서 끔
SpringArm->bEnableCameraLag = false;
Camera->FieldOfView = 45;
}
void UCAim::OffZoom()
{
CheckFalse(bInZoom); // 줌 상태가 아니라면 빠져나옴
bInZoom = false;
SpringArm->TargetArmLength = 250.0f;
SpringArm->SocketOffset = FVector(0, 0, 0);
SpringArm->bEnableCameraLag = true;
Camera->FieldOfView = 90;
}
여기까지 결과를 보면 뚝뚝 끊긴다.
-> 그래서 우리는 커브를 사용했다.
* C_Aim CurveBP 생성
이전에서는 0 ~ 0.1초 가는 것을 했다.
이번에는 0 ~ 10 초까지를 해서
- 0초일때 FOV 90도, 10초일때는 FOV 45도로 조정
- 부드럽게 자동 탄젠트 적용해주면 된다.
10초 동안 FOV 90 ~ 45까지 바뀜
* CAim에서는 이 커브 객체를 불러와서 쓸 것임
private:
class UCurveFloat* Curve; // 내부적으로 상수처럼 불러서 고정
UCAim::UCAim()
{
// UCAim 클래스의 객체가 다른클래스의 생성자에서 생성된다면 GetAsset()
// BeginPlay()에서 호출된다면 GetAssetDynamic()을 사용해야 한다.
// 지금은 CDoAction_Throw에서는 BeginPlay()에서 이 생성자를 호출한다.
CHelpers::GetAssetDynamic<UCurveFloat>(&Curve, "CurveFloat'/Game/Actions/C_Aim.C_Aim'");
}
<타임라인 생성>
타임라인을 BP로 다뤄왔지만, 여기서는 직접 C로 다뤄본다.
#include "Components/TimelineComponent.h"
으로 해서 컴포넌트로 다뤄도 되지만, 여기서는 Timeline으로만 다룬다.
* CAim.h에 추가
#include "Components/TimelineComponent.h"
FTimeline Timeline;
FOnTimelineFloat TimelineFloat; // 타임라인 동안 호출될 함수가 연결된 델리게이트
private:
UFUNCTION()
void Zooming(float Output);
// BeginPlay()
// AddInterpFloat() : 타임라인이 실행될 동안 호출될 델리게이션 함수를 연결하는 함수
// 함수 오버로딩된 2개가 있는데 그 중 파라미터 설명
// FOnTimeLineFloatStatic : 일반 델리게이트
// FOnTimeLineFloat : 다이나믹 델리게이트
// 얘는 커브에 의해서 값이 콜이 될건데, 들어올 함수가 있어야 하는데
// 그 함수를 연결한다는 의미
// BP에서는 Timeline함수를 콜해서 값을 리턴받아 왔지만, C에서는
// 델리게이션을 이용
TimelineFloat.BindUFunction(this, "Zooming");
Timeline.AddInterpFloat(Curve, TimelineFloat);
// SetPlayRate를 이용해 속도를 조정
// 이거 안해주면 딱딱 끊김
Timeline.SetPlayRate(200);
// OnZoom()
// Play()는 멈추게 되면, 멈춘부분 부터 실행됨
// PlayFromStart()는 멈추게 되면, 시작부터 실행됨
Timeline.PlayFromStart();
// OffZoom()
// 이것도 Reverse()면 중간에 멈추면 이어서 되지만,
// ReverseFromEnd()는 중간에 멈추면 다시 역에서 재생
Timeline.ReverseFromEnd();
// Tick()
// Tineline에 Tick에 델타타임을 계속 넘겨줘야함
Timeline.TickTimeline(DeltaTime);
// 커브에서 지정한 값이 계속 들어오게 됨
void UCAim::Zooming(float Output)
{
Camera->FieldOfView = Output;
}
<HUD 생성 -> CrossHair 생성 위해>
WidgetBlueprint를 생성하면 기본으로 CanvasPanel이 생김
CanvasPanel : 화면(게임 실행한 화면인 Viewport)과 1 : 1로 매칭시켜줌
UIWidget을 직접 디자인해서 출력할수도 있음
프로젝트 세팅 -> 프로젝트 -> 맵&모드에 보면 GameMode에 HUD Class 지정하는 게 있다.
HUD : 화면 앞에 나타나는 클래스를 의미하며, 이를 통해 UI를 코드를 통해 디자인이 가능함
게임화면이 뒤에 있고, 메인화면 바로 앞이 HUD
* AHUD를 상속받는 CHUD 클래스 생성
UserWidget은 직접 디자인 했지만, HUD는 코드로 디자인 한다.
얘도 최종적으로는 BP로 만들어서 사용할 것임
* Material의 BlendMode 및 FCanvasItem의 BlendMode 수식 정리
// FCanvasItem의 BlendMode
enum ESimpleElementBlendMode
{
SE_BLEND_Opaque = 0,
SE_BLEND_Masked,
SE_BLEND_Translucent,
SE_BLEND_Additive,
SE_BLEND_Modulate,
SE_BLEND_MaskedDistanceField,
SE_BLEND_MaskedDistanceFieldShadowed,
SE_BLEND_TranslucentDistanceField,
SE_BLEND_TranslucentDistanceFieldShadowed,
SE_BLEND_AlphaComposite,
SE_BLEND_AlphaHoldout,
SE_BLEND_AlphaBlend,
SE_BLEND_TranslucentAlphaOnly,
SE_BLEND_TranslucentAlphaOnlyWriteAlpha,
SE_BLEND_RGBA_MASK_START,
SE_BLEND_RGBA_MASK_END = SE_BLEND_RGBA_MASK_START + 31,
SE_BLEND_MAX,
}
Material의 BlendMode와 유사함
Material도 이미 그려진 거에 조합이 될때 어떻게 섞일 건지, 약간의 공식이 있음
먼저 게임화면이 그려지고, 나중에 HUD가 그려진다.
HUD가 그려질 때도 Material과 마찬가지로 그러진 거에 조합이 될떄 어떻게 섞일 건지
AlphaBlending 식
Src(0, 0, 1) * Alpha(0.5) op Dest(1, 0, 0) * ZeroSrcAlpha(1 - 0.5)
Src * 0.5 op Dest * 0.5
(0, 0 ,1) * 0.5 + (1, 0, 0) * 0.5 = (0.5, 0, 0.5) -> 마젠타 색이 나옴
이 가운데 op 공식이 어떤게 들어가느냐에 따라 달라짐
보통은 op를 더하기(+)를 준다.
DirectX의 BlendState와 개념이 같음(AlphaBlend 개념)
Material의 BlendMode 중에 Translucent(투명한)이 있다.
-> 언리얼에서는 이게 AlphaBlending이다.(반투명을 만들어줌)
* FCanvasItem의 BlendMode에서는 SE_BLEND_AlphaBlend, SE_BLEND_Translucent의 차이
차이는 SE_BLEND_AlphaBlend는 위의 수식이 맞고, SE_BLEND_Translucent는 일정 투명값 이하는 아예 그리지 않음
SE_BLEND_Translucent : Alpha값이 특정 값 이하일시 그리지 않겠다는 의미 -> 일정 투명값 이하는 아예 그리지 않겠다는 의미
SE_BLEND_Modulate : Modulate(곱한다는 의미)
* SE_BLEND_Modulate 수식
Src * Dest + Dest * Zero(그려진 색은 버리겠다는 의미 : 화면)
-> Src * Dest만 남음
SE_BLEND_Opaque(불투명) : 그려져 있는 색은 무시하고 현재 그려질 색만 그림
* SE_BLEND_Opaque 수식
Src * One + Dest * Zero
-> Src 원본 값만 남음(원본만 그려짐)
Material의 BlendMode의 Masked는 저격모드의 앞에 동그란데 제외하고 나머지 검은색 부분 가려짐
그럴 때 사용
Material의 BlendMode의 Addtive는 2개를 더하라는 옵션(색이 더 밝아짐)
물감은 섞으면 섞을 수록 검어짐(->감선 혼합)
컴퓨터는 더하면 더할수록 값이 밝아짐(->가산 혼합)
정규화로 1로 떨어지는 색상값이다.
(2, 1, 0) 이라면 -> (1, 0.5, 0) 비율로 1로 맞춰서 떨어트림
곱할 수 있는것이 1보다 작은 값이다.(큰 값은 곱해도 1로 맞춰서 떨어짐)
그래서 어떤 값의 색을 어둡게 만들기 위해서는 곱해주고(1로 정규화가 되니까)
어떤 색의 값을 밝게 만들기 위해서는 더해준다.
(1, 0.8, 0) + (1, 0.2, 0) -> (2, 1, 0) -> 값이 1로 줌(1, 0.5, 0)
결국 더하면 앞의 값이 밝아짐
* Addtive 수식
Src * One + Dest * One
-> Src + Dest(두개의 색이 합해져서 더 밝아짐)
Material의 BlendMode의 AlphaComposite, AlphaHoldout
-> 이 두 개는 이전의 알파값을 유지한다는 내용, 컴퓨터가 색상을 표현하는 체계를 이해해야 해서 건너뜀
T_CrossHair Texture2D를 보면
빨간색 부분은 Alpah값이 1이고, 그 외의 회색 배경부분들은 Alpha값이 0(투명)이다.
우리는 SE_BLEND_Translucent로 그려버리면 됨(굳이 SE_BLEND_AlphaBlend를 쓰지 않아도 됨)
SE_BLEND_Translucent -> 특정 알파값 이하는 그리지 마라
* CHUD에 추가
UPROPERTY(EditDefaultsOnly)
class UTexture2D* Texture;
public:
FORCEINLINE void SetDrawMode() { bDraw = true; }
FORCEINLINE void SetUndrawMode() { bDraw = false; }
public:
ACHUD();
// 부모에 있음
// HUD를 그리기 위한 코드가 들어감(매프레임 마다 동작)
virtual void DrawHUD() override;
private:
bool bDraw; // 허드를 그릴 상황인지
ACHUD::ACHUD()
{
// 디폴트 텍스쳐 지정
CHelpers::GetAsset<UTexture2D>(&Texture, "Texture2D'/Game/Textures/T_Crosshair.T_Crosshair'");
}
void ACHUD::DrawHUD()
{
// 오버라이딩은 항상 부모 콜
Super::DrawHUD();
// 그릴 수 있는 상황에 그림
CheckFalse(bDraw);
// 텍스쳐가 있어야 그릴 수 있음
CheckNull(Texture);
// 화면은 2D로 이루어져서 FVector2D를 사용
// 위젯 블루프린트의 Canvas 패널과 여기에서 사용되는 Canvas 객체가
// 같은 개념으로 사용됨
// Canvas는 부모의 정의됨
// ClipX : 화면의 넓이, ClipY : 화면의 높이
// 반을 나눠서 중앙지점을 구함
FVector2D center(Canvas->ClipX * 0.5f, Canvas->ClipY * 0.5f);
// GetSurfaceWidth() : 텍스쳐의 크기
FVector2D size(Texture->GetSurfaceWidth() * 0.5f, Texture->GetSurfaceHeight() * 0.5f);
// 캔버스(화면) 센터에서 텍스쳐 사이즈의 반만큼 위, 왼쪽으로 가야 이미지가 화면의 정중앙의 위치하게 됨
FVector2D position = center - size;
// Canvas는 Item이 굉장히 많음
// FCanvasTileItem : 타일종류로 까는 것임
// 1. 그릴 위치, 2. 어떤 텍스쳐를 그릴건지 3. 어떤 색상으로 그릴건지(이미지 재활용 가능하도록 있는 색상 값)
FCanvasTileItem item(position, Texture->Resource, FLinearColor::White);
// 보통 디자이너들이 이미지를 재활용하기 위해서 빨간색 텍스쳐라면, 흰색칠을 해놔서
// 상황에 따라 3번인자를 사용함, 텍스쳐의 색상 * 3번의 색상을 곱해서 화면에 나옴(ex) Red * White = Red)
// 그린을 출력하고 싶다면 3번인자에 그린을 주면 됨
// BlendMode 위의 설명 참고
item.BlendMode = SE_BLEND_Translucent;
// Item을 그려라
Canvas->DrawItem(item);
}
* BP_CHUD 생성
*CGameMode에 세팅
CHelpers::GetClass<AHUD>(&HUDClass, "Blueprint'/Game/BP_CHUD.BP_CHUD_C'");
* Aiming 상태에서 Draw(그려주도록) 해줄 수 있도록 세팅
CHUD를 가져와서 Draw 상태를 해줘야함
CAim에 추가
class ACHUD* HUD;
// BeginPlay()
// 플레이어가 있는 World를 가져오고 플레이어의 컨트롤러를 가져오고 HUD를 가져옴
// GetHUD()는 템플릿형(바로 원하는 형태로 가져옴)
HUD = OwnerCharacter->GetWorld()->GetFirstPlayerController()->GetHUD<ACHUD>();
// OnZoom()
HUD->SetDrawMode();
// OffZoom()
HUD->SetUndrawMode();
* Throw를 담당할 클래스 만듬
Actor를 상속받는 CThrow
어떤걸 던질지, 맞았을시 어떤 폭발이 일어날지를 위해
따로 BP로 만들어서(어떤 위치에서 터질지, 어떤 파티클이 일어날지도 설정 가능)
구조가 SphereComponent에 OnComponentBeginOverlap에 AddDynamic으로 우리가 정의한 함수 바인딩해주고
충돌될시에 우리가 정의한 함수로 들어올 것이다. 여기서 OnThrowBeginOverlap에 바인딩된 함수들이 전부 실행될 것이다. 인자에는 충돌결과인 FHitResult를 넘긴다.
이렇게 하면 CThrow를 가지고 있는 클래스에서 충돌된 결과를 받을 수 있음
CThrow에 추가
// 충돌 처리할 델리게이트
// CThrow 생성하는 쪽에서 이벤트를 연결해줄 것이다.(즉 여기선 CDoAction_Throw에 다가)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FThrowBeginOverlap, FHitResult, InHitResult);
private:
UPROPERTY(EditDefaultsOnly)
class UParticleSystem* Explosion; // 충돌했을때 터질 이펙트
UPROPERTY(EditDefaultsOnly)
FTransform ExplosionTransform; // Explosion의 SRT 조정위해
private:
UPROPERTY(VisibleDefaultsOnly)
class USphereComponent* Sphere; // 날라갈 충돌체
UPROPERTY(VisibleDefaultsOnly)
class UParticleSystemComponent* Particle; // 날라가면서 Particle
private:
// 날라갈거니까 ProjectileMovementComponent
UPROPERTY(VisibleDefaultsOnly)
class UProjectileMovementComponent* Projectile;
private:
UFUNCTION()
void OnComponentBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
public:
UPROPERTY(BlueprintAssignable)
FThrowBeginOverlap OnThrowBeginOverlap;
ACThrow::ACThrow()
{
CHelpers::CreateComponent<USphereComponent>(this, &Sphere, "Sphere");
CHelpers::CreateComponent<UParticleSystemComponent>(this, &Particle, "Particle", Sphere);
CHelpers::CreateActorComponent<UProjectileMovementComponent>(this, &Projectile, "Projectile");
// Life 가능 시간 : 3초
InitialLifeSpan = 3.0f;
Projectile->InitialSpeed = 4000.0f;
Projectile->MaxSpeed = 8000.0f;
Projectile->ProjectileGravityScale = 0.0f; // 0으로 줘서 정면으로 날아갈 수 있도록
}
void ACThrow::BeginPlay()
{
Super::BeginPlay();
TArray<USphereComponent*> components;
// 현재는 CThrow에 충돌체 하나만 넣지만, 우리가 작업하면서 여러개의 충돌체를 사용할 수도 있으므로
// 여러개의 충돌체를 검색하는 방식으로 수행
GetComponents<USphereComponent>(components);
for (USphereComponent* sphere : components)
sphere->OnComponentBeginOverlap.AddDynamic(this, &ACThrow::OnComponentBeginOverlap);
}
void ACThrow::OnComponentBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (!!Explosion)
{
FTransform transform = ExplosionTransform;
// 충돌 결과 위치에 더해줌
transform.AddToTranslation(SweepResult.Location);
// 충돌한 면의 수직벡터로 회전을 처리해서 폭발이 자연스럽게 보이도록 함
transform.SetRotation(FQuat(SweepResult.ImpactNormal.Rotation()));
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), Explosion, transform);
if (OnThrowBeginOverlap.IsBound())
OnThrowBeginOverlap.Broadcast(SweepResult);
// 지워줌
Destroy();
}
}
* BP_CThrow_IceBall 생성
- Explosion, ExplosionTransform 변수 값 세팅
- Particle 값 세팅
* CDoAction_Throw에 추가
// DoAction()
// Aiming이 가능한 상황이라면 Zoom 상태에서만 발사해야 한다.
if (Aim->IsAvaliable())
CheckFalse(Aim->IsZoom());
<Throw 클래스를 던지기 위한 과정>
* 스켈레톤에 던지기 위해 Hand_Throw_Projectile 소켓 추가
* CActionData의 FDoActionData 구조체에 추가 Throw할 클래스 추가
// 어떤 Throw 객체를 지정해서 던질건지
UPROPERTY(EditAnywhere)
TSubclassOf<class ACThrow> ThrowClass;
* CDoAction_Throw에 추가
void ACDoAction_Throw::Begin_DoAction()
{
FVector location = OwnerCharacter->GetMesh()->GetSocketLocation("Hand_Throw_Projectile");
// 플레이어의 전방 or 적의 전방
FRotator rotator = OwnerCharacter->GetController()->GetControlRotation();
FTransform transform = Datas[0].EffectTransform;
transform.AddToTranslation(location);
transform.SetRotation(FQuat(rotator));
FActorSpawnParameters params;
params.Owner = this; // 스폰한 액터
// CThrow의 GetWorld()로 사용해도 무방하나 캐릭터가 배치되어 있는 월드에 스폰
ACThrow* throwObject = OwnerCharacter->GetWorld()->SpawnActor<ACThrow>(Datas[0].ThrowClass, transform, params);
throwObject->OnThrowBeginOverlap.AddDynamic(this, &ACDoAction_Throw::OnThrowBeginOverlap);
}
void ACDoAction_Throw::OnThrowBeginOverlap(FHitResult InHitResult)
{
// 데미지만 준다.
FDamageEvent e;
InHitResult.GetActor()->TakeDamage(Datas[0].Power, e, OwnerCharacter->GetController(), this);
}
CActionComponent.h 추가된 내용
...
UENUM(BlueprintType)
enum class EActionType: uint8
{
Unarmed, Fist, OneHand, TwoHand, Warp, FireStorm, IceBall, Max,
};
...
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class UONLINE_04_ACTION_API UCActionComponent : public UActorComponent
{
GENERATED_BODY()
...
public:
...
UFUNCTION(BlueprintPure)
FORCEINLINE bool IsIceBallMode() { return Type == EActionType::IceBall; }
public:
...
void SetIceBallMode();
public:
...
void DoAim(); // 에임 실행
void UndoAim(); // 에임 해제
private:
// 에임 수행여부를 bool 타입으로
// 내부함수
void SetAim(bool InAim);
};
CActionComponent.cpp 추가된 내용
...
void UCActionComponent::SetIceBallMode()
{
SetMode(EActionType::IceBall);
}
void UCActionComponent::DoAim()
{
SetAim(true);
}
void UCActionComponent::UndoAim()
{
SetAim(false);
}
void UCActionComponent::SetAim(bool InAim)
{
// 액션이 있는 상태에서만 수행
CheckTrue(IsUnarmedMode());
if (!!Datas[(int32)Type])
{
ACDoAction* action = Datas[(int32)Type]->GetDoAction();
if (!!action)
InAim ? action->OnAim() : action->OffAim();
}
}
CDoAction_Throw.h
#pragma once
#include "CoreMinimal.h"
#include "Actions/CDoAction.h"
#include "CDoAction_Throw.generated.h"
UCLASS()
class UONLINE_04_ACTION_API ACDoAction_Throw : public ACDoAction
{
GENERATED_BODY()
protected:
virtual void BeginPlay() override;
public:
virtual void DoAction() override;
virtual void Begin_DoAction() override;
virtual void End_DoAction() override;
virtual void OnAim() override;
virtual void OffAim() override;
virtual void Tick(float DeltaTime) override;
private:
UFUNCTION()
void OnThrowBeginOverlap(FHitResult InHitResult);
private:
class UCAim* Aim;
};
CDoAction_Throw.cpp
#include "CDoAction_Throw.h"
#include "Global.h"
#include "CAim.h"
#include "CThrow.h"
#include "GameFramework/Character.h"
#include "Components/CStateComponent.h"
#include "Components/CStatusComponent.h"
void ACDoAction_Throw::BeginPlay()
{
Super::BeginPlay();
// UObject로 상속 받지 않는 일반애들은 이렇게 가능하지만..
// Aim = new UCAim();
// NewObject : BeginPlay 이후에 동적할당을 수행하며
// 가비지 컬렉션에 의해 자동으로 제거되도록 해주는 함수(즉 관리받는 애)
Aim = NewObject<UCAim>();
Aim->BeginPlay(OwnerCharacter);
// 가비지 컬렉터에 별로 의존하고 싶지 않고, 가비지 컬렉션은 일정 모아서
// 지우기 때문에, 지우는 속도가 느림
// 지우는 부분만 스레드로 처리하는 경우도 있지만
// 바로 지우는 명령어도 있다.
// ConditionalBeginDestroy() : 가비지 컬렉터에 등재, 가비지 컬렉터 이후의 삭제가 됨
// ConditionalFinishDestroy() : 가비지 컬렉터에 등재, 가비지 컬렉터를 동작시켜서 바로 지움, 얘는 언제든지 바로 삭제 가능함
// Aim->ConditionalFinishDestroy();
}
void ACDoAction_Throw::DoAction()
{
// Aiming이 가능한 상황이라면 Zoom 상태에서만 발사해야 한다.
if (Aim->IsAvaliable())
CheckFalse(Aim->IsZoom());
CheckFalse(State->IsIdleMode());
State->SetActionMode();
OwnerCharacter->PlayAnimMontage(Datas[0].AnimMontage, Datas[0].PlayRatio, Datas[0].StartSection);
Datas[0].bCanMove ? Status->SetMove() : Status->SetStop();
}
void ACDoAction_Throw::Begin_DoAction()
{
FVector location = OwnerCharacter->GetMesh()->GetSocketLocation("Hand_Throw_Projectile");
// 플레이어의 전방 or 적의 전방
FRotator rotator = OwnerCharacter->GetController()->GetControlRotation();
FTransform transform = Datas[0].EffectTransform;
transform.AddToTranslation(location);
transform.SetRotation(FQuat(rotator));
FActorSpawnParameters params;
params.Owner = OwnerCharacter; // 스폰한 액터
// FActorSpawnParameters안에 ESpawnActorCollisionHandlingMethod가 있음
// ESpawnActorCollisionHandlingMethod : 현재 Spawn하려는 위치에 다른액터가 있을때 어떻게 처리할지를 결정하는 열거형
// 위치에 스폰되면서 얘가 자동으로 충돌을 한다. 그래서 그 자리에 있으면
// Undefined : 스폰시키지 않음
// AlwaysSpawn : 있든 없든 충돌 무시하고, 무조건 스폰 시켜라
// AdjustIfPossibleButAlwaysSpawn : 위치를 조정한다음 스폰시켜라
// AdjustIfPossibleButDontSpawnIfColliding : 충돌되었다면 조정해서 스폰시켜라
// DontSpawnIfColliding : 아예 스폰시키지 않겠다.
// params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
// CThrow의 GetWorld()로 사용해도 무방하나 캐릭터가 배치되어 있는 월드에 스폰
// SpawnActor() 함수는 반드시 스폰한다고 보장하지 않음
// ACThrow* throwObject = OwnerCharacter->GetWorld()->SpawnActor<ACThrow>(Datas[0].ThrowClass, transform, params);
// 그래서 반드시 스폰되는 함수를 사용
ACThrow* throwObject = OwnerCharacter->GetWorld()->SpawnActorDeferred<ACThrow>(Datas[0].ThrowClass, transform, OwnerCharacter, NULL, ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
throwObject->OnThrowBeginOverlap.AddDynamic(this, &ACDoAction_Throw::OnThrowBeginOverlap);
// 등장 확정 처리
UGameplayStatics::FinishSpawningActor(throwObject, transform);
}
void ACDoAction_Throw::End_DoAction()
{
State->SetIdleMode();
Status->SetMove();
}
void ACDoAction_Throw::OnAim()
{
Aim->OnZoom();
}
void ACDoAction_Throw::OffAim()
{
Aim->OffZoom();
}
void ACDoAction_Throw::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
Aim->Tick(DeltaTime);
}
void ACDoAction_Throw::OnThrowBeginOverlap(FHitResult InHitResult)
{
// 데미지만 준다.
FDamageEvent e;
InHitResult.GetActor()->TakeDamage(Datas[0].Power, e, OwnerCharacter->GetController(), this);
}
CAim.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Components/TimelineComponent.h"
#include "CAim.generated.h"
UCLASS()
class UONLINE_04_ACTION_API UCAim : public UObject
{
GENERATED_BODY()
public:
FORCEINLINE bool IsAvaliable() { return SpringArm != NULL && Camera != NULL; }
FORCEINLINE bool IsZoom() { return bInZoom; }
public:
UCAim();
// 얘는 AActor로 부터 상속받는 클래스가 아니어서 BeginPlay가 없음
void BeginPlay(class ACharacter* InCharacter);
void OnZoom();
void OffZoom();
// Tick()도 없음
void Tick(float DeltaTime);
private:
UFUNCTION()
void Zooming(float Output);
private:
class UCurveFloat* Curve;
class ACHUD* HUD;
class ACharacter* OwnerCharacter;
class UCStateComponent* State;
class USpringArmComponent* SpringArm;
// FOV 조정을 위해
class UCameraComponent* Camera;
bool bInZoom; // 현재 줌이 되고 있는지
FTimeline Timeline;
FOnTimelineFloat TimelineFloat;
};
CAim.cpp
#include "CAim.h"
#include "Global.h"
#include "CHUD.h"
#include "GameFramework/Character.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/CStateComponent.h"
UCAim::UCAim()
{
// UCAim 클래스의 객체가 다른클래스의 생성자에서 생성된다면 GetAsset()
// BeginPlay()에서 호출된다면 GetAssetDynamic()을 사용해야 한다.
// 지금은 CDoAction_Throw에서는 BeginPlay()에서 이 생성자를 호출한다.
CHelpers::GetAssetDynamic<UCurveFloat>(&Curve, "CurveFloat'/Game/Actions/C_Aim.C_Aim'");
}
void UCAim::BeginPlay(ACharacter* InCharacter)
{
OwnerCharacter = InCharacter;
// 스프링암이 있는 여부에 따라 에임이 가능한 캐릭터 인지 판단할 것이다.
// 적 같은 경우에는 Aiming을 할 필요가 없기 때문에
SpringArm = CHelpers::GetComponent<USpringArmComponent>(OwnerCharacter);
Camera = CHelpers::GetComponent<UCameraComponent>(OwnerCharacter);
State = CHelpers::GetComponent<UCStateComponent>(OwnerCharacter);
// 플레이어가 있는 World를 가져오고 플레이어의 컨트롤러를 가져오고 HUD를 가져옴
// GetHUD()는 템플릿형(바로 원하는 형태로 가져옴)
HUD = OwnerCharacter->GetWorld()->GetFirstPlayerController()->GetHUD<ACHUD>();
// AddInterpFloat() : 타임라인이 실행될 동안 호출될 델리게이션 함수를 연결하는 함수
// 함수 오버로딩된 2개가 있는데 그 중 파라미터 설명
// FOnTimeLineFloatStatic : 일반 델리게이트
// FOnTimeLineFloat : 다이나믹 델리게이트
// 얘는 커브에 의해서 값이 콜이 될건데, 들어올 함수가 있어야 하는데
// 그 함수를 연결한다는 의미
// BP에서는 Timeline함수를 콜해서 값을 리턴받아 왔지만, C에서는
// 델리게이션을 이용
TimelineFloat.BindUFunction(this, "Zooming");
Timeline.AddInterpFloat(Curve, TimelineFloat);
// SetPlayRate를 이용해 속도를 조정
// 이거 안해주면 딱딱 끊김
// 10초 동안 갈거를 1/100이 0.01초니까 0.005초 정도로 가게됨
Timeline.SetPlayRate(200);
}
void UCAim::OnZoom()
{
// Aim이 가능한(SpringArm과 Camera 컴포넌트를 가지고 있는) 상황에서만 수행
CheckFalse(IsAvaliable());
CheckTrue(bInZoom); // 현재 줌이 되고 있는 상황이면 실행하면 안됨
bInZoom = true;
SpringArm->TargetArmLength = 100.0f;
SpringArm->SocketOffset = FVector(0, 30, 10);
// bEnableCameraLag : 카메라를 부드럽게 이동하는 처리
// 이거 키면 시야를 땡겨놓으면 좀 뒤에 나타남, 그래서 끔
SpringArm->bEnableCameraLag = false;
//Camera->FieldOfView = 45;
// Play()는 멈추게 되면, 멈춘부분 부터 실행됨
// PlayFromStart()는 멈추게 되면, 시작부터 실행됨
Timeline.PlayFromStart();
HUD->SetDrawMode();
}
void UCAim::OffZoom()
{
CheckFalse(bInZoom); // 줌 상태가 아니라면 빠져나옴
bInZoom = false;
SpringArm->TargetArmLength = 250.0f;
SpringArm->SocketOffset = FVector(0, 0, 0);
SpringArm->bEnableCameraLag = true;
HUD->SetUndrawMode();
// Camera->FieldOfView = 90;
// 이것도 Reverse()면 중간에 멈추면 이어서 되지만,
// ReverseFromEnd()는 중간에 멈추면 다시 역에서 재생
Timeline.ReverseFromEnd();
}
void UCAim::Tick(float DeltaTime)
{
// Tineline에 Tick에 델타타임을 계속 넘겨줘야함
Timeline.TickTimeline(DeltaTime);
}
// 커브에서 지정한 값이 계속 들어오게 됨
void UCAim::Zooming(float Output)
{
Camera->FieldOfView = Output;
}
CPlayer.h 추가된 내용
...
UCLASS()
class UONLINE_04_ACTION_API ACPlayer : public ACharacter, public IICharacter
{
GENERATED_BODY()
...
private:
...
void OnAim();
void OffAim();
};
CPlayer.cpp 추가된 내용
...
void ACPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
...
PlayerInputComponent->BindAction("Aim", EInputEvent::IE_Pressed, this, &ACPlayer::OnAim);
PlayerInputComponent->BindAction("Aim", EInputEvent::IE_Released, this, &ACPlayer::OffAim);
}
void ACPlayer::OnAim()
{
Action->DoAim();
}
void ACPlayer::OffAim()
{
Action->UndoAim();
}
CHUD.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "CHUD.generated.h"
UCLASS()
class UONLINE_04_ACTION_API ACHUD : public AHUD
{
GENERATED_BODY()
private:
// 게임에서 사용되는 이미지들을 Texture라고 부르고,
// 2D게임에서는 일반적으로 Sprite라고 부르는 경우가 있음
// 1D Texture : 한줄만 있는 텍스쳐
// 3D Texture : 2D 텍스쳐가 여러면이 있음
// CubeTexture : 정육면체, 환경매핑때 사용(하늘 같은 것)
// BP에서 Texture 세팅 가능
UPROPERTY(EditDefaultsOnly)
class UTexture2D* Texture;
public:
FORCEINLINE void SetDrawMode() { bDraw = true; }
FORCEINLINE void SetUndrawMode() { bDraw = false; }
public:
ACHUD();
// 부모에 있음
// HUD를 그리기 위한 코드가 들어감(매프레임 마다 동작)
virtual void DrawHUD() override;
private:
bool bDraw; // 허드를 그릴 상황인지
};
CHUD.cpp
#include "CHUD.h"
#include "Global.h"
#include "Engine/Texture2D.h"
#include "Engine/Canvas.h"
ACHUD::ACHUD()
{
// 디폴트 텍스쳐 지정
CHelpers::GetAsset<UTexture2D>(&Texture, "Texture2D'/Game/Textures/T_Crosshair.T_Crosshair'");
}
void ACHUD::DrawHUD()
{
// 오버라이딩은 항상 부모 콜
Super::DrawHUD();
// 그릴 수 있는 상황에 그림
CheckFalse(bDraw);
// 텍스쳐가 있어야 그릴 수 있음
CheckNull(Texture);
// 화면은 2D로 이루어져서 FVector2D를 사용
// 위젯 블루프린트의 Canvas 패널과 여기에서 사용되는 Canvas 객체가
// 같은 개념으로 사용됨
// Canvas는 부모의 정의됨
// ClipX : 화면의 넓이, ClipY : 화면의 높이
// 반을 나눠서 중앙지점을 구함
FVector2D center(Canvas->ClipX * 0.5f, Canvas->ClipY * 0.5f);
// GetSurfaceWidth() : 텍스쳐의 크기
FVector2D size(Texture->GetSurfaceWidth() * 0.5f, Texture->GetSurfaceHeight() * 0.5f);
// 캔버스(화면) 센터에서 텍스쳐 사이즈의 반만큼 위, 왼쪽으로 가야 이미지가 화면의 정중앙의 위치하게 됨
FVector2D position = center - size;
// Canvas는 Item이 굉장히 많음
// FCanvasTileItem : 타일종류로 까는 것임
// 1. 그릴 위치, 2. 어떤 텍스쳐를 그릴건지 3. 어떤 색상으로 그릴건지(이미지 재활용 가능하도록 있는 색상 값)
FCanvasTileItem item(position, Texture->Resource, FLinearColor::White);
// 보통 디자이너들이 이미지를 재활용하기 위해서 빨간색 텍스쳐라면, 흰색칠을 해놔서
// 상황에 따라 3번인자를 사용함, 텍스쳐의 색상 * 3번의 색상을 곱해서 화면에 나옴(ex) Red * White = Red)
// 그린을 출력하고 싶다면 3번인자에 그린을 주면 됨
// AlphaBlending 식
// Material의 BlendMode와 유사함
// Material도 이미 그려진 거에 조합이 될때 어떻게 섞일 건지
// 약간의 공식이 있음
// 어떻게 섞일 건지
// 먼저 게임화면이 그려지고
// 나중에 HUD가 그려진다.
// HUD가 그려질 때도 Material과 마찬가지로 그러진 거에 조합이 될떄 어떻게 섞일 건지
// Src(0, 0, 1) * Alpha(0.5) op Dest(1, 0, 0) * ZeroSrcAlpha(1 - 0.5)
// Src * 0.5 op Dest * 0.5
// (0, 0 ,1) * 0.5 + (1, 0, 0) * 0.5 = (0.5, 0, 0.5) -> 마젠타 색이 나옴
// 이 가운데 op 공식이 어떤게 들어가느냐에 따라 달라짐
// 보통은 op를 더하기를 준다.
// DX의 BlendState와 개념이 같음(AlphaBlend 개념)
// Material의 BlendMode 중에 Translucent(투명한)이 있다. -> 언리얼에서는 이게 AlphaBlending이다.(반투명을 만들어줌)
// 그런데 여기선 SE_BLEND_AlphaBlend도 있고, SE_BLEND_Translucent도 있음
// 차이는 SE_BLEND_AlphaBlend는 위의 수식이 맞고
// SE_BLEND_Translucent : Alpha값이 특정 값 이하일시 그리지 않겠다는 의미 -> 일정 투명값 이하는 아예 그리지 않겠다는 의미
// SE_BLEND_Modulate : Modulate(곱한다는 의미)
// SE_BLEND_Modulate 수식
// Src * Dest + Dest * Zero(그려진 색은 버리겠다는 의미 : 화면)
// -> Src * Dest만 남음
// SE_BLEND_Opaque(불투명) : 그려져 있는 색은 무시하고 현재 그려질 색만 그림
// SE_BLEND_Opaque 수식
// Src * One + Dest * Zero
// -> Src 원본 값만 남음(원본만 그려짐)
// Material의 BlendMode의 Masked는 저격모드의 앞에 동그란데 제외하고 나머지 검은색 부분 가려짐
// 그럴 때 사용
// Material의 BlendMode의 Addtive는 2개를 더하라는 옵션(색이 더 밝아짐)
// 물감은 섞으면 섞을 수록 검어짐(->감선 혼합)
// 컴퓨터는 더하면 더할수록 값이 밝아짐(->가산 혼합)
// 정규화로 1로 떨어지는 색상값이다.
// (2, 1, 0) 이라면 -> (1, 0.5, 0) 비율로 1로 맞춰서 떨어트림
// 곱할 수 있는것이 1보다 작은 값이다.(큰 값은 곱해도 1로 맞춰서 떨어짐)
// 그래서 어떤 값의 색을 어둡게 만들기 위해서는 곱해주고(1로 정규화가 되니까)
// 어떤 색의 값을 밝게 만들기 위해서는 더해준다.
// (1, 0.8, 0) + (1, 0.2, 0) -> (2, 1, 0) -> 값이 1로 줌(1, 0.5, 0)
// 결국 더하면 앞의 값이 밝아짐
// Addtive 수식 : Src * One + Dest * One -> Src + Dest(두개의 색이 합해져서 더 밝아짐)
// Material의 BlendMode의 AlphaComposite, AlphaHoldout 이 두 개는 이전의 알파값을 유지한다는 내용
// 컴퓨터가 색상을 표현하는 체계를 이해해야 해서 건너뜀
item.BlendMode = SE_BLEND_Translucent;
// Item을 그려라
Canvas->DrawItem(item);
}
CThrow.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CThrow.generated.h"
// 충돌 처리할 델리게이트
// CThrow 생성하는 쪽에서 이벤트를 연결해줄 것이다.(즉 여기선 CDoAction_Throw에 다가)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FThrowBeginOverlap, FHitResult, InHitResult);
UCLASS()
class UONLINE_04_ACTION_API ACThrow : public AActor
{
GENERATED_BODY()
private:
UPROPERTY(EditDefaultsOnly)
class UParticleSystem* Explosion; // 충돌했을때 터질 이펙트
UPROPERTY(EditDefaultsOnly)
FTransform ExplosionTransform; // Explosion의 SRT 조정위해
private:
UPROPERTY(VisibleDefaultsOnly)
class USphereComponent* Sphere; // 날라갈 충돌체
UPROPERTY(VisibleDefaultsOnly)
class UParticleSystemComponent* Particle; // 날라가면서 Particle
private:
// 날라갈거니까 ProjectileMovementComponent
UPROPERTY(VisibleDefaultsOnly)
class UProjectileMovementComponent* Projectile;
public:
ACThrow();
protected:
virtual void BeginPlay() override;
private:
UFUNCTION()
void OnComponentBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
public:
UPROPERTY(BlueprintAssignable)
FThrowBeginOverlap OnThrowBeginOverlap;
};
CThrow.cpp
#include "CThrow.h"
#include "Global.h"
#include "Components/SphereComponent.h"
#include "Particles/ParticleSystemComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
ACThrow::ACThrow()
{
CHelpers::CreateComponent<USphereComponent>(this, &Sphere, "Sphere");
CHelpers::CreateComponent<UParticleSystemComponent>(this, &Particle, "Particle", Sphere);
CHelpers::CreateActorComponent<UProjectileMovementComponent>(this, &Projectile, "Projectile");
// Life 가능 시간 : 3초
InitialLifeSpan = 3.0f;
Projectile->InitialSpeed = 4000.0f;
Projectile->MaxSpeed = 8000.0f;
Projectile->ProjectileGravityScale = 0.0f; // 0으로 줘서 정면으로 날아갈 수 있도록
}
void ACThrow::BeginPlay()
{
Super::BeginPlay();
TArray<USphereComponent*> components;
// 현재는 CThrow에 충돌체 하나만 넣지만, 우리가 작업하면서 여러개의 충돌체를 사용할 수도 있으므로
// 여러개의 충돌체를 검색하는 방식으로 수행
GetComponents<USphereComponent>(components);
for (USphereComponent* sphere : components)
sphere->OnComponentBeginOverlap.AddDynamic(this, &ACThrow::OnComponentBeginOverlap);
}
void ACThrow::OnComponentBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (!!Explosion)
{
FTransform transform = ExplosionTransform;
// 충돌 결과 위치에 더해줌
transform.AddToTranslation(SweepResult.Location);
// 충돌한 면의 수직벡터로 회전을 처리해서 폭발이 자연스럽게 보이도록 함
transform.SetRotation(FQuat(SweepResult.ImpactNormal.Rotation()));
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), Explosion, transform);
if (OnThrowBeginOverlap.IsBound())
OnThrowBeginOverlap.Broadcast(SweepResult);
// 지워줌
Destroy();
}
}
결과
'Unreal Engine 4 > C++' 카테고리의 다른 글
<Unreal C++> 73 - Action RPG (Inverse Kinemetics(IK)) (0) | 2022.06.07 |
---|---|
<Unreal C++> 70 - Action RPG (UserWidget(무기선택)) (0) | 2022.05.30 |
<Unreal C++> 63 - Action RPG (FireStorm) (0) | 2022.05.30 |
<Unreal C++> 61 - Action RPG (Dead) (0) | 2022.05.30 |
<Unreal C++> 58 - Action RPG (Targeting) (0) | 2022.05.16 |