본문 바로가기
Programming/Windows Phone

생애 첫 번째 윈도우 폰 7 애플리케이션 만들기 4회

by 강철 벼룩 2011. 4. 7.
[퍼즐 애플리케이션의 로직 프로그래밍]

 이번 시간에는 애플리케이션 로직을 프로그래밍 한다. 로직에는 퍼즐 보드를 초기화 하고 애플리케이션의 리소스에서 이미지를 읽고 사용자 인터페이스로 부터 이벤트에 대한 핸들러를 생성한다.

1.  프로젝트에 게임의 로직을 담는 클래스를 추가한다. [솔루션 탐색기]에서  WindowsPhonePuzzle 프로젝트 노드를 오른 클릭하고 [추가]|[기존 항목] 선택한다. [기존 항목 추가] 대화 상자에서, 설치한 랩의 [Source]폴더의 [Assets] 찾아서 PuzzleGame.cs 선택하고 [추가] 클릭한다.


[그림 10] 추가된 프로젝트 파일 표시


추가한 클래스는 새로운 게임을 시작하고 퍼즐 조각을 옮기며, 게임의 상태를 저장하고 복원한다. 여기서는 클래스의 소스 코드 자체를 자세히 살펴보지는 않는다.

2.  3회에서 생성한 퍼즐 페이지의 코드 숨김 파일을 열고 PuzzlePage.xaml 파일을 오른 클릭한 [코드 보기] 선택한다.

3.  PuzzlePage.xaml.cs에 다음 네임스페이스 선언을 삽입한다.

using System.IO;
using System.Windows.Media.Imaging;
using System.Windows.Resources;

4. PuzzlePage 클래스에서 아래 강조 표시한 멤버 변수 선언을 삽입한다.

 

public partial class PuzzlePage : PhoneApplicationPage
{
  private const double DoubleTapSpeed = 500;
  private const int ImageSize = 435;
  private PuzzleGame game;
  private Canvas[] puzzlePieces;
  private Stream imageStream;

public PuzzlePage()
  {
    InitializeComponent();
  }
}


5. 이제 아래 코드 조각에서 강조 표시한 ImageStream 속성을 추가한다.

 

public partial class PuzzlePage : PhoneApplicationPage
{
  ...
  public Stream ImageStream
  {
    get
    {
      return this.imageStream;
    }

set
    {
      this.imageStream = value;

BitmapImage bitmap = new BitmapImage();
      bitmap.SetSource(value);
      this.PreviewImage.Source = bitmap;

int i = 0;
      int pieceSize = ImageSize / this.game.ColsAndRows;
      for (int ix = 0; ix < this.game.ColsAndRows; ix++)
      {
        for (int iy = 0; iy < this.game.ColsAndRows; iy++)
        {
          Image pieceImage = this.puzzlePieces[i].Children[0] as Image;
          pieceImage.Source = bitmap;
          i++;
        }
      }
    }
  }

public PuzzlePage()
  {
    InitializeComponent();
  }
}


 ImageStream 속성은 퍼즐 이미지용으로 사용하는 스트림을 반환한다. 이것은 배경 이미지와 퍼즐 조각을 새로 고침한다. 해당 속성을 모든 유효한 비트맵을 담고 있는 새로운 Stream으로 설정해 퍼즐 이미지를 대체할 있다. 예를 들면 폰의 카메라에서 이미지를 가져올 있다.

6. 아래 코드 조각에서 강조 표시한 PuzzlePage의 생성자를 업데이트 한다.

 

public partial class PuzzlePage : PhoneApplicationPage
{
  ...
  public PuzzlePage()
  {
    InitializeComponent();

SupportedOrientations = SupportedPageOrientation.Portrait | SupportedPageOrientation.Landscape;

// Puzzle Game
    this.game = new PuzzleGame(3);

this.game.GameStarted += delegate
    {
      this.StatusPanel.Visibility = Visibility.Visible;
      this.TapToContinueTextBlock.Opacity = 0;
      this.TotalMovesTextBlock.Text = this.game.TotalMoves.ToString();
    };

this.game.GameOver += delegate
    {
      this.TapToContinueTextBlock.Opacity = 1;
      this.StatusPanel.Visibility = Visibility.Visible;
      this.TotalMovesTextBlock.Text = this.game.TotalMoves.ToString();
    };

this.game.PieceUpdated += delegate(object sender, PieceUpdatedEventArgs args)
    {
      int pieceSize = ImageSize / this.game.ColsAndRows;
      this.AnimatePiece(this.puzzlePieces[args.PieceId], Canvas.LeftProperty, (int)args.NewPosition.X * pieceSize);
      this.AnimatePiece(this.puzzlePieces[args.PieceId], Canvas.TopProperty, (int)args.NewPosition.Y * pieceSize);
      this.TotalMovesTextBlock.Text = this.game.TotalMoves.ToString();
    };

this.InitBoard();
  }
}


생성자는 PuzzleGame 클래스에 의해 은닉된 게임 로직을 초기화 이벤트에 결합한다. PuzzleGame 클래스는 다음의 이벤트를 정의한다.

GameStarted: 이벤트는 새로운 게임이 시작할 일어난다. 이벤트에 대한 핸들러는 움직인 조각의 수를 패널에 표시하고 게임을 시작하는 방법을 지시하는 범례를 숨긴 움직인 조각의 수를 설정한다.

GameOver: 이벤트는 퍼즐을 풀었을 일어난다. 이벤트에 대한 핸들러는 게임을 시작하는 방법을 지시하는 범례를 표시 움직인 카운트를 업데이트한다.

PieceUpdated: 이벤트는 조각 이동이 일어날 마다 발생한다. 이벤트에 대한 핸들러는 움직인 조각을 애니메이션 이동 카운트를 업데이트한다.

 
마지막으로 이벤트 핸들러를 구독한 , 생성자는 InitBoard  메서드를 호출해 게임 보드를 초기화 한다.


7. 다음 PuzzlePage 클래스에서 InitBoard 메서드를 정의해 게임 보드를 초기화 한다.

 

private void InitBoard()
{
  int totalPieces = this.game.ColsAndRows * this.game.ColsAndRows;
  int pieceSize = ImageSize / this.game.ColsAndRows;

this.puzzlePieces = new Canvas[totalPieces];
  int nx = 0;
  for (int ix = 0; ix < this.game.ColsAndRows; ix++)
  {
    for (int iy = 0; iy < this.game.ColsAndRows; iy++)
    {
      nx = (ix * this.game.ColsAndRows) + iy;
      Image image = new Image();
      image.SetValue(FrameworkElement.NameProperty, "PuzzleImage_" + nx);
      image.Height = ImageSize;
      image.Width = ImageSize;
      image.Stretch = Stretch.UniformToFill;
      RectangleGeometry r = new RectangleGeometry();
      r.Rect = new Rect((ix * pieceSize), (iy * pieceSize), pieceSize, pieceSize);
      image.Clip = r;
      image.SetValue(Canvas.TopProperty, Convert.ToDouble(iy * pieceSize * -1));
      image.SetValue(Canvas.LeftProperty, Convert.ToDouble(ix * pieceSize * -1));

this.puzzlePieces[nx] = new Canvas();
      this.puzzlePieces[nx].SetValue(FrameworkElement.NameProperty, "PuzzlePiece_" + nx);
      this.puzzlePieces[nx].Width = pieceSize;
      this.puzzlePieces[nx].Height = pieceSize;
      this.puzzlePieces[nx].Children.Add(image);
      this.puzzlePieces[nx].MouseLeftButtonDown += this.PuzzlePiece_MouseLeftButtonDown;
      if (nx < totalPieces - 1)
      {
        this.GameContainer.Children.Add(this.puzzlePieces[nx]);
      }
    }
  }

// Retrieve image
  StreamResourceInfo imageResource = Application.GetResourceStream(new Uri("WindowsPhonePuzzle;component/Assets/Puzzle.jpg", UriKind.Relative));
  this.ImageStream = imageResource.Stream;

this.game.Reset();
}


InitBoard 메서드는 해당 퍼즐의 조각을 담고 있는 Canvas 컨트롤들을 생성한다. 조각은 퍼즐에 사용된 전체 이미지의 잘라낸 일부분이다.

8.  PuzzlePage 클래스에 AnimatePiece 삽입하기

 

private void AnimatePiece(DependencyObject piece, DependencyProperty dp, double newValue)
{
    Storyboard storyBoard = new Storyboard();
    Storyboard.SetTarget(storyBoard, piece);
    Storyboard.SetTargetProperty(storyBoard, new PropertyPath(dp));
    storyBoard.Children.Add(new DoubleAnimation
    {
        Duration = new Duration(TimeSpan.FromMilliseconds(200)),
        From = Convert.ToInt32(piece.GetValue(dp)),
        To = Convert.ToDouble(newValue),
        EasingFunction = new SineEase()
    });
    storyBoard.Begin();
}


9.  MouseLeftButtonDown 이벤트에 대한 핸들러를 추가한다. 방법은 PuzzlePage 클래스에 다음의 강조 표시한 코드를 삽입한다.

 

private void PuzzlePiece_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
  if (!this.game.IsPlaying)
  {
    this.game.NewGame();
  }
}


이벤트 핸들러는 보드 표면을 클릭해 발생하고 이미 하나가 진행 중이지 않는 새로운 게임을 시작한다.

10. 이제 "Solve" 버튼의 Click 이벤트에 대한 핸들러를 추가한다.

 

private void SolveButton_Click(object sender, RoutedEventArgs e)
{
    this.game.Reset();
    this.game.CheckWinner();
}


Solve 버튼에 대한 이벤트 핸들러는 게임을 재설정시켜 원래 이미지를 표시하고 퍼즐을 있도록 한다. PuzzleGame 클래스에서  PuzzleGame.CheckWinner 메서드를 호출한다. 메서드는 모든 조각이 알맞은 위치에 있는지 확인 축하 메시지를 표시하는 UI 나타내는 GameOver 이벤트를 일으킨다.


11.  [F5] 눌러 애플리케이션을 빌드하고 윈도우 에뮬레이터로 배포한다. 잠깐 기다리면 시작화면이 표시되고 바로 [START] 클릭한다.

[그림 11] 게임 시작하기


12.  에뮬레이터 윈도우에서 이미지를 터치하면 퍼즐이 시작한다. 이미지는 여러 조각으로 나눠지고 랜덤한 방식으로 보드에 배열된다. AnimatePiece 메서드가 적용된 애니메이션 스토리보드의 결과로 부드럽게 조각이 슬라이딩 되지 않고 순간적으로 조각들이 위치를 바꾼다. 메서드는 퍼즐의 왼쪽 위쪽 좌표를 움직여  변형한다.

[그림 12] 게임 시작 보드에서 임의로 배열된 퍼즐 조각


13. 조각을 선택해 드래그를 시도해 보면, 현재는 아무런 효과도 없을 것이다.  부분은 다음 시간에 멀티 터치를 지원하는 코드를 추가해 것이다.

14. 이제 [SOLVE] 버튼을 클릭하고 보드의 조각들이 재배치 되어 원리의 이미지를 표시하는 것을 확인하자.

15. [SHIFT+F5] 눌러 디버깅 세션을 종료한다.