ProudNet 유틸리티

콜렉션 사용하기

STL, ATL 등도 이미 콜렉션 클래스들(std.map, std.vector, CFastArray, CAtlArray, CAtlMap 등)을 제공합니다. 하지만 ProudNet은 STL과 ATL을 모두 사용하기 곤란한 상황에서나, 고속의 성능을 요구하는 상황에서 효과적인 클래스들을 제공합니다

클래스함수설명

배열 클래스

Proud.CFastArray

내부적으로 Fast heap을 사용합니다.

링크드 리스트 클래스

Proud.CFastList

내부적으로 Fast Heap을 사용합니다. CFastArray와 달리 클래스는 생성자, 소멸자 및 복사 할당 연산자와 함께 사용할 수 있습니다.

맵 클래스

Proud.CFastMap

(Key,Value) 쌍의 해시 알고리즘을 사용합니다. CAtlMap과 사용법이 매우 유사하고, STL.map의 반복자 및 일부 메소드를 동일하게 사용할 수 있습니다.

셋 클래스

Proud.CFastSet

CFastMap과 달리 Key만 소유 하는 클래스로 나머지는 CFastMap과 동일합니다.

빠른 메모리 관리자

ProudNet은 고성능 메모리 관리자가 내장되어 있어 개발자는 고성능 메모리 관리자를 이용하여 응용 프로그램의 처리 성능을 가속할 수 있습니다.

ProudNet에서 지원하는 메모리 관리자는 크게 다음과 같습니다.

- Lookaside allocator

Lookaside allocator 는 통상적인 memory pool 기법을 응용합니다. 만약 항상 동일한 크기의 메모리를 자주 할당/해제해야 한다면 Lookaside allocator를 사용하는 것이 좋습니다.

주요 메카니즘은 아래와 같습니다.

  • 새로운 메모리 블럭을 할당할 때, 새로운 시스템 메모리가 할당된다.

  • 메모리 블럭을 해제할 때, 해제된 블럭은 lookaside allocator에 반환된다.

  • 다시 메모리 블럭을 할당할 때 lookaside allocator에 반환되었던 메모리 블럭이 재활용된다.

이 과정은 매우 빠른 속도로 실행됩니다. OS 환경에서의 메모리 할당 속도보다 훨씬 빠릅니다.

하지만 단점도 존재합니다.

  • Lookaside allocator 는 항상 같은 크기의 메모리만 할당할 수 있습니다.

  • Lookaside allocator 로 할당된 메모리 블럭은 Lookaside allocator 가 파괴되기 전에 모두 해제되어야 합니다.

Lookaside allocator 를 쓰는 방법은 다음과 같습니다.

  • 먼저 Proud.CLookasideAllocator.New 메서드로 객체를 생성합니다. 전역 객체로 생성해도 됩니다.

  • Proud.CLookasideAllocator.Alloc 메서드로 메모리 블럭을 할당합니다.

  • 해제는 Proud.CLookasideAllocator.Free 로 합니다. Realloc은 따로 존재하지 않습니다.

  • 모든 메모리 블럭을 해제한 후 Proud.CLookasideAllocator 객체를 파괴합니다.

- Fast Heap

ProudNet의 Fast heap Lookaside allocator 보다 약간 더 느리지만 OS 환경에서의 메모리 할당/해제 속도보다 훨씬 빠르고, 다양한 크기의 메모리 블럭을 할당/해제가 가능합니다.

ProudNet의 Fast heap의 구현 클래스는 Proud.CFastHeap 입니다. Proud.CFastHeap 또한 모든 메모리 블럭이 파괴된 후 Proud.CFastHeap 객체를 제거할 수 있습니다.

Fast heap을 쓰는 방법은 다음과 같습니다.

  • 먼저 Proud.CFastHeap.New 메서드로 Fast heap 객체를 생성합니다. 전역 객체로 생성해도 됩니다.

  • Proud.CFastHeap.Alloc 메서드로 메모리 블럭을 할당합니다.

  • 해제는 Proud.CFastHeap.Free로 합니다. CFastHeap.Realloc 으로 메모리 블럭을 재할당 할 수 있습니다.

  • 모든 메모리 블럭을 해제한 후 Proud.CFastHeap 객체를 파괴합니다.

C++ 클래스의 기본 할당자로 지정하기

Fast heap 이나 Lookaside allocator 를 C++ 클래스에서 가속을 쉽게 받는 방법은 C++의 operator new, delete 메서드를 오버라이드하는 것입니다.

이렇게 하면 C++ 클래스를 new, delete 연산자로 생성, 파괴할 때 시스템의 메모리 heap 대신 Fast heap이나 Lookaside allocator가 사용됩니다.

아래 예시에서, 클래스가 operator newdelete로 인스턴스화 할 때 고성능 메모리 관리자가 대신 메모리를 할당/해제하게 합니다. 이들은 각각 다른 장단점이 존재하니 적절히 선택하여 사용하시기 바랍니다.

➡️ 예시
class Example
{
    static CLookasideAllocatorPtr gAlloc; // 혹은 CFastHeap을 써도 OK.
public:
    void* operator new(size_t size)
    {
        return gAlloc->Alloc(size);
    }
    void operator delete(void* ptr, size_t size)
    {
        gAlloc->Free(ptr);
    }
};
 
CLookasideAllocatorPtr Example::gAlloc(CLookasideAllocator::New()); // 혹은 CFastHeap을 써도 OK.

스마트 포인터

ProudNet은 스마트 포인터 Proud.RefCount 클래스를 보유하고 있습니다.

스마트 포인터란, 생성된 객체을 참조하는 변수들이 존재하는 한 해당 객체의 존재 자체를 보장하는 역할을 합니다. 그리고 해당 객체을 참조하는 변수들이 더 이상 존재하지 않을 때만 그 객체는 파괴됩니다.

또한 개발자가 만든 버그로 인해 이미 파괴된 객체를 참조하는 문제(dangling)나 미처 객체를 파괴하지 않는 문제(leak)를 해소하는 역할을 하기도 합니다.

스마트 포인터 변수는 복사될 때마다 객체 참조 카운트가 1씩 증가하는데 아래 그림에서 Object Instance는 참조 카운트가 0이 될 때까지 존재하게 됩니다.

➡️ 예제 코드
class A {...};
 
void Foo()
{
    // A 객체가 생성
    Proud::RefCount<A> a(new A);
    
    // 변수 b가 변수 a와 공유됨. A의 참조 카운트는 2가 됨.
    Proud::RefCount<A> b = a;
    
    // 변수 a가 해제됨. 그러나 A는 참조 카운트가 아직 1이므로 파괴되지 않음.
    a = Proud::RefCount<A>();
    
    // 변수 b가 해제됨. 더 이상 A를 참조하는 변수가 없으므로 A가 파괴됨. (즉 delete가 호출됨)
    b = Proud::RefCount<A>();
}

객체를 참조하는 스마트 포인터가 여러 군데 있는 상태에서 강제로 객체를 파괴하고 싶을 때가 있습니다.

예를 들어, 열려있는 파일 핸들을 보유한 객체를 스마트 포인터에서 참조하고 있을 경우, 당장 그 파일 핸들을 보유한 객체를 파괴해야 하는 때가 있을 것입니다.

하지만 스마트 포인터가 여기저기서 그 객체를 참조하고 있다면 객체가 파괴되는 시점을 알 수 없기 때문에 난관에 봉착할 수 있습니다. 이러한 경우를 위해 Dispose Pattern을 구사하여 여러 군데에서 참조하고 있는 객체의 파괴를 명시적으로 수행할 수 있습니다.

- Dispose Pattern

Dispose Pattern 은 한 개 이상의 스마트 포인터가 참조하는 객체가 파괴될 시점을 정확히 몰라도 객체를 명시적으로 파괴하는 효과를 얻기 위한 프로그램 패턴입니다.

스마트 클래스로 다뤄질 객체에 Dispose Pattern 을 구사하려면 객체의 멤버 변수로서 '자기 상태'를 가지는데, 이는 이미 객체가 파괴되어 사용 불가 상태인지를 의미해야 합니다. 만약 자기 상태가 '이미 파괴된 상태'라면 객체 참조 시 에러를 발생시키고, 그렇지 않은 경우 정상 수행을 하도록 만들어야 합니다.

아래는 Dispose Pattern을 구사한 예입니다.

Dispose Pattern 은 Java나 C# 등 스마트 포인터Garbage Collector 가 있는 프로그래밍 언어에서도 다뤄지고 있습니다.

➡️ 예제 코드
class A
{
    // true인 경우 이 객체는 파괴(dispose)된 상태임을 의미한다.
    bool m_disposed;
    public:
    
    A()
    {
        // 갓 생성된 객체는 dispose되어있지 않은 상태이다.
        m_disposed = false;
    }
 
    ~A()
    {
        Dispose();
    }
 
    void Dispose()
    {
        if(m_disposed == false)
        {
            // 객체 파괴에 관련된 실제 수행을 한다.
            // 예를 들어 갖고 있던 파일 핸들을 닫는다.
            ...
        
            m_disposed = true;
        }
    }
};
 
typedef Proud::RefCount<A> APtr;
 
void Foo()
{
    APtr a(new A); // 객체 생성
    APtr b = a;     // 두 개의 스마트 포인터 변수가 한 객체를 공유
    
    a->Dispose();   // 강제로 객체를 파괴합니다.
    
    // 이제 a,b 모두 객체는 파괴된 상태입니다.
    // 따라서 a,b가 참조하고 있는 객체를 접근하면 안됩니다.
    ...
}

스레드 유틸리티

ProudNet은 몇 가지 스레드 유틸리티 클래스를 제공합니다.

클래스설명

Proud.Thread

스레드를 쉽게 생성, 파괴할 수 있습니다.

Proud.CriticalSection

critical section을 만들 수 있습니다.

Proud.CriticalSectionLock

lock 및 unlock 을 할 수 있습니다.

문자열 클래스

ProudNet은 문자열 클래스 Proud.String, Proud.StringA 는 문자열을 ATL이나 STL의 문자열 클래스처럼 간편하게 문자열을 다룰 수 있게 해줍니다.

.Net Framework를 사용하는 프로그램은 System.string이라는 심볼을 가지고 있습니다. 따라서 .Net Framework를 혼용하는 경우 System 혹은 Proud 중 하나의 네임스페이스를 명시해야 할 수도 있습니다.

예를 들면 아래와 같습니다.

Proud::String a;  // 빈 문자열
a = L"123";         // 문자열에 값 넣기.
puts(a);            // a 자체가 직접 문자열 버퍼를 제공한다.
a += L"abc";        // 문자열에 다른 문자열 덧붙이기
if(L"123abc" == a)  // 문자열의 내용 비교 방법
{
    a.Replace(L"123", "def");   // 문자열 내용 치환
}

- 문자열 만들기 기능(format)

Proud.StringTsprintf() 처럼 문자열 만들기 기능을 제공합니다.

Proud::String a;
a.Format(L"%d %d %s", 1, 2, L"hahaha");
// 이제 a = "1 2 hahaha"가 됩니다.

- 문자열 처리 성능

Copy-on-write

// Proud.StringT의 copy-on-write 기능은 문자열이 꼭 필요한 경우에만 사본이 떠집니다. 
// 그 전에는 문자열 데이터를 서로 공유하게 됩니다.
 
Proud::String a = L"abc"; // a는 문자열 'abc'를 소유
Proud::String b = a; // b는 a와 같은 문자열 데이터를 공유
Proud::String c = b; // 이제 a,b,c는 모두 같은 문자열 데이터를 공유
c = L"bcd"; // a,b는 여전히 문자열 데이터 'abc'를 공유하고 있으나 c는 더 이상 공유를 하지 않고 'bcd'를 별도로 소유
b = c; // b는 a와의 'abc' 공유를 포기하고 c가 가진 'bcd'를 공유

문자열 길이 측정

Proud.StringT.GetLength 는 호출 즉시 미리 측정되었던 문자열 길이를 리턴합니다. 즉, strlen() 과 다릅니다.

Proud.StringT 는 int나 float와 마찬가지로 thread safe 하지 않습니다. 따라서 여러 스레드에서 동시에 같은 문자열 객체를 접근하는 것은 (모든 스레드가 읽기만 하는 경우를 제외하고) 안전하지 않습니다.

이 점은 ATL이나 STL의 문자열 클래스와 마찬가지입니다.

Timer Queue (타이머 큐)

Timer Queue 스레드 풀에서 tick event를 수행하는 모듈로 Windows XP, 2000 이후 버전의 운영체제에서는 Windows Timer Queue라는 API를 제공하고 있습니다.

일정 시간마다 사용자가 지정한 함수를 실행시키되, 그 함수는 스레드 풀의 스레드 중 하나에서 실행됩니다. 만약 모든 스레드가 뭔가를 실행중인 경우(running state) 함수의 실행은 스레드 중 과거 작업이 완료되는 스레드가 등장할 때까지 보류됩니다.

Timer Queue 에서 호출하는 유저 함수는 스레드 풀에 있는 스레드 중 하나가 선택되는데, 만약 앞서 실행중이던 유저 함수가 실행이 완료되지 않은 상태이더라도 할 일이 없는 스레드(idle state)가 있는 경우 그 스레드가 선택되어서 유저 함수를 실행합니다.

다음과 같은 작업 리스트가 있다고 가정합시다.

검은 화살표는 0.1초이며 A,B,C,D,E는 매 0.1초마다 해야 할 작업 항목입니다. A,D는 0.1초 이내에 끝나며, B는 딱 0.1초에 끝나며, C,E는 0.1초 안에 끝나지 못하는 작업입니다.

서버 메인 루프 방식인 경우 이들 작업 항목은 다음 그림처럼 수행됩니다. 한 개의 스레드에서 모든 작업 항목을 실행하기 때문에 D,E는 제때 시작하지 못합니다.

그러나 Timer Queue 방식에서는 D를 실행하기 위해 또 다른 스레드가 동원되며 E는 앞서 C를 완료한 스레드에서 제때에 실행되고 있습니다.

제 때 필요한 작업을 실행하되 필요한 경우 스레드를 더 동원합니다.

Timer Queue 는 유저 함수가 동시에 두 개 이상의 스레드에서 실행되고 있을 수 있는 특징 때문에 주로 서버 프로그램에서 사용됩니다.

병렬성을 가능하게 해주는 대신 병렬성이 가지고 있는 위험성을 감수해야 하므로 사용 전에 이 기능이 반드시 필요한지 판단하시기 바랍니다.

Timer Queue 를 잘못 사용할 경우 서버 프로세스의 스레드가 폭발적으로 증가하여 성능에 역효과를 줄 수 있습니다. Timer Queue 를 꼭 써야 하는 이유가 없다면 Proud.CTimerThread 또는 서버에서 타이머 루프, RMI, 이벤트 처리하기 를 사용하시기 바랍니다.

- 타이머 큐 사용 방법

Proud.CTimerQueue 클래스를 접근해야 합니다. 이 클래스는 singleton입니다.

일정 시간마다 호출될 함수와 호출 주기를 Proud::NewTimerParam 구조체에 설정하여 Proud.CTimerQueue.NewTimer 에 인자로 넣어주면 Proud.CTimerQueueTimer 객체를 받게 됩니다. 그리고 Proud.CTimerQueueTimer 객체를 파괴하기 전까지는 지정한 유저 함수가 일정 시간마다 실행됩니다.

➡️ 예제 코드
VOID NTAPI UserFunction(void* context, BOOLEAN TimerOrWaitFired)
{
    int *pCallCount = static_cast<int *>(context);
 
    // 유저 함수
    std::cout << "UserFunction : " << ++(*pCallCount) << std::endl;
}
 
int _tmain(int argc, TCHAR* argv[])
{
    // NewTimerParam 구조체 변수 선언.
    NewTimerParam p1;
 
    // 테스트로 카운팅에 쓰일 변수 선언
    int callCount = 0;
 
    // 유저 함수 설정.
    p1.m_callback = UserFunction;
 
    // 매개변수로 받을 포인터 설정.
    p1.m_pCtx = &callCount;
 
    // 1초 이후부터 콜백이 시작되도록 설정.
    p1.m_DueTime = 1000;
 
    // 0.1초 간격으로 콜백 되도록 설정.
    p1.m_period = 100;
 
    // 일정 시간마다 스레드 풀에서 유저 함수가 0.1초마다 호출되도록 한다.
    Proud::CTimerQueueTimer* ret = Proud::CTimerQueue::GetSharedPtr()->NewTimer(p1);
 
    std::cout << "PRESS ANY KEY TO EXIT" << std::endl;
 
    // 유저 콜백이 호출될 때까지 대기
    _getch();
 
    // 타이머 객체를 파괴한다. 파괴한 후에는 유저 함수가 더 이상 호출되지 않는다.
    delete ret;
}

로그 남기기

온라인 게임을 개발하다 보면 여러가지 실행 기록(로그)를 남기는 기능이 필요하기 마련입니다. ProudNet은 이를 위한 로그 남기기 기능을 제공합니다.

ProudNet의 로그 남기기 기능은 비동기로 실행되기에 로그를 남기겠다는 메서드를 호출하는 즉시 메서드는 리턴합니다. 또한 별도의 스레드에서 실제 로그를 파일이나 데이터베이스에 기록합니다.

- 파일에 로그 기록

Proud::CLogWriter 클래스는 파일에 로그를 기록할 수 있게 해주는 클래스 입니다.

➡️ 예제 코드
// CLogWriter를 생성한다.
CAutoPtr<Proud::CLogWriter> logwriter;
logwriter.Attach(Proud::CLogWriter::New(L"log.txt"));
 
// 로그를 써봅시다. 2개의 WriteLine함수를 제공합니다.
logwriter->WriteLine( Proud::TraceID::TID_System, L"시스템로그 입니다. );
logwriter->WriteLine( "%d번째 로그입니다.", 1 );
 
// 새로운 Log File로 바꿔봅시다. 만약 새로운 파일 생성이 실패하면 Proud::Exception 으로 예외처리 됩니다.
logwriter->SetFileName(L"log2.txt);

- 데이터베이스에 로그 기록

Proud::CDbLogWriter 클래스는 DB에 로그를 기록할수 있게 해주는 클래스 입니다.

이를 사용하기 위해서는 Sample/DbmsSchema 폴더에 있는 LogTable.sql을 실행하여 LogTable을 미리 생성해주어야 합니다. DBMS 구축은 샘플 데이터베이스 구축하기 의 절차를 참고하십시오.

➡️ 예제 코드
// 에러를 받아 처리할 함수입니다.
class CTestLogWriterDelegate : public ILogWriterDelegate
{
    virtual void OnLogWriterException(Proud::AdoException& Err) override
    {
        // ...
    }
};
 
CTestLogWriterDelegate g_dblogDelegate;
 
void main()
{
    // ...
    // CDbLogParameter에 값을 채운다.
    Proud::CDbLogParameter dbparam;
    dbparam.m_dbmsConnectionString = L"Data Source=localhost;Database=Log-Test;Trusted_Connection=yes";
    dbparam.m_loggerName = L"LoggerName";
    dbparam.m_dbLogTableName = L"DbLog";
 
    // CDbLogWriter를 생성한다.
    CAutoPtr<Proud::CDbLogWriter> dbLogWriter;
    dbLogWriter.Attach(Proud::CDbLogWriter::New(dbparam, &g_dblogDelegate));
    
    // ProudNet에서 제공하지 않는 새로운 Field를 넣어봅시다.
    // 주의!! 사용자가 원하는 필드를 넣고싶을때에는 DBMS의 Log-Test Table에 미리 필드를 생성시켜 주어야 합니다.
    // TestField를 DBMS에 만들고 datatype은 int라고 했을때 아래구문처럼 CPropNode를 생성하여 WriteLine에 넣어주시면 됩니다.
    Proud::CProperty newnode;
    Proud::String TestField = L"TestField";
    Proud::CVariant TestValue = 123;
    newnode.Add(TestField, TestValue);
    dbLogWriter->WriteLine(L"로그 내용입니다.", &newnode);
    
    // ...
}

레이턴시 측정 기능

ProudNet은 StopWatch의 형태로 레이턴시 측정 기능을 제공합니다.

사용 가능한 버전이라면 서버의 GetLastPing이나 GetRecentPing 대신 기존의 레이턴시 측정 함수들보다 더 정확한 이 기능을 이용해주셔야 됩니다.

(1) 레이턴시 측정 시작

StartRoundTripLatencyTest 를 호출하면 레이턴시를 측정할 대상과 relay를 시작합니다.

➡️ 예제 코드
StartRoundTripLatencyTestParameter testParameter;
testParameter.testDuration = 40 * 1000; // RoundTripLatencyTest 도중에 언제까지 StopRoundTripLatencyTest가 호출되기를 기다릴 것인지 지정하는 변수입니다. 단위는 밀리초입니다. 디폴트로 500ms를 가집니다.
testParameter.pingIntervalMs = 400; // RoundTripLatencyTest 도중에 핑을 보내는 주기를 지정하는 변수이다. 단위는 밀리초입니다. 디폴트로 300ms를 가집니다.
ErrorType startError = netClient->StartRoundTripLatencyTest(HostID_Server, testParameter);
switch(startError)
{
    case ErrorType_Ok:
        std::cout << "성공" << std::endl;
        break;
    case ErrorType_InvalidHostID:
        std::cout << "HostID가 자기 자신이거나, 연결된 피어가 아닌 경우" << std::endl;
    break;
}

(2) 레이턴시 측정 종료

앞서 지정한 testDuration 이전에 테스트를 중단하고자 한다면 StopRoundTripLatencyTest 함수를 호출합니다. testDuration이 지날 때까지 StopRoundTripLatencyTest 호출을 안하면 자동적으로 측정이 중단됩니다.

➡️ 예제 코드
ErrorType stopError = StopRoundTripLatencyTest(HostID_Server);
switch(stopError)
{
    case ErrorType_Ok:
        std::cout << "성공" << std::endl;
        break;
    case ErrorType_InvalidHostID:
        std::cout << "연결된 피어가 아닌 경우" << std::endl;
        break;
}

(3) 레이턴시 측정 값 얻어오기

➡️ 예제 코드
RoundTripLatencyTestResult testResult;
ErrorType getError = GetRoundTripLatency(HostID_Server, testResult);
switch(getError)
{
    case ErrorType_Ok:
        std::cout << "성공" << std::endl;
        std::cout << "측정 기간 동안의 핑퐁 평균 : " << testResult.latencyMs 
<< ", 측정 기간 동안의 핑퐁 표준편차 : " << testResult.standardDeviationMs 
<< ", 측정 기간 동안 실행된 핑퐁 횟수 : " << testResult.totalTestCount << std::endl;
        break;
    case ErrorType_InvalidHostID:
        std::cout << "연결된 피어가 아닌 경우" << std::endl;
        break;
    case ErrorType_ValueNotExist:
        std::cout << "핑퐁이 한번도 오가지 않은 경우" << std::endl; // 이 경우에 RoundTripLatencyTestResult의 totalTestCount가 0입니다.
        break;
}

PIDL 컴파일러 애드온

PIDL(ProudNet IDL) 파일의 Custom Build 설정을 손쉽게 할수있도록 도와주는 비주얼 스튜디오용 애드온 입니다.

- 설치

1. <설치경로>\ProudNet\util\PIDL-addon.vsix 실행 합니다.

2. 애드온을 설치할 Visual Stuio를 선택하고 설치(Install)를 진행합니다.

3. 설치가 정상적으로 완료되면 아래와 같이 표시 됩니다.

- 삭제

ToolsExtensions and Updates 메뉴를 통해 삭제가 가능합니다.

Visual Studio 2010의 경우 ToolsExtension Manager

- PIDL 파일 추가 창

PIDL 추가 창을 호출 합니다. 프로젝트 선택우클릭Add new PIDL file 선택

  1. PIDL 파일명을 입력합니다.

  2. PIDL 파일이 추가될 경로를 표시합니다.

  3. 외부 PIDL 파일을 추가합니다.

  4. 새로운 PIDL 파일을 생성합니다.

  5. PIDL 추가 창을 종료합니다.

- PIDL 파일 추가

  1. 추가할 PIDL 파일명 입력

  2. New PIDL 클릭

위 그림과 같이 PIDL 파일이 추가되며 해당 파일의 Custom Build 설정은 기본적으로 되어 있습니다. 기본 설정과 다른 경우 파일 속성 변경 기능을 통해 변경 가능합니다.

- PIDL 파일 속성 창

PIDL 속성 창을 호출 합니다.

PIDL 파일 선택우클릭Properties 선택

  1. PIDL 파일의 Custom Build Tool 설정을 확인/변경 합니다. PIDL Compiler Location: PIDL 컴파일러 위치를 설정합니다. PIDL Compiler Location is Relative: 입력한 경로의 절대/상대 여부 값 입니다. Output Directory (All Languages): 출력 경로를 설정 합니다. Output Directory is Relative (All Languages): 입력한 경로의 절대/상대 여부 값 입니다.

  1. 언어 별 설정 창: 각 C++, C#, Java, Unreal Script 언어 별로 생성 가능합니다.

  2. 언어 별 설정 창: Generate Code: C++ 기본 Yes, 나머지 언어는 False (필요에 따라 설정 가능) C++ Implementation File Extension: C++만 해당 됩니다.

Last updated