TIL_049: 언리얼 수학, LineTrace, 감지 시스템, GoF(생성패턴)

김펭귄·2025년 10월 21일

Today What I Learned (TIL)

목록 보기
49/142

오늘 학습 키워드

  • 언리얼 수학

  • LineTrace, 감지 및 추적시스템

  • GoF 디자인 패턴 - 생성 패턴

1. 언리얼 수학

  • 1U = 1cm

  • Local->World->View->Clip Space로 좌표를 변환하기 위해 행렬 이용
    World Matrix, View Matrix, Projection Matrix

  • 언리얼에서의 각도 : degree
    C++ / HLSL에서의 각도 : Radian

  • 오일러 각의 문제
    1. 회전 순서에 따라 결과 달라짐
    2. 짐벌락 발생
  • 따라서 쿼터니언 사용.
  • 언리얼 내부 회전은 모두 쿼터니언으로 처리됨. Rotator값으로 Degree를 넣어도 계산은 쿼터니언으로

2. LineTrace, 감지 시스템

LineTrace

  • Get Mouse Position on ViewPort
    게임 화면 내에서의 마우스 위치를 반환

  • Convert Screen Location To World Space
    스크린의 위치를 월드 스페이스에서의 위치와 방향을 반환
    단, 2D에서 3D로의 변환이므로 월드에서 카메라화면이 위치한 좌표를 반환
    따라서 방향과 LineTrace를 이용하여 원하는 좌표를 얻음

  • Line Trace By Channel
    LineTrace하여 Hit했는지에 대한 bool값과 Hit 결과 Structure을 반환

감지 시스템

  • Draw Debug Cone
    디버깅 용도의 원뿔을 그림

  • 해당 원뿔이 있는 위치를 감지 구역으로 설정하고, 이 위치 안에 들어왔을 시 "Find!"를 출력하는 시스템

  • 적 AI에 넣어주어 플레이어를 감지하고 이후 추격 로직을 추가할 수 있음

3. GoF 디자인 패턴 - 생성 패턴

코드를 집 짓기에 비교한다면

  • 클린코드 : 벽돌이 하나하나 깔끔하게
  • 리팩터링 : 벽돌을 다시 예쁘게 짓기
  • 디자인 패턴 : 이렇게 짓는게 이쁘다는 설계도

패턴 종류

  • 생성 패턴 : 객체 생성 메커니즘을 다루는 패턴

  • 구조 패턴 : 클래스나 객체를 조합, 연결하는 패턴

  • 행동 패턴 : 객체 간 상호작용과 책임 분배 패턴

  • 패턴을 사용하면, 유지보수와 확장성 등에서 용이하지만, 성능에서는 떨어짐

  • 무분별한 패턴은 오히려 디버깅에도 어려워짐

  • 각 패턴들의 장단점을 알고 써야함

생성 패턴

싱글톤 패턴

  • 하나만 있어야 하는 객체의 생성 패턴

  • 캐릭터, 게임인스턴스, 게임 매니저 등에 사용

class GameManager {
private:
    // 1. static 인스턴스
    static GameManager* instance;
    
    // 2. private 생성자 (외부에서 new 금지)
    GameManager() {
        score = 0;
        gameTime = 0.0f;
    }
    
public:
    // 3. 접근 메서드
    static GameManager* GetInstance() {
        if (instance == nullptr) {
            instance = new GameManager();
        }
        return instance;
    }
};

// static 멤버 초기화
GameManager* GameManager::instance = nullptr;
  • 장점
    1. 전역처럼 사용하지만, 접근과 생성을 통제하므로 안전한 구조
    2. 사용하기 편함
  • 단점
    1. 이전 테스트의 상태가 항상 남아있어 독립적인 테스트가 어려움
    2. 코드 간 결합도가 높아짐. 모든 클래스가 싱글톤에 접근하게 되므로
    3. 멀티쓰레드의 경우 동시에 호출할 경우, 2개가 생길수도 있음
  • 사용처 : 게임 전체를 관리하면서 하나만 존재해야하는 경우, 중앙관리자.

팩토리 패턴

  • 객체 생성을 별도의 메서드로 캡슐화하여, 생성 로직을 한 곳에서 관리하는 패턴

  • 필요한 상황

    • 다양한 종류의 몬스터/아이템을 생성할 때
    • 조건에 따라 다른 객체를 생성해야 할 때
    • 생성 로직이 복잡하고 자주 변경될 때
// 문제상황
class Game {
    void SpawnEnemy() {
    	// 적 종류 추가되면 하드코딩으로 추가해줘야함
        Enemy* enemy = new Zombie();  
        enemy->Spawn();
    }
};
  • 따라서 Game 클래스는 적 소환하라는 메서드만 호출하고, 실제 호출은 하위 클래스가 함

  • 생성할 몬스터/아이템의 기본 인터페이스를 먼저 생성 (Product)

// 모든 적의 기본 인터페이스
class Enemy {
public:
    virtual void Attack() = 0;    
};
  • 그리고 실제 자손 객체들 생성 (Concrete Product)
// 진짜로 게임에 등장할 몬스터들
class Zombie : public Enemy {
	// ... //
    }
};

class Skeleton : public Enemy {
	// ... //
};
  • 객체들 생성할 Factory 클래스 생성
class EnemySpawner {
	// SpawnEnemy함수 //
};

// 좀비 공장
class ZombieSpawner : public EnemySpawner {
	// SpawnEnemy함수 override //
};

// 스켈레톤 공장
class SkeletonSpawner : public EnemySpawner {
	// SpawnEnemy함수 override //
}
  • Factory 클래스를 이용하여 소환
class GameMode {
    std::unique_ptr<EnemySpawner> currentSpawner;
    
    // 스테이지에 따라 다른 스포너 사용
    void SetupStage(int stageNumber) {
        switch (stageNumber) {
            case 1:
                currentSpawner = std::make_unique<ZombieSpawner>();
                break;
            case 2:
                currentSpawner = std::make_unique<SkeletonSpawner>();
                break;
        }
    }
    
    // 적 스폰
    void SpawnEnemyWave() {
        if (currentSpawner) 
            currentSpawner->SpawnEnemy();
    }
};
  • 단점
    1. 기존 New로만 생성하던 것보다 생성 과정이 굉장히 복잡해짐
    2. 객체가 어디에서 생성되는지 바로 추적하기 어려움
    3. 다양한 객체 생성할 때만 사용할 것

빌더 패턴

  • 복잡한 객체를 단계별로 생성할 수 있게 하는 패턴
// 문제상황: 생성자가 너무 복잡함
class Character {
public:
    Character(
        string name, 
        string className,
        int level,
        float health,
        // ... 더 많은 멤버변수
    );
};

// 사용에도 문제
Character* hero = new Character(
    "Arthas",      
    "Warrior",    
    // ...  헷갈리기 시작
);
  • 메서드 체인을 이용하여 순차적으로 생성
class Character {
private:  
    FString Name;
    FString Class;
    int Level;
    // ...
    
public:
    friend class CharacterBuilder;
};
  • friend class CharacterBuilder : Builder만 접근 가능
class CharacterBuilder {
private:
    Character* character;  // 조립 중인 캐릭터
    
public:
    CharacterBuilder() {
        character = new Character();
        
        // 기본값 세팅
        character->Level = 1;
        character->Height = 1.8f;
        character->Health = 100.0f;
        character->Mana = 100.0f;
        character->Strength = 10.0f;
        character->Intelligence = 10.0f;
        character->Agility = 10.0f;
    }
    
    // 모든 메서드는 자기 자신의 참조자를 반환하여 체인 가능하도록
    CharacterBuilder& WithName(const FString& name) {
        character->Name = name;
        return *this;  // 자기 자신 참조 반환
    }
    
    // 직업 설정
    CharacterBuilder& WithClass(const FString& className) {
        character->Class = className;
        return *this;
    }
    
   // 설정 메서드들 //
    
    // 최종 빌드
    Character* Build() {
        // 마지막 검증
        if (character->Name.IsEmpty()) {
            character->Name = "Unknown Hero";  // 이름 없으면 기본값
        }        
        return character;  // 최종 캐릭터를 반환
    }
};
  • 기본값을 세팅해주어, Builder로 생성시 몇 개 빼먹어도 괜찮도록 설게

  • 각 메서드들은 자기 자신의 참조자를 반환하여 체인 가능하도록 설계

  • 마지막에 Build를 호출하여 완성된 캐릭터를 반환

Character* wizard = CharacterBuilder()
    .AddSkill("Fireball")       
    .WithClass("Mage")          
    .WithName("Gandalf")         
    .AddSkill("Teleport")
    .WithLevel(99)
    .Build();
  • 순서 상관없고 멤버변수 빼먹어도 상관없음. 마지막에 Build만 호출하면 됨

  • 장점 : 복잡한 객체생성에 사용하기 편하고 안전성도 높음

  • 단점

    1. 코드가 길어지기에, 복잡하지 않은 객체에는 좋지 않음
    2. 객체 구조가 자주 변경되면, builder도 바꿔주며, 거대해짐
  • 정말 복잡한 객체 생성에서만 사용하기

profile
반갑습니다

0개의 댓글