1. Dash 애니메이션 세팅
Dash 애니메이션 같은 경우는 대체로 Root Motion 애니메이션이 많다.
그러나 우리가 원하는 속도나 거리, 시간을 이동시키기 위해서는 루트 모션을 사용하지 않고, 아래와 같은 세탕으로 코드를 잠금시킨 후 코드에서 움직이도록 하였다.
해당 애니메이션을 이용해 애니메이션 몽타주를 만들어 주면 된다.
2. 스킬 시스템과 연동
현재 스킬은 아래와 같은 구조로 구성되어 있다.
- UObject를 상속받은 SkillBase 를 상속하여 만든 각각의 스킬 클래스 (실제 스킬의 실체)
- UActorComponent를 상속받은 SkillComp를 통해 스킬 사용
스킬을 사용하는 흐름은 아래와 같이 정리할 수 있다.
- PlayerController와 IMC 바인딩
void AGS_GuardianController::SetupInputComponent()
{
Super::SetupInputComponent();
UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent);
if (IsValid(EnhancedInputComponent))
{
if (IsValid(CtrlInputAction))
{
EnhancedInputComponent->BindAction(CtrlInputAction, ETriggerEvent::Triggered, this, &AGS_GuardianController::CtrlInput);
EnhancedInputComponent->BindAction(CtrlInputAction, ETriggerEvent::Completed, this, &AGS_GuardianController::CtrlStop);
}
if (IsValid(LeftMouseInputAction))
{
EnhancedInputComponent->BindAction(LeftMouseInputAction, ETriggerEvent::Started, this, &AGS_GuardianController::LeftMouseInput);
}
if (IsValid(RightMouseInputAction))
{
EnhancedInputComponent->BindAction(RightMouseInputAction, ETriggerEvent::Started, this, &AGS_GuardianController::RightMouseInput);
}
}
}
- 바인딩 된 함수에서 각 Player의 함수 호출
void AGS_GuardianController::RightMouseInput(const FInputActionValue& InputValue)
{
if (IsLocalController())
{
AGS_Guardian* Guardian = Cast<AGS_Guardian>(GetPawn());
if (IsValid(Guardian))
{
Guardian->RightMouse();
}
}
}
- 바인딩 된 함수에서 SkillComp를 통해 Skill 실행
void AGS_Drakhar::RightMouse()
{
if (IsLocallyControlled())
{
//ultimate skill
if (GuardianState != EGuardianState::Skill)
{
if (GetSkillComp()->IsSkillActive(ESkillSlot::Ready))
{
GetSkillComp()->TryActivateSkill(ESkillSlot::Ultimate);
ServerRPCStartSkill();
}
//dash skill
else
{
GetSkillComp()->TryActivateSkill(ESkillSlot::Moving);
ServerRPCStartSkill();
}
}
}
}
- TryActivateSkill은 Server RPC 함수로 SkillBase의 ActivateSkill 함수를 실행
void UGS_SkillComp::TryActivateSkill(ESkillSlot Slot)
{
if (!bCanUseSkill)
{
UE_LOG(LogTemp, Warning, TEXT("TryActivateSkill failed: bCanUseSkill = false"));
return;
}
if (!GetOwner()->HasAuthority())
{
Server_TryActiveSkill(Slot);
return;
}
if (SkillMap.Contains(Slot))
{
UGS_SkillBase* Skill = SkillMap[Slot];
if (Skill)
{
if (Skill->CanActive())
{
Skill->ActiveSkill();
}
}
}
}
- ActivateSkill 함수에서 실제 스킬들의 동작이 이루어진다.
3. Dash 스킬의 로직
Dash 스킬의 동작은 아래와 같다.
- 일정 시간동안 빠르게 일정 거리를 이동해야 함
- 지나간 거리에 있는 몬스터들에게 데미지를 주어야 함
그러나 위의 로직을 UObject를 상속받은 클래스에서는 구현할 수 없다고 생각하여 SkillBase를 상속받은 클래스에서는 몽타주 재생만 실행하고, "애님 노티파이 스테이트" 를 사용하여, Dash 로직을 구현했다.
void UGS_DrakharDraconicFury::ActiveSkill()
{
if (!CanActive())
{
return;
}
ExecuteSkillEffect();
}
void UGS_DrakharDraconicFury::ExecuteSkillEffect()
{
StartCoolDown();
//OwnerCharacter->GetSkillComp()->StartTimer(ESkillSlot::Ultimate);
OwnerCharacter->MulticastRPCPlaySkillMontage((SkillAnimMontages[0]));
}
애님 노티파이 스테이트는 UAnimNotifyState 를 상속하여 만들 수 있고, Begin, Tick, End 함수가 존재하기 때문에 몽타주가 재생되는 동안의 로직을 구성하기에 적합하다.
애님 노티파이 스테이트를 만들면 아래와 같이 보라색으로 구간을 설정할 수 있다.
애님 노티파이 스테이트에서의 로직은 아래와 같이 구성했다.
- 데미지 처리나, 거리 계산 등의 게임상에서 중요한 로직의 경우 서버에서 실행해야 하기 때문에 Server RPC를 통해 처리했다.
- NotifyBegin
- 대쉬 도중에는 콜리젼을 꺼서 다른 몬스터와 충돌되지 않도록 구현
- 플레이어의 현재 위치를 기준으로 도착할 위치를 계산
- NotifyTick
- Lerp를 통해 이동할 거리를 계속 계산한 후 SetActorLocation으로 위치 이동
- NotifyEnd
- 콜리젼을 다시 원래 상태로 돌려두고
- 지나온 길에 존재하는 모든 몬스터에게 데미지 부여
void UGS_ANS_DrakharDash::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference)
{
Super::NotifyBegin(MeshComp, Animation, TotalDuration, EventReference);
if (AActor* Owner = MeshComp->GetOwner())
{
if (AGS_Drakhar* Drakhar = Cast<AGS_Drakhar>(Owner))
{
Drakhar->GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Drakhar->ServerRPCCalculateDashLocation();
}
}
}
void UGS_ANS_DrakharDash::NotifyTick(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float FrameDeltaTime, const FAnimNotifyEventReference& EventReference)
{
Super::NotifyTick(MeshComp, Animation, FrameDeltaTime, EventReference);
if (AActor* Owner = MeshComp->GetOwner())
{
if (AGS_Drakhar* Drakhar = Cast<AGS_Drakhar>(Owner))
{
//attack and moving
Drakhar->ServerRPCDoDash(FrameDeltaTime);
}
}
}
void UGS_ANS_DrakharDash::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
Super::NotifyEnd(MeshComp, Animation, EventReference);
if (AActor* Owner = MeshComp->GetOwner())
{
if (AGS_Drakhar* Drakhar = Cast<AGS_Drakhar>(Owner))
{
Drakhar->GetMesh()->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
//데미지 처리
Drakhar->ServerRPCEndDash();
}
}
}
NotifyTick 에서 동작하는 함수
- Alpha 값은 DeltaTime을 대시를 진행할 시간인 Duration으로 나눠 보간을 진행하였다.
- 프레임에 관계없이 Duration이 되는 순간 우리가 도착지점으로 가야하기 때문에 Alpha값을 계산하였고
- Lerp를 통해 플레이어를 이동시켜 주었다.
- 이 함수가 Tick으로 돌아가는 동안 DashAttackCheck()를 통해 공격 처리를 해주었다.
void AGS_Drakhar::ServerRPCDoDash_Implementation(float DeltaTime)
{
DashInterpAlpha += DeltaTime / DashDuration;
DashAttackCheck();
if (DashInterpAlpha >= 1.f)
{
SetActorLocation(DashEndLocation);
}
else
{
const FVector NewLocation = FMath::Lerp(DashStartLocation, DashEndLocation, DashInterpAlpha);
SetActorLocation(NewLocation, true);
DashStartLocation = NewLocation;
}
}
데미지 처리 로직
- 구현하면서 신경 쓴 부분은 대시를 하며 이동하는 동안에 충돌한 것들에 대해서 한번씩만 데미지를 입히기 위해 TSet 자료 구조를 사용했다.
TSet<AGS_Character*> DamagedCharacters;
void AGS_Drakhar::DashAttackCheck()
{
TArray<FHitResult> OutHitResults;
const FVector Start = GetActorLocation();
const FVector End = Start + GetActorForwardVector() * 100.f;
FCollisionQueryParams Params(NAME_None, false, this);
bool bIsHitDetected = GetWorld()->SweepMultiByChannel(OutHitResults, Start, End, FQuat::Identity,
ECC_Camera, FCollisionShape::MakeCapsule(100.f, 200.f), Params);
if (bIsHitDetected)
{
for (auto const& OutHitResult : OutHitResults)
{
AGS_Character* DamagedCharacter = Cast<AGS_Character>(OutHitResult.GetActor());
if (IsValid(DamagedCharacter))
{
DamagedCharacters.Add(DamagedCharacter);
}
}
}
}
4. 트러블 슈팅
대쉬 이동 스킬 시 벽을 뚫고 지나가는 등의 문제가 존재
- SetActorLocation 함수의 두번째 파라메터를 true로 하여 충돌이 발생하도록 구현
- 그러나 이 경우, 몬스터와도 충돌하여 날아가는 문제가 생긴다.
void AGS_Drakhar::ServerRPCDoDash_Implementation(float DeltaTime)
{
DashInterpAlpha += DeltaTime / DashDuration;
DashAttackCheck();
if (DashInterpAlpha >= 1.f)
{
SetActorLocation(DashEndLocation);
}
else
{
const FVector NewLocation = FMath::Lerp(DashStartLocation, DashEndLocation, DashInterpAlpha);
SetActorLocation(NewLocation, true);
DashStartLocation = NewLocation;
}
}
콜리젼 채널을 사용하여 문제 해결
- 스킬 시도 중에는 Pawn 채널을 무시하고
- 스킬이 끝나면 Pawn 채널을 다시 Block 하도록 구현
void AGS_Drakhar::ServerRPCDoDash_Implementation(float DeltaTime)
{
GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore);
...
}
void AGS_Drakhar::ServerRPCEndDash_Implementation()
{
...
GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block);
}
결과
'Unreal Engine' 카테고리의 다른 글
Unreal Engine - 데디케이티드 서버 11 (콤보 공격 최적화) (0) | 2025.06.02 |
---|---|
Unreal Engine - 데디케이티드 서버 10 (콤보 공격) (0) | 2025.05.30 |
Unreal Engine - 데디케이티드 서버 9 (게임 종료) (0) | 2025.05.04 |
Unreal Engine - 데디케이티드 서버 8 (게임 흐름) (0) | 2025.05.02 |
Unreal Engine - AI (2) (0) | 2025.04.29 |