Shader 강좌와 함께 배워보는 XNA Framework (1) - 기초 프로그램 구조
Shader 강좌가 지금 "Visual Studio 2010 팀 블로그"와 저자의 개인 홈페이지에서 동시 진행되고 있는데요.
그동안 DirectX 관련해서 전혀 해본 적이 없기 때문에 낯설긴 하지만, 이번에는 견문을 넓히자는 차원에서 직접 따라해 보기로 했습니다. ^^
그런데, 소스 코드를 따라하면서 왠지 수동적인 자세로 되어버리는 것 같아서... 잠시 생각하다가 이참에 DirectX의 닷넷 버전이라고 할 수 있는 XNA Framework으로 함께 변환을 해보기로 했습니다. (참고로, XNA Framework도 처음입니다. ^^;)
우선, 오늘 글은 다음의 내용에서 사용된 Visual C++ 코드에 대한 XNA Framework 버전을 만드는 것입니다.
01. 쉐이더란 무엇이죠? Part 2
; http://kblog.popekim.com/2011/12/01-part-2.html
위의 글에서는 앞으로의 Shader 강좌를 실습하기 위한 DirectX 응용 프로그램의 뼈대를 잡고 있는데요. 맨땅에 헤딩한다는 생각으로... 변환을 시작해 보겠습니다. ^^
우선, XNA Framework 프로젝트를 하나 만들어야 하는데요. Visual Studio에서 "Add New Project" 대화상자를 통해 "XNA Game Studio 4.0" / "Windows Game (4.0)" 프로젝트를 선택하시면 됩니다.
기본적으로 Program.cs와 Game1.cs 파일이 생성되는데요. 각각의 기본 코드는 다음과 같습니다.
===== Program.cs =====
static class Program
{
static void Main(string[] args)
{
using (Game1 game = new Game1())
{
game.Run();
}
}
}
===== Game1.cs =====
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}
재미있게도 강좌를 진행하는 분의 C++ 소스 코드 구조가 위의 XNA 응용 프로그램 구조와 크게 다르지 않습니다. ^^
자, 그럼 첫 작업으로 XNA 응용 프로그램의 윈도우 크기 조절부터 어떻게 하는지 살펴볼까요? 기본적으로 XNA는 순수하게 contents 윈도우 영역만 800 * 600으로 생성합니다. Visual C++로 만든 코드에서는 윈도우 영역을 조절하기 위해 다시 GetClientRect 등의 Win32 API를 사용해서 복잡하게 다음과 같이 크기 조정을 해주고 있는데요.
// Client Rect 크기가 WIN_WIDTH, WIN_HEIGHT와 같도록 크기를 조정한다.
POINT ptDiff;
RECT rcClient, rcWindow;
GetClientRect(hWnd, &rcClient);
GetWindowRect(hWnd, &rcWindow);
ptDiff.x = (rcWindow.right - rcWindow.left) - rcClient.right;
ptDiff.y = (rcWindow.bottom - rcWindow.top) - rcClient.bottom;
MoveWindow(hWnd,rcWindow.left, rcWindow.top, WIN_WIDTH + ptDiff.x, WIN_HEIGHT + ptDiff.y, TRUE);
XNA 응용 프로그램에서는 graphics 개체에서 제공되는 속성을 통해서 간단하게 설정하는 것이 가능합니다. (역시 Managed 코드가 좋습니다. ^^)
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
this.graphics.PreferredBackBufferHeight = 600;
this.graphics.PreferredBackBufferWidth = 800;
}
크기 조절은 이것으로 해결되었고, 그다음 ESC 키를 누르면 종료하는 조건을 추가하는 작업으로 넘어갑니다.
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
if (Keyboard.GetState().IsKeyDown(Keys.Escape) == true)
{
this.Exit();
}
base.Update(gameTime);
}
와~~~ 직관적이죠. ^^ 계속해서, 이번엔 폰트 부분으로 넘어가겠습니다. 원문에서는 D3DXCreateFont로 아래와 같이 생성을 해주는데요.
D3DXCreateFont( gpD3DDevice, 20, 10, FW_BOLD, 1, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY,
(DEFAULT_PITCH | FF_DONTCARE), "Arial", &gpFont );
XNA에서는 이런 폰트 자원을 별도의 "Content"로 생성해서 관리를 하는 것 같습니다. 이에 대해서는 저도 단번에 알 수 없기 때문에 ^^ 다음의 글을 참고했습니다.
Loading Fonts in XNA
; http://www.stromcode.com/2008/03/02/loading-fonts-in-xna/
이 글에 따라, 다음과 같이 XNA 프로젝트 생성 시에 부가적으로 함께 생성된 Content 프로젝트에서 "Sprite Font" 파일을 추가했습니다.
추가된 SpriteFont1.spritefont 파일을 보면 XML 파일 형식으로 폰트에 대해 서술된 것임을 쉽게 알 수 있습니다.
<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
<Asset Type="Graphics:FontDescription">
<FontName>Segoe UI Mono</FontName>
<Size>14</Size>
<Spacing>0</Spacing>
<UseKerning>true</UseKerning>
<Style>Regular</Style>
<CharacterRegions>
<CharacterRegion>
<Start> </Start>
<End>~</End>
</CharacterRegion>
</CharacterRegions>
</Asset>
</XnaContent>
이렇게 정의된 폰트 자원을 코드에서 사용하기 위해 다음과 같이 SpriteFont 개체를 생성해서 로드하고,
SpriteFont _spriteFont;
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
_spriteFont = Content.Load<SpriteFont>("SpriteFont1");
}
Draw 메서드에서 사용해 보았습니다.
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
Vector2 vect2;
vect2.X = 5;
vect2.Y = 5;
spriteBatch.DrawString(_spriteFont, "데모 프레임워크\n\nESC: 데모종료", vect2, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
잘 될줄 알았는데 ^^ 왠 걸... DrawString에서 다음과 같은 오류가 발생했습니다.
System.ArgumentException was unhandled
Message=The character '데' (0xb370) is not available in this SpriteFont. If applicable, adjust the font's start and end CharacterRegions to include this character.
Parameter name: character
Source=Microsoft.Xna.Framework.Graphics
ParamName=character
StackTrace:
at Microsoft.Xna.Framework.Graphics.SpriteFont.GetIndexForCharacter(Char character)
at Microsoft.Xna.Framework.Graphics.SpriteFont.InternalDraw(StringProxy& text, SpriteBatch spriteBatch, Vector2 textblockPosition, Color color, Single rotation, Vector2 origin, Vector2& scale, SpriteEffects spriteEffects, Single depth)
at Microsoft.Xna.Framework.Graphics.SpriteBatch.DrawString(SpriteFont spriteFont, String text, Vector2 position, Color color)
...[생략]...
at Microsoft.Xna.Framework.Game.Run()
at WindowsGame1.Program.Main(String[] args) in D:\Settings\desktop\dxf\WindowsGame1\WindowsGame1\Program.cs:line 15
InnerException:
대강 CharacterRegions에 지정된 Start/End 값의 범위를 넘었다는 것은 알겠는데... 그렇다면 구체적으로 무슨 값을 넣어주어야 할까요?
검색하다 보니, 황당하더군요. ^^;
[XNA 3.0 ~ 3.1] Localization Sample을 이용한 초간단 예제 입니다.
; http://blog.naver.com/fly33499/120094588332
DirectX C++ 소스 코드에서는 그냥 DrawText에 유니코드 문자열을 지정해 주면 되었는데, XNA에서는 관련 문자를 모두 자원으로 포함하고 있어야 한다는 이야기입니다.
어쨌든, 한글 출력을 위해서는 위의 글을 읽고 따라해 보시면 됩니다. 간단하게 설명하면
LocalizationSample_4_0 프로젝트를 내려받아서 빌드하면 생성되는 LocalizationPipeline.dll 파일과 LocalizationContent 프로젝트에 포함되어 있던 Font.spritefont를 제가 만든 게임 솔루션의 WindowsGame1Content 프로젝트 폴더에 복사하고 참조 및 추가를 해줍니다. (이전에 추가해 두었던 SpriteFont1.spritefont 파일은 그냥 삭제합니다.) Font.spriteFont의 속성창에서 "Content Processor"를 "LocalizedFontProcessor"로 바꿔줍니다.
"WindowsGame1" 프로젝트에서 리소스 파일을 하나 추가하고 거기에 프로그램에서 사용될 문자를 포함하는 한글을 입력해 둔 후, Font.spritefont에 포함된 "<ResourceFiles/>" 하위에 <Resx /> 노드로 그 리소스 파일의 상대 경로를 입력해 두었습니다. 아래는 이 과정을 하나의 그림으로 표현한 것입니다.
이렇게 구성한 후, 바뀐 Sprite 폰트 파일을 코드에 반영해 주고,
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
_spriteFont = Content.Load<SpriteFont>("Font");
}
빌드하면, 최초의 ^^ XNA 프로그램이 완성됩니다.
참고로, 2개의 프로젝트 파일을 첨부했는데
하나는 여기서 사용된 XNA 프로젝트이고, 다른 하나는 "
쉐이더란 무엇이죠? Part 2"
글에서 제공된 예제 코드를 Unicode 지원으로 바꾼 것입니다.
만약, 위의 예제에서 모든(?) 한글을 표현하고 싶도록 리소스 파일을 변경하고 싶다면 "
[XNA] K-Zune에서 사용한 한글, 한자, 일본어, 로마자 및 기타부호 파일입니다." 글에 첨부된 "한글.TXT" 파일의 내용을 "Resources.resx" 파일에 담아주면 됩니다.
=== 아래의 리소스는 '데', '모', '프', '레', '임', '워', '크', '종', '료' 글자가 들어간 경우에만 한글이 정상적으로 출력됨.
<data name="Hangul" xml:space="preserve">
<value>데모프레임워크종료</value>
</data>
=== 아래의 리소스는 대부분의 한글을 담고 있으므로 웬만한 한글들은 모두 정상적으로 출력됨.
<data name="Hangul" xml:space="preserve">
<value>가각간갇갈갉갊감갑값갓갔강갖갗
같갚갛개객갠갤갬갭갯갰갱갸갹갼걀
걋걍걔걘걜거걱건걷걸걺검겁것겄겅
겆겉겊겋게겐겔겜겝겟겠겡겨격겪견
...[생략]...
활홧황홰홱홴횃횅회획횐횔횝횟횡효
횬횰횹횻후훅훈훌훑훔훗훙훠훤훨훰
훵훼훽휀휄휑휘휙휜휠휨휩휫휭휴휵
휸휼흄흉흐흑흔흖흗흘흙흠흡흣흥
흩희흰흴흼흽힁히힉힌힐힘힙힛힝</value>
</data>
풀리지 않는 숙제 하나!
윈도우 타이틀을 다음과 같이 변경해 보았는데,
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
this.graphics.PreferredBackBufferHeight = 600;
this.graphics.PreferredBackBufferWidth = 800;
Window.Title = "초간단 쉐이더 데모 프레임워크";
}
문자열이 '?'로 깨져나오는데 해결책을 모르겠습니다. (물론, Resource.resx에도 해당 '문자'들을 넣어주었습니다.)
어쨌든... 이번엔 여기까지만! ^^ (이 글을 읽으시는 분들 중에 제목이 깨지는 문제에 대해서 아시는 분은 덧글 부탁드립니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]