안녕하세요. 너만바라보면 입니다.

저번시간에 이어서 애플리케이션을 만들어 보도록 하겠습니다.

이번 시간은 퍼즐 보드를 구현해 볼 것인데요. 퍼즐 보드 UI와 애플리케이션 로직을 프로그래밍 하고 멀티터치가 가능하도록 멀티터치를 지원할 것입니다.

 2.PNG

17강에서 첨부되었던 Asset 파일을 보시면 PuzzleGame.cs 파일이 있을 것입니다. 이 파일을 본 프로젝트 내에 추가해 주시기 바랍니다.

위의 그림처럼 추가를 시키고 나서 시작해 보겠습니다. 앞에서는 UI를 지정해 주었으므로 이제 애플리케이션 로직을 프로그래밍 해보겠습니다.

using System.IO;

using System.Windows.Media.Imaging;

using System.Windows.Resources;

 

PuzzlePage.xaml.cs 파일을 열어서 이제 로직을 짜볼것입니다. 네임스페이스부터 추가해 주시고!

 

 

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();

}

}

 

그 다음에 멤버변수를 선언해 줍니다. 이미지 사이즈가 다를 경우에는 이를 수정해 주셔야 합니다.

 

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++;

                    }

                }

            }

        }

 

그 다음에 해야할 일은 바로 ImageStream 부분을 추가해 주어야 합니다. 이 속성은 퍼즐 이미지에 사용될 스트림 값을 리턴하는데요. 배경이미지와 각각의 퍼즐 조각이 자동적으로 새로고침이 됩니다. 우리가 지정한 이미지 말고도 카메라로 촬영하여 그 이미지를 포함한 스트림으로 속성을 설정할 수 있죠. 현재 실습에서는 카메라로 촬영하는 것 까지는 구현하지 않고 현재 프로젝트에 속한 리소스의 이미지를 활용하겠습니다. 카메라로 촬영하여 스트림값을 받아오는건 저번시간에 했었습니다. 여러분들이 저번 강의를 잘 활용하신다면 지정된 이미지 말고도 이미지를 찍어서 스트림으로 속성을 설정하여 퍼즐게임을 할 수 있게 구현할 수도 있으실 것입니다.

public PuzzlePage()

        {

            InitializeComponent();

 

 

SupportedOrientations = SupportedPageOrientation.Portrait | SupportedPageOrientation.Landscape;

 

           

            this.game = new PuzzleGame(3);

 

            this.game.GameStarted += delegate

            {

                this.ResetWinTransition.Begin();

                this.StatusPanel.Visibility = Visibility.Visible;

                this.TapToContinueTextBlock.Opacity = 0;

                this.TotalMovesTextBlock.Text = this.game.TotalMoves.ToString();

            };

 

            this.game.GameOver += delegate

            {

                this.WinTransition.Begin();

                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();

        }

 

위를 참고하여 코드를 작성합니다. PuzzlePage 클래스에 대한 생성자를 업데이트 하는 것인데요. 이 생성자가 하는 역할은 바로 게임로직을 인스턴스화 하고 해당 이벤트를 바인딩 합니다.

우선 게임을 하기 위해서는 시작을 해야 겠죠. 시작이벤트가 있어야 합니다. GameStared 이벤트는 새 게임이 시작될 때 발새오디는 이벤트인데 이 이벤트 핸들러는 이동된 조각수를 포함한 패널을 표시하고, 게임 시작 방법을 설명하는 페이지를 숨기며 이동된 조각수를 재설정 하는 역할을 합니다.

GameOver 이벤트는 퍼즐이 풀리고 게임이 끝났을 때 발생하는데 게임이 끝났으니 안내 페이지로 다시 돌아가야겠죠? 그리고 이동수를 업데이트 합니다.

PieceUpdated 이벤트는 조각이 이동될때마다 발생하는 이벤트 입니다. 게임의 시작과 끝 이벤트만 있다면 퍼즐을 옮길때는 아무런 이벤트가 발생하지 않겠죠? 그렇기 때문에 퍼즐 조각이 이동이동될 때 발생되는 이벤트 입니다. 이 이벤트는 이동된 조각을 애니메이션화하고 이동수를 업데이트 합니다.

이렇게 게임이 다 끝나고 나면 초기화를 시키는데 바로 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]);

                    }

                }

            }

 

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

            this.ImageStream = imageResource.Stream;

 

            this.game.Reset();

        }

 

위와 같이 코드를 작성하면서 어떻게 초기화를 시키는지 알아봅시다.

InitBoard 메서드는 각 조각에 하나씩 퍼즐 조각을 포함하는 Canvas 컨트롤을 만듭니다. 또한 애플리케이션에 있는 리소스에서 퍼즐이미지를 가져옵니다. URI를 통해 말이죠. 이렇게 가져온 결과를 포함한 ImageStream reset 합니다.

private void PuzzlePiece_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)

        {

            if (!this.game.IsPlaying)

            {

                this.game.NewGame();

            }

        }

 

 

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();

        }

 

 

AnimatePiece 메서드는 퍼즐조각을 애니메이션화하기 위한 핼퍼메서드 입니다. 이 메서드에서는 코드를 사용하여 스토리 보드를 만들고 컨트롤의 DependencyProperty를 업데이트 하는 목적으로 이용할 수 있는데요. 이 메서드는 핼퍼메서드 즉 도움 역할만 하는 메서드입니다.

MouseLeftButtonDown 이벤트 핸들로는 보드화면을 클릭할 때 작동하게 됩니다. 이미 진행중인 게임이 없을경우 새 게임을 실행하도록 하죠.

private void SolveButton_Click(object sender, RoutedEventArgs e)

        {

            this.game.Reset();

            this.game.CheckWinner();

        }

 

풀기! 버튼을 누르면 게임을 강제로 재설정하여 원래 이미지를 표시하고 퍼즐을 풀게 됩니다. 그 다음 PuzzleGame 클래스에서 CheckWinner 를 호출합니다. 이 역할은 바로 이사람이 퍼즐을 잘 맞췄는지 체크하고 완성되었다는 메시지를 뜨게 합니다.

이제 프로그램 로직도 구성하였으므로 이제 멀티터치가 가능하도록 구현해 봅시다.

public partial class PuzzlePage : PhoneApplicationPage

    {

        private const double DoubleTapSpeed = 500;

        private const int ImageSize = 435;

        private PuzzleGame game;

        private Canvas[] puzzlePieces;

        private Stream imageStream;

 

        private long lastTapTicks;

        private int movingPieceId = -1;

        private int movingPieceDirection;

        private double movingPieceStartingPosition;

 

 

위와 같이 기존 멤버변수 밑에 새로운 멤버변수를 추가해 줍니다.

private void PhoneApplicationPage_ManipulationStarted(object sender, ManipulationStartedEventArgs e)

        {

            if (this.game.IsPlaying && e.ManipulationContainer is Image && e.ManipulationContainer.GetValue(FrameworkElement.NameProperty).ToString().StartsWith("PuzzleImage_"))

            {

                int pieceIx = Convert.ToInt32(e.ManipulationContainer.GetValue(FrameworkElement.NameProperty).ToString().Substring(12));

                Canvas piece = this.FindName("PuzzlePiece_" + pieceIx) as Canvas;

                if (piece != null)

                {

                    int totalPieces = this.game.ColsAndRows * this.game.ColsAndRows;

                    for (int i = 0; i < totalPieces; i++)

                    {

                        if (piece == this.puzzlePieces[i] && this.game.CanMovePiece(i) > 0)

                        {

                            int direction = this.game.CanMovePiece(i);

                            DependencyProperty axisProperty = (direction % 2 == 0) ? Canvas.LeftProperty : Canvas.TopProperty;

                            this.movingPieceDirection = direction;

                            this.movingPieceStartingPosition = Convert.ToDouble(piece.GetValue(axisProperty));

                            this.movingPieceId = i;

                            break;

                        }

                    }

                }

            }

        }

 

위의 코드를 추가해 줍니다. MainpulationStared 이벤트는 사용자가 조작을 위해 UIElement를 터치할때 발생하며 이는 MouseDown 이벤트와 유사합니다. 이 이벤트 핸들러는 현재 게임이 진행중인지 확인하고 조각에 대한 해당 Canvas 요소를 찾은 뒤 게임로직을 호출하여 조각을 이동시킬 수 있는지 여부를 결정합니다. 조각을 이동하려면 조각 주변에 빈 칸이 있어야 하겠죠? 그래서 이를 여기서 다음에 선택되는 조각을 저장 후 이동가능한 위치의 방향과 축을 기록합니다.

 

private void PhoneApplicationPage_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)

        {

            if (this.movingPieceId > -1)

            {

                int pieceSize = ImageSize / this.game.ColsAndRows;

                Canvas movingPiece = this.puzzlePieces[this.movingPieceId];

 

               

                DependencyProperty axisProperty;

                double normalizedValue;

 

                if (this.movingPieceDirection % 2 == 0)

                {

                    axisProperty = Canvas.LeftProperty;

                    normalizedValue = e.CumulativeManipulation.Translation.X;

                }

                else

                {

                    axisProperty = Canvas.TopProperty;

                    normalizedValue = e.CumulativeManipulation.Translation.Y;

                }

 

               

               

                if (this.movingPieceDirection == 1 || this.movingPieceDirection == 4)

                {

                    if (normalizedValue < -pieceSize)

                    {

                        normalizedValue = -pieceSize;

                    }

                    else if (normalizedValue > 0)

                    {

                        normalizedValue = 0;

                    }

                }

               

                else if (this.movingPieceDirection == 3 || this.movingPieceDirection == 2)

                {

                    if (normalizedValue > pieceSize)

                    {

                        normalizedValue = pieceSize;

                    }

                    else if (normalizedValue < 0)

                    {

                        normalizedValue = 0;

                    }

                }

 

               

                movingPiece.SetValue(axisProperty, normalizedValue + this.movingPieceStartingPosition);

            }

        }

 

 

위의 화면에서와 같이 ManipulationDelta 이벤트에 대한 Handler를 만듭니다. 이 이벤트 핸들러의 경우 현재 이동중인 조각이 있는지 확인하고 만약 이동중인 조각이 있다면 가능한 방향과 축에서만 델타값을 캐치합니다.

private void PhoneApplicationPage_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)

        {

            if (this.movingPieceId > -1)

            {

                int pieceSize = ImageSize / this.game.ColsAndRows;

                Canvas piece = this.puzzlePieces[this.movingPieceId];

 

               

                if (TimeSpan.FromTicks(DateTime.Now.Ticks - this.lastTapTicks).TotalMilliseconds < DoubleTapSpeed)

                {

                   

                    this.game.MovePiece(this.movingPieceId);

                    this.lastTapTicks = int.MinValue;

                }

                else

                {

                   

                    DependencyProperty axisProperty = (this.movingPieceDirection % 2 == 0) ? Canvas.LeftProperty : Canvas.TopProperty;

                    double minRequiredDisplacement = pieceSize / 3;

                    double diff = Math.Abs(Convert.ToDouble(piece.GetValue(axisProperty)) - this.movingPieceStartingPosition);

 

                   

                    if (diff > minRequiredDisplacement)

                    {

                       

                        this.game.MovePiece(this.movingPieceId);

                    }

                    else

                    {

                       

                        this.AnimatePiece(piece, axisProperty,

this.movingPieceStartingPosition);

                    }

                }

 

                this.movingPieceId = -1;

                this.movingPieceStartingPosition = 0;

                this.movingPieceDirection = 0;

                this.lastTapTicks = DateTime.Now.Ticks;

            }

        }

    }

}

 

위의 ManipulationCompleted 이벤트는 퍼즐게임에서 사용자가 화면에서 손가락을 들어올리는 경우에 발생합니다. 이 이벤트 핸들러는 현재 이동중인 퍼즐조각이 있는지 확인하고 마지막 anipulationCompleted가 실행된 시간을 확인하여 시간 간격이 DoulbeTapSpeed 로 지정된 값보다 적은 경우 이 이벤트를 Double Tap 이벤트로 해석하고 조각을 인접한 빈 칸으로 이동하게 됩니다.

 

이번시간에는 퍼즐 게임의 UI를 만들어 보았으며 게임 로직을 구성해 보았고 멀티터치가 가능하도록 구현해 보았습니다. 조금 내용이 많고 어려웠을 것이라 생각됩니다. 이제 다음 강의에서는 마지막으로 애니메이션 효과를 만들어 보고 Isolate Storage를 이용하여 게임상태를 유지하는 것에 대해서 구현해 보도록 하겠습니다.

 

감사합니다. 다음시간에 뵙겠습니다.





안녕하세요.윈도우폰에 관심이 많은 너만바라보면 입니다.