0. 개요
오늘은 어제 만들지 못했던 레이저를 모두 구현하고, 점프패드까지 만들었다.
음... 하면서 좀 다양한 것들을 조금씩 사용해 봤는데,
예를 들자면, Unreal Interface, C++ 클래스 상속 받아 C++ 클래스 만들기, 머티리얼 색상 변경하기, 블루프린트 레퍼런스 사용하기, LaunchCharacter() 사용하기 등등이 있다.
아무래도 C++로 언리얼을 제대로 다룬게 처음이어서 기능 하나를 만들더라도 대부분이 처음 사용하는 것들이었다. ;ㅅ;
성장통 빡쎄다.....
여하튼.. 이제 구현 목표중 남은 것은 랜덤 생성 부분이다.
오늘 하루종일 작업하면서 아이디어를 계속 생각중이 었는데, 꽤 괜찮은 아이디어가 떠올랐다.
사실 완전 랜덤 배치를 하는데, 시작점부터 끝지점까지 플레이어가 충분히 갈 수 있는 그런 랜덤 맵을 만들 수 있다면 좋겠지만, 지금 내가 떠올리기에는 너무 어려웠다.
고려해야할게 한 두개가 아닌 것 처럼 보였다. 플레이어가 다음 발판으로 이동할 때, 충분히 도달할 수 있는 위치인지, 발판들끼리 서로 이동하다가 충돌하지는 않는지 등등이었다..
음.. 한마디로 정말 말이 되는 디자인으로 생성되길 바라는데, 그렇게 안될 것 같았다. ;ㅅ; 실력 부족...
ㄴ> 우선 이부분은 더 고민해보고, 찾아봐야겠다..
그래서 2번째 아이디어는 떨어지는 발판을 구현했으니까, 랜덤 미로 생성 알고리즘을 통해 안떨어지는 발판과 떨어지는 발판으로 플레이어가 안떨어지면서 길을 찾아가야하는 함정을 만들어보려 한다.
이때, 길은 게임이 종료되고 다시 시작할때 랜덤하게 생성되도록 하는게 목표이다.
ㄴ> 떨어졌을 땐 플레이어가 리스폰 하도록만.
이거는 주말간 구현해 볼 예정이다.
오늘은 새로 써본 내용들까지는 정리하지 못할 것 같고, 우선 진행 사항들을 쭉 작성하고, 주말이나 설연휴를 이용해 오늘 공부한 내용들에 대해 정리해볼 생각이다.
1. 과제 6. 회전 발판과 움직이는 장애물 퍼즐 스테이지
1. 목표
!! 스테이지 하나의 퍼즐 게임 만들기 !!
1인칭으로 플레이어가 직접 움직이면서 장애물들이 움직이는 공간에서 퍼즐을 푸는 일종의 퍼즐 게임?
<구현 목표>
- [O] 이동하는 발판 만들기
- [O] 회전하는 발판 만들기
- [O] 일정시간 뒤 떨어지는 발판 만들기
- [ ] 발판들 랜덤 스폰 만들기
- [O] 트램펄린 만들기
- [O] 레이저 퍼즐 만들기
- [O] 레이저 스포너
- [O] 레이저
- [O] 레이저 리플렉션
- [O] 레이저 타겟
- [O] 레이저 쪼개기..? 만들기
으아!! 이제 발판 랜덤 스폰만 남았다..!!! 이건 주말간 무조건 끝낸다!
2. 레이저 만들기
< 공통 부모 클래스 만들기 >
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Laser_Parents.generated.h"
class ALaserSpawner;
class ALaser;
UCLASS()
class SPARTA_HOMEWORK_06_API ALaser_Parents : public AActor
{
GENERATED_BODY()
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Laser|Components")
USceneComponent* SceneRootComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Laser|Components")
UStaticMeshComponent* StaticMeshComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Laser|Properties")
bool bActive;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Laser|Properties")
FLinearColor LaserColor;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Laser|Properties")
ALaserSpawner* LaserSpawnerRef;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Laser|Properties")
TArray<ALaser*> LaserAttached;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Laser|Properties")
bool bLaserHit;
public:
ALaser_Parents();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
void SetLaserColor(FLinearColor Color);
FLinearColor GetLaserColor() {return LaserColor;}
void SetbActive(bool Active) {bActive = Active;}
void SetLaserSpawnerRef(ALaserSpawner* Spawner);
ALaserSpawner* GetLaserSpawner() {return LaserSpawnerRef;}
};
레이저 관련 액터들이 가져야하는 공통된 부분들을 모아두었다.
SceneComponent, StaticComponent가 공통된 컴포넌트들이고
LaserColor, bActive, LaserSpawnerRef(레이저 스포너 주소), LaserAttached(나중에 반사 등 할 때 레이저를 이어 붙이는 개념으로 생성시 등록해주고, 해제시 삭제해주도록), bLaserHit 이 변수들이다.
또 각각 상황에 알맞게 UPROPERTY를 지정해주었다.
함수의 경우 대부분 Getter Setter이다.
< 인터페이스 만들기 >
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "LaserInterface.generated.h"
class ALaser;
UINTERFACE(MinimalAPI)
class ULaserInterface : public UInterface
{
GENERATED_BODY()
};
class SPARTA_HOMEWORK_06_API ILaserInterface
{
GENERATED_BODY()
public:
virtual ALaser* LaserBounce(ALaser* LaserHit, bool HitResponse, const FHitResult& HitInformation) = 0;
};
레이저 액터들이 공통으로 가지는 인터페이스를 따로 만들어 주었다.
이로써 레이저가 충돌할 때 대상 액터가 해당 인터페이스를 지니고 있는지, 아닌지 판별해 다른 동작을 해주게하고, 각 레이저 액터들은 LaserBounce라는 순수 가상 함수를 재정의해 서로 다른 동작을 하도록 구현해주었다.
이러한 인터페이스는 레이저 액터인 ALaser는 갖지 않도록했고, ALaser와 충돌되어 어떤 행동을 해주어야 하는 액터들이 공통으로 지니는 인터페이스로 구현해주었다.
위와 같은 형태로 나타낼 수 있을 것 같다.
사실 지금은 굳이 인터페이스로 저렇게 만들 필요는 없는것 같은데.. ALaserParents를 상속받지 않는 액터도 레이저와 만난다면, 어떤 일을 수행해야할 때를 생각하면 인터페이스를 활용해 사용해보는게 이후 작업에서 도움이 많이 될 것 같아 사용해보았다.
[UE4] 인터페이스 (Interface)
What is "Interface"? 인터페이스(interface)란 특정 기능을 구현할 것을 약속한 추상 형식을 말합니다. Java나 C#등 다른 객체지향 언어에서는 인터페이스 형식을 제공하지만 C++언어에서는 제공하지 않습
coding-hell.tistory.com
[UE] 언리얼 C++ 설계 1 - 인터페이스
언리얼 C++ 인터페이스 클래스를 사용해 보다 안정적으로 클래스를 설계하는 기법을 학습하자. 목차 언리얼 C++ - 인터페이스 언리얼 C++ 인터페이스 인터페이스란?객체가 반드시 구현
designerd.tistory.com
< 레이저 액터 만들기 >
우선 레이저 액터를 C++ 클래스로 만들어주었다.
물론 이번에도 나이아가라 시스템으로 만든게 아니다.. 헿..
UCLASS()
class SPARTA_HOMEWORK_06_API ALaser : public ALaser_Parents
{
GENERATED_BODY()
private:
FHitResult HitResult;
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Laser|Properties")
AActor* LastHitActor;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Laser|Properties")
AActor* CurrentHitActor;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Laser|Properties")
FString LaserID;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Laser|Properties")
ALaser* BounceLaser;
public:
ALaser();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
void ChangeColor() const;
void SetLaserEndPoint();
FString GetLaserID() const;
bool CancelTargetHit(bool IsOverride);
FString UpdateLaserID(FString strAppend) const;
};
레이저는 ALaser_Parents를 상속 받고, ILaserInterface를 함께 상속 받는다.
마지막에 부딪힌 액터와 현재tick에 부딪힌 액터, 본인의 ID, 새로 생성된 다음 Laser 주소를 지닌다.
SetLaserEndPoint()를 tick에 배치해 라인 트레이스를 쏴서 다른 액터와 부딪히면, 거기까지 레이저가 이어질 수 있도록한다.
부딪힌 액터가 없다면 레이저를 엄~~청 길게 연장되도록 구현했다.
BeginPlay()에서는 레이저의 색상을 초반에 변경하도록 해주었다.
이전 BP로 만들었을 때는, 동적 머티리얼을 생성해 변경헀는데, C++로 하니까 머티리얼의 파라미터를 변경하는 것 만으로 충분함을 깨달았다.
<수정해야할 사항>
- SetLaserEndPoint() 함수에서 하는 변수들 정리하기
- Laser의 ID를 다른 것으로 변경하도록 고민하기
- 리플렉션 등록 지정자들 다시한 번 더 생각해보기
<레이저 스포너 만들기>
다음으로는 레이저를 처음 생성해주는 부분인 레이저 스포너를 만들었다.
레이저 액터를 잘 생성해주고, 레이저 액터도 잘 동작하는 것을 확인할 수 있다.
UCLASS()
class SPARTA_HOMEWORK_06_API ALaserSpawner : public ALaser_Parents
{
GENERATED_BODY()
private:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "LaserSpawner|Properties" , Meta = (AllowPrivateAccess = "true"))
TSubclassOf<ALaser> BPLaserRef;
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "LaserSpawner|Properties")
TArray<ALaser*> ChildLasers;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "LaserSpawner|Properties")
FString ID;
UPROPERTY(visibleAnywhere, BlueprintReadOnly, Category = "LaserSpawner|Properties")
ALaser* SpawnedLaser;
UPROPERTY(EditAnywhere, BlueprintReadWrite, category = "LaserSpawner|Components")
UArrowComponent* SpawnLaserArrow;
public:
ALaserSpawner();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
void RemoveFollowingLaser(ALaser* LaserRef);
void ClearAllLaserAtteched();
void AddLaserChild(ALaser* LaserRef);
};
- BPLaserRef
- 스포너에서 생성할 레이저는 블루프린트 액터를 참조받을 수 있도록 변수로 만들어주었다.
- Private 필드에 배치했지만, 에디터에서는 수정이 가능하도록 UPROPERTY를 지정해주었다.
- SpawnedLaser
- 처음 생성한 레이저의 주소를 가질 수 있도록 해주었다.
- ChildLasers
- 생성한 레이저의 자식 레이저들을 관리할 수 있도록 TArray로 레이저 포인터들을 관리해주도록 구현했다.
- SpawnLaserArrow
- 레이저 생성 방향을 에디터에서 작업할 때 표시해줄 수 있도록 ArrowComponent를 추가해주었다.
- 이 뿐만 아니라 레이저의 생성 위치를 잡아줄 때, ArrowComponent의 위치를 사용했다. 이러면 에디터에서 레이저 생성 위치도 나타내 줄 수 있고, 작업하기가 용이해 보였다.
<수정해야할 사항>
- 사용하지 않는 함수 지우기
- 리플렉션 시스템 지정자들 다시 생각해보기
- 부분부분 함수화하기
< 레이저 리플랙션 만들기.. >
아아.. 아름 다운 반사다..
UCLASS()
class SPARTA_HOMEWORK_06_API ALaserReflection : public ALaser_Parents, public ILaserInterface
{
GENERATED_BODY()
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LaserReflection|Components")
UArrowComponent* ArrowComp;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "LaserSpawner|Properties" , Meta = (AllowPrivateAccess = "true"))
TSubclassOf<ALaser> BPLaserRef;
public:
ALaserReflection();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
FRotator CalRotation(const FVector& ImpactPoint, const FVector& ImpactNormal, const FVector& TraceStart) const;
void ClearAttachedLasers(const FString& ClearLaserID);
virtual ALaser* LaserBounce(ALaser* LaserHit, bool HitResponse, const FHitResult& HitInformation) override;
};
- ArrowComp
- 마찬가지로 리플렉션 기준이되는 법선 벡터 방향을 표시해준다. (사실상 액터의 정면 벡터 방향 표시용)
- BPLaserRef
- 생성할 레이저를 블루프린트를 사용하기 위해 에디터에서 지정해줄 수 있도록 해주었다.
- 해당 부분은 Hit된 Laser에게 SpawnerRef를 받아서 가져오도록 구현하는쪽이 훨씬 나아보인다.
- 현재는 에디터에 리플렉션 액터가 추가될 때마다 설정해줘야하고, 설정을 깜빡하면 터져서 문제가 된다.
<수정해야할 사항>
이쪽도 마찬가지로..
- 겹치는 기능들 함수화하기
- BPLaserRef를 Spawner로부터 받아오도록 변경하기
< 레이저 삭제 버그 수정하기 >
중간에 레이저가 막혀도 생성된 레이저가 삭제되지 않았다.
<이유>
삭제 기능을 넣어뒀었는데, 삭제를 할때 Laser의 ID를 부여해 ID로 관리하고 있었다.
문제는 ID를 이상한 것을 넣어두어 삭제할 때 ID 검사 규칙에 맞지 않아 전혀 삭제가 안되고 있었던 것이었다.
=> Laser가 생성되면 Laser의 이름을 본인의 ID로 하게해두었다.
=> 하지만 Laser 반사쪽에서 생성된 애들은 뒤에 B를 붙여 본인이 어떤 Laser로부터 생성된 애인지 알도록하는게 본래 목표였기 때문에 한참 잘못되었다.. 헤헤; 이런 실수를...
<해결방법>
아이디를 만들 때, UKismetSystemLibrary::GetDisplayName(Object) 해당 함수를 사용했는데, 음…
나중에 리팩토링을 할때는 ID를 이렇게 만들게 아니라 진짜 고유한 숫자같은걸로 관리해주는게 좋을 것 같았다.
<수정 후>
수정 후 레이저가 막히면 생성되었던 반사 레이저들이 잘 지워지는 것을 확인할 수 있었다.
< 레이저 타겟 만들기 >
지금은 단순히 PuzzleSolved() 라는 함수가 색상만 Laser의 색상으로 바뀌고 있지만, 나중에 퍼즐이 다 풀렸으면 해야하는 일을 추가적으로 넣어주거나, bool값을 true로 변경해 트리거로써 동작하도록 연결만 해주면 될 것 같다.
<버그 수정>
진행하다가 버그를 하나 발견했는데, 레이저를 스폰하고나서 Laser의 Color를 넣어주는데, Laser의 Material의 컬러를 바꿔주는 함수 호출이 BeginPlay()에서 하고 있었어서, 색이 바뀌지 않는 문제가 있었다.
그동안은 디폴트 색상을 넣어주었어서 그 디폴트 색상으로 Material의 Color가 변경되어 출력되었던 것이다.
< 레이저 쪼개기 만들기 >
우선.. 레이저 쪼개는 액턱를 만들어서 레이저가 들어온다면, ArrowComponent 쪽으로 새로운 레이저를 만드는 것까지 했다.
이제 해줄 것은 원점을 기준으로 일정 각도 간격으로 분산 시켜주도록 만들 것이다.
<아이디어>
음..타입을 조금 고민해봤는데,
1. SpotLight처럼 분산시키는 것
2. X,Y,Z 축 중 한 축을 기준으로 회전해 분산 시킨다.
정도를 생각해 보았다.
더 정확한 목표는 해당 C++ 클래스를 기반으로 블루프린트 액터를 만들면, 더 이상 무엇인가를 안해도 에디터에서 프로퍼티들만 조절해 원하는 레이저 분산을 만들 수 있도록 하는 것이다.
<기초 구현>
하지만 우선, 완성도를 높이는 것은 추후에 리팩토링을 진행하면서 해도 충분할 것 같다 생각해
Z축을 기준 일정 간격으로 레이저를 분산 시켜주는 기능만 구현할 것이다.
레이저를 쪼갰다..!
삭제도 잘 된다… 이게 뭐라고 이렇게 오래걸렸는지.. 슬퍼졌당..
void ALaserSplit::SpawnSplitLaser(ALaser* LaserHit)
{
float Deg = 360.f / SpawnLaserCount;
float AccDeg = 0;
for (int i = 0; i < SpawnLaserCount; i++)
{
FVector NewLaserLocation = ArrowComp->GetComponentLocation();
FVector TranslatedPoint = NewLaserLocation - GetActorLocation();
TranslatedPoint = TranslatedPoint.RotateAngleAxis(AccDeg, FVector::ZAxisVector);
NewLaserLocation = TranslatedPoint + GetActorLocation();
FRotator NewLaserRotation = ArrowComp->GetComponentRotation();
NewLaserRotation.Add(0.f, AccDeg, 0.f);
ALaser* NewLaser = GetWorld()->SpawnActor<ALaser>(BPLaserRef, NewLaserLocation, NewLaserRotation);
NewLaser->SetLaserID(LaserHit->GetLaserID().Append("S"));
NewLaser->SetLaserColor(LaserHit->GetLaserColor());
NewLaser->ChangeColor();
NewLaser->SetLaserSpawnerRef(LaserSpawnerRef);
NewLaser->AttachToActor(this, FAttachmentTransformRules::KeepWorldTransform);
LaserAttached.Add(NewLaser);
LaserSpawnerRef->AddLaserChild(NewLaser);
AccDeg += Deg;
}
}
ALaser* ALaserSplit::LaserBounce(ALaser* LaserHit, bool HitResponse, const FHitResult& HitInformation)
{
if (IsValid(LaserHit))
{
LaserSpawnerRef = LaserHit->GetLaserSpawner();
if (!HitResponse || (!bActive && HitResponse))
{
bActive = HitResponse;
if (bActive)
{
SpawnSplitLaser(LaserHit);
return nullptr;
}
}
}
return nullptr;
}
< 이후에 추가 수정해야할 사항 >
생성된 레이저가 라인 트레이싱을 할때, 본인을 생성한 액터는 무시하게 하면 좋을 것 같다.
왜냐하면, 지금은 ArrowComponent의 위치를 받아, StaticMeshComponent의 위치를 공전하도록 구현했는데, 이렇다보니 구형 액터가 아닌 이상 레이저 시작점이 필연적으로 떨어진 곳이 발생할 수 밖에 없다…
사진은 모서리를 기준으로 했기 때문에 면에서 나오는 레이저가 액터에 딱 붙어있지 않는다.
⇒ 면을 기준으로하면, 액터 안쪽에 레이저가 생성되어, 라인 트레이스 시 본인을 생성한 액터와 히트되어 레이저가 밖으로 연장되지 못한다… ;ㅅ;
3. 점프패드 만들기
단순히 Hit이벤트에 함수를 바인딩해 구현해주었다.
<수정할 사항>
음.. 지금 문제는 체공 상태일 때 이동이 안먹는지, 진짜 조금조금 앞으로 가진다.
2주차 수업을 듣고 직접 캐릭터를 만들어 입력을 받아 움직이게 만들 때, 이런 점들도 고려해서 조작감이 좋아질 수 있도록 노력해야할 것 같다.
2. 마무리
이렇게 이번주가 마무리 되었다...
이번 과제의 목표였던, "이전에 Blueprint로 만들었던 여러 발판들과 레이저 퍼즐을 C++클래스로 구현해보자"를 성공적으로 마무리 할 수 있어서 좋았다.
사실 빙판 발판 처럼 타일마다 마찰력을 다르게 해주기 위해서 피직스 머티리얼쪽도 찾아봤었는데, 우선 이 발판은 다음주에 캐릭터 구현까지 듣고나면, 캐릭터를 직접 C++클래스로 만들어보고, 움직임까지 만들고 나서 구현할 예정이다.
또, 이번 목표가 퍼즐 게임을 만드는 것이었는데, 지금 플레이어 캐릭터를 C++로 구현한게 아니라 언리얼 1인칭 게임 템플릿을 사용한 것이기 때문에 이번 과제는 테스트레벨 처럼 지금 단계에서 랜덤 생성까지만 하고 마무리하도록 할 것이다.
이후에 2주차 강의를 모두 듣고나서 간단한 레벨 디자인 후에 이번에 만들었던 장애물들 + 캐릭터 움직임 구현 + 레이저 퍼즐로 간단한 퍼즐 게임 1스테이지를 만드는 것을 목표로 작업할 예정이다.
이예이~!
주말간 어서 랜덤 생성 부분을 마무리하고, 2주차 강의도 빨리빨리 들어야겠다.
이후에 바로 과제 달린다...!!!
-끝!-
'프로그래밍 > Unreal 부트캠프' 카테고리의 다른 글
TIL 2025.02.03 기록 (0) | 2025.02.03 |
---|---|
TIL 2025.01.31 기록 (0) | 2025.01.31 |
TIL 2025.01.23 기록 (0) | 2025.01.23 |
TIL 2025.01.22 기록 (0) | 2025.01.22 |
TIL 2025.01.21 기록 (0) | 2025.01.21 |