뭐 야매 메크로 문법이 있다. 델레게이트 사용할 수 있는.
이렇게 함수 호출하면 FOnAttackHit을 구독하는 애들한테 다 알려주는 거임.
AnimNotify_AttackHit 이 함수는 뭐 였냐하면은 애니매이션 Notify할 때 사용했던 함수임.
우리 애니매이션 몽타주 만들 때 사용했던 애들임 (아래사진의 노티파이 부분)
그리고 해당 애니매이션의 어느 지점일때 알려주는 거임.
그러면 코드가 아래로 들어오는데
이때 BraodCast를 하는데 알림을 받는애가 BeginPlay이전에 호출되는 함수에서
(PostInitializeComponent가 모든 컴포넌트들이 초기화 되고 호출되는 함수임)
AttackCheck로 C#으로 치면 구독을 한셈임.
그러면 애니매이션 노티파이 울릴 때마다 이 AttackCheck함수가 구독을 했기 때문에 알림을 받는 구조임.
#include "DrawDebugHelpers.h"
void AMyCharacter::AttackCheck()
{
FHitResult HitResult;
FCollisionQueryParams Params(NAME_None, false, this);
float AttackRange = 100.f;
float AttackRadius = 50.f;
bool bResult = GetWorld()->SweepSingleByChannel
(
OUT HitResult,
GetActorLocation(),
GetActorLocation() + GetActorForwardVector() * AttackRange,
FQuat::Identity,
ECollisionChannel::ECC_GameTraceChannel2,
FCollisionShape::MakeSphere(AttackRadius),
Params
);
FVector Vec = GetActorForwardVector() * AttackRange;
FVector Center = GetActorLocation() + Vec * 0.5f;
float HalfHeight = AttackRange * 0.5f + AttackRadius;
FQuat Rotation = FRotationMatrix::MakeFromZ(Vec).ToQuat();
FColor DrawColor;
if (bResult)
DrawColor = FColor::Green;
else
DrawColor = FColor::Red;
DrawDebugCapsule(GetWorld(), Center, HalfHeight, AttackRadius, Rotation, DrawColor, false, 2.f);
if (bResult && HitResult.Actor.IsValid())
{
++asdasdqw;
UE_LOG(LogTemp, Log, TEXT("Hit Actor : %s %d"), *HitResult.Actor->GetName(), asdasdqw);
}
}
AttackCheck함수는 이런식으로 사용함 (중간에 헤더파일)
실제 칼의 궤도를 계산을 해서 충돌처리를 하는 것은 아니고 Actor의 ForwardVector를 구한다음에 (단위벡터)에다가 크기를 곱해주어서 애니매이션 노티파이 울릴때 범위안에서 충돌이 일어났는지 아닌지 판별하는 식임.
캐릭터 무기 감싸쥐거나 그런 부분들임.
아래처럼 우리의 SkeletalMesh찾아서 왼손 소켓 찾는다. 그리고 소캣 추가를 해준다.
코드로 돌아가서
이딴식으로 StaticMesh추가를 함(캐릭터 처럼 움직이는 애가 아니니까)
생성자 부분에서 이렇게 로드를 한뒤에 Succeeded이면 붙여준다.
Weapon을 현재 Mesh의 WeapoSocket에다가 붙여주어야함.
적용이 잘되어있음.
RPG의 경우 땅바닥에 있는 아이템 줍는경우 땅바닥에 있는 애를 하나의 Actor로 만들어서 하는 방법이 있다.
먼저 C++클래스 Actor상속 받아서 파일 만들어준다.
이후에 MyWeapon 코드를 아래처럼 작성해주고 MyCharacter에서 GetWorld에 접근하여
스폰을 해주면 레벨에서 0, 0, 0 위치에 스폰됨.
근데 이것을 다시 플레이어 한테 붙이고싶으면
void AMyCharacter::BeginPlay()
{
Super::BeginPlay();
FName WeaponSocket(TEXT("hand_l_Socket"));
auto CurrentWeapon = GetWorld()->SpawnActor<AMyWeapon>(FVector::ZeroVector, FRotator::ZeroRotator);
if (CurrentWeapon)
{
CurrentWeapon->AttachToComponent
(
GetMesh(),
FAttachmentTransformRules::SnapToTargetNotIncludingScale,
WeaponSocket
);
}
}
이전이랑 비쥬얼 적인 차이는 없는데 MyWeapon이라는 Actor를 따로 만들어서 붙여준게 다른점이다.
이전에는 그냥 아예 붙어있던 개념이라면..
현재 아래 코드 부분 주석처리 한다음에
이 상태에서 저거 이상한거 닿이면 팔에 붙일 수 있는 부분 ㄱㄱ.
그 다음에 새로운 충돌 규약을 만들어 주어야한다.
먼저 Object Channel에서 MyCollectible 만들어주고 ignore로 셋팅한다음에 Preset설정을 하러간다.
지금 MyCollectible은 물리적으로 뭔가를 막는 역할을 할게 아니라
Query즉 => 이벤트를 발생시킬 녀석이기 때문에 오브젝트 유형을 Query Only Physics로 설정해준다.
그 다음에 다른애들은 다 무시하고 MyCharacter 랑만 overlap으로 해준다.
이렇게. 추가로 MyCharaceter도
이렇게 overlap으로 수정 ㄱ.
이후에 충돌을 해주게하는 함수를 만들어야하는데 UBoxComponent에 충돌해주는 함수가 있다.
근데 인자를 받는데 인자가 좀 많다. Delegate를 받아주는 함수가 이 시그니처를 맞추어야지만 Delegate를 받을 수 있다.
public:
UFUNCTION()
void OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
이렇게 선언해주고
void AMyWeapon::PostInitializeComponents()
{
Super::PostInitializeComponents();
Trigger->OnComponentBeginOverlap.AddDynamic(this, &AMyWeapon::OnCharacterOverlap);
}
void AMyWeapon::OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
UE_LOG(LogTemp, Log, TEXT("Overlapped!"));
// Cast == c++ 로 치면은 dynamic_cast랑 비슷하다.
AMyCharacter* MyCharacter = Cast<AMyCharacter>(OtherActor);
if (MyCharacter)
{
FName WeaponSocket(TEXT("hand_l_Socket"));
AttachToComponent
(
MyCharacter->GetMesh(),
FAttachmentTransformRules::SnapToTargetNotIncludingScale,
WeaponSocket
);
}
}
아까처럼 PostInitializeComponents에서 구독한다음에 OnCharacveterOverlap함수에서 이런식으로 연결 해주어야함.
c++로 치면은 dynamic_cast랑 비슷하다.
유니티에서는 스탯을 설정할 때 약간 커스텀 하는 방식으로 DataManager라는 것을 만들어서 관리를 할 수 있었는데
언리얼은 그런게 좀 힘들고 안된다. 이미 틀이 잡혀있기 때문에...
그래서 게임이 실행이 되면 딱 한번 초기화가되는 애를 만들어 주도록 하자.
애는 게임이 실행이되는 순간에 초기화가 딱 한번이 된다.
그래서 나중에 전역 Manager로 활용하기에 나쁘지 않다.
그리고나서 아래와 같이 정의 해줌.
그리고 나서 Data폴더 만들고 기타 -> 데이터 테이블 만들면됨.
데이터를 따로 빼서 관리를 하는데 바이너리에 묶여서 같이 나가는 파일이 아니다.
그리고 코드를 작성해주면 된다.
이따위로 해주도록 하자.
월드 셋팅 같은 부분 때문에 로그가 안뜨는데
여기서 설정을 해주어야함.
하고나면은 아래처럼 잘 뜬다.
뭔가 DataManager같은 중개인이 피격판정한뒤에 체력을 깍아야할까
때리는쪽에서 맞는애한테 접근해서 깍아야할까?
이런 고민들이 있는데 거의 대부분 맞는 쪽에서 자신의 HP를 관리를 하는게 유지보수에 좋다.
Stat같은 수치들도 Character에다가 int32 HP이런식으로 넣어서 관리를 할게 아니라
ActorComponent를 따로 빼놔서 Stat은 Stat끼리 관리하는게 더 좋?다?
그래서 ActorComponent를 만들 것이다.
아래 함수 사용할려면 PostInitializeComponents같은 함수인데
아래 부분 true로 켜주어야함.
이후 코드 끼리 연결 - 연결 하는 부분이 좀 복잡한데
MyGameInstance에서 StatTable데이터를 가지고 있을 것이다.
유니티의 DataManager역할처럼 할 것임.
이런식으로 만들어줌. 위에서보면은 생성자에서 StatTable경로 Load하고있음.
그리고 ActorComponent를 상속하여 MySTatcomponent라고 만들어준다.
여기서 모든 스탯을 관리를 할 것임.
void UMyStatComponent::SetLevel(int32 NewLevel)
{
auto MyGameInstance = Cast<UMyGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
if (MyGameInstance)
{
auto StatData = MyGameInstance->GetStatData(NewLevel);
if (StatData)
{
Level = StatData->Level;
Attack = StatData->Attack;
HP = StatData->MaxHp;
}
}
}
초기화할 때 SetLevel에서 Level,Hp, Attack같은거 초기화를 받고
MyCharaceter는 데미지를 받으면 자신의 컴포넌트 한테 아래처럼 데이터만 알려주는 식이다.
잘 뜬다.
UI폴더 만들고 아래처럼 진행
만든 파일 연다음에 아래처럼 대충 만들면됨.
C#에서 했던거랑 비슷하게 하나의 프리팹을 만들고 거기에 대응하는 cpp파일을 만들것임.
지금 이 Progress Bar는 아래에서 보는것처럼 User Widget을 상속을 받는 것을 볼 수 있는데
지금 아래의 MyCharacterWidget을 상속받도록 수정할 것이다.
그리고 아래 처럼 만들어 주도록 하자.
지금 ConstructorHelpers::에서 FClassFinder를 통해서 클래스를 찾을 것인데
블루 프린트 클래스를 찾을 경우에는 컨벤션에 "_C" 붙여야 하는 규칙이 있다.
블루프린트로 만들애를 클래스로 사용할 경우
아래처럼
이렇게.
그리고 아래 부분 C#파일 UMG추가 해야됨.
아래 처럼 meta 이렇게 설정하면 Tool상에서 만들었던 ProgressBar랑 PB_HPBar랑 바로 바인딩이 된다.
지금은 인자를 아무것도 안받는 버젼의 Delegate를 바인딩 해주었기 때문에
이런식으로 BindHp에서 UpdateHp에 데이터를 전달할 방법이 없기 때문에
클래스의 멤버 변수로 UMyStatComponent*를 가지고 있는 다음에 데이터를 UpdateHP에 데이터 전달 씹가능.
(인자를 받는 버젼의 Delegate쓰던가..)
그리고 쌩포인터를 사용하고 있기 때문에 스마트 포인터로 대체를 하도록 하자.
이렇게하면 아래의 변수명과 Tool상에서 작업한 Progressbar자동으로 연결해줌.
아래의 이름과 같이 변수명을 만들어야 바로 meta BindWidget 바인딩 씹가능임.
이렇게 변수이름과 맞춰줘야됨
이후 코드 흐름이 조금 복잡할 수 있는데
먼저 아래 애니매이션의 노티파이를 설정해서 애니매이션의 특정 지점에서 알람을 주도록 한다.
저 노티파이를 받는 방법은 AnimNotify_노티파이 이름 이런식이다. (아래 사진 참고)
이렇게해주면 노티파이가 울릴 때 OnAttackHit이라는 Delegate를 호출을 할 것이다.
아래의 Delegate임.
DECLARE_MULTICAST_DELEGATE(FOnAttackHit);
FOnAttackHit OnAttackHit;
그리고 MyCharacter에서 모든 컴포넌트들이 초기화가 되고나서 호출되는 PostInitialize함수에서
Delegate에서 받을 함수를 Binding해준다.
위와 같이 AttackCheck라는 함수를 바인딩 해줌.
아래의 함수는 노티파이가 울릴 때 범위를 지정해서해당 범위 안에 상대방이 있는지 없는지를 ForwardVector로 체크하는 부분이다.
void AMyCharacter::AttackCheck()
{
FHitResult HitResult;
FCollisionQueryParams Params(NAME_None, false, this);
float AttackRange = 100.f;
float AttackRadius = 50.f;
bool bResult = GetWorld()->SweepSingleByChannel
(
OUT HitResult,
GetActorLocation(),
GetActorLocation() + GetActorForwardVector() * AttackRange,
FQuat::Identity,
ECollisionChannel::ECC_GameTraceChannel2,
FCollisionShape::MakeSphere(AttackRadius),
Params
);
FVector Vec = GetActorForwardVector() * AttackRange;
FVector Center = GetActorLocation() + Vec * 0.5f;
float HalfHeight = AttackRange * 0.5f + AttackRadius;
FQuat Rotation = FRotationMatrix::MakeFromZ(Vec).ToQuat();
FColor DrawColor;
if (bResult)
DrawColor = FColor::Green;
else
DrawColor = FColor::Red;
DrawDebugCapsule(GetWorld(), Center, HalfHeight, AttackRadius, Rotation, DrawColor, false, 2.f);
if (bResult && HitResult.Actor.IsValid())
{
UE_LOG(LogTemp, Log, TEXT("Hit Actor : %s"), *HitResult.Actor->GetName());
// HitResult의 Actor가 피해자임.
FDamageEvent FDamage;
HitResult.Actor->TakeDamage(StatComp->GetAttack(), FDamage, GetController(), this);
}
}
만약 있다면은 초록색으로 Sphere가 뜨고 아니면 빨간색으로 뜰거임.
그리고 PostInitialize부분에서 HpBar도 바인딩을 해놨는데
BindHP에서도 Delegate를 받을 함수를 연결을 해놓은 부분이다. 아래처럼.
그래서 StatComp의 HP가 변동이 생기면 UpdateHP가 알림을 받아서 호출을 하는 것이다.
그러면 아래의 PB_HpBar의 SetPercent함수를 통해서 StatComponent에 정의해놓은
GetHpRatio함수를 호출하여 Percent조절을 하는 것임.