프로그래밍/WPF, Silverlight

[WPF/Silverlight] 게임루프 구현하기 - CompositionTarget을 이용한 방법

당근천국 2012. 4. 25. 12:19

일반적으로 게임을 만들 때는 게임 루프라는 무한 반복되는 루프를 만들어 만들게 됩니다.
이런 방법을 이용하는 이유는 검색하면 많이 나오기 때문에 따로 설명하진 않겠습니다.
( 이 정도 글을 읽으시면서 설마 이 정도도 모를라고? ㅎㅎㅎㅎ )

여하튼 게임을 만드는데 꼭 게임 루프가 있어야 하느냐? 그건 아닙니다.
특히나 WPF나 실버라이트 같은 경우 자체적으로 UX프레임웍이 잘돼 있어서 더더욱 게임 루프 없이도 게임을 만드는 데 지장이 없습니다.
(참고 : 아이유vs지연 프로젝트 )
하지만 게임 루프가 필요할 때가 있죠..

여전히 기존 프래임웍과 게임 루프를 어떻게 연결하여 사용해야 할지 의문이긴 합니다만...
뭐 일단 게임 루프부터 만들고 알아보도록 하죠 ㅎㅎㅎ


프로젝트 생성은 "GameLoop_CompositionTarget"로 하였습니다.


1. 페이지 구성

페이지에 대해선 딱히 설명하지 않겠습니다.
그냥 복사해서 쓰세요.

<Window x:Class="GameLoop_CompositionTarget.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:GameLoop_CompositionTarget" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid Background="Black">
            <Canvas x:Name="gameSurface">
                <local:Ship x:Name="ship" Canvas.Left="305" Canvas.Top="220"/>
            </Canvas>
            <TextBlock x:Name="clickToStart" Width="250" Height="80" Foreground="White" VerticalAlignment="Top" Margin="50" FontSize="35" TextAlignment="Center"><Run Text="Click"/><Run Text=" to Start!"/></TextBlock>
            <TextBlock x:Name="test1" Width="250" Height="80" Foreground="White" FontSize="16" TextAlignment="Center" d:LayoutOverrides="Width" HorizontalAlignment="Left" VerticalAlignment="Top"><Run Text="Click to Start!"/></TextBlock>
            <TextBlock x:Name="test2" Width="250" Text="Click to Start!" Foreground="White" FontSize="16" TextAlignment="Center" d:LayoutOverrides="Width" HorizontalAlignment="Left" Margin="0,134,0,98"/>
        </Grid>
    </Grid>
</Window>

 

 

아래 코드는 화살표용 유저 컨트롤입니다.

<UserControl x:Class="GameLoop_CompositionTarget.Ship"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             Width="26" Height="40">
    
        <Canvas x:Name="LayoutRoot" RenderTransformOrigin="0.5,0.5">
            <Canvas.RenderTransform>
                <TransformGroup>
                    <RotateTransform x:Name="rotateTransform" Angle="0"/>
                </TransformGroup>
            </Canvas.RenderTransform>
            <Path Data="M0,38 L12,0,24,38,18,32,7,32z" Stroke="#FFFFFFFF" StrokeThickness="2"/>
        </Canvas>
</UserControl>

 

 

요런 결과물이 나오죠.
클릭하신 후 방향키를 눌러 보아요~

 

1-2. 화살표 코드

여기서 화살표의 용도는 앵글(Angle, 각도)만 변경하는 것이라 별다른 코드가 없습니다.

namespace GameLoop_CompositionTarget
{
	/// <summary>
	/// Ship.xaml에 대한 상호 작용 논리
	/// </summary>

	public partial class Ship : UserControl
	{
		public Ship()
		{
			InitializeComponent();
		}

		public double RotationAngle
		{
			get { return rotateTransform.Angle; }
			set { rotateTransform.Angle = value; }
		}
	}
}

 

2. 키보드 핸들러 만들기

게임 루프에서는 모든 요청을 일단 가지고 있다가 프레임이 오면 그때 처리를 하게 됩니다.
그렇기 때문에 키보드도 마찬가지죠.
원래는 범용 클래스를 만들어서 인풋을 처리하는 것이 좋은데 여기선 샘플이고 하니 그냥 처리 합니다.
(애초에 참조한 프로젝트도 이렇게 되있어서 ㅎㅎㅎ)

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;

namespace GameLoop_CompositionTarget
{
	class KeyHandler
	{
		Dictionary isPressed = new Dictionary();
		FrameworkElement targetElement = null;
		public void ClearKeyPresses()
		{
			isPressed.Clear();
		}

		public KeyHandler(FrameworkElement target)
		{
			ClearKeyPresses();
			targetElement = target;
			target.KeyDown += new KeyEventHandler(target_KeyDown);
			target.KeyUp += new KeyEventHandler(target_KeyUp);
			target.LostFocus += new RoutedEventHandler(target_LostFocus);
		}

		void target_KeyDown(object sender, KeyEventArgs e)
		{
			if (!isPressed.ContainsKey(e.Key))
			{
				isPressed.Add(e.Key, true);
			}
		}

		void target_KeyUp(object sender, KeyEventArgs e)
		{
			if (isPressed.ContainsKey(e.Key))
			{
				isPressed.Remove(e.Key);
			}
		}

		void target_LostFocus(object sender, RoutedEventArgs e)
		{
			ClearKeyPresses();
		}

		public bool IsKeyPressed(Key k)
		{
			return isPressed.ContainsKey(k);
		}
	}
}

내용을 보시면 알겠지만, 별거 없습니다.
마지막으로 눌린 키를 가지고 있다가 리턴해주는게 다입니다.

 

3. 게임 루프 클래스 만들기

게임 루프를 만들기 위해서 'CompositionTarget'를 이용할 때는 'Rendering'이벤트를 이용합니다.
"CompositionTarget.Rendering"에 이벤트를 걸어주면 화면을 그리기 직전에 이벤트가 넘어오게 되죠.
"CompositionTarget.Rendering"에서는 지속해서 업로드 이벤트를 발생시켜 외부에 화면에 프레임이 돌아왔음을 알립니다.
외부에서는 'public event UpdateHandler OnUpdate;'에 이벤트를 연결하여 사용하게 되죠.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media;

namespace GameLoop_CompositionTarget
{
	public class GameLoop
	{
		/// <summary>
		/// 마지막으로 기록된 시간
		/// </summary>

		protected DateTime m_datetimeLastTick;

		/// <summary>
		/// 게임 루프 발생용 핸들러
		/// </summary>
		public delegate void UpdateHandler(object sender, TimeSpan elapsed);
		/// <summary>
		/// 게임 루프가 동작하면 발생 합니다.
		/// </summary>
		public event UpdateHandler OnUpdate;


		/// <summary>
		/// 현재시간 기록.
		/// - 반복해서 호출되기 때문에 가비지컬랙터를 거치치지 않게 하여 성능을 늘리기 위해 선언함
		/// </summary>
		private DateTime m_datetimeNow;
		/// <summary>
		/// 경과 시간
		/// - 반복해서 호출되기 때문에 가비지컬랙터를 거치치지 않게 하여 성능을 늘리기 위해 선언함
		/// </summary>
		private TimeSpan m_timespanElapsed;

		/// <summary>
		/// 연결된 이벤트의 카운트
		/// </summary>
		public int EventCount = 0;

		public void Tick()
		{
			//지금 시간 기록
			m_datetimeNow = DateTime.Now;

			//경과시간 = 지금시간 - 마지막 시간
			m_timespanElapsed = m_datetimeNow - m_datetimeLastTick;
			//마지막 시간 기록
			m_datetimeLastTick = m_datetimeNow;

			//업데이터 이벤트가 연결 되어 있나?
			if (OnUpdate != null)
			{
				//되어 있으면 동작시킨다.
				OnUpdate(this, m_timespanElapsed);
			}
		}

		void CompositionTarget_Rendering(object sender, EventArgs e)
		{
			Tick();
		}

		/// <summary>
		/// 게임루프를 실행합니다.
		/// 무한루프 형태로 진행 됩니다.
		/// 베이스의 Start()를 오버라이드했기 때문에 계속 자신이 호출되는 효과가 있습니다.
		/// </summary>
		public void Start()
		{
			//연결된 이벤트가 있는가?
			if (0 < EventCount)
			{
				//연결된 이벤트가 있다.
				//있으면 동작을 막는다. 
				return;
			}

			//틱호출 이벤트를 연결해줌
			CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering);
			//이벤트가 추가 되었음을 표시
			++EventCount;

			//마지막 틱 기록
			m_datetimeLastTick = DateTime.Now;

			//베이스에 스타트를 실행함
			Start();
		}

		/// <summary>
		/// 게임루프를 정시 시킵니다.
		/// </summary>
		public void Stop()
		{
			//연결된 이벤트가 있는지?
			if (0 >= EventCount)
			{
				//연결된 이벤트가 없다.
				//없으면 처리할게 없다.
				return;
			}

			//틱호출 이벤트를 끊습니다.
			CompositionTarget.Rendering -= new EventHandler(CompositionTarget_Rendering);
			//이벤트가 제거 되었음을 표시
			--EventCount;
			//베이스에 스타트를 종료함
			Stop();
		}
	}
}

 

4. 게임 루프 이용하기

이제 게임 루프를 이용한 프로그램을 작성하면 됩니다.

 

namespace GameLoop_CompositionTarget
{
	/// <summary>
	/// MainWindow.xaml에 대한 상호 작용 논리
	/// </summary>
	public partial class MainWindow : Window
	{
		/// <summary>
		/// 게임 루프용 클래스
		/// </summary>
		private GameLoop m_insGameLoop;
		/// <summary>
		/// 키보드 핸들러
		/// </summary>
		private KeyHandler keyHandler;
		/// <summary>
		/// 회전속도
		/// </summary>
		private double rotationSpeed = 150;

		/// <summary>
		/// 루프회전수 체크
		/// </summary>
		private long m_long = 0;

		public MainWindow()
		{
			InitializeComponent();

			this.GotFocus += new RoutedEventHandler(Page_GotFocus);
			this.LostFocus += new RoutedEventHandler(Page_LostFocus);
			this.MouseLeftButtonDown += new MouseButtonEventHandler(Page_MouseLeftButtonDown);

			m_insGameLoop = new GameLoop();
			m_insGameLoop.OnUpdate += new GameLoop.UpdateHandler(gameLoop_Update);

			keyHandler = new KeyHandler(this);
		}

		void Page_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
		{
			clickToStart.Visibility = Visibility.Collapsed;
			m_insGameLoop.Start();
		}

		void Page_LostFocus(object sender, RoutedEventArgs e)
		{
			clickToStart.Visibility = Visibility.Visible;
			m_insGameLoop.Stop();
		}

		void Page_GotFocus(object sender, RoutedEventArgs e)
		{
			clickToStart.Visibility = Visibility.Collapsed;
			m_insGameLoop.Start();
		}

		void gameLoop_Update(object sender, TimeSpan elapsed)
		{
			if (keyHandler.IsKeyPressed(Key.A) || keyHandler.IsKeyPressed(Key.Left))
			{
				ship.RotationAngle -= rotationSpeed * elapsed.TotalSeconds;
			}
			else if (keyHandler.IsKeyPressed(Key.D) || keyHandler.IsKeyPressed(Key.Right))
			{
				ship.RotationAngle += rotationSpeed * elapsed.TotalSeconds;
			}

			test2.Text = (++m_long / 60).ToString();
		}
	}
}

 

5. FPS 계산하기

이제 초당 프레임 수를 계산하여 출력하겠습니다.
FPS를 계산하기 위하여 다음과 같이 변수를 선언합니다.

//☆☆☆☆☆ FPS용 ☆☆☆☆☆
/// <summary>
/// 경과시간
/// </summary>
private double m_dElapsed;
/// <summary>
/// 총 경과 시간
/// </summary>
private double m_dTotalElapsed;

/// <summary>
/// 마지막 틱
/// </summary>
private int m_nLastTick;
/// <summary>
/// 현재 틱
/// </summary>
private int m_nCurrentTick;

/// <summary>
/// 프레임 카운트(이 녀석이 최종 FPS가 됨)
/// </summary>
private int m_nFrameCount;
/// <summary>
/// 프레임 카운트 시간(1초동안 카운트가 되었는지 확인)
/// </summary>
private double m_dFrameCountTime;
/// <summary>
/// 프래임 결과를 임시 보관하는 변수
/// </summary>
private int m_nFrameRate;
//☆☆☆☆☆ 여기까지 - FPS용 ☆☆☆☆☆

 

게임 루프가 한 바퀴 돌 때마다 발생하는 메소드(여기서는 gameLoop_Update가 됩니다.)에 다음 코드를 넣습니다.

 

this.m_nCurrentTick = Environment.TickCount;
this.m_dElapsed = (double)(this.m_nCurrentTick - this.m_nLastTick) / 1000.0;
this.m_dTotalElapsed += this.m_dElapsed;
this.m_nLastTick = this.m_nCurrentTick;

m_nFrameCount++;
m_dFrameCountTime += this.m_dElapsed;
if (m_dFrameCountTime >= 1.0)
{
	m_dFrameCountTime -= 1.0;
	m_nFrameRate = m_nFrameCount;
	m_nFrameCount = 0;
	test1.Text = "FPS: " + m_nFrameRate.ToString();
}

 

그리고 처음에 잘못된 계산이 되는 것을 막기 위해 MainWindow()에 다음과 같은 초기화 코드를 넣습니다.

//마지막 틱을 설정해준다.
this.m_nLastTick = Environment.TickCount;

 

소스 및 참고 자료

이제 프로그램을 실행하면 게임 루프로 구성된 프로그램이 작동하는 것을 볼 수 있습니다.
방향키를 눌러 회전시켜 봅시다.

 

GameLoop.zip
다운로드

 

bluerosegames - Silverlight CompositionTarget.Rendering Game Loop
MSDN - CompositionTarget 클래스
windowsclient.net - Application (Game) Loop, FPS and Sprite like Animations