Unreal 개발 본 캠프 18일차

HappyCircle·2025년 12월 22일

Unreal 개발

목록 보기
35/163
post-thumbnail

오늘 학습 진행 내용

게임 개발자를 위한 C++ 문법

챕터 3-1: 디자인 패턴

디자인 패턴이란 개발 시 반복적으로 등장하는 문제를 해결하기 위한 일반화된 솔루션
클래스 구조 + 객체 관계 + 책임 분리 방법
유지 보수성, 확장성, 재사용성을 높이기 위한 설계 도구
패턴의 종류는 아래와 같이 구성

- 디자인 패턴의 구분

생성 패턴(Creational Patterns)
새로운 것을 만들어 내는 방법과 관련된 패턴
공장에서 물건(객체)을 찍어내는 것과 유사한 방식

  • 싱글톤 패턴(Singleton)
    클래스의 인스턴스를 오직 하나만 생성하고 전역에서 동일한 객체에 접근하도록 보장하는 패턴
    사용처 : 설정 관리자, 로그 시스템, 사운드 매니저, 게임 매니저
  • Abstract Factory
    서로 관련된 개체들의 제품군을 생성하는 인터페이스 제공
    함께 써야 하는 객체들을 묶어서 생성하는 방식
    구체적인 클래스를 숨기고, 일관된 객체 세트를 보장
    사용처 : 플랫폼 별 UI,테마별 몬스터 세트, 무기+방어구 세트
  • Builder
    복잡한 객체를 단계적으로 생성
    조립 설명서와 결과물을 분리하는 방식
    사용처 : 캐릭터 생성, 옵션이 많은 설정 객체, JSON/파일 로딩 객체
  • Factory
    객체 생성을 전담하는 클래스/메서드를 제공
    객체 생성 책임을 외부로 위임하는 방식
    사용처 : 몬스터 생성, 아이템 생성, 타입별 객체 생성
  • Prototype
    기존 객체를 복사해서 새 객체 생성
    복사해서 찍어내는 방식
    사용처 : 몬스터 대량 생성, 기본 설정이 동일한 객체 반복 생성, new 비용이 큰 객체

구조 패턴(Structual Patterns)
여러 부품을 어떻게 조립하고 연결하는 방법에 대한 패턴
여러 개의 객체들의 구조를 어떻게 구성할지가 해당 패턴의 주 관심사

  • Adapter
    기존 클래스의 인터페이스를 원하는 형태로 변환
    기존 클래스를 수정하지 않고 호환하는 방식
    인터페이스 변환기
    사용처 : 외부 라이브러리 연동, 레거시 코드 재사용, 엔진 API 래핑
  • Bridge
    추상화(기능)와 구현을 분리하여 독립적으로 확장
    기능과 구현을 가로로 분리
    사용처 : 플랫폼 별 구현 분리, 렌더링 API 분리, 무기 x 속성 조합 폭발 방지
  • Composite
    부분 객체와 전체 객체를 동일한 인터페이스로 다룸
    트리를 하나처럼 다루는 방식
    사용처 : UI 트리, 씬 그래프, 폴더/파일 구조
  • Decorator
    기존 객체를 감싸서 기능을 동적으로 추가하는 패턴
    상속을 사용하지 않고 기능을 조합하는 방식
    사용처 : 캐릭터 버프/디버프, 아이템 옵션, 스킬 강화 효과, 로그/성능 측정 래핑
  • Facade
    복잡한 서브 시스템을 단순 인터페이스로 감싸는 패턴
    사용자용 정문 인터페이스 방식
    사용처 : 엔진 초기화, 시스템 통합 API, 외부 모듈 진입점
  • Flyweight
    공통 데이터를 공유하여 메모리 사용 최소화하는 패턴
    공통 데이터와 개별 데이터 분리하는 방식
    사용처 : 총알, 파티클, 동일한 모델 다수 배치, 타일맵
  • Proxy
    실제 객체에 대한 접근을 제어하는 대리자
    실제 객체와 동일한 인터페이스 방식
    사용처 : 지연 로딩, 접근 권한 제어, 네트워크 객체

행동 패턴(Behavioral Patterns)
부품이 서로 어떻게 상호작용할지에 대한 패턴
특정 객체가 변할 때 다른 객체들에 이 상태를 어떻게 전달할지 다루는 패턴

  • Observer
    객체 상태 변화가 발생하면, 등록된 객체들에게 자동으로 알림
    상태가 바뀔 때 자동으로 통지하는 방식
    사용처 : UI 갱신, 체력/점수 변경 알림, 이벤트 시스템
  • Command
    요청(행동)을 객체로 캡슐화하는 패턴
    사용처 : 입력 처리, Undo/Redo, 매크로 기능
  • Interpreter
    간단한 언어/문법을 해석하는 구조의 패턴
    문법을 객체로 표현해 해석하는 방식
    사용처 : 스크립트 명령, 수식 계산, 조건 표현식
  • Iterator
    내부 구조를 노출하지 않고 순차 접근 제공하는 패턴
    순회 방법을 캡슐화
    사용처 : 컨테이너 순회, 컬렉션 추상화
  • Mediator
    객체 간 직접 통신을 중재자에게 위임하는 패턴
    사용처 : UI 컨트롤 간 상호작용, 복잡한 객체 관계 정리
  • Memento
    객체의 상태를 저장하고 복원하는 패턴
    상태 스냅샷 저장하는 방식
    사용처 : Undo/Redo, 세이브/로드, 체크 포인트
  • State
    객체의 상태에 따라 행동을 위임하는 패턴
    사용처 : 캐틱터 상태, AI 상태 머신
  • Strategy
    알고리즘을 교체 가능하게 분리하는 패턴
    사용처 : 이동 방식, 공격 로직, AI 판단
  • Template Method
    알고리즘의 뼈대를 정의하고 일부 단계만 자식이 구현하는 패턴
    사용처 : 공통 흐름 유지, 확장 포인트 제공
  • Visitor
    객체 구조와 알고리즘을 분리하는 패턴
    사용처 : 트리 구조 연산, 새로운 기능 추가 빈번한 경우

생성 패턴 중 싱글톤 패턴

싱글톤 패턴 구현
다음과 같은 요구사항에 대해서 구현
비행기 게임 제작
게임 내 제어하는 비행기는 반드시 하나만 존재
비행기 관리

1️⃣ 게임 내 비행기는 오직 하나만 존재합니다. 2️⃣ 비행기의 초기 위치는 (0,0) 좌표입니다.

비행기 이동

1️⃣ 비행기는 `move(int deltaX, int deltaY)` 메서드를 통해 이동하고, 위치를 업데이트합니다. 2️⃣ 현재 비행기 좌표를 확인할 수 있는 `getPosition()`을 제공합니다.

생성 및 대입은 private로 처리, getInstance를 통해서 인스턴스를 받음

구현 예제 코드

#include <iostream>
using namespace std;
/*
생성 패턴 중 싱글톤 패턴 구현 예제
클래스의 인스턴스를 오직 하나만 생성하고 전역에서 동일한 객체에 접근
비행기를 하나의 인스턴스만 생성 가능(복사, 복사 대입 금지)하게 하고 전역에서 동일한 인스턴스에 접근
*/
class Airplane{
    private:
        static Airplane* instance; //유일한 비행기 객체를 가리킬 정적 포인터
        int positionX; //비행기의 X 위치
        int positionY; //비행기의 Y 위치

        //private 생성자 : 외부에서 객체 생성 금지
        Airplane() : positionX(0), postionY(0){
            cout << "Airplane Created at (" <<postionX << positionY <<")"<<endl;
        }
    public:
        /*
        복사 생성자와 대입 연산자를 삭제하여 복사 방지
        Airplane b=(a);(복사) , Airplane c(a);(복사 대입) 와 같은 것들 사용 못하게 막는 것
        이미 있는 객체에 덮어쓰는 것 혹은 새로 만들면서 복사하는 상황 방지
        */
        Airplane(const Airplane&) = delete; //복사 생성자 금지
        Airplane& operator = (const Airplane&) = delete; //복사 대입 연산자 금지

        //정적 메서드 : 유일한 비행기 인스턴스를 반환
        static Airplane* getInstance(){ 
            if(instance==nullptr){
                instance = new Airplane();
            }
            return instance;
        }

        //비행기 위치 이동
        void move(int deltaX, int deltaY){
            positionX += deltaX;
            postionY += deltaY;
            cout <<"Airplane moved to (" <<positionX << ","<<postionY <<")"<<endl;
        }

        //현재 위치 출력
        void getPosition() const {
            cout<<"Airplane Position: ("<<positionX <<","<<positionY<<")"<<endl;
        }

};

//정적 멤버 초기화
Airplane* Airplane::instance = nullptr;

//메인 함수(사용 예시)
int main(){
    //유일한 비행기 인스턴스를 가져옴
    //Airplane 객체 생성된 것에 접근하는 것이 아니라 클래스에 접근하는 방식이어서 :: 사용
    Airplane* airplane = Airplane::getinstance();
    airplane->move(10,20); //비행기 이동
    airplane->getPosition();

    //또 다른 요청도 같은 인스턴스를 반환 (sameAirplane이랑 airplane 같은 인스턴스)
    Airplane* sameAirplane = Airplane::getInstance();
    sameAirplane->move(-5,10); //비행기 이동
    sameAirplane->getPosition();

    return 0;

}

구조 패턴 중 데코레이터 패턴

데코레이터 패턴 구현
문제 상황
피자를 만들려고 합니다.
이때 피자의 토핑을 원하는 대로 추가하면서 동적으로 피자를 만들 수 있어야 합니다.
다음과 같은 요구사항에 대해서 구현
기본 피자

1️⃣피자의 기본 베이스가 반드시 필요합니다. 2️⃣기본 피자에는 이름과 가격이 정의되어 있습니다.

토핑 추가

1️⃣피자에 토핑(치즈, 페퍼로니, 올리브)를 동적으로 추가할 수 있습니다. 2️⃣ 토핑은 이름과 추가 비용을 가집니다.

최종 피자의 이름(기본 피자 + 추가된 토핑들)과 가격을 제공합니다.

구현 예제 코드

  #include <iostream>
 #include <string>

 using namespace std;

 /*
 구조 패턴 중 데코레이터 패턴 구현 예제
 기존 객체를 감싸서 기능을 동적으로 추가하는 패턴
 기능의 공통 인터페이스(추상 컴포넌트) 기준으로 실제 객체(구체 컴포넌트)와 
 그 객체를 감싸 기능을 확장하는 데코레이터 계층을 분리해서 구현
 추상 인터 페이스 -> 기본 기능 구현한 클래스 -> 추상 데코레이터 -> 구체 기능 확장
 추상 인터 페이스(피자) -> 기본 피자 -> 확장용 껍데기 -> 피자 토핑
 추상 컴포넌트 (Component) : Pizza
 피자 객체의 기본 구조를 정의하는 인터페이스
 모든 피자는 이름(getName)과 가격(getPrice)을 가져야 합니다
 데코레이터는 객체를 런타임에 감싸서 교체,중첩하므로
 수명과 소유권을 명확히 관리할 수 있는 동적 생성이 필요
 */
 class Pizza{
     public:
         virtual ~Pizza(){}
         virtual string getName() const = 0; //순수 가상 함수, 피자의 이름을 반환
         virtual double getPrice() const = 0; //순수 가상 함수, 피자의 가격 반환
 };

 /*
 구체 컴포넌트 : BasicPizza
 기본 피자 클래스
 피자의 기본 베이스(이름과 가격)을 구현
 */
 class BasicPizza : public Pizza {
     public:
         string getName() const {
             return "Basic Pizza"; //기본 피자 이름
         }
         double getPrice() const {
             return 5.0; // 기본 피자 가격
         }
 }

 /*
 데코레이터 추상 클래스 : PizzaDecorator
 기존 피자의 기능을 확장하기 위한 데코레이터의 기본 구조를 정의
 내부적으로 Pizza 객체를 감싸며, 이름과 가격에 추가적인 기능을 제공
 */
 class PizzaDecorator : public Pizza {
     protected:
         Pizza* pizza; //기존의 피자 객체를 참조
     public:
         //데코레이터는 피자 객체를 받아서 감쌈(내부 피자 객체에 대한 소유권을 확보하기 위해서 포인터로 처리)
         PizzaDecorator(Pizza* p) : pizza(p) {}

         //소멸자에서 내부 피자 객체를 삭제
         virtual ~PizzaDecorator(){
             delete pizza; //Pizza 클래스 객체 포인터로 선언된 Pizza삭제함으로써 내부 피자 개체가 삭제되는 것으로 처리
         }
 }

 /*
 구체 데코레이터 : Cheese, Pepperoni, Olive
 각각의 토핑 데코레이터는 PizzaDecorator를 상속받아 이름과 가격을 확장
 */
 //치즈 토핑 데코레이터
 class CheeseDecorator : public Pizza Decorator {
     public:
         CheeseDecorator(Pizza* p) : PizzaDecorator(p) {
             string getName const{
                 //기존 피자의 이름에 " + Cheese"를 추가
                 retrun pizza->getName() + " + Cheese";
             }
         }
         double getPrice() const {
             //기존 피자의 가격에 치즈 추가 비용 1.5를 더함
             return pizza->getPrice() + 1.5;
         }
 }

 //페퍼로니 토핑 데코레이터
 class PepperoniDecorator : public  PizzaDecorator {
     public:
         PepperoniDecorator(Pizza* p): PizzaDecorator(p){}
         string getName() const {
             //기존 피자의 이름에 " + Pepporni"를 추가
             return pizza->getName() + " + Pepperoni";
         }
         double getPrice() const {
             //기존 피자의 가격에 페페로니 추가 비용 2.0을 더함
             return pizza->getPrice() + 2.0;
         }
 }

 //올리브 토핑 데코레이터
 class OliveDecorator : public PizzaDecorator {
     public:
         OliveDecorator(Pizza* p) : PizzaDecorator(p){}
         string getName() const{
             //기존 피자의 이름에 " + Olive"를 추가
             return pizza->getName() + " + Olive";
         }
         double getPrice() const{
             //기존 피자의 가격에 올리브 추가 비용 0.7를 더함
             return pizza->getPrice()+0.7;
         }
 }

 //클라이언트 코드
 //피자와 데코레이터를 조합하여 최종 피자를 생성하고, 정보를 출력
 int main(){
     //매 라인마다 pizza가 가리키는 객체 바뀌 이전 객체는 새 객체 안으로 포장됨
     //런타임에 반복된므로 정적 객체로는 불가능한 구조
     //1. 기본 피자를 생성
     Pizza* pizza = new BasicPizza();
     //2. 치즈 토핑을 추가
     pizza = new CheeseDecorator(pizza);
     //3. 페퍼로니 토핑을 추가
     pizza = new PepperoniDecorator(pizza);
     //4. 올리브 토핑을 추가
     pizza = new OliveDecorator(pizza);
     //5. 최종 피자 정보 출력
     cout<<"Pizza: "<<pizza->getName()<<endl; //피자의 이름 출력
     cout<<"Price: "<<pizza->getPrice()<<endl; //피자의 가격 출력
     //6. 메모리 해제
     delete pizza;

     return 0;
 }

행동패턴 중 옵저버 패턴

옵저버 패턴 구현
문제 상황

엑셀 프로그램을 만들려고 합니다.
엑셀 데이터값을 변경할 때 해당 데이터로 생성한 모든 차트의 데이터가 반영되게 하고 싶습니다.
차트의 개수나 종류가 많을 때 매번 각 차트의 데이터를 수동으로 업데이트하는 것은 비효율적입니다.

어떻게 하면 이를 자동으로 할 수 있을까요?

아래와 같은 요구사항을 받았다고 해봅시다.
엑셀 시트와 차트 관리

1️⃣ 엑셀 시트는 데이터를 입력받을 수 있습니다. 2️⃣ 여러 차트는 엑셀 데이터를 기반으로 업데이트합니다. 3️⃣ 엑셀에 새로운 값이 입력되면 연결된 모든 차트가 자동으로 업데이트됩니다. 주목해서 보셔야 할 부분은 아래와 같습니다. 1️⃣Subject

상태를 관리하고, 변경되었음을 옵저버에게 알립니다.

2️⃣ Observer

Subject를 관찰하며 상태 변경 시 반응합니다.

3️⃣ 연결 구조

Observer들은 Subect에 등록하고 변경이 발생하면 알람을 받습니다.

구현 예제 코드

#include <iostream>
#include <vector>
#include <string>
using namespace std;
/*
행동 패턴 중 옵저버 패턴 구현 예제
객체 상태 변화가 발생하면 등록된 객체에 자동으로 알리는 패턴

Observer 인터페이스
Observer 패턴에서 상태 변화를 알림받는 객체들의 공통 인터페이스
Observer들은 이 인터페이스를 구현하여 'update' 메서드를 통해 데이터를 전달받음
*/
class Observer{
    public:
        virtual ~Obeserver() = default; //가상 소멸자, 기반 클래스 포인터로 객체를 삭제할 때 실제 파생 클래스의 소멸자까지 올바르게 호출되게 함
        virtual void update(int data) = 0; //데이터 업데이트 메서드 (순수 가상 함수)
};

//Subject 클래스 (엑셀 시트 역할)
//데이터의 상태 변화를 관리하며, 모든 등록된 Observer들에게 변경 사항을 알림
class ExcelSheet{
    private:
        vector<Observer*> observers; //Observer들을 저장하는 리스트
        int data; //현재 데이터 상태
    public:
        ExcelSheet() : data(0) {} //생성자: 초기 데이터 값은 0

        //Observer 등록 메서드
        //새로운 Observer 등록하면 변경 사항 알림을 받을 수 있도록 추가
        void attach(Observer* observer){
            observers.push-back(observer);
        }        
        //데이터 변경 알림 메서드
        //등록된 모든 Observer들의 update 메서드를 호출하여 데이터 변경 사항을 알림
        void notify(){
            for(Observer* observer : observers){
                observer->updat(data); //각 Observer에게 데이터를 전달
            }
        }

        //데이터 설정 메서드
        //데이터를 변경하고 변경 사항을 모든 Observer에게 알림
        void setData(int newData){
            data = newData; //새로운 데이터로 갱신
            cout << "ExcelSheet: Data updated to "<<data<<endl; //Observer들에게 알림
        }
};

//구체적인 Observer 클래스 : BarChart(막대 차트)
class BarChart : public Observer{
    public:
        void update(int data){ //데이터 업데이트 시 호출됨
            cout<<"BarChar: Displaying data as vertical bars: ";
            for(int i=0; i<data; ++i){
                cout<<"|" //데이터 값만큼 막대 출력
            }
            cout<<"("<<data<<")"<<endl;
        }
};

//구체적인 Observer 클래스 : LineChart(라인 차트)
//데이터를 선형 그래프로 표현
class LineChart : public Observer{
    public:
        void update(int data){ //데이터 업데이트 시 호출됨
            cout<<"LineChart: Plotting data as a line: ";
            for(int i=0; i<data; ++i){
                cout<<"-"; //데이터 값만큼 선 출력
            }
            cout <<" (" <<data<<")"<<endl;
        }
};

//구체적인 Observer 클래스 : PieChart (파이 차트)
//데이터를 파이 그래프로 표현
class PieChart : public Observer {
    public:
        void update(int data){ //데이터 업데이트 시 호출됨
            cout<<"PieChart: Displaying data as a pie chart slice :";
            cout<<"Pie ["<<data<<"%]"<<endl; //데이터 값 출력(가정 : % 비율로 표현)
        }
};

//메인 함수
int main(){
    //Subject 생성
    ExcelSheet excelSheet; //데이터를 관리하는 엑셀 시트 객체 생성

    //Observer 객체 생성(각 차트 객체)
    BarChart* barChart = new BarChart(); //막대 차트 생성
    LineChart* lineChart = new LineChart(); //라인 차트 생성
    PieChart* pieChart = new PieChart(); //파이 차트 생성

    //Observer 등록
    //각 차트(Observer)를 엑셀 시트(Subject)에 등록
    excelSheet.attach(barChart);
    excelSheet.attach(lineChart);
    excelSheet.attach(pieChart);

    //데이터 변경  테스트
    //데이터를 변경하면 등록된 모든 Observer들이 알림을 받고 화면에 갱신
    excelSheet.setData(5); //데이터 변경 : 5
    excelSheet.setData(10); //데이터 변경 : 10

    //메모리 해제
    //동적 할당된 Observer(차트) 객체 삭제
    delete barChart;
    delete lineChart;
    delete pieChart;
    
    return 0;
}

디자인 패턴 실습

OCP원칙이 적용된 Animal 클래스에 기능 추가

  • 숙제 설명

    💡 OCP 원칙은 소프트웨어는 확장에는 열려 있고 수정에는 닫혀 있어야 한다는 원칙입니다. 제시된 코드를 수정하지 않고 새로운 기능을 추가해 보세요.
    • 기본 코드

      #include <iostream>
      #include <vector>
      using namespace std;
      
      // 기본 Animal 클래스 (수정 금지)
      class Animal {
      public:
          virtual void speak() = 0; // 순수 가상 함수
          virtual ~Animal() = default; // 가상 소멸자
      };
      
      // Dog 클래스 (이미 구현됨)
      class Dog : public Animal {
      public:
          void speak() {
              cout << "Woof! Woof!" << endl;
          }
      };
      
      // Cat 클래스 (이미 구현됨)
      class Cat : public Animal {
      public:
          void speak() {
              cout << "Meow! Meow!" << endl;
          }
      };
      
      // 동물의 소리를 출력하는 함수 (수정 금지)
      void makeAnimalsSpeak(vector<Animal*> &animals) {
          for (Animal* animal : animals) {
              animal->speak();
          }
          // 동적으로 할당된 메모리를 해제합니다.
          for (Animal* animal : animals) {
              delete animal;
          }
      }
      
      int main() {
          vector<Animal*> animals;
          animals.push_back(new Dog());
          animals.push_back(new Cat());
      
          makeAnimalsSpeak(animals);
          return 0;
      }

    Animal 클래스와 몇 가지 동물 클래스가 이미 구현되어 있습니다.
    새 동물 클래스를 추가해서 동물의 소리를 출력해 보세요.
    (어떤 동물을 추가하는지는 자유입니다. 요구사항만 만족하시면 됩니다.)

    실습 구현 코드

    #include <iostream>
    #include<vector>
    using namespace std;
    
    // 기본 Animal 클래스 (수정 금지)
    class Animal{ 
        public:
            virtual void speak()=0; //순수 가상 함수
            virtual ~Animal() = default; //가상 소멸자
    };
    
    class Dog : public Animal{
        public:
            void speak(){
                cout<<"Woof! Woof!"<<endl;
            }
    
    };
    
    class Cat : public Animal{
        public:
            void speak(){
                cout<<"Meow! Meow!"<<endl;
            }
    
    };
    
    class Tiger : public Animal{
        public:
            void speak(){
                cout<<"Kuwang! KuWang!"<<endl;
            }
    };
    
    // 동물의 소리를 출력하는 함수 (수정 금지)
    void makeAnimalsSpeak(vector<Animal*> &animals) {
        for (Animal* animal : animals) {
            animal->speak();
        }
        // 동적으로 할당된 메모리를 해제합니다.
        for (Animal* animal : animals) {
            delete animal;
        }
    };
    
    int main(){
        vector<Animal*> animals;
        animals.push_back(new Dog());
        animals.push_back(new Cat());
        animals.push_back(new Tiger());
        makeAnimalsSpeak(animals);
        return 0;
    }
profile
개발합시다!

0개의 댓글