필수 과제 (기본 요구 사항)
필수 과제 1번 - 서로 다른 Actor 클래스 2개 이상 구현
- 각각 StaticMeshComponent를 가지며, 맵에 배치 가능한 형태여야 합니다.
- 두 클래스는 서로 다른 동작 로직을 수행해야 합니다.
필수 과제 2번 - Tick 함수 기반 동적 Transform 변경
- 회전 기능
- Tick(float DeltaTime)에서 AddActorLocalRotation() 사용하여 매 프레임 회전 처리
- 이동 기능
- Tick(float DeltaTime)에서 위치를 변경하여 왕복 이동 구현
- MoveSpeed, MaxRange, StartLocation 등을 고려해 일정 범위를 벗어나면 이동 방향을 반전시키는 로직 구성
- 프레임 독립성
- 이동/회전 시 반드시 DeltaTime을 활용하여 하드웨어 성능에 관계없이 일정한 움직임을 보장해야 합니다.
필수 과제 3번 - 리플렉션 적용
- 주요 변수 (회전 속도, 이동 속도, 이동 범위 등)를 UPROPERTY로 선언하여 에디터에서 조정 가능하게 만들어야 합니다.
- EditAnywhere, BlueprintReadWrite, Category 등을 적절히 활용하여 Details 패널에서 편집 가능하게 만듭니다.
- 플레이 중 Details 패널에서 값 변경 시 즉시 반영되는지 확인합니다.
과제를 시작할 때
1. 좌우 이동 발판
2. 상하 이동 발판
3. 사각형으로 이동하며 회전하는 발판
총 3개의 발판을 만들어 보려고 했다.
좌우 이동 발판(MovingPlatformHorizontal)
1. 헤더에 루트 컴포넌트와 스태틱 메시를 선언하고 소스에서 생성자에 컴포넌트 생성 후 연결만 해주었다.
스태틱 메시는 에디터에서 설정 하기 위해 스태틱 메시 지정자를 EditDefaultsOnly로 사용했다.
UPROPERTY(VisibleAnywhere)
USceneComponent* SceneRoot;
UPROPERTY(EditDefaultsOnly)
UStaticMeshComponent* StaticMeshComp;
AMovingPlatformHorizontal::AMovingPlatformHorizontal()
{
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Platform"));
StaticMeshComp->SetupAttachment(SceneRoot);
PrimaryActorTick.bCanEverTick = true;
}
2. P = P0 + VT 공식을 이용하여 움직임을 구현하기 위해 각 변수들을 선언했다.
과제를 끝내고 보니 모두 C++로 구현하고 블루프린트는 사용하지 않아서 BlueprintReadWrite 지정자는 필요가 없었다.
FVector StartLocation; // 시작 위치
FVector CurrentLocation; // 현재 위치
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Direction; // 방향
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MoveSpeed; // 이동 속도
//생성자에 추가
MoveSpeed = 300;
Direction = 1;
void AMovingPlatformHorizontal::BeginPlay()
{
Super::BeginPlay();
StartLocation = GetActorLocation();
CurrentLocation = StartLocation;
}
3. 우선 왕복 이동은 생각하지 않고 이동이 되는지 부터 테스트를 하며 구현했다.
void AMovingPlatformHorizontal::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
CurrentLocation.X = CurrentLocation.X + Direction * MoveSpeed * DeltaTime;
SetActorLocation(FVector(CurrentLocation.X, StartLocation.Y, StartLocation.Z));
}
4. 왕복 이동을 하기 위한 이동 거리 변수를 선언하고 조건을 추가했다.
// 헤더에 추가
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MaxRange; // 이동 거리
// 생성자에 추가
MaxRange = 450;
void AMovingPlatformHorizontal::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
CurrentLocation.X = CurrentLocation.X + Direction * MoveSpeed * DeltaTime;
if (CurrentLocation.X >= StartLocation.X + MaxRange)
{
CurrentLocation.X = StartLocation.X + MaxRange;
Direction = -1;
}
else if (CurrentLocation.X < StartLocation.X)
{
CurrentLocation.X = StartLocation.X;
Direction = 1;
}
SetActorLocation(FVector(CurrentLocation.X, StartLocation.Y, StartLocation.Z));
}
이 부분에서 조건을 구현할 때 같은 자리에서 진동을 하듯이 버벅이고 한 번 왕복 후 같은 방향으로 쭉 나아간다던지의 문제가 있었지만 현재 X좌표(CurrentLocation.X)를 왕복해야하는 도착지점(StartLocation.X + MaxRange과 StartLocation.X)으로 보정을 하니 왕복이 잘 작동이 됐다.
좌우 이동 발판을 구현하며 아직 코드들을 많이 사용해보지 않아서 손에 안익어서 다른 발판들의 이동로직을 구현할 때 거의 비슷하지만 복사 붙여넣기 하지 않고 직접 타이핑을 하며 손에 익혔다.
상하 이동 발판(MovingPlatformVertical)
좌우 이동 발판과 코드가 거의 똑같지만 X 좌표를 Z좌표로만 수정하면 된다.
// 헤더
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MovingPlatformVertical.generated.h"
UCLASS()
class HW6_API AMovingPlatformVertical : public AActor
{
GENERATED_BODY()
public:
AMovingPlatformVertical();
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
UPROPERTY(VisibleAnywhere)
USceneComponent* SceneRoot;
UPROPERTY(EditDefaultsOnly)
UStaticMeshComponent* StaticMeshComp;
FVector StartLocation;
FVector CurrentLocation;
UPROPERTY(EditAnywhere)
int32 MoveSpeed;
UPROPERTY(EditAnywhere)
int32 Direction;
UPROPERTY(EditAnywhere)
int32 MaxRange;
};
// 소스
#include "MovingPlatformVertical.h"
AMovingPlatformVertical::AMovingPlatformVertical()
{
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Elevator"));
StaticMeshComp->SetupAttachment(SceneRoot);
PrimaryActorTick.bCanEverTick = true;
Direction = 1;
MoveSpeed = 150;
MaxRange = 300;
}
void AMovingPlatformVertical::BeginPlay()
{
Super::BeginPlay();
StartLocation = GetActorLocation();
CurrentLocation = StartLocation;
}
void AMovingPlatformVertical::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
CurrentLocation.Z = CurrentLocation.Z + Direction * MoveSpeed * DeltaTime;
if (CurrentLocation.Z >= StartLocation.Z + MaxRange)
{
CurrentLocation.Z = StartLocation.Z + MaxRange;
Direction = -1;
}
else if (CurrentLocation.Z < StartLocation.Z)
{
CurrentLocation.Z = StartLocation.Z;
Direction = 1;
}
SetActorLocation(FVector(StartLocation.X, StartLocation.Y, CurrentLocation.Z));
}
사각형으로 이동하며 회전하는 발판
원래는 사각형으로 이동하며 코너마다 랜덤한 방향으로 90도 회전하는 큐브형 발판을 만들려고 여러가지 방법을 구현을 몇 시간동안 해봤는데 현재 배운 내용으로는 구현이 어려워서 조건을 두지 않고 사각형으로 이동하며 회전하는 발판을 구현했다.
이 발판을 구현할 땐 우선 회전은 생각하지 않고 사각형으로 이동 로직을 먼저 구현했다.
위에서 구현한 가로, 세로 이동하는 로직과 같지만 이걸 +가로이동 -> +세로이동 -> -가로이동 -> -세로이동 순서대로 구현해야했다.
여러가지 방법들을 사용해서 구현해봤는데 잘 안됐다.
과제가 도전과제까지 하느라 하루 넘게 걸려서 어제 했던거라 어떻게 구현 했었는지는 기억이 잘 안나는데 한가지 기억나는건 Tick 함수에서 While문을 사용했는데 While문이 끝나기 전에 Tick 함수가 호출되지 않아 DeltaTime이 갱신 되지 않는다는 것을 배웠다.
가로, 세로로만 이동하던 발판과 다른점
1. 방향 변수를 2개로 나눴다.(DirectionX, DirectionZ)
2. 가로 이동하는 조건, 세로 이동하는 조건에 사용할 변수를 만들었다.(MoveAxis : 0이면 가로, 1이면 세로 이동)
3. 회전 기능을 넣기 위한 회전 속도 변수(RotSpeed) 추가.
// 헤더
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "RotatingMovingPlatform.generated.h"
UCLASS()
class HW6_API ARotatingMovingPlatform : public AActor
{
GENERATED_BODY()
public:
ARotatingMovingPlatform();
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
USceneComponent* SceneRoot;
UPROPERTY(EditDefaultsOnly)
UStaticMeshComponent* StaticMeshComp;
FVector StartLocation;
FVector CurrentLocation;
UPROPERTY(EditAnywhere)
int32 MoveSpeed;
UPROPERTY(EditAnywhere)
int32 MaxRange;
UPROPERTY(EditAnywhere)
int32 DirectionX; // X축 방향
UPROPERTY(EditAnywhere)
int32 DirectionZ; // Z축 방향
int32 MoveAxis; // X축 이동, Z축 이동 조건
UPROPERTY(EditAnywhere)
float RotSpeed; // 회전 속도
};
// 소스
#include "RotatingMovingPlatform.h"
ARotatingMovingPlatform::ARotatingMovingPlatform()
{
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Cube"));
StaticMeshComp->SetupAttachment(SceneRoot);
PrimaryActorTick.bCanEverTick = true;
MoveSpeed = 250;
DirectionX = 1;
DirectionZ = 1;
MaxRange = 500;
RotSpeed = 360.0f;
MoveAxis = 0;
}
void ARotatingMovingPlatform::BeginPlay()
{
Super::BeginPlay();
StartLocation = GetActorLocation();
CurrentLocation = StartLocation;
}
void ARotatingMovingPlatform::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
AddActorWorldRotation(FRotator(0.0f, RotSpeed * DeltaTime, 0.0f)); // 계속 회전
if (MoveAxis == 0) // 0이면 x축, 1이면 z축 이동
{
CurrentLocation.X = CurrentLocation.X + DirectionX * MoveSpeed * DeltaTime;
if (CurrentLocation.X >= StartLocation.X + MaxRange) // X축 도착지점에 도착하면 Z축 이동 시작
{
CurrentLocation.X = StartLocation.X + MaxRange;
DirectionX = -1;
MoveAxis = 1; // 각 도착지점 도착마다 축 이동 바꿈
}
else if (CurrentLocation.X < StartLocation.X) // X축 시작지점에 도착하면 Z축 이동 시작
{
CurrentLocation.X = StartLocation.X;
DirectionX = 1;
MoveAxis = 1; // 각 도착지점 도착마다 축 이동 바꿈
}
}
else // z축 이동
{
CurrentLocation.Z = CurrentLocation.Z + DirectionZ * MoveSpeed * DeltaTime;
if (CurrentLocation.Z >= StartLocation.Z + MaxRange) // Z축 도착지점에 도착하면 X축 이동 시작
{
CurrentLocation.Z = StartLocation.Z + MaxRange;
DirectionZ = -1;
MoveAxis = 0; // 각 도착지점 도착마다 축 이동 바꿈
}
else if (CurrentLocation.Z < StartLocation.Z) // Z축 시작지점 도착하면 X축 이동 시작
{
CurrentLocation.Z = StartLocation.Z;
DirectionZ = 1;
MoveAxis = 0; // 각 도착지점 도착마다 축 이동 바꿈
}
}
SetActorLocation(FVector(CurrentLocation.X, StartLocation.Y, CurrentLocation.Z));
}
이동 속도나 회전 속도, 이동 거리같은 변수들은 모두 에디터에서도 설정할 수 있도록 EditAnywhere 지정자를 사용했다.
도전 과제 (선택 요구 사항)
도전 과제 1번 - 타이머 시스템 활용 (난이도 중상)
- 타이머 활용
- FTimerHandle과 GetWorld()->GetTimerManager().SetTimer(...)를 사용해 특정 시간 후 또는 주기적으로 함수를 호출합니다.
- 매 프레임 호출(Tick)보다 효율적인 퍼포먼스를 제공합니다.
- 시간 기반 로직 구현
- 일정 시간 후 발판이 사라지거나, 주기적으로 다른 위치로 이동하는 로직을 추가합니다.
도전 과제 2번 - 랜덤 퍼즐 생성 (난이도 상)
- 동적 스폰
- 게임 시작 시 SpawnActor를 통해 회전 발판/이동 플랫폼을 임의 좌표에 여러 개 배치합니다.
- 로그라이크 또는 랜덤 스테이지의 기초 개념을 체험할 수 있습니다.
- 랜덤 속성 부여
- 회전/이동 속도, 이동 범위, 각도 등을 FMath::RandRange로 생성하여 매번 다른 퍼즐 코스를 구성합니다.
도전 과제의 함수들은 강의에서 나오진 않았던 함수들이라 함수를 어떻게 사용하는지 인터넷에 검색하며 찾아봤다.
도전 과제 1번은 타이머 시스템을 활용해서 2초마다 사라졌다가 나타나는 벽을 구현했다.
도전 과제 2번은 실행할 때 마다 랜덤한 위치에 랜덤한 크기, 랜덤한 이동 거리, 랜덤한 회전 속도를 가진 발판들이 25개 스폰되도록 구현했다.
도전 과제 1번 - 타이머 시스템 활용
우선 타이머 시스템을 어떻게 사용하는지 몰라서 찾아가며 이해를 했다. 타이머는 시간을 지정한 후 함수를 호출해주는 시스템이다.
FTimerHandle 타이머이름; 으로 h에 선언 후 cpp에서
GetWorld()->GetTimerManager().SetTimer(타이머이름, 적용할 객체, 호출할 함수 포인터, 주기, 반복여부, 첫 실행 전 대기시간)로 타이머를 사용하면 된다.
여기서 함수 포인터는 반환형이 반드시 void여야 한다.
h에서 루트컴포넌트와 스태틱메시컴포넌트, 타이머, 주기, 현재 사라졌는지 판단하는 변수를 선언했다.
cpp에서 처음에는 함수를 2개 만들어서 사라지는 함수, 나타나는 함수를 구현했었다.
void ADisappearingPlatform::Disappear()
{
SetActorHiddenInGame(true); // 게임에서 안보이고
SetActorEnableCollision(false); // 콜리전 비활성화
}
void ADisappearingPlatform::Appear()
{
SetActorHiddenInGame(false); // 게임에서 보이고
SetActorEnableCollision(true); // 콜리전 활성화
}
이렇게 하니 타이머 변수가 두 개 필요하고 타이머 함수도 두 개를 사용해야했다.
이를 현재 사라졌는지 판단하는 bool 변수를 추가해서 하나의 함수로 합쳤다.
// .h
USceneComponent* SceneRoot;
UPROPERTY(EditDefaultsOnly)
UStaticMeshComponent* StaticMeshComp;
UPROPERTY(EditAnywhere)
float cycle;
bool bHidden;
FTimerHandle Timer;
// .cpp
#include "DisappearingPlatform.h"
ADisappearingPlatform::ADisappearingPlatform()
{
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Platform"));
StaticMeshComp->SetupAttachment(SceneRoot);
PrimaryActorTick.bCanEverTick = false;
bHidden = true;
cycle = 2.0f;
}
void ADisappearingPlatform::BeginPlay()
{
Super::BeginPlay();
GetWorld()->GetTimerManager().SetTimer(Timer, this, &ADisappearingPlatform::ToggleVisibility, cycle, true);
}
void ADisappearingPlatform::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void ADisappearingPlatform::ToggleVisibility()
{
SetActorHiddenInGame(!bHidden);
SetActorEnableCollision(bHidden);
bHidden = !bHidden;
}
이렇게 2초(cycle)마다 사라졌다가 나타나는 벽을 만들었다. cycle은 에디터에서 수정이 가능하다.
도전과제 2번 - 랜덤 퍼즐 생성
소환될 액터(SpawnPlatform)와 소환을 하는 액터(SpawnActor) 2개의 클래스를 만들었다.
먼저 소환될 액터(SpawnPlatform)을 구현했다.
소환될 액터(SpawnPlatform)
이 액터는 필수 과제에서 구현한 이동과 회전을 동시에 하는 발판이다. 그래서 변수와 로직은 거의 똑같다.
다른점은 이 액터는 이동 속도, 회전 속도, 이동 거리, 크기를 인스턴스마다 랜덤한 값으로 가지도록 구현했다.
// .h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SpawnPlatform.generated.h"
UCLASS()
class HW6_API ASpawnPlatform : public AActor
{
GENERATED_BODY()
public:
ASpawnPlatform();
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
USceneComponent* SceneRoot;
UPROPERTY(EditDefaultsOnly)
UStaticMeshComponent* StaticMeshComp;
FVector StartLocation;
FVector CurrentLocation;
UPROPERTY(EditAnywhere)
int32 MoveSpeed;
UPROPERTY(EditAnywhere)
float RotatorSpeed;
UPROPERTY(EditAnywhere)
int32 MaxRange;
int32 Direction;
};
// .cpp
#include "SpawnPlatform.h"
ASpawnPlatform::ASpawnPlatform()
{
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
SetRootComponent(SceneRoot);
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Platform"));
StaticMeshComp->SetupAttachment(SceneRoot);
PrimaryActorTick.bCanEverTick = true;
Direction = 1;
}
void ASpawnPlatform::BeginPlay()
{
Super::BeginPlay();
StartLocation = GetActorLocation();
CurrentLocation = StartLocation;
MoveSpeed = FMath::RandRange(100, 500);
RotatorSpeed = FMath::RandRange(0.0f, 90.0f);
MaxRange = FMath::RandRange(100, 500);
SetActorScale3D(FVector(FMath::RandRange(0.7f, 1.5f)));
}
void ASpawnPlatform::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
AddActorWorldRotation(FRotator(0.0f, RotatorSpeed * DeltaTime, 0.0f));
CurrentLocation.X = CurrentLocation.X + Direction * MoveSpeed * DeltaTime;
if (CurrentLocation.X >= StartLocation.X + MaxRange)
{
CurrentLocation.X = StartLocation.X + MaxRange;
Direction = -1;
}
else if (CurrentLocation.X < StartLocation.X)
{
CurrentLocation.X = StartLocation.X;
Direction = 1;
}
SetActorLocation(FVector(CurrentLocation.X, StartLocation.Y, StartLocation.Z));
}
Tick 함수의 이동, 회전 로직은 똑같다. Y축도 X축과 같이 동시에 이동을 시킬까 생각했는데 너무 어지러워질것 같아서 X축으로만 이동을 구현했다.
소환을 하는 액터(SpawnActor)
타이머 시스템과 똑같이 처음보는 함수라 인터넷에서 SpawnActor 함수의 사용법을 찾아봤다.
GetWorld()->SpawnActor<소환할 액터 타입>(소환할 액터, 소환시킬 위치값, 소환할 때의 회전값);으로 사용하면 된다.
위치값과 회전값을 담기 위한 FVector, Frotator 변수를 선언했고 회전은 소환될 액터가 알아서 회전할것이기 때문에 회전값은 0,0,0으로 초기값을 줬다.(Rotation = { 0,0,0 };)
위치는 랜덤한 위치에서 생성하기 위해 FMath::RandRange 함수를 사용하여 구현했다.
(Location = {FMath::RandRange(-1500.0f,2200.0f),FMath::RandRange(-7500.0f, -4500.0f), 1470.0f};)
// .h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SpawnPlatform.h"
#include "SpawnActor.generated.h"
UCLASS()
class HW6_API ASpawnActor : public AActor
{
GENERATED_BODY()
public:
ASpawnActor();
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
FVector Location;
FRotator Rotation;
};
이제 소환할 액터 인자만 남았는데 처음에는 ASpawnPlatform::StaticClass()를 적어서 SpawnPlatform 액터를 소환하도록 했다.
ASpawnActor::ASpawnActor()
{
PrimaryActorTick.bCanEverTick = false;
Rotation = { 0,0,0 };
}
void ASpawnActor::BeginPlay()
{
Super::BeginPlay();
for(int i = 0; i < 25; ++i)
{
Location = {FMath::RandRange(-1500.0f,2200.0f),FMath::RandRange(-7500.0f, -4500.0f), 1470.0f};
GetWorld()->SpawnActor<ASpawnPlatform>(ASpawnPlatform::StaticClass(), Location, Rotation);
}
}
ASpawnPlatform::StaticClass()로 액터를 지정했더니 아무것도 소환이 되지 않길래 에디터의 아웃라이너를 봤더니 소환은 되고있었는데 투명상태(스태틱 메시가 할당되지 않은 상태)로 소환이되고 있었다.
알아보니 ASpawnPlatform::StaticClass()는 C++로 구현한 액터를 소환하는데 현재 SpawnPlatform은 C++에서는 스태틱 메시 컴포넌트 만 생성해주고 스태틱 메시를 설정하는건 블루프린트 에디터에서 해서 스태틱 메시가 할당되지 않은 상태의 SpawnPlatform이 소환되고 있던것이다.
이에 해결 방법이 2가지가 있었는데 한가지는 C++에서 직접 스태틱 메시 에셋 경로를 넣어서 직접 구현해주는것.
다른 한가지는 블루프린트 액터를 담는 변수를 만들어서 이 변수를 소환할 액터에 넣어주는 것이였다.
스태틱 메시 애셋의 경로를 가져와서 직접 설정하는 것은 강의에서 해봤기 때문에 새로운 방법인 블루프린트 액터를 C++로 가져와서 소환하는 방법을 선택했다.
헤더에서 TSubclassOf<ASpawnPlatform> PlatformClass;를 선언하고
GetWorld()->SpawnActor<ASpawnPlatform>(PlatformClass, Location, Rotation);를 해주면 끝이였다.
물론 SpawnPlatform의 블루프린트 에디터에서 스태틱 메시가 설정되어 있어야한다.
'숙제' 카테고리의 다른 글
팀 프로젝트(텍스트 기반 RPG 게임 제작 프로젝트) (0) | 2025.09.12 |
---|---|
4번 과제 복습(연금술 공방 관리 시스템 구현) (0) | 2025.09.02 |
3번 과제 복습(인벤토리 시스템 구현) (1) | 2025.09.01 |
2번 과제 복습(전직 시스템과 전투 시스템) (1) | 2025.08.29 |
1번 과제 복습(상태창 구현) (2) | 2025.08.29 |