0. 개요
1. 과제 6. 회전 발판과 움직이는 장애물 퍼즐 스테이지
1.목표
!! 스테이지 하나의 퍼즐 게임 만들기 !!
1인칭으로 플레이어가 직접 움직이면서 장애물들이 움직이는 공간에서 퍼즐을 푸는 일종의 퍼즐 게임?
<구현 목표>
- [O] 이동하는 발판 만들기
- [O] 회전하는 발판 만들기
- [O] 일정시간 뒤 떨어지는 발판 만들기
- [ ] 발판들 랜덤 스폰 만들기
- [ ] 트램펄린 만들기
- [ ] 레이저 퍼즐 만들기
- [ ] 레이저 스포너
- [ ] 레이저
- [ ] 레이저 리플렉션
- [ ] 레이저 타겟
2. 이동하는 발판 만들기
FVector로 방향을 받고, float으로 목표 거리, 속도를 받아 동작하도록 구현했다.
<FVector::Dist(), FVector::Distance() 차이>
FVector::Distance()는 단순히 내부적으로 FVector::Dist()를 호출해준다.
즉, 다를게 없다..;
<FVector::Normalize(), FVector::GetSafeNormal()차이>
FVector::Normalize() :
주어진 허용 오차보다 크면 메소드를 호출한 인스턴스를 정규화 하고, 그렇지 않으면 변경되지 않는다.
FVector::GetSafeNormal() :
벡터의 정규화된 복사본을 가져오며, 길이를 기준으로 안전하게 정규화 할 수 있는지 확인한다.
벡터 길이가 너무 작아서 안전하게 정규화할 수 없는 경우 0 벡터를 반환한다.
3. 회전하는 발판 만들기
FRotator로 원하는 방향의 회전 속도를 넣어주면 해당 방향으로 회전하도록 구현했다.
4. 일정시간 뒤 떨어지는 발판 만들기
< BeginOverlap 사용 방법 (아오!!) >
BeginOverlap을 C++에서 사용하는 방법은 2가지가 있다.
- Override : Actor가 충돌 시 호출되는 함수를 재정의하여 사용하는 법
- Delegate Binding : Actor의 Component가 충돌 시 호출할 함수를 바인딩(Binding)해놓고 Overlap이 발생시 해당 함수를 호출하는 방법
이 두가지 모두 사용해보았다.
1. NotifyActorBeginOverlap() 오버라이딩
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
void AFallPlatform::NotifyActorBeginOverlap(AActor* OtherActor)
{
Super::NotifyActorBeginOverlap(OtherActor);
if (Cast<ACharacter>(OtherActor))
{
if (!GetWorld()->GetTimerManager().IsTimerActive(FallingTimerHandle))
{
GetWorld()->GetTimerManager().SetTimer(FallingTimerHandle, this, &AFallPlatform::FallingPlatform, HoldingTime, false);
}
}
}
단순히 NotifyActorBeginOverlap()이라는 함수를 재정의해두면 된다.
2. Delegate Binding
UFUNCTION(BlueprintCallable, Category="FallPlatform|Functions")
void OnComponentBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
// ...
void AFallPlatform::BeginPlay()
{
Super::BeginPlay();
BoxComp->OnComponentBeginOverlap.AddDynamic(this, &AFallPlatform::OnComponentBeginOverlap);
}
바인딩할 함수를 만들어서 정의해주고, BeginOverlap 이밴트를 호출해줄 Component의 OnComponentBeginOverlap.AddDynamic()에 바인딩 걸어준다.
!! 주의할 점 !!
근본적인 문제가 있었는데.. ㅠ
만약에 오버랩을 이용할 거라면, 콜리전 프리셋이 Block이면 안된다.
만약 Block일 경우에는 콜리전 컴포넌트를 하나 붙여서 Overlap으로 처리해주거나 Hit이벤트를 사용해주어야 할 것 같다.
< HitEvent 사용 방법 >
BeginOverlap과 동일하다.
음.. 하지만 오버라이딩해서 사용하는 방법은 어느것을 오버라이딩해야할지 모르겠어서 엔진코드를 살펴봐야할 것 같다.
그래서 일단 바인딩을 거는 방법으로만 사용해보았다.
// .... .h
UFUNCTION(BlueprintCallable, Category="FallPlatform|Functions")
void OnPlatformHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
//.... .cpp
void AFallPlatform::BeginPlay()
{
Super::BeginPlay();
StaticMeshComp->OnComponentHit.AddDynamic(this, &AFallPlatform::OnPlatformHit);
}
void AFallPlatform::OnPlatformHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,
FVector NormalImpulse, const FHitResult& Hit)
{
UE_LOG(LogTemp, Warning, TEXT("히트 이밴트 발생!"));
if (Cast<ACharacter>(OtherActor))
{
if (!GetWorld()->GetTimerManager().IsTimerActive(FallingTimerHandle))
{
GetWorld()->GetTimerManager().SetTimer(FallingTimerHandle, this, &AFallPlatform::FallingPlatform, HoldingTime, false);
}
}
}
주의사항
함수를 등록할 때! 반드시 해당 함수는 UFUNCTION 메크로가 사용된 함수여야한다.
음.. 아마 리플렉션 시스템에 등록되어있지 않는다면, 연결이 안되는 것 같다..?
의문인점
log를 출력해보니, Hit이벤트는 단순히 닿았다가 아니라 일정량의 충격기 가해져야 호출되는것 처럼 보였다.
또한 점프 후 착지 시에는 히트 이벤트가 1회 발생 하지만, 달려가서 충돌할 경우 4회씩 호출되는 경우가 있었다.
음.. 왜 그런지 좀 더 조사하고 공부해봐야할 것 같다.
< Timer 사용 방법 >
몇 초 뒤에 발판이 떨어지게 구현하려했다.
지금까지의 프로젝트에서 구현하던 것 처럼 AccTime을 만들어서 tick에서 처리를 해주려했다.
하지만, Timer라는게 언리얼에는 이미 존재하고 있어서 해당 내용을 공부하고 사용해보았다.
<FTimerManager란?>
언리얼 엔진에서 타이머를 관리하기 위한 클래스이다.
이 클래스는 게임 내에서 특정 작업을 지연 후 실행하거나, 주기적으로 반복 실행하는 데 사용한다.
타이머 시스템은 UWorld 클래스의 인스턴스에 포함되어 있으며, 이를 통해 타이머를 설정, 관리, 제거할 수 있다.
<FTimerManager의 주요 특징>
- 비동기 작업 스케줄링
- 특정 시간 이후 실행할 작업을 예약하거나, 일정 간격으로 반복 실행할 작업을 설정할 수 있다.
- 정확한 시간 관리
- FTimerManager는 게임의 델타 타임(Delta Time)을 고려해 타이머를 정확하게 관리한다.
- 이를 통해 게임 속도에 관계없이 일관된 시간 동작을 보장한다.
- 콜백 함수 실행
- 타이머가 만료되었을 때, 지정된 함수나 델리게이트를 호출한다.
- 이를 통해 비동기 이벤트를 간단히 구현할 수 있다.
- 효율적인 관리
- 타이머를 설정하거나 제거할 수 있으며, 타이머의 상태(활성화 여부, 남은 시간 등)를 확인할 수 있다.
<FTimerManager의 주요 함수>
void SetTimer(
FTimerHandle& InOutHandle, // <- 타이머를 식별하는
UObject* InObj, // <- 호출할 함수를 소유한 객체
FName InFunctionName, // <- 호출할 함수의 이름
float InRate, // <- 타이머 간격(초)
bool bLoop = false, // <- 반복 실행 여부
float InFirstDelay = -1.0f // <- 첫 실행까지의 지연시간
);
// 설정된 타이머를 제거한다.
void ClearTimer(FTimerHandle InHandle);
PauseTimer / UpPauseTimer : 설정된 타이머를 정지한다.
GetTimerRemaining : 타이머가 종료되기까지 남은 시간을 반환
IsTimerActive : 특정 타이머가 활성화 상태인지 확인
<활용 시 유의사항>
- TimerHandle 관리
- 타이머를 설정할 때 반환된 FTimerHandle을 반드시 관리해야, 필요시 타이머를 중지하거나 상태를 확인할 수 있다.
- Garbage Collection과 타이머
- 타이머가 호출할 객체는 가비지 컬렉션으로 파괴되지 않도록 보장해야한다. 예를 들어 UObject가 파괴되면 해당 타이머는 정상적으로 동작하지 않을 수 있다.
- 타이머의 정확성
- 타이머는 게임의 프레임 속도와 델타 타임에 영향을 받으므로, 매우 높은 정밀도가 요구되는 작업에는 적합하지 않을 수 있다.
https://dev.epicgames.com/documentation/ko-kr/unreal-engine/gameplay-timers-in-unreal-engine
https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/FTimerManager
5. 일정시간 뒤 떨어진 발판 복귀 시키기
발판이 떨어지면, 일정 시간 뒤 원래 자리로 복귀하도록 구현했다.
사실 StaticMesh의 피직스 시뮬레이트를 켜준것이기 때문에 루트 컴포넌트의 위치는 달라지지 않는다.
때문에 Location과 Roatation을 0.f으로 전달해주면 된다.
하지만, StaticMeshComponent의 상대 Transform을 움직여 사용할 가능성도 있기 때문에 BeginPlay시에 StaticMeshComponent의 Transform 정보를 멤버변수에 담아 관리하도록 만들었다.
void AFallPlatform::BeginPlay()
{
Super::BeginPlay();
BoxComp->OnComponentBeginOverlap.AddDynamic(this, &AFallPlatform::OnComponentBeginOverlap);
OriginMeshTransform = StaticMeshComp->GetComponentTransform();
}
void AFallPlatform::OnComponentBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
UE_LOG(LogTemp, Warning, TEXT("CallEvent"));
if (Cast<ASparta_Homework_06Character>(OtherActor))
{
if (!IsFalling)
{
GetWorld()->GetTimerManager().SetTimer(FallingTimerHandle, this, &AFallPlatform::FallingPlatform, HoldingTime, false);
IsFalling = true;
}
}
}
void AFallPlatform::FallingPlatform()
{
StaticMeshComp->SetSimulatePhysics(true);
GetWorld()->GetTimerManager().ClearTimer(FallingTimerHandle);
GetWorld()->GetTimerManager().SetTimer(FallingTimerHandle, this, &AFallPlatform::ResetPlatform, ResetTime, false);
}
void AFallPlatform::ResetPlatform()
{
StaticMeshComp->SetSimulatePhysics(false);
GetWorld()->GetTimerManager().ClearTimer(FallingTimerHandle);
StaticMeshComp->SetRelativeTransform(OriginMeshTransform);
IsFalling = false;
}
그냥 간단하게 하나씩 설명하자면,
- BeginPlay
- BeginOverlap 이벤트에 함수 바인딩
- StaticMeshComponent의 기존 Transform 정보를 저장
- OverlapEvent 발생
- Player 캐릭터인지 확인 후 맞다면 진행
- 아직 피직스 시뮬레이션이 켜지지 않았다면 진행
- 피직스 시뮬레이션이 켜졌으니 true로 변경
- 타이머인해 HoldingTime이 지난 후 FallingPlatform() 호출
- StaticMeshComponent의 피직스 시뮬레이션 On
- 타이머 제거
- 타이머인해 ResetTime이 지난 후 ResetPlatform() 호출
- 물리 시뮬레이션 off
- 타이머 제거
- StatitcMeshComponent의 위치를 원래대로 변경
- 물리 시뮬레이션을 껐으니 false로 변경
이렇게 진행된다.
음.. 사실 타이머 핸들을 재사용할 수 있을까? 하는 궁금증에 이렇게 구현했는데, 역시 가능은 했다.
이번의 경우 타이머 해제/설정 시점이 순차적으로 진행되기 때문에 오류가 발생할 일을 거의 없을거라 판단해 핸들 하나로 사용하도록 구현했지만, 이렇게 사용하는건 좋지는 않아보인다.. ㅠ
잘못하다가 꼬일 가능성이 있어보이기 때문..
6. 발판 수정
<문제>
RootComponent말고 어태치 시킨 컴포넌트의 물리 시뮬레이션을 런타임중 키면, 어태치 관계가 깨지는지는 것 같다.
⇒ 컴포넌트의 상대 경로를 가지고 기록해뒀다가 나중에 넣어줘서 원복시키는 과정에서 어태치 관계가 깨져서 월드상 0,0,0 포지션으로 이동해버림
살펴보니, SceneComponent는 피직스 시뮬레이트를 하는 기능 자체가 없었다.
때문에 어태치된 자식은 물리 시뮬레이션을 하는데 부모는 물리 시뮬을 하지 않는다는게 이상해서 언리얼쪽에서 때어내는 것 같다..
<해결>
튜터님께 여쭤보니, 어태치 관계를 강제해주는 함수가 있다고는 하셨는데, 내가 찾지를 못해서.. 우선 RootComponent를 StaticComponent로 해주는 것으로 해결했다.
AFallPlatform::AFallPlatform()
: IsFalling(false)
, HoldingTime(0.f)
, ResetTime(0.f)
{
PrimaryActorTick.bCanEverTick = true;
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMeshRoot"));
SetRootComponent(StaticMeshComp);
BoxComp = CreateDefaultSubobject<UBoxComponent>(TEXT("BoxComponent"));
BoxComp->SetupAttachment(StaticMeshComp);
}
// ...
// ...
void AFallPlatform::ResetPlatform()
{
StaticMeshComp->SetSimulatePhysics(false);
GetWorld()->GetTimerManager().ClearTimer(FallingTimerHandle);
SetActorTransform(OriginTransform);
IsFalling = false;
}
해결할 수 있는 방법이 몇 가지 더 있었는데,
그 중 하나는 어태치가 때진 StaticMeshComponent를 원래 위치로 옮길때 WorldLocation을 저장하고 있다가 넣어주는 것이었다.
근데, 이런 방법 보다는 위처럼 RootComponent로 지정해주는게 훨씬 깔끔해보였다.
'프로그래밍 > Unreal 부트캠프' 카테고리의 다른 글
TIL 2025.01.31 기록 (0) | 2025.01.31 |
---|---|
TIL 2025.01.24 기록 (0) | 2025.01.24 |
TIL 2025.01.22 기록 (0) | 2025.01.22 |
TIL 2025.01.21 기록 (0) | 2025.01.21 |
WIL 2025.01.20 기록 (0) | 2025.01.20 |