일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- Security
- MAC
- pdlc
- gameplay effect
- gas
- Unreal Engine
- 게임 개발
- reverse gravity
- 메카님
- 언리얼 엔진
- gameplay ability
- 게임개발
- dirty cow
- MLFQ
- 운영체제
- sampling theory
- dtft
- CTF
- 언리얼엔진
- 유스케이스
- ability task
- Rr
- stride
- frequency-domain spectrum analysis
- DP
- Race condition
- 유니티
- DSP
- linear difference equation
- ret2libc
- Today
- Total
다양한 기록
[UE Game Framework] #6 Character Attack Hit Check 본문
[UE Game Framework] #6 Character Attack Hit Check
라구넹 2025. 1. 5. 02:01캐릭터 액션의 충돌 판정
- 월드가 제공하는 충돌 판정 서비스를 사용
- 월드는 크게 세 가지의 충돌 판정 서비스를 제공함
- LineTrace : 지정한 방향으로 선을 투사
- Sweep : 지정한 방향으로 도형을 투사
- Overlap : 지정한 영역에 큰 범위의 도형을 설정해서 해당 볼륨 영역과 물체가 충돌하는지 검사
- 월드 내 배치된 충돌체와 충돌하는지 파악하고 충돌한 액터 정보를 얻을 수 있음
트레이스 채널과 충돌 프로필 생성
- 액션 판정을 위한 트레이스 채널의 생성 : ABAction, 기본 반응은 무시
- 캐릭터 캡슐용 프로필 : ABAction 트레이스 채널에 반응, 오브젝트 타입은 Pawn
- 스켈레탈 메시용 프로필 : 랙돌 구현을 위해 주로 활용됨
- 기믹 트리거용 프로필 : 폰 캡슐에만 반응하도록 설정, 오브젝트 타입은 WorldStatic(안움직임)
블록은 모든 물체가 반응함
지정한 캡슐 컴포넌트만 반응하게 할 것이라 변경 필요
여기서 이제 필요한 거 추가
Trace 타입은 이그노어, 블록만 신경쓰면 되고
Object 타입은 이그노어, 오버랩, 블록 세가지를 신경 써야 함
오버랩은 길은 안막는데 이벤트가 발생, 블록은 길막기
ex. 파괴한 조각들이 길을 막으면 안되니 Ignore
캡슐용 프로필이니 쿼리용으로, 폰타입으로 설정하고
ABAction에 블록되도록 함
트리거도 만들어주는데, 폰만 겹치는 반응 걸어주면 됨
이런 정보들은 DefaultEngine.ini에 다 저장됨
열어보면 확인 가능
중요한 정보
DefaultChannelResponses라는 정보의 ABAction이라는 이름이
엔진에서 지정한 내부 열거형에서 ECC_GameTraceChannel1라는 값을 사용
코드 내에서는 저 값을 써주는게 더 편리
그 위엔 추가한 프로필들 확인 가능
몽타주의 애니메이션에서 이벤트를 발생시켜서 공격 판정을 구현할 것
여기서 공격 판정을 주고 싶음 -> 애니메이션 노티파이 활용
선택하고 만들어줌 (이름 AnimNotify_AttackHitCheck)
이제 만들어주기 가능
각 섹션마다 설정해줌
이제 UAnimNotify의 함수를 오버라이드해서 구현해야 함
위 Notify는 언리얼 5부터 안씀
Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
스켈레탈 메쉬 컴포넌트, 애니메이션 정보가 인자
그리고 추가적인 레퍼런스 정보들 ..
// Fill out your copyright notice in the Description page of Project Settings.
#include "Animation/AnimNotify_AttackHitCheck.h"
#include "Character/ABCharacterBase.h"
void UAnimNotify_AttackHitCheck::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
if ( MeshComp )
{
}
}
오너가 우리가 선언한 캐릭터인지 체크해야 함 -> 다른 헤더를 인클루드 해야 함
=> 의존성 증가
노티파이는 공용으로 쓰는 일이 많은데, 이러면 별로 안좋음
=> 인터페이스 활용
만들어주기
어쩔 수 없는 의존성 발생 시 가급적이면 인터페이스를 통해 해결
// Fill out your copyright notice in the Description page of Project Settings.
#include "Animation/AnimNotify_AttackHitCheck.h"
#include "Interface/ABAnimationAttackInterface.h"
void UAnimNotify_AttackHitCheck::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
if ( MeshComp )
{
}
}
이렇게 바꿔줄 수 있음
인터페이스에서 함수 하나 추가
// Fill out your copyright notice in the Description page of Project Settings.
#include "Animation/AnimNotify_AttackHitCheck.h"
#include "Interface/ABAnimationAttackInterface.h"
void UAnimNotify_AttackHitCheck::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
Super::Notify(MeshComp, Animation, EventReference);
if ( MeshComp )
{
IABAnimationAttackInterface* AttackPawn = Cast<IABAnimationAttackInterface>(MeshComp->GetOwner());
if ( AttackPawn )
{
AttackPawn->AttackHitCheck();
}
}
}
결과적으로는 이렇게 바뀜
이제 AttackHitCheck 구현하면 됨
공격 판정의 구현
월트 트레이싱 함수의 선택
- 세가지 카테고리로 원하는 함수 이름을 얻을 수 있음
- 카테고리 1 : 처리 방법
- LineTrace
- Sweep
- Overlap
- 카테고리 2 : 대상
- Test : 무언가 감지되었는지를 테스트
- Single or AnyTest : 감지된 단일 물체 정보를 반환 (LineTrace, Sweep -> Single, Overlap -> AnyTest)
- Multi : 감지된 모든 물체 정보를 배열로 반환
- 카테고리 3: 처리 설정
- ByChannel : (트레이스) 채널 정보를 사용해 감지
- ByObjectType : 물체에 지정된 물리 타입 정보를 사용해 감지
- ByProfile : (물리에 설정된)프로필 정보를 사용해 감지
=> {처리방법}{대상}{처리설정}
캐릭터 공격 판정의 구현
- 캐릭터의 위치에서 시선 방향으로 물체가 있는지 감지
- 작은 구체를 제작하고 시선 방향으로 특정 거리까지만 투사
- 하나의 물체만 감지
- 트레이스 채널을 사용해 감지
=> SweepSingleByChannel
트레이스 채널을 써야 하는데, 간단한 헤더 파일에서 매크로 만들어 쓰면 편함
(Source/Physics/ABCollision.h) ..
파일 탐색기에서 만들어서 그런가 재생성해도 빨간줄이 뜨긴 하는데,
일단 빌드는 됨
프로필 두개랑 트레이스 채널
void AABCharacterBase::AttackHitCheck()
{
FHitResult OutHitResult;
FCollisionQueryParams Params(SCENE_QUERY_STAT(Attack), false, this);
const float AttackRange = 40.0f;
const float AttackRadius = 50.0f;
const float AttackDamage = 30.0f;
const FVector Start = GetActorLocation() + GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius();
const FVector End = Start + GetActorForwardVector() * AttackRange;
bool HitDetected = GetWorld()->
SweepSingleByChannel(OutHitResult, Start, End,
FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
if (HitDetected)
{
}
}
Params
첫번째 인자: InTraceTag, 콜리전 분석 시 식별자
두번째 인자: bInTraceComplex : 복잡한 형태의 충돌체도 감지할지에 대한 옵션
세번째 인자: 무시할 액터들, 자기 자신만 무시하면 됨
물리 충돌 테스트
- 디버그 드로잉 함수를 사용해 물리 충돌을 시각적으로 테스트
- 90도로 회전시킨 캡슐을 그리기
- Origin
- HalfHeight
- Radius
void AABCharacterBase::AttackHitCheck()
{
FHitResult OutHitResult;
FCollisionQueryParams Params(SCENE_QUERY_STAT(Attack), false, this);
const float AttackRange = 40.0f;
const float AttackRadius = 50.0f;
const float AttackDamage = 30.0f;
const FVector Start = GetActorLocation() + GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius();
const FVector End = Start + GetActorForwardVector() * AttackRange;
bool HitDetected = GetWorld()->
SweepSingleByChannel(OutHitResult, Start, End,
FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
if (HitDetected)
{
}
#if ENABLE_DRAW_DEBUG
FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
float CapsuleHalfHeight = AttackRange * 0.5f;
FColor DrawColor = HitDetected ? FColor::Green : FColor::Red;
DrawDebugCapsule(GetWorld(), CapsuleOrigin,
CapsuleHalfHeight, AttackRadius,
FRotationMatrix::MakeFromZ(GetActorForwardVector()).ToQuat(),
DrawColor, false, 5.0f);
#endif
}
** false랑 5.0f는 계속 유지할지 말지와, 유지 안할 거면 얼마나 유지할지
캐릭터랑만 충돌 처리해놔서 뭘 해도 빨간색만 나옴
=> NPC 생성해서 테스트
// Fill out your copyright notice in the Description page of Project Settings.
#include "Character/ABCharacterBase.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "ABCharacterControlData.h"
#include "Animation/AnimMontage.h"
#include "ABComboActionData.h"
#include "Physics/ABCollision.h"
// Sets default values
AABCharacterBase::AABCharacterBase()
{
// Pawn rotation
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
// Capsule
GetCapsuleComponent()->InitCapsuleSize(42.0f, 96.0f);
GetCapsuleComponent()->SetCollisionProfileName(CPROFILE_ABCAPSULE);
// Movement
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
GetCharacterMovement()->JumpZVelocity = 700.0f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->MaxWalkSpeed = 500.0f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.0f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.0f;
// Mesh
GetMesh()->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -100.0f), FRotator(0.0f, -90.0f, 0.0f));
GetMesh()->SetAnimationMode(EAnimationMode::AnimationBlueprint);
GetMesh()->SetCollisionProfileName(TEXT("NoCollision"));
static ConstructorHelpers::FObjectFinder<USkeletalMesh> CharacterMeshRef(TEXT("/Script/Engine.SkeletalMesh'/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Cardboard.SK_CharM_Cardboard'"));
if (CharacterMeshRef.Object)
{
GetMesh()->SetSkeletalMesh(CharacterMeshRef.Object);
}
static ConstructorHelpers::FClassFinder<UAnimInstance> AnimInstanceClassRef(TEXT("/Game/Animation/ABP_ABCharacter.ABP_ABCharacter_C"));
if (AnimInstanceClassRef.Class)
{
GetMesh()->SetAnimInstanceClass(AnimInstanceClassRef.Class);
}
static ConstructorHelpers::FObjectFinder<UABCharacterControlData> ShoulderObjectRef(TEXT("/Script/ArenaBattle.ABCharacterControlData'/Game/ArenaBattle/CharcaterControl/ABC_Shoulder.ABC_Shoulder'"));
if (ShoulderObjectRef.Object)
{
CharacterControlManager.Add(ECharacterControlType::Shoulder, ShoulderObjectRef.Object);
}
static ConstructorHelpers::FObjectFinder<UABCharacterControlData> QueterObjectRef(TEXT("/Script/ArenaBattle.ABCharacterControlData'/Game/ArenaBattle/CharcaterControl/ABC_Quater.ABC_Quater'"));
if (QueterObjectRef.Object)
{
CharacterControlManager.Add(ECharacterControlType::Quater, QueterObjectRef.Object);
}
static ConstructorHelpers::FObjectFinder<UAnimMontage> ComboActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/Animation/AM_ComboAttack.AM_ComboAttack'"));
if (ComboActionMontageRef.Object)
{
ComboActionMontage = ComboActionMontageRef.Object;
}
static ConstructorHelpers::FObjectFinder<UABComboActionData> ComboActionDataRef(TEXT("/Script/ArenaBattle.ABComboActionData'/Game/ArenaBattle/CharacterAction/ABA_ComboAttack.ABA_ComboAttack'"));
if (ComboActionDataRef.Object)
{
ComboActionData = ComboActionDataRef.Object;
}
}
void AABCharacterBase::SetCharacterControlData(const UABCharacterControlData* CharacterControlData)
{
// Pawn
bUseControllerRotationYaw = CharacterControlData->bUseControllerRotationYaw;
// CharacterMovement
GetCharacterMovement()->bOrientRotationToMovement = CharacterControlData->bOrientRotationToMovement;
GetCharacterMovement()->bUseControllerDesiredRotation = CharacterControlData->bUseControllerDesiredRotation;
GetCharacterMovement()->RotationRate = CharacterControlData->RotationRate;
}
void AABCharacterBase::ProcessComboCommand()
{
if ( CurrentCombo == 0 )
{
ComboActionBegin();
return;
}
if ( !ComboTimerHande.IsValid() ) // 타이밍을 놓쳤거나, 더 이상 진행 못하거나
{
HasNextComboCommand = false;
}
else
{
HasNextComboCommand = true;
}
}
void AABCharacterBase::ComboActionBegin()
{
// Combo Started
CurrentCombo = 1;
// Movement Setting
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
// Animation Setting
const float AttackSpeedRate = 1.0f;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->Montage_Play(ComboActionMontage, AttackSpeedRate);
FOnMontageEnded EndDelegate;
EndDelegate.BindUObject(this, &AABCharacterBase::ComboActionEnd);
AnimInstance->Montage_SetEndDelegate(EndDelegate, ComboActionMontage);
ComboTimerHande.Invalidate();
SetComboCheckTimer();
}
void AABCharacterBase::ComboActionEnd(UAnimMontage* TargetMontage, bool IsProperlyEnded)
{
ensure(CurrentCombo != 0);
CurrentCombo = 0;
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
}
void AABCharacterBase::SetComboCheckTimer()
{
int32 ComboIndex = CurrentCombo - 1;
ensure(ComboActionData->EffectiveFrameCount.IsValidIndex(ComboIndex));
const float AttackSpeedRate = 1.0f;
float ComboEffectiveTime = (ComboActionData->EffectiveFrameCount[ComboIndex] /
ComboActionData->FrameRate) / AttackSpeedRate;
if (ComboEffectiveTime > 0.0f)
{
GetWorld()->GetTimerManager().SetTimer(
ComboTimerHande, this, &AABCharacterBase::ComboCheck, ComboEffectiveTime, false);
}
}
void AABCharacterBase::ComboCheck()
{
ComboTimerHande.Invalidate();
if ( HasNextComboCommand )
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
CurrentCombo = FMath::Clamp(CurrentCombo + 1, 1, ComboActionData->MaxComboCount);
FName NextSection = *FString::Printf(TEXT("%s%d"), *ComboActionData->MontageSectionNamePrefix, CurrentCombo);
AnimInstance->Montage_JumpToSection(NextSection, ComboActionMontage);
SetComboCheckTimer();
HasNextComboCommand = false;
}
}
void AABCharacterBase::AttackHitCheck()
{
FHitResult OutHitResult;
FCollisionQueryParams Params(SCENE_QUERY_STAT(Attack), false, this);
const float AttackRange = 40.0f;
const float AttackRadius = 50.0f;
const float AttackDamage = 30.0f;
const FVector Start = GetActorLocation() + GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius();
const FVector End = Start + GetActorForwardVector() * AttackRange;
bool HitDetected = GetWorld()->
SweepSingleByChannel(OutHitResult, Start, End,
FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
if (HitDetected)
{
}
#if ENABLE_DRAW_DEBUG
FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
float CapsuleHalfHeight = AttackRange * 0.5f;
FColor DrawColor = HitDetected ? FColor::Green : FColor::Red;
DrawDebugCapsule(GetWorld(), CapsuleOrigin,
CapsuleHalfHeight, AttackRadius,
FRotationMatrix::MakeFromZ(GetActorForwardVector()).ToQuat(),
DrawColor, false, 5.0f);
#endif
}
베이스에서 몽타주랑 데이터 애셋 기본값 주고
캡슐 컴포넌트랑 메쉬 프로필 이름 설정하고 실행
NPC는 인식해서 초록색으로 나옴
다음은 죽는 기능
(* 여기서부터 코드가 좀 달라진게 있음, 폴더 잘못 만든게 있어서 수정함)
데드 몽타주 생성
애니메이션 블렌드 안되고 바로 실행되도록 오토 블렌드 해제
몽타주는 그룹별로 관리할 수 있는데 이게 슬롯임 (그룹이 상위, 슬롯이 하위)
기본은 디폴트 슬롯
데드슬롯 추가
데드슬롯으로 설정
애니메이션 블루프린트도 변경
이제 공격을 받으면 바로 죽도록 할 것임
언리얼 엔진 액터 설정에 TakeDamage 가 존재
리턴값은 최종으로 액터가 받은 대미지의 양을 의미
인스티게이터는 피해를 입힌 가해자
커저는 가해자가 사용한 무기, 아니면 빙의한 폰, 액터 정보
float AABCharacterBase::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
SetDead();
return DamageAmount;
}
void AABCharacterBase::SetDead()
{
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
PlayDeadAnimation();
SetActorEnableCollision(false);
}
void AABCharacterBase::PlayDeadAnimation()
{
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
AnimInstance->StopAllMontages(0.0f);
AnimInstance->Montage_Play(DeadMontage, 1.0f);
}
공격을 받으면 움직임 멈추고, 충돌 판정 전부 제거
그리고 실행 중이던 몽타주 있으면 전부 정지시키고 데드 몽타주 실행
한 대라도 맞으면 바로 SetDead 실행됨
대미지 입히는 건 이렇게 하면 됨
죽음
그런데 NPC는 일정 시간 지나면 사라지는게 나음
NPC에만 추가 구현
void AABCharacterNonPlayer::SetDead()
{
Super::SetDead();
FTimerHandle DeadTimerHandle;
GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle,
FTimerDelegate::CreateLambda(
[&]()
{
Destroy();
}
), DeadEventDelayTime, false);
}
타이머 쓰면 되는데, SetTimer에서 멤버 함수 만들어서 부착하긴 번거로우니
람다 함수 사용
사라지는 것 확인
'언리얼 엔진 > Unreal Game Framework' 카테고리의 다른 글
[UE Game Framework] #8 Item System (0) | 2025.01.06 |
---|---|
[UE Game Framework] #7 Character Stat & Widget (0) | 2025.01.05 |
[UE Game Framework] #5 Character Combo Action (0) | 2025.01.04 |
[UE Game Framework] #4 Character Animation Setting (0) | 2025.01.04 |
[UE Game Framework] #3 Character Control Setting (0) | 2025.01.03 |