TIL
06.25 TIL
2026. 6. 25. 20:33

일정표

시간 할 일 비고
08:00~10:00 코드카타, 언리얼 학습  
10:00~12:00 TA 수업  
12:00~13:00 TA 수업 복습 및 과제  
14:00~18:00 팀프로젝트  
19:00~21:00 팀프로젝트  
21:00~22:00 운동  
22:00~23:30 개인 공부  

*오늘의 코드카타*

 

문제. 문자열 내 마음대로 정렬하

더보기

문자열로 구성된 리스트 strings와, 정수 n이 주어졌을 때, 각 문자열의 인덱스 n번째 글자를 기준으로 오름차순 정렬하려 합니다. 예를 들어 strings가 ["sun", "bed", "car"]이고 n이 1이면 각 단어의 인덱스 1의 문자 "u", "e", "a"로 strings를 정렬합니다

strings는 길이 1 이상, 50이하인 배열입니다.

strings의 원소는 소문자 알파벳으로 이루어져 있습니다.

strings의 원소는 길이 1 이상, 100이하인 문자열입니다.

모든 strings의 원소의 길이는 n보다 큽니다.

인덱스 1의 문자가 같은 문자열이 여럿 일 경우, 사전순으로 앞선 문자열이 앞쪽에 위치합니다.

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<string> solution(vector<string> strings, int n) {
    sort(strings.begin(), strings.end(), 
         [n](const string& a, const string& b)
         {
            if (a[n] != b[n]) { return a[n] < b[n]; }
             
            return a < b;
         });
    return strings;
}

(1) sort + 람다 함수를 사용한 방법. 보통 이런 식으로 커스텀 사전을 만들어 내므로 숙지하자.

 

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<string> solution(vector<string> strings, int n) {
    
    for (string& s : strings)
    {
        s = s[n] + s;
    }
    
    sort(strings.begin(), strings.end());
    
    for (string& s : strings)
    {
        s = s.substr(1);
    }
    return strings;
}

(2) 복잡한 커스텀 비교 함수를 만들기 번거로울 때 사용하는 방법. 정렬 기준이 되는 n번째 문자를 단어의 맨 앞에 붙여버리고 일반 sort()를 돌린 뒤 마지막에 맨 앞 글자만 지워내는 방법이다. 이렇게 하면 if 조건문 없이 sort 함수만으로도 자동 오름차순 정렬된다. 최종 시간 복잡도는 (1) 방법과 거의 동일하지만 문자열을 재할당하는 미세한 오버헤드가 추가된다.

 

*참고*

1) C++ 표준 라이브러리의 std::sort가 내부적으로 사용하는 인트로 정렬(Intro-sort)퀵 정렬로 시작하지만 재귀 깊이가 너무 깊어지면 힙 정렬(Heap Sort)로 전환하고, 데이터 개수가 적어지면 삽입 정렬(Insertion Sort)을 결합하여 최악의 경우에도 O(N log N)을 보장한다.

 

2) std::sort 대신에 std::stable_sort를 사용하고 comp 에서 오직 a[n] < b[n] 조건만 체크하도록 하면, 동일한 n번째 문자를 가진 문자열들의 원래 순서가 사전순이라는 보장이 없으므로 오답이 될 가능성이 높다. stable_sort는 기존 입력 배열의 순서를 유지해줄 뿐 입력 배열 자체가 사전순으로 정렬되어있지 않다면 최종 결과도 사전순으로 정렬되지 않는다.

 

3) const 키워드는 해당 참조 변수를 읽기 전용으로 만들어 정렬 로직 내부에서 원본 데이터가 훼손되는 것을 컴파일 타임에 막아준다.

 

4) C++에서 비교 함수(comp)를 설계할 때 반드시 지켜야하는 '엄격한 약한 순서' 규정이 있다. 바로 <= 이하 연산자를 사용하면 안된다는 것인데, 두 값이 같을 때 a <= b도 true이고 b <= a 도 true이므로 순서의 모순이 발생하게 되어 std::ssort 내부에서 무한 루프나 분류 오류가 발생할 수 있다. 따라서 순서의 모순이 발생하지 않도록 비교 조건을 명확히 해야 한다.

 

5) substr()은 새로운 string 객체를 생성하여 반환하므로, 기존 메모리를 그대로 재활용하지 못하고 복사 비용이 발생하게 된다(오버헤드). 따라서 문자열의 전체 길이가 매우 길고, 배열의 크기가 클 때 성능적인 단점이 생길 수 있다.


*TA 수업*

주제 : 좌표계, Transform, 이동, 회전, 스케일

 

1. 좌표계 : 3D 공간에서는 위치를 표현하기 위해 X, Y, Z 세 축을 사용한다. 좌표값은 단순한 숫자가 아니라 3D 공간 안에서의 위치를 표현한다.

 

2. World Coordinate : World 좌표는 게임 전체 공간을 기준으로 하는 좌표이다. World 좌표는 다음 상황에서 사용된다.

더보기
상황 예시
Actor의 절대 위치 확인 GetActorLocation()
특정 위치에 오브젝트 생성 SpawnActor
두 Actor 사이 거리 계산 Target - Start
전체 맵 기준 위치 저장 Save Data, Map Data

3. Local Coordinate : Local 좌표는 오브젝트 자신을 기준으로 하는 좌표이다. Actor가 바라보는 방향과 자신의 축을 기준으로 판단한다. Local 좌표는 다음 상황에서 중요하게 사용된다.

더보기
상황 예시
캐릭터가 바라보는 방향으로 이동 Forward Movement
무기 Socket 기준 이펙트 생성 Weapon Socket VFX
자식 오브젝트 배치 Parent / Child
회전된 오브젝트 기준 이동 AddActorLocalOffset

4. World와 Local의 차이 : 따라서 World, Local 좌표를 잘 구분하는 것이 중요하다. 캐릭터 이동, 카메라 이동, 총알 발사 방향, 이펙트 생성 위치 등을 구분하기 위해서 매우 중요하다.

더보기
구분 기준 의미
World 게임 전체 공간 레벨의 고정된 축
Local 오브젝트 자신 Actor의 회전과 방향 기준

5. Transform 개념 : Transform은 오브젝트가 공간 안에서 어떻게 배치되어 있는지를 나타내는 값이다. Location, Rotation, Scale 세 가지 정보로 구성된다.

 

5-1. Location : 이동은 Location 값이 변하는 것이고,

SetActorLocation(NewLocation), AddActorWorldOffset(Movement), AddActorLocalOffset(Movement) 함수와 연결된다.

 

5-2. Rotation : 오브젝트가 바라보는 방향. 보통 Pitch, Yaw, Roll 로 확인한다.

 

5-3. Scale : 오브젝트의 크기 배율이다. 단순히 크기만 바꾸는 값이 아니라, 실제 프로젝트에서는 영향을 주는 요소가 많다. 예를 들어,

Collision 크기, 물리 반응, 애니메이션, 머티리얼 타일링, 이펙트 크기, 배치 간격 등에 영향을 줄 수 있다. 특히 비균등 Scale(ex) (2, 1, 0.5))은 주의해야 한다. 메시가 늘어나보이거나 충돌 크기와 시각적 크기가 어긋나는 문제가 발생할 수 있다.

 

6. Viewport와 카메라

 

6-1. Viewport : 언리얼에서 개발자나 아티스트가 레벨을 바라보고 편집하는 작업 화면이다.

Actor의 위치, Actor의 회전, Move / Rotate / Scale Gizmo, World/Local 좌표 전환, Perspective View, Camera View 등을 확인할 수 있다. 

 

6-2. Camera : 실제 게임 화면을 결정하는 Actor이다. Actor이기 때문에 Transform을 가진다.

 

7. 언리얼 Transform 관련 함수들

더보기
목적 함수
현재 위치 가져오기 GetActorLocation()
현재 회전 가져오기 GetActorRotation()
현재 스케일 가져오기 GetActorScale3D()
위치 설정 SetActorLocation()
회전 설정 SetActorRotation()
스케일 설정 SetActorScale3D()
월드 기준 이동 AddActorWorldOffset()
로컬 기준 이동 AddActorLocalOffset()
월드 기준 회전 AddActorWorldRotation()
로컬 기준 회전 AddActorLocalRotation()

8. World 이동과 Local 이동 비교 : 모두 Actor를 이동시키지만 '기준'이 다르다. 월드 좌표를 기준으로 이동하는 것인지, Actor 자신의 로컬 좌표 기준으로 이동하는 것인지에서 차이가 발생한다.

 

9. 과제 : Actor Transform 분석과 World/Local 이동 비교

목적	함수
현재 위치 가져오기	GetActorLocation()
현재 회전 가져오기	GetActorRotation()
현재 스케일 가져오기	GetActorScale3D()
위치 설정	SetActorLocation()
회전 설정	SetActorRotation()
스케일 설정	SetActorScale3D()
월드 기준 이동	AddActorWorldOffset()
로컬 기준 이동	AddActorLocalOffset()
월드 기준 회전	AddActorWorldRotation()
로컬 기준 회전	AddActorLocalRotation()

 

>> 실습까지 진행하였음.

 


*개인 프로젝트 및 개인 공부*

1. 기획서(초안) 작성

Dungeon Travelers Beta - 종합 시스템 기획서 (v1.0)

1. 기본 개요 (Overview)
프로젝트 이름: Dungeon Travelers Beta

엔진 버전: UE 5.7.4 고정 (사용 IDE: Rider)

게임 시점/장르: 탑다운(Top-Down) / 쿼터뷰 액션 + 오토배틀러 용병 영입 + 경영 게임

핵심 학습 목표:

에디터 조작 최소화 및 핵심 시스템의 C++(Rider) 구현화. 블루프린트는 최종 에셋 할당 및 순수 연출용으로만 제한 활용.

개방-폐쇄 원칙(OCP)에 따른 완전 무수정 확장 아키텍처 구축 (Subsystem, Component, Delegate 적극 활용).

라이라(Lyra) 스타일의 글로벌 시스템 구조 분리 (GameplayTags, Subsystem 기반 제어).

2. 레벨 흐름 및 구조 (Level Flow)
레벨 파일명 변경 시의 코드 크래시를 원천 차단하기 위해 GameplayTag-소프트 포인터 매핑 방식을 채택합니다.

[인게임 레벨 리스트]
MainMenuLevel (Location.MainMenu): 게임 타이틀 및 메인 메뉴 UI 진입 레벨

NachoVillageLevel (Location.NachoVillage): 플레이어의 거점이자 용병단 메인 마을 (대형 레벨)

BasicDungeonLevel (Location.BasicDungeon): 실시간 전투 및 플레이 가능 던전 레벨

NachoCastleLevel (Location.NachoCastle): 중세풍 성 레벨 (왕과 수하들 상주)

[레벨 간 공통 전환 규칙]
전환 신호 발생 ➔ 1초간 입력 차단 및 카메라 페이드 아웃 ➔ 레벨 스트리밍/오픈 ➔ 1초간 카메라 페이드 인 및 입력 활성화.

3. 단계별 핵심 기능 (Core Mechanics)
[기능 1] 메인 메뉴 UI 및 진입 연출
화면 구성: 타이틀 텍스트, Start, Load, Credit, Tips, Library, Quit 버튼.

입력 규칙: 글로벌 상태 태그(State.Game.MainMenu) 전환에 의해 자동으로 마우스 커서 표시 및 UI 전용 입력 모드 진입.

[기능 2] 레벨 이동 및 캐릭터 조작권 활성화
스폰 규칙: C++ 기반 글로벌 게임모드에서 플레이어 스타트 위치에 BP_TopDownCharacter 자동 스폰 및 빙의 통제. 레벨 블루프린트는 중복 스폰 코드를 완전히 배제함.

알림 연출: 레벨 진입 시 뷰포트에 맵 이름 UI를 동적으로 생성, 4초 출력 애니메이션 후 UI 컴포넌트를 메모리에서 완전 파괴.

[기능 3] NPC 배치 및 절대 회전 이름표
마을/용병/몬스터 NPC: 시조 액터(ANPCBase) 구조를 유지하되, 컴포넌트 탈부착 방식으로 기능 확장.

이름표 UI 규칙: World Space 위젯 컴포넌트를 사용하되, 5.7.4 버전에 최적화된 절대 회전(World Rotation) 세팅을 적용하여 카메라 회전 시에도 글자 왜곡 없이 정면을 상시 바라봄.

[기능 4] 트리거 영역 진입 및 일시정지 대화창
트리거 및 월드 변화: NPC 근처 200유닛 진입 시 글로벌 상태를 State.Game.DialoguePaused로 전환. 배경 블러 처리 및 월드 일시정지(Pause) 진입.

텍스트 출력: 가만히 두면 0.05초 간격 타이핑 효과, 도중 좌클릭 시 즉시 전체 출력 스킵 기능.

복구 조건: '알겠습니다' 클릭 시 일시정지 해제 및 탑다운 마우스 조작권으로 복구.

[기능 5] 글로벌 인게임 UI & 퀘스트 시스템
기본 UI UI 시스템: 화면 좌측 상단에 현재 목표(퀘스트) UI가 상시 연동됨.

HUD 구조: 인벤토리, 길드, 설정, 내 정보 UI가 상시 대기 및 단축키 바인딩 연동.

실시간 동기화: 몬스터 처치 등의 월드 변화 이벤트 발생 시 델리게이트(Delegate)를 통해 UI 데이터 실시간 반영.

4. 라이라 스타일 아키텍처 설계 (Architecture)
UDTLevelManagerSubsystem (GameInstanceSubsystem): GameplayTag 기반의 안전한 레벨 전환 및 페이드 통제.

UDTGameStateManagerSubsystem (GameInstanceSubsystem): 현재 게임의 모드(Menu, Playing, Dialogue)를 Tag로 전역 관리.

UDTUIManagerSubsystem (GameInstanceSubsystem): UI 레이어(Menu, GameHUD, Overlay) 전역 통제 및 위젯 생명주기 관리.

IInteractableInterface + UDTInteractionComponent: NPC 상호작용의 무한 확장을 위한 인터페이스 지향 탈부착 컴포넌트.

5. 트랙별 명칭 규칙 (Naming Conventions)
블루프린트 트랙: WBP_MainMenu, BP_Character, BP_NPC, WBP_Dialogue, WBP_QuestHUD

C++ 트랙 (Rider): UDTLevelManagerSubsystem, ADTGameModeBase, ADTPlayerController, UDTDialogueWidget

 

2. Player Journey 작성.

첫번째로 만들어 볼 유저 여정 위주로 정리.

더보기

PIE 버튼 클릭 ➔ MainMenuLevel 로드 ➔ Start 버튼 클릭 ➔ UDTLevelManagerSubsystem이 레벨 이동 요청 접수 ➔ 화면 1초 페이드 아웃 ➔ Location.NachoVillage 태그에 바인딩된 실제 레벨 오픈 ➔ 화면 1초 페이드 인 ➔ 조작권 활성화


*팀 프로젝트 진행*

1. 클래스 다이어그램, 시퀀스 다이어그램을 작성

2. Git Repository, Git Rule 작성 후 공유

3. main, dev, features(로컬) 브랜치 구성

4. Miro, 칸반 보드 및 간트 차트 사용한 일정 공유

 

5. DeliveryBox AActor 클래스,  DeliveryTypes USTRUCT 작성

특히 나중에 개발 과정에서 추가 컴포넌트, 추가 인터페이스 등 여러 기능이 더해져도 코드가 꼬이지 않고 수정을 쉽게 할 수 있도록 구조를 잡는 것을 우선시했다.

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "GameplayTagContainer.h"
#include "DeliveryTypes.generated.h"

USTRUCT(BlueprintType)
struct FBoxData : public FTableRowBase
{
	GENERATED_BODY()

	/** 택배 고유 종류 태그. 에디터 내에서 RowName과 일치시켜 관리 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Delivery Box Data")
	FGameplayTag BoxTypeTag;

	/** UI 및 화면에 표시할 이름 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Delivery Box Data")
	FString DisplayName;

	/** 배송 성공 시 기본 점수 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Delivery Box Data")
	int32 BaseScore = 1;

	/** 물리 연산 및 무게감에 적용할 무게 값 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Delivery Box Data")
	float Weight = 1.0f;

	/** 해당 상자를 들었을 때 플레이어에게 적용할 이동 속도 배율 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Delivery Box Data")
	float MoveSpeedMultiplier = 1.0f;

	/** 파손 판정을 유도할 물리 충격량 임계값 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Delivery Box Data")
	float DamageThreshold = 500.0f;

	/** 파손되었을 때 차감될 점수 페널티 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Delivery Box Data")
	int32 DamagePenalty = -1;

	/** 택배 상자의 외형 에셋 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Delivery Box Data")
	TObjectPtr<UStaticMesh> BoxMeshAsset;
};

 

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GameplayTagContainer.h"
#include "DeliveryTypes.h"
#include "Delivery/Carryable.h"
#include "DeliveryBox.generated.h"

class UStaticMeshComponent;

UCLASS()
class PARCEL_KNIGHT_API ADeliveryBox : public AActor, public ICarryable
{
	GENERATED_BODY()
	
public:	
	ADeliveryBox();

protected:
    virtual void BeginPlay() override;
    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
    TObjectPtr<UStaticMeshComponent> BoxMesh;

    /** 매니저 서브시스템에서 발급받을 고유 인덱스 ID (Replicated) */
    UPROPERTY(Replicated, VisibleAnywhere, BlueprintReadOnly, Category = "Delivery Box")
    int32 BoxID;

    /** 현재 박스가 가진 모든 상태 태그 */
    UPROPERTY(ReplicatedUsing = OnRep_BoxStateTags, VisibleAnywhere, BlueprintReadOnly, Category = "Delivery Box")
    FGameplayTagContainer BoxStateTags;

    /** 초기화 시 데이터 테이블에서 받아온 박스의 데이터 */
    UPROPERTY(ReplicatedUsing = OnRep_BoxData, EditAnywhere, BlueprintReadOnly, Category = "Delivery Box")
    FBoxData BoxData;

    /** 현재 이 상자를 들고 있는 플레이어 컨트롤러*/
    UPROPERTY(Replicated, BlueprintReadOnly, Category = "Delivery Box")
    TObjectPtr<APlayerController> HolderPlayer;

    /** [Client] 태그 변경 시 클라이언트 연출용 RepNotify */
    UFUNCTION()
    void OnRep_BoxStateTags();

    /** [Client] 데이터 설정 시 클라이언트 Mesh 동기화용 RepNotify */
    UFUNCTION()
    void OnRep_BoxData();
	
	//Todo : 물리적 충돌

public:
    /** 서버에서 안전하게 상태 태그를 제어하는 권한 전용 함수들 */
    UFUNCTION(BlueprintAuthorityOnly, BlueprintCallable, Category = "Delivery Box")
    void AddStateTag(FGameplayTag NewStateTag);

    UFUNCTION(BlueprintAuthorityOnly, BlueprintCallable, Category = "Delivery Box")
    void RemoveStateTag(FGameplayTag StateTag);

    UFUNCTION(BlueprintPure, Category = "Delivery Box")
    bool HasStateTag(FGameplayTag StateTag) const;

    /** [Subsystem] 스폰 직후 서버에서 박스를 초기화할 때 호출 */
    void InitializeBox(int32 InBoxID, const FBoxData& InBoxData);

    /** [Interface] ICarryable 인터페이스 구현 상속 */
    virtual bool CanCarry(AActor* Carrier) override;
    virtual void OnPickedUp(AActor* Carrier) override;
    virtual void OnDropped() override;

    /** Getter */
    FORCEINLINE int32 GetBoxID() const { return BoxID; }
    FORCEINLINE FGameplayTagContainer GetBoxStateTags() const { return BoxStateTags; }
    FORCEINLINE FBoxData GetBoxData() const { return BoxData; }
    FORCEINLINE float GetDamageThreshold() const { return BoxData.DamageThreshold; }

};

 

#include "Delivery/DeliveryBox.h"
#include "Components/StaticMeshComponent.h"
#include "Net/UnrealNetwork.h"

ADeliveryBox::ADeliveryBox()
{
	PrimaryActorTick.bCanEverTick = false; // 호스트 PC 퍼포먼스를 위해 틱 비활성화
	bReplicates = true;
	SetReplicateMovement(true); // 리슨 서버 물리 동기화 활성화

	BoxMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BoxMesh"));
	RootComponent = BoxMesh;

	// 기본 무거운 물리 바디 콜리전 세팅
	BoxMesh->SetSimulatePhysics(true);
	BoxMesh->SetCollisionProfileName(TEXT("PhysicsBody"));
    
	BoxID = -1;
}

void ADeliveryBox::BeginPlay()
{
	Super::BeginPlay();
	
	//Todo : 물리적 충돌 이벤트
}

void ADeliveryBox::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(ADeliveryBox, BoxID);
	DOREPLIFETIME(ADeliveryBox, BoxStateTags);
	DOREPLIFETIME(ADeliveryBox, BoxData);
	DOREPLIFETIME(ADeliveryBox, HolderPlayer);
}

void ADeliveryBox::InitializeBox(int32 InBoxID, const FBoxData& InBoxData)
{
	if (!HasAuthority()) return;

	BoxID = InBoxID;
	BoxData = InBoxData;
    
	// 서버에서 StaticMesh 즉시 적용
	if (BoxData.BoxMeshAsset)
	{
		BoxMesh->SetStaticMesh(BoxData.BoxMeshAsset);
		// 무게 적용 (밸런싱 수치 조절 (현재 1.0f))
		BoxMesh->SetMassOverrideInKg(NAME_None, BoxData.Weight * 1.0f, true);
	}

	// 초기 상태인 Spawned 태그 부여
	AddStateTag(FGameplayTag::RequestGameplayTag(TEXT("Box.State.Spawned")));
}

void ADeliveryBox::OnRep_BoxData()
{
	// 클라이언트 측에서도 서버가 채워준 BoxData를 받으면 메시 외형을 동기화함
	if (BoxData.BoxMeshAsset)
	{
		BoxMesh->SetStaticMesh(BoxData.BoxMeshAsset);
	}
}

void ADeliveryBox::AddStateTag(FGameplayTag NewStateTag)
{
	if (!HasAuthority() || !NewStateTag.IsValid()) return;

	if (!BoxStateTags.HasTagExact(NewStateTag))
	{
		BoxStateTags.AddTag(NewStateTag);
        
		// 서버 측에서 상태 변경 시 즉각 처리할 물리 제어 규칙
		if (NewStateTag.MatchesTag(FGameplayTag::RequestGameplayTag(TEXT("Box.State.Held"))))
		{
			BoxMesh->SetSimulatePhysics(false);
			BoxMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); // 들려있을 땐 환경 충돌 무시
		}
	}
}

void ADeliveryBox::RemoveStateTag(FGameplayTag StateTag)
{
	if (!HasAuthority() || !StateTag.IsValid()) return;

	if (BoxStateTags.HasTagExact(StateTag))
	{
		BoxStateTags.RemoveTag(StateTag);
        
		// 상태가 제거될 때의 예외 복구 로직
		if (StateTag.MatchesTag(FGameplayTag::RequestGameplayTag(TEXT("Box.State.Held"))))
		{
			BoxMesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
			BoxMesh->SetSimulatePhysics(true);
		}
	}
}

bool ADeliveryBox::HasStateTag(FGameplayTag StateTag) const
{
	return BoxStateTags.HasTag(StateTag);
}

void ADeliveryBox::OnRep_BoxStateTags()
{
	// 클라이언트 사이드 시각/청각 연출 분기점
	FGameplayTag HeldTag = FGameplayTag::RequestGameplayTag(TEXT("Box.State.Held"));
	FGameplayTag DamagedTag = FGameplayTag::RequestGameplayTag(TEXT("Box.State.Damaged"));

	if (BoxStateTags.HasTag(HeldTag))
	{
		BoxMesh->SetSimulatePhysics(false);
		BoxMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	}
	else
	{
		BoxMesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
		BoxMesh->SetSimulatePhysics(true);
	}

	if (BoxStateTags.HasTag(DamagedTag))
	{
		// Todo : 찌그러진 메쉬로 교체하거나 내용물이 튀어나오는 이펙트/사운드 호출
	}
}

/* ==========================================================================
   ICarryable 인터페이스 Todo: Character와 연동 필요
   ========================================================================== */

bool ADeliveryBox::CanCarry(AActor* Carrier)
{
	// 월드에 생성되어 놓여있으면 집기 가능 허용 (여러 상태 이상일 때 못집게 할 수 있음)
	return BoxStateTags.HasTagExact(FGameplayTag::RequestGameplayTag(TEXT("Box.State.Spawned")));
}

void ADeliveryBox::OnPickedUp(AActor* Carrier)
{
	if (!HasAuthority() || !Carrier) return;
	
	APawn* CarrierPawn = Cast<APawn>(Carrier);
	if (CarrierPawn)
	{
		HolderPlayer = Cast<APlayerController>(CarrierPawn->GetController());
		SetOwner(CarrierPawn); // 리슨 서버 소유권(Owner) 변경
	}

	RemoveStateTag(FGameplayTag::RequestGameplayTag(TEXT("Box.State.Spawned")));
	AddStateTag(FGameplayTag::RequestGameplayTag(TEXT("Box.State.Held")));
}

void ADeliveryBox::OnDropped()
{
	if (!HasAuthority()) return;

	HolderPlayer = nullptr;
	SetOwner(nullptr); // 소유권 월드로 반환

	RemoveStateTag(FGameplayTag::RequestGameplayTag(TEXT("Box.State.Held")));
	AddStateTag(FGameplayTag::RequestGameplayTag(TEXT("Box.State.Spawned")));
}

//Todo : OnPhysicsHit 물리적 충돌 구현
// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class Parcel_Knight : ModuleRules
{
	public Parcel_Knight(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GameplayTags"});

		PrivateDependencyModuleNames.AddRange(new string[] {  });

		// Uncomment if you are using Slate UI
		// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
		
		// Uncomment if you are using online features
		// PrivateDependencyModuleNames.Add("OnlineSubsystem");

		// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
	}
}

GameplayTags 를 사용하기 위해서 "GameplayTags" 모듈을 Build.cs 파일에 추가하였다.


*오늘의 총평*

멀티플레이가 가능한 게임을 만들기 위해서 처음부터 여러 개념을 추가 학습하고, 구조를 짜고 시작하다보니 난이도가 갑자기 올라간 느낌이었다. 개인 공부도 더 많이 해 놓아야 할 것 같다는 생각을 했다. 추가적으로 분업 과정에서 어떻게 하면 내 시간을 잘 쪼개서 사용할 수 있는지도 생각해보았다.

'TIL' 카테고리의 다른 글

06.29 TIL  (0) 2026.06.29
06.26 TIL  (0) 2026.06.26
06.24 TIL  (0) 2026.06.24
06.23 TIL  (0) 2026.06.23
06.22 TIL  (0) 2026.06.22