이 글은 놀개영 http://www.gamedevforever.com 에서 연재 중인 글입니다.



요즘 툴을 만든다 하면 많은 분들이 C#를 선호하고 있습니다. MFC에 비해 생산성이 높기 때문이죠. 저도 마찬가지입니다. 개인 프로젝트에서 툴을 C#의 윈폼을 사용해 만들고 있습니다. 최근에는 WPF에도 관심을 가지게 되었습니다. 작년에 있었던 KGC11의 엔진과 툴의 그렇고 그런 사이 발표 자료를 보고 많은 감명을 받았죠.


이런 것도 있구나!!


윈폼이 MFC에 비해 편하기는 하지만, 컨트롤 구성에 제약이 있는 것은 윈폼도 마찬가지였습니다. 그에 반해 WPF는 윈폼에 비해 확장성과 유연성이 굉장히 높아 거의 원하는대로의 컨트롤 구성을 만들어낼 수 있습니다. 조금만 노력하면 언리얼 에디터 만큼의 퀄리티도 만들어낼 수 있을 정도죠.


이렇게 카와이하게 만들 수 있어요

출처 : http://wpfpropertygrid.codeplex.com/


이렇게 좋은 도구가 있는데, 문제는 역시 이놈도 C#이라는 것 입니다. 보통 게임엔진을 만든다 하면 대부분 C++로 만듭니다. 하지만 C#과 C++은 전혀 다른 언어죠. 그렇기에 C#으로 만들어진 툴이 C++로 만들어진 엔진과 연계가 되려면 둘 사이에 중계자가 필요합니다. 그 것이 바로 C++/CLI이죠. 이 C++/CLI란 놈은 C++과 C#의 영역을 자유자재(?)로 접근할수 있는 힘을 가지고 있습니다. C++에서 작성된 클래스 객체에 접근 할수 있고, C#의 객체에도 접근을 할 수 있습니다.


네... 뭐 그런놈이 있습니다.


이 중계 기능을 만들때 흔히 랩핑이라는 기법을 씁니다. C++에서 만들어진 클래스를 C++/CLI에서 가져와 그 클래스의 기능을 감싸는 클래스를 하나 더 만듭니다. 이 C++/CLI에서 만들어진 클래스는 [ 관리 되는 ] 클래스로 이 클래스는 C#에서 사용이 가능해집니다. 즉, C++/CLI를 통해 간접적으로 C#에서 C++ 클래스를 사용하는 것이지요.


C++

class DLL_API CSceneManager
{
public:
	void DoSomething( void )
	{
		// Do~ Do~ Do~
	}
};


C++/CLI

public ref class CWrappedSceneManager
{
public:
	void DoSomething( void )
	{
		m_pNativeSceneManager->DoSomething();
	}

private:
	CSceneManager* m_pNativeSceneManager;
};


C#

public partial class MainForm : Form
{
    private CWrappedSceneManager m_WrrapedSceneManager;
    public void DoSomething()
    {
        m_WrrapedSceneManager = new CWrappedSceneManager();
        m_WrrapedSceneManager.DoSomething();
    }
}


이 짓을 기능 추가때 마다 하라고??


대략 이와 같은 구조를 갖게 되는 겁니다. 벌써부터 머리가 지끈거려 오죠? 기능이 많으면 많을 수록 랩핑할 것도 많고, 손도 많이 갑니다. 정말 귀찮죠. 하지만 이렇게 일일히 클래스와 함수를 랩핑을 하는 것이 아니라 명령 단위로 처리한다면 어떨가요? 예로 게임 오브젝트를 하나 생성하는데, 게임 오브젝트 생성 부터 데이터를 채워넣는 것 까지 전부다 C# 툴 레벨에서 처리하는 것이 아니라 단지 C# 툴에서는 게임 오브젝트를 생성해라~ 명령어만 전달하는 것이지요.


그렇게 하면 C++/CLI 레벨에서는 단지 명령어를 전달 해줄 기능만 있으면 됩니다. 나머지는 엔진쪽에서 해당 명령에 대한 수행 로직만 만들면 되는 것이지요. 여기서 좀더 나아가 보겠습니다. 만약 이 C++/CLI 조차 필요없이 바로 C++쪽에 명령을 전달할수 있다면 어떨까요? 불필요한 파일도 없어지고, 좀더 수월해지겠죠?


하지만 C++과 C#은 같이 쓸수 없다고 했잖아요? 어떻게 하면 될까요? 바로 DLL Interop을 통해서 할 수 있습니다. C#에서도 DLL Interop을 통해 외부 DLL의 함수를 불러와 사용할 수 있습니다. 일단 엔진을 DLL로 빌드 하고, C#에서 사용될 명령어 처리 함수를 만듭니다. 그리고 C#에서 이 DLL을 가져와 함수를 사용하는 것이지요.


C++

extern "C"
{
	DLL_API bool SendCommand( const wchar_t* szCommand );
}


C#

public partial class MainWindow : Window
{
    [DllImport("Engine.dll", EntryPoint = "SendCommand", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    private static extern bool SendCommand(string strCommand);

    public void DoSomething()
    {
        SendCommand("DoSomething");
    }
}


이 얼마나 아름답고 간결한 코드인가~


이전 C++/CLI 사용때 보다 훨씬 간단해졌죠? 아주 좋습니다. 이제 C#과 C++ 연동은 알겠어요. 그럼 WPF로 게임 엔진 툴을 만들어 봅시다. 뚝딱~ 뚝딱~ 카와이 하게 언리얼 에디터 같은 것을 만들어보죠. 그런데 문제가 생겼습니다. WPF로 멋지게 툴을 만들었다고 생각했는데, 화면이 렌더링 될 부분을 어떻게 만들어야 하죠?


윈폼에서는 ImageBox 같은 더미 컨트롤을 하나 만들어서 그 컨트롤의 핸들을 엔진쪽에 넘겨주면 엔진쪽에서 그 핸들을 받아 그 곳에 화면을 렌더링할 수 있었습니다. 그런데 WPF는 컨트롤의 핸들이 없습니다. 오로지 윈도우의 핸들만 존재합니다. 열심히 컨트롤 붙이고 툴을 만들었는데, 정작 화면을 렌더링 할수 없으면 아무 소용이 없습니다.


난 그동안 뭐한건가...


다행히 방법은 있습니다. WPF Win32 Content Hosting 이라는 요상한 기능이 하나 있습니다. 즉, WPF 상에 Win32에서 만든 컨텐츠를 표시할 수 있게 해주는 기능이죠. 이를 이용해 우리는 엔진쪽에서 만든 Render Window를 WPF에 갖다 붙일 수 있습니다. 일단 이 기능을 쓰기 위해서는 HwndHost 라는 클래스를 이용해야 합니다.


C#

public class RenderViewHwndHost : HwndHost
{
    // Win32 창 생성 함수
    [DllImport("Engine.dll", EntryPoint = "CreateRenderWindow", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
    private static extern IntPtr CreateRenderWindow(IntPtr applicationInstance, IntPtr hWndParent, int screenWidth, int screenHeight);

    // 소멸시 할 일
    [DllImport("Engine.dll", EntryPoint = "DestroyWindow", CallingConvention = CallingConvention.Cdecl)]
    private static extern void DestroyWindow();

    protected override HandleRef BuildWindowCore(HandleRef hwndParent)
    {
        IntPtr instanceHandle = System.Runtime.InteropServices.Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]);
        IntPtr hwndHost = CreateRenderWindow(instanceHandle, hwndParent.Handle, 1024, 760);

        return new HandleRef(this, hwndHost);
    }

    protected override void DestroyWindowCore(HandleRef hwnd)
    {
        DestroyWindow();
    }
}


C++ 엔진쪽에서 WPF에 붙일 RenderWindow를 생성하는 함수 하나를 정의합니다. 그리고 C#에서 HwndHost 클래스를 재정의 하여, 이 함수를 호출해줍니다. HwndHost에서 재정의 해야될 멤버 함수가 BuildWindowCore와 DestroyWindowCore 두 가지가 있습니다. 전자는 창 생성, 후자는 창 소멸시 호출되는 HwndHost 멤버 함수입니다. 이 두 함수는 Hosting시에 자동으로 호출 되므로, 직접 호출해줄 일은 없습니다.


HwndHost를 만들었으면 이제 생성한 창을 붙일 일만 남았습니다. WPF에서 편집기를 통해 Border 컨트롤을 이곳에 렌더 화면을 띄어야지~~ 할 곳에 적당히 붙여둡니다. 이 Border 컨트롤은 더미 역할을 하게 됩니다. 그리고 재정의한 HwndHost를 생성해 이 Border의 Child에 찰싹~ 붙여주면, 이제 WPF에서도 렌더링 화면을 볼수 있게 됩니다.


C#

public partial class MainWindow : Window
{
    public void CreateRenderView()
    {
        RenderViewHwndHost renderViewHost = new RenderViewHwndHost();
        border1.Child = renderViewHost; // 찰싹~
    }
}


위 기능을 구현하기 위해 드플의 핵심 인재 끼로(@kgun86)님께서 많은 도움을 주셨습니다.

이 자리를 빌어 감사드립니다.


  1. BlogIcon 구차니 2012.05.13 22:41 신고

    외계어 ㅠ.ㅠ
    전 언제쯤 C에서 벗어나볼수 있을까요 ㅠ.ㅠ

    아무튼 이제 조금 숨돌리고 C++ 템플렛과 CUDA를 다시 보고 있어요
    CUDA를 보는데 치사하게(?) C++ 템플렛을 쓰더라구요 ㅠ.ㅠ

  2. BlogIcon 솔솔이 2012.08.29 22:06

    네이티브 코드에서 CreateRenderWindow 함수 부분 자세하세 설명해주실 수 없나요?
    winapi 지식이 없어서 도무지 감이 안잡히네요 ㅠ

+ Recent posts