일정표
| 시간 | 할 일 | 비고 |
| 08:30~10:00 | 코드카타, 개인 프로젝트 | |
| 10:00~13:00 | 팀 프로젝트, 개인 프로젝트 | |
| 14:00~18:00 | 팀 프로젝트, 언리얼 수업 | |
| 19:00~21:00 | 팀 프로젝트, 개인 프로젝트 | |
| 21:00~22:00 | 운동 | |
| 22:00~23:30 | 개인 프로젝 |
*오늘의 코드카타*
문제. 카드 뭉치
코니는 영어 단어가 적힌 카드 뭉치 두 개를 선물로 받았습니다. 코니는 다음과 같은 규칙으로 카드에 적힌 단어들을 사용해 원하는 순서의 단어 배열을 만들 수 있는지 알고 싶습니다
원하는 카드 뭉치에서 카드를 순서대로 한 장씩 사용합니다.
한 번 사용한 카드는 다시 사용할 수 없습니다.
카드를 사용하지 않고 다음 카드로 넘어갈 수 없습니다.
기존에 주어진 카드 뭉치의 단어 순서는 바꿀 수 없습니다
예를 들어 첫 번째 카드 뭉치에 순서대로 ["i", "drink", "water"], 두 번째 카드 뭉치에 순서대로 ["want", "to"]가 적혀있을 때 ["i", "want", "to", "drink", "water"] 순서의 단어 배열을 만들려고 한다면 첫 번째 카드 뭉치에서 "i"를 사용한 후 두 번째 카드 뭉치에서 "want"와 "to"를 사용하고 첫 번째 카드뭉치에 "drink"와 "water"를 차례대로 사용하면 원하는 순서의 단어 배열을 만들 수 있습니다
문자열로 이루어진 배열 cards1, cards2와 원하는 단어 배열 goal이 매개변수로 주어질 때, cards1과 cards2에 적힌 단어들로 goal를 만들 있다면 "Yes"를, 만들 수 없다면 "No"를 return하는 solution 함수를 완성해주세요
#include <string>
#include <vector>
using namespace std;
string solution(vector<string> cards1, vector<string> cards2, vector<string> goal) {
int idx1 = 0;
int idx2 = 0;
for (const string& word : goal)
{
if (idx1 < cards1.size() && cards1[idx1] == word)
{
idx1++;
}
else if (idx2 < cards2.size() && cards2[idx2] == word)
{
idx2++;
}
else
{
return "No";
}
}
return "Yes";
}
(1)
'큐 시뮬레이션 문제'이지만 인덱스를 사용해서 풀 수 있다. 사실 queue로 옮겨 담는 메모리 복사 과정을 거치지 않고, 인덱스 변수만으로 포인터처럼 활용하는 것이 메모리 효율과 속도 면에서 더 뛰어나다.
#include <string>
#include <vector>
#include <queue>
using namespace std;
string solution(vector<string> cards1, vector<string> cards2, vector<string> goal) {
queue<string> q1, q2;
for (const string& s : cards1) q1.push(s);
for (const string& s : cards2) q2.push(s);
for (const string& target : goal) {
if (!q1.empty() && q1.front() == target) {
q1.pop();
}
else if (!q2.empty() && q2.front() == target) {
q2.pop();
}
else {
return "No";
}
}
return "Yes";
}
(2) std::queue를 이용한 방법.
queue를 이용하면 카드 꺼내기를 직관적으로 표현할 수 있다. 먼저 카드를 q1, q2에 담는다. 이후 for문을 돌면서 goal 배열의 단어를 하나씩 확인하고, target과 일치하는 경우 pop을 통해서 카드를 꺼낸다. 둘다 아니면 "No" 이고, 만약 조건과 일치한다면 "Yes"를 반환한다.
*팀 프로젝트*
1. 시작하기에 앞서 개념 공부
<인터페이스를 호출할 땐 Execute_함수명 정적함수 사용하기. (+) 인터페이스는 BlueprintNativeEvent>
언리얼 엔진에서 UINTERFACE(Blueprintable)로 만든 인터페이스는 C++ 클래스뿐만 아니라 블루프린트(Blueprint) 애셋에서도 상속받아 구현할 수 있다. 이때 두 경우의 메모리 구조가 완전히 달라진다.
오브젝트가 C++에서 인터페이스를 상속받은 경우: C++의 다중 상속 구조를 따르기 때문에 Cast<IInteractableInterface>(Actor)가 정상적으로 작동하고 포인터를 반환한다. 즉, 인터페이스를 가상 함수 형식으로 작성할 수 있다.
오브젝트가 블루프린트(BP)에서 인터페이스를 상속받은 경우: 블루프린트는 C++ 타입 시스템에 직접 묶여있지 않고 언리얼 리플렉션(VM) 시스템 위에서 동작한다. 따라서 C++ 관점에서는 이 클래스가 인터페이스를 상속받았는지 알 방법이 없기 때문에, Cast<IInteractableInterface>를 실행하면 무조건 nullptr을 반환하게 된다.
결과적으로, 기획자나 디자이너가 에디터에서 BP_Button이나 BP_Lever 같은 상호작용 오브젝트를 블루프린트로 만들고 인터페이스를 구현해두어도, 레이저 컴포넌트(Cast)에서 전부 nullptr로 씹혀서 조준은 되는데 정작 E키를 누르면 아무 반응도 없는 치명적인 오류가 발생하게 된다.
언리얼 엔진은 이 문제를 완벽하게 해결하기 위해 내부적으로 컴파일 시점에 Execute_함수명(오브젝트, 인자...)이라는 정적(Static) 함수를 자동으로 만들어준다. 따라서, 해당 Execute_ 정적 함수를 사용하면 대상이 C++ 객체이든 블루프린트 객체이든 상관없이 리플렉션 시스템을 거쳐 안전하고 유연하게 인터페이스 함수를 호출할 수 있게 된다.
이제, 인터페이스 헤더 파일 자체를 BlueprintNativeEvent 규격으로 수정해야 한다(애초에 인터페이스 헤더 파일을 BlueprintNativeEvent 규격으로 작성하자). 프로젝트의 미래를 위해서 인터페이스 헤더와 상속받는 구조를 완벽하게 언리얼 표준 규격으로 만드는 것이다.
(보완 설명)
예를 들어, 조준/상호작용 컴포넌트에서 만약 ADeliveryBox를 상속받은 무겁거나 깨지기 쉬운 상자들을 기획자가 블루프린트 클래스(BP_HeavyBox 등)로 확장하고 인터페이스 이벤트를 오버라이딩했다면 이 Cast는 nullptr을 반환하여 작동하지 않게 된다. 따라서 안전하게 ImplementsInterface 검사 후 Execute_OnDropped 정적 매크로 호출 방식으로 전환해야 한다.
2. 지난 코드 리뷰 및 피드백
기존에 Cast<> 형식으로 인터페이스를 사용한 코드 파일들은 다음과 같다.
1) CharacterCarryComponent
//변경 전
if (ICarryableInterface* Carryable = Cast<ICarryableInterface>(CarriedBox))
{
Carryable->OnDropped();
}
//변경 후
if (CarriedBox->GetClass()->ImplementsInterface(UCarryableInterface::StaticClass()))
{
ICarryableInterface::Execute_OnDropped(CarriedBox);
}
2) ParcelInteractionComponent
// 변경 전
if (TargetActor->GetClass()->ImplementsInterface(UInteractableInterface::StaticClass()))
{
IInteractableInterface* InteractableTarget = Cast<IInteractableInterface>(TargetActor);
if (InteractableTarget && InteractableTarget->CanInteract(OwnerCharacter))
{
// 변경 후
if (HitActor && HitActor->GetClass()->ImplementsInterface(UInteractableInterface::StaticClass()))
{
if (IInteractableInterface::Execute_CanInteract(HitActor, OwnerCharacter))
3) CarryableInterface.h
//변경 전
(...)
UINTERFACE(MinimalAPI, Blueprintable)
class UCarryableInterface : public UInterface { GENERATED_BODY() };
class PARCEL_KNIGHT_API ICarryableInterface
{
GENERATED_BODY()
public:
virtual bool CanCarry(AActor* Carrier) = 0;
virtual void OnPickedUp(AActor* Carrier) = 0;
virtual void OnDropped() = 0;
};
//변경 후
(...)
UINTERFACE(MinimalAPI, Blueprintable)
class UCarryableInterface : public UInterface
{
GENERATED_BODY()
};
class PARCEL_KNIGHT_API ICarryableInterface
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Carryable")
bool CanCarry(AActor* Carrier);
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Carryable")
void OnPickedUp(AActor* Carrier);
.
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Carryable")
void OnDropped();
};
4) DeliveryBox
- 변경 후
// [Interface] ICarryableInterface 오버라이드
virtual bool CanCarry_Implementation(AActor* Carrier) override;
virtual void OnPickedUp_Implementation(AActor* Carrier) override;
virtual void OnDropped_Implementation() override;
5) PhysicsJudgeManager
ProcessBoxDamage 상단에서 서버 권한을 체크할 때, 월드의 넷 모드를 검사하는 방식보다 조작하려는 액터(Box)가 현재 서버 권한을 가지고 있는지 직접 물어보는 방식이 언리얼에서 가장 많이 쓰이고 가독성이 좋다. 따라서 해당 코드만 수정.
- 변경 전
if (GetWorld() && GetWorld()->GetNetMode() == NM_Client) return;
- 변경 후
if (!Box->HasAuthority()) return;
6) DeliverySubsystem
- 변경 전
void UDeliverySubsystem::DespawnBox(AActor* Box)
{
if (!Box) return;
ActiveBoxes.Remove(Box);
Box->Destroy();
}
- 변경 후
void UDeliverySubsystem::DespawnBox(AActor* Box)
{
if (!Box) return;
if (!Box->HasAuthority()) return;
ActiveBoxes.Remove(Box);
Box->Destroy();
}
SpawnBox에서는 HasAuthority()를 쓸 수 없지만, DespawnBox에서는 Box->HasAuthority()를 사용하는 게 훨씬 좋은 방향이다.
UDeliverySubsystem은 액터(Actor)가 아니라 UWorldSubsystem (UObject)이다. 서브시스템 자체는 네트워크 복제 플래그나 역할(Role)이 없으므로, 언리얼 엔진에서 HasAuthority()나 HasLocalAuthority() 같은 함수들은 오직 액터(AActor)와 액터 컴포넌트(UActorComponent)에만 존재하는 개념이다.
- 상자가 스폰되기 전: 아직 액터가 세상에 태어나지 않았기 때문에 권한을 물어볼 "대상"이 없다. 따라서, 월드 인스턴스가 상자를 찍어낼 수 있는 "서버 월드"인지, 아니면 화면만 그리는 "클라이언트 월드"인지를 GetNetMode() == NM_Client를 통해 월드 자체에 직접 물어봐야만 한다.
- 액터 생성 시점(SpawnBox): 생성 전이라 액터가 없으므로 기존처럼 월드의 GetNetMode() != NM_Client 방식을 유지해야 한다.
- 액터 소멸 시점(DespawnBox): 대상 액터 포인터가 있으므로 if (!Box->HasAuthority()) return;으로 교체하는 것이 언리얼 정석 스타일이다.
7) DeliveryZone
불필요한 이중 캐스팅(Double Casting)을 발견하고 이를 리팩토링함.
OnZoneOverlap에서 Cast<ADeliveryBox>로 상자 객체를 ProcessDelivery(Box)로 넘겨주고 있다. 하지만 정작 수신하는 ProcessDelivery(AActor* InBox) 함수는 매개변수를 generic 한 AActor*로 받기 때문에, 내부에서 똑같은 캐스팅을 이중으로 수행하고 있다.
- 변경 전
void ADeliveryZone::ProcessDelivery(AActor* InBox)
{
ADeliveryBox* Box = Cast<ADeliveryBox>(InBox);
...
- 변경 후
void ADeliveryZone::ProcessDelivery(ADeliveryBox* Box)
{
if (!Box || !HasAuthority()) return;
FBoxData Data = Box->GetBoxData();
...
3. 작업한 내용들
[1. 1차 테스트] : 트러블 슈팅 & 해결
1) E키를 누르면 상호작용이 실행되는 로그까지는 뜨는데 상자가 캐릭터의 손에 붙질 않음
2) 기능에 붙어있는 로그를 살펴본 결과 코드에는 문제가 없었고, 에디터의 콜리전 세팅이 문제일 것으로 추정.
3) 에디터에서 로그 분석하기
LogWorld: Bringing World /Game/Maps/TestMaps/UEDPIE_0_Testing_DF_Stage01.Testing_DF_Stage01 up for play (max tick rate 0) at 2026.07.07-17.57.00
LogWorld: Bringing up level for play took: 0.005162
LogOnline: OSS: Created online subsystem instance for: :Context_8
LogGameMode: Display: Match State Changed from EnteringMap to WaitingToStart
LogGameState: Match State Changed from EnteringMap to WaitingToStart
LogGameMode: Display: Match State Changed from WaitingToStart to InProgress
LogHeroComp: Enhanced Input 바인딩 완료.
LogViewport: Display: Viewport MouseCaptureMode Changed, CapturePermanently_IncludingInitialMouseDown -> CapturePermanently
LogHeroComp: [BP_ParcelCharacter_C_0] 캐릭터에 카메라 컴포넌트 부착 완료.
LogParcelMovementStat: [Server] 통합 연산 속도(450.000000)가 적용되었습니다.
LogGameRule: Match Started — StartRound 호출
LogGameState: Match State Changed from WaitingToStart to InProgress
PIE: Server logged in
PIE: Play in editor total start time 0.092 seconds.
LogParcelInteraction: 조준 타겟 변경됨. [BP_DeliveryBox_C_1]
LogParcelInteraction: 조준 타겟 잃음. (이전 대상: BP_DeliveryBox_C_1)
LogDeliveryBox: Warning: [Server] -1번 상자 물리 충돌 발생. 충돌 대상: StaticMeshActor_6, 검출된 충격량 수치: 19422.566406 (파손 임계값: 500.000000)
LogDeliveryBox: [Server] -1번 상자에 새로운 상태 태그 추가됨: Box.State.Damaged
LogDeliveryPhysics: Warning: [Server] Box ID -1 was damaged by impact force of 19422.566406 (Threshold: 500.000000)!
LogParcelMovementStat: [Server] 통합 연산 속도(450.000000)가 적용되었습니다.
LogParcelMovementStat: [Server] 통합 연산 속도(450.000000)가 적용되었습니다.
LogHeroComp: 상호작용 조작(E키) 감지: InteractionComponent 호출
런타임 로그를 분석해보니, E키 입력(LogHeroComp: 상호작용 조작(E키) 감지)도 컴포넌트까지 정상 도달하고 있다. 그럼에도 상자가 손에 붙지 않는 결정적인 이유는 E키를 누르기 전에 이미 캐릭터가 상자의 조준을 잃어버렸기 때문(조준 타겟 잃음)이다. 포커싱된 타겟(CurrentFocusedActor)이 nullptr인 상태에서 E키를 누르니 상호작용 로직이 가동되지 않는 것이다.
문제는 게임이 시작되자마자 언리얼 물리 엔진의 버그(Physics Explosion)로 인해 상자가 엄청난 속도(충격량 19,422)로 맵 밖으로 튕겨 날아갔거나 땅 밑으로 꺼져버렸던 것이다.
상자가 순식간에 시야에서 사라지니까 컴포넌트는 자연스럽게 조준 타겟 잃음 처리를 하고 포커스를 nullptr로 비운 거고, 그 상태에서 E키를 누르니 포커싱된 타겟이 없어 내부 방어 코드에 걸려 아무 반응도 일어나지 않았다. 상호작용의 문제가 아니라 물리가 튀어서 상자가 실종된 게 원인이다.
4) 기존에 Box.cpp 파일에 작성해두었던 물리 작용 C++ 코드와 연계하기 위해서,
BP_DeliveryBox 에디터에서,
1. Collision Component > Collision Presets(Custom) > Collision Enabled(Collision Enabled (Query and Physics)) > Object Type(PhysicsBody)
2. Box Mesh > Collision Presets(NoCollision)
이렇게 세팅한다. 이렇게 해야, 임계값을 넘는 충돌값이 도출되어 박스가 나타나자마자 파괴되지 않는다.
5) 그런데, 문제는 simulate Physics를 끄면 상자게 핸드소켓에 잘 달라붙는데, 다시 Collision Component의 물리 작용을 활성화하고 E키 상호작용으로 손에 붙이려하면 붙지를 않는다. 물리 작용을 끄고 했을 때는 상호작용이 잘 이루어졌으므로, 상호작용 관련 코드는 문제가 없다는 결론을 내렸다. 그렇다면 체크해볼곳은 '멀티플레이' 환경이었다.
6) 다시 한번 로그를 살펴보았다.
LogParcelInteraction: 조준 타겟 변경됨. [BP_DeliveryBox_C_1] : 상자를 조준함 (성공)
LogParcelInteraction: 조준 타겟 잃음. (이전 대상: BP_DeliveryBox_C_1) : 상자를 놓침 (유실)
LogDeliveryBox: Warning: [Server] -1번 상자 물리 충돌 발생... : 상자가 바닥에 안착함 (충격량 110)
LogHeroComp: 상호작용 조작(E키) 감지: InteractionComponent 호출 : 조준이 풀린 뒤에 E키를 누름
결국 E키 상호작용 입력은 정상적으로 컴포넌트까지 들어갔지만, E키를 누르기 바로 직전 프레임에 시선 레이저가 상자를 놓쳐서 CurrentFocusedActor가 nullptr 모드가 되었기 때문에 내부 안전 가드 조건문에 걸려 집기 RPC 신호가 서버로 날아가지 못했던 것이었다. 이럴 때 일반적인 FPS 멀티플레이 게임에서 사용하는 것이, LineTraceSingleByChannel이 아니라, SweepSingleByChannel 이다. 따라서 코드를 다음과 같이 수정했다. 15cm 정도의 Sweep 범위를 주고, 미세하게 발생할 수 있는 멀티 플레이 '지터(jitter, 서버가 계산한 상자의 물리 좌표와 클라이언트 기기가 보정(Interpolation)해서 화면에 그리는 상자의 시각적 좌표는 네트워크 레이턴시 때문에 수 밀리미터(mm)에서 센티미터(cm)까지 무조건 미세한 오차가 발생하는 현상)' 버그를 수정했다.
FCollisionShape SweepSphere = FCollisionShape::MakeSphere(15.f); // 15cm SweepSingleByChannel로 변경
if (GetWorld()->SweepSingleByChannel(HitResult, TraceStart, TraceEnd, FQuat::Identity, ECC_Visibility, SweepSphere, QueryParams))
{
AActor* HitActor = HitResult.GetActor();
if (HitActor && HitActor->GetClass()->ImplementsInterface(UInteractableInterface::StaticClass()))
{
if (IInteractableInterface::Execute_CanInteract(HitActor, OwnerCharacter))
{
NewFocusedActor = HitActor;
}
}
}
그러나 여전히 박스는 손에 붙지 않았다. 다시 한번 테스트 해보고, 로그 파일을 뜯어보았다.
우선, Sweep 방식으로 바꾸니까 상자가 튕겨 나가도 조준선이 끝까지 자석처럼 상자를 물고 늘어진 덕분에 BP_DeliveryBox_C_1 포커스가 풀리지 않는 것 까지는 확인했다.
로그 중에 Gameplay Tag 누락 에러의 경우 Character.State.InAir 태그를 추가하여 문제를 해결했다. 문제는 계산된 속도 변화량 수치가 무려 8978로 로, 레벨에 배치한 것 만으로 파손이 되는 문제가 다시 발생했다. 이유를 살펴보니, 에디터 레벨 뷰포트에 직접 드래그해서 배치한 상자들은 서브시스템의 InitializeBox() 함수를 거치지 않아서 BoxData 구조체가 비어있는 상태이다. 즉, 모든 값이 0.0으로 세팅되어있기 때문에 버그가 발생하는 것이다. 따라서 박스 소스코드의 BeginPlay에 만약 BoxID가 -1 이라면 테스트용 목업 수치를 주입하도록 임시로 변경했다. (Todo : 기획자가 직접 박스를 드래그해서 레벨에 배치해도 정상적으로 만들어질 수 있도록 박스 BP를 종류별로 만들어 놓을 필요가 있다)
결론적으로 계산된 속도 변화량 수치는 매우 안정화되었다. 그런데 여전히 손에 붙지 않았다.
7) 다시 로그 파일을 뜯어보았다. 이번엔 리슨 서버 환경에 맞추어 PIE 설정을 조정하고 실시했다.
컴포넌트의 입력 바인딩은 성공해서 신호가 매번 깨끗하게 들어가고 있지만, 입력이 일어나는 동안 로그 창 어디에도 LogParcelInteraction: 조준 타겟 변경됨이라는 상호작용 컴포넌트의 로그가 한 줄도 올라오지 않았다.
즉, 클라이언트의 화면 정중앙에서 발사되는 시선 레이저가 현재 눈앞에 있는 상자를 완전히 '유령'처럼 통과(Ignore)하고 있는 상태라는 뜻이다.
조준된 타겟(CurrentFocusedActor)이 없으니 E키를 누르는 순간 내부 안전 조건문에서 컷당해 집기 신호가 서버로 전송되지 못한 것이다.
이렇게 물리를 켰을 때 클라이언트에서 상자가 레이저를 관통해버리는 이유는 언리얼 엔진 넷코드의 초기화 타이밍과 콜리전 매트릭스의 사각지대 두 가지가 맞물렸기 때문이다. 왜일까?
먼저 bReplicateMovement 초기화 타이밍의 문제이다. 현재 박스 소스코드에서는 무브먼트 복제 플래그를 BeginPlay에서 켜주고 있다. 사실 BeginPlay 시점은 레벨 배치 액터에게는 너무 늦다. 따라서 생성자로 옮기는 것이 바람직하다.
두번째, 부모 CollisionComponent를 에디터에서 Custom으로 바꾼 뒤 오브젝트 타입을 PhysicsBody로 지정하면, 언리얼 엔진의 기본 규칙상 PhysicsBody 계층은 플레이어의 시야나 카메라 레이저를 방해하지 않도록 Visibility 채널과 Camera 채널의 반응 값을 기본적으로 Ignore(무시)로 세팅하게 된다.
8) 또다른 범인 : 카메라 스프링 암 vs 레이저 거리
SpringArm->TargetArmLength = 350.f;
TraceDistance = 300.f;
이렇게 되면 카메라의 위치를 시작점으로 잡고(GetPlayerViewPoint) 정면으로 레이저를 쏜다. 그런데 스프링 암보다 길이가 짧아서 안된다... 즉, 레이저가 캐릭터 본체 근처에도 도달하지 못하고 등 뒤에서 허무하게 끊어지기 때문에, 정면에 있는 상자는커녕 캐릭터 자신도 뚫고 나가지 못해 아무것도 맞추지 못하고 무조건 false를 반환했던 것
왜 테스트(물리 Off) 때는 손에 붙었던 걸까?
물리를 끄면 상자가 공중에 고정되어 있거나 캐릭터가 상자 안으로 걸어 들어가 겹칠 수 있게 된다. 이때 스프링 암의 충돌 테스트 옵션(bDoCollisionTest = true)에 의해 카메라 암의 길이가 캐릭터 본체 수준(0~100cm)으로 꽉 압축되면서, 300cm 짜리 레이저가 캐릭터 앞으로 길게 뻗어 나가 상자를 운 좋게 맞출 수 있었던 착시 현상이다. 물리를 켜면 상자가 정상 안착하여 정상 거리를 유지하므로 암이 350cm로 펴져서 레이저가 닿지 않았던 것이다.
따라서 여기에서 두가지 방법 중 하나를 선택하기로 했다.
1) 3인칭 정석 레이저 연산(TraceDistance)을 카메라 암 길이를 반영해서 넉넉하게 늘려주는 것
2) 기존 게임 기획에 맞게 1인칭 시점으로 변경하거나, 또는 별도의 1인칭 전용 HeroComponent를 만드는 것.
3인칭 게임이 본래 기획이므로, TraceDistance를 수정하기로 했다.
TraceDistance = 800.f;
800 정도로 넉넉하게 줘보자.
이제 리슨 서버 테스트 환경에서도 손에 박스가 잘 달라붙는 것을 확인할 수 있다. 드디어ㅜㅜ
[2. 코드 보완점 (콜리전 문제)]
좋은 피드백을 받았고, 1차 테스트 이후 멀티 플레이까지 구현이 완료되면 세부적인 내용들을 채우고, 박스를 집어들었을 때 박스 콜리전이 해제되는 문제는 확실히 수정이 필요해서 가장 우선적으로 수정할 예정.
*언리얼 수업*
주제 1 : <Lyra 데이터 기반 설계>
1. 언리얼의 레퍼런스와 메모리 관리
1-1. 하드 레퍼런스 (Hard Reference)?
1-2. 소프트 레퍼런스 (Soft Reference)
1-3. 소프트 레퍼런스 사용법
1-4. 소프트 레퍼런스 타입 비교
2. Asset Manager - 언리얼의 에셋 관리
2-1. Primary Asset - AssetManager가 관리하는 중요 데이터
2-2. Bundle System - 필요한 파트만 부분적으로 로드하기
2-3. 런타임 사용 - 진짜 게임에서 쓰기
3. 데이터 중심 설계의 3단계 진화 (Lyra의 방향성)
Level 1: 값 추출 (Value Extraction)
Level 2: 동작 추상화 (Behavior Abstraction)
Level 3: 정의의 데이터화 (Definition as Data)
주제 2 : <캐릭터 애니메이션 수정 / 상태머신을 사용한 점프 구현>
1. C++ — 점프 상승/하강 값 + 점프 락아웃
1-1.UPlayerAnimInstance에 VerticalSpeed 추가
1-2. 점프 락아웃 —Landed오버라이드
2. 이동 BlendSpace — BS_Player_Move
2-1. BlendSpace 생성
2-2. 축 설정 (Asset Details 좌측)
2-3. 샘플 애니 배치 (5개)
3. ABP_Player에 상태머신 구축
3-1. 4강 그래프 떠올리기
3-2. 상태머신 노드 만들기
3-3. 상태 4개 + 진입(Entry)
3-4. Ground상태 — 이동 BlendSpace
3-5. 점프 3단 — 포즈 3개로 잘라 쓰기
3-6. 전환 규칙 (레퍼런스 그대로 — 검증됨)
3-7. Compile + Save
4. 폴더 정리
4-1. 몽타주 —Animations→Montages
4-2. 점프 시퀀스 —Animations→Sequences
4-3. 무기 데이터 —Weapons→Data
5. 최종 PIE 테스트
*개인 프로젝트 및 개인 공부*
1. Native GameplayTags(C++)와 중앙 집중형 로깅 시스템 구축 : 로그 카테고리를 시스템/기능별로 쪼개서 DgaLog.h 파일에 분류.
2. 시퀀스 다이어그램 정립
*오늘의 총평*
디버깅하고, 모자란 부분 채우고, 리팩토링 하고 바쁜 하루였다.