Reversi 샘플에서 Windows 스토어 앱 기능을 사용하는 방법 알아보기

Applies to Windows only

Reversi 샘플은 XAML 및 C#으로 작성된 Windows 스토어 앱의 여러 가지 일반적인 기능을 사용합니다. 이 항목에서는 샘플이 이러한 일부 기능을 사용하는 방법에 대해 설명하고 핵심 기능 항목에 대한 링크를 제공합니다.

이 항목을 학습하기 위해 전체 샘플을 이해할 필요는 없지만, 이미 XAML과 C#을 잘 알고 각 기능의 기본 개념을 이해하고 있거나, 기꺼이 연관 항목을 읽어볼 의향이 있어야 합니다. 앱 개발의 기본에 대한 정보는 C# 또는 Visual Basic을 사용하여 첫 Windows 스토어 앱 만들기를 참조하세요.

샘플에 대한 전반적인 소개를 보려면 XAML, C# 및 C++로 작성한 Windows 스토어 게임 Reversi를 참조하세요. 여러 기능이 어떻게 전체적으로 함께 작동하는지 이해하려면 Reversi 앱 구조 이해를 참조하세요. 원래 C# 게임 엔진을 C++로 이식하는 방법을 알아보려면 Reversi C++ 게임 엔진에 대해 알아보기를 참조하세요.

Reversi 샘플 앱을 다운로드하거나소스 코드를 찾아보세요.

타일 및 시작 화면

앱 타일 및 시작 화면은 앱의 사용자가 보게 되는 첫 번째 요소입니다. 앱 타일과 시작 화면을 사용하여 강렬한 첫인상을 주고 브랜드를 표시할 수 있습니다. 기본 규칙은 평범하지만 설명서에 나와 있는 대로 보다 복잡한 게임을 만들 수 있습니다.

주요 리소스:

Reversi는 기본 타일과 시작 화면 지원만 제공합니다. 여기에는 정사각형 및 와이드 타일과 시작 화면이 포함되며, 여기서는 축소하여 표시했습니다.

Reversi 타일 및 시작 화면

이미지 파일 이름은 Package.appxmanifest 파일에 설정됩니다. 여러 화면 크기를 지원하기 위해 여러 배율로 이미지를 제공할 수 있습니다. 이렇게 간단하게 사용할 때는 다른 구현이 필요하지 않습니다.

앱 바

앱 바는 앱 명령을 입력할 표준 위치를 제공합니다. 기본적으로 사용자의 필요에 맞게 앱 바를 표시하거나 숨길 수 있어 자주 사용하지 않는 명령을 보관해 두기에 편리합니다. 따라서 주 UI가 콘텐츠와의 직접 상호 작용에 더 집중할 수 있습니다.

주요 리소스:

Reversi에는 앱 바에 사용하기 적합한 몇 가지 보조 명령이 있습니다. 즉, 시계를 일시 중지하고, 이동을 취소하거나 다시 실행하는 기능입니다. 정상적인 게임 플레이 중에는 앱 바가 숨겨지지만 화면 위쪽이나 아래쪽에서 살짝 밀면 앱 바를 표시하거나 숨길 수 있습니다.

Reversi 앱 바

GamePage.xaml의 다음 코드는 앱 바 정의를 보여 줍니다. 배경과 테두리는 투명하지만 보이지 않는 앱 바가 탭 및 클릭을 방해하지 않도록 Background 속성이 {x:Null}로 설정됩니다. 이러한 설정이 필요한 이유는 앱 바가 전체 화면에 걸쳐 확장되고 게임판의 아래쪽 행 위에 겹쳐지기 때문입니다.


<Page.BottomAppBar>
  <CommandBar x:Name="GamePageAppBar" Background="{x:Null}" 
    BorderBrush="Transparent" IsSticky="True" Margin="9,0">
    <CommandBar.SecondaryCommands>
      <AppBarButton Icon="Pause" Label="Pause"
        Command="{Binding Clock.PauseCommand}" Click="DismissAppBar"
        Visibility="{Binding Clock.IsPauseButtonVisible, 
          Converter={StaticResource BooleanToVisibilityConverter}}"/>
      <AppBarButton Icon="Play" Label="Play"
        Command="{Binding Clock.PlayCommand}" Click="DismissAppBar"          
        Visibility="{Binding Clock.IsPauseButtonVisible,
          Converter={StaticResource BooleanToVisibilityConverter}, 
          ConverterParameter=Reverse}"/>
      <AppBarButton Icon="Undo" Label="Undo" Command="{Binding UndoCommand}"/>
      <AppBarButton Icon="Redo" Label="Redo" Command="{Binding RedoCommand}"/>
    </CommandBar.SecondaryCommands>
  </CommandBar>
</Page.BottomAppBar>


Reversi는 CommandBarAppBarButton 컨트롤을 사용하여 기본 동작과 스타일을 가져옵니다. 명령 섹션에 나와 있는 대로 단추 동작 및 단추의 활성화 상태는 단추 Command 속성에 바인딩된 보기 모델 명령에 의해 제공됩니다.

재생일시 중지 단추는 단일 토글 단추처럼 작동합니다. 이 효과를 얻기 위해 단추의 Visibility 속성은 같은 보기 모델 속성에 바인딩됩니다. 두 바인딩은 모두BooleanToVisibilityConverter를 사용하는데, 그중 하나에는 바인딩 효과를 되돌리는 ConverterParameter 속성이 설정됩니다. 이러한 방식으로 각 단추는 나머지 단추가 보이지 않을 때만 표시됩니다. 자세한 내용은 데이터 바인딩 섹션을 참조하세요.

Toast 알림

알림 메시지는 중요한 이벤트가 발생하면 사용자에게 이를 알립니다. 이는 다른 앱이 현재 활성 상태인 경우에도 예외가 아닙니다.

주요 리소스:

Reversi에서는 컴퓨터가 이동을 실행하기까지 약간의 시간이 걸릴 수 있습니다. 기다리는 동안 다른 앱으로 전환할 경우 사용자의 차례가 되면 알림 메시지가 알려 줍니다.

Reversi 알림 메시지

Reversi는 알림 메시지에 필요한 최소한의 코드만 사용하며, Package.appxmanifest 디자이너에서 알림 가능 필드를 로 설정합니다. 알림 코드는 쉽게 재사용이 가능하므로 Common 폴더의 도우미 클래스에 있습니다.

GameViewModel.cs의 경우:


var window = Windows.UI.Xaml.Window.Current;
if (window != null && !window.Visible && !IsCurrentPlayerAi)
{
    Toast.Show("It's your turn!");
}


Toast.cs의 경우:


public static void Show(string text)
{
    const string template = 
        "<toast duration='short'><visual><binding template='ToastText01'>" +
        "<text id='1'>{0}</text></binding></visual></toast>";
    var toastXml = new XmlDocument();
    toastXml.LoadXml(String.Format(template, text));
    var toast = new ToastNotification(toastXml);
    ToastNotificationManager.CreateToastNotifier().Show(toast);
}


설정 플라이아웃

설정 참 메뉴는 앱 설정에 대한 표준화된 액세스를 제공합니다.

주요 리소스:

Reversi에는 두 개의 설정 플라이아웃이 있습니다. 하나는 표시 옵션에 대한 것이고, 다른 하나는 새 게임 옵션에 대한 것입니다.

Reversi 설정 플라이아웃

다음 App.xaml.cs의 코드는 Reversi가 SettingsPane.CommandsRequested 이벤트를 처리하여 SettingsCommand 개체를 만드는 방법을 보여 줍니다. 활성화된 경우 각 명령은 SettingsFlyout 컨트롤을 만들고 표시합니다.


SettingsPane.GetForCurrentView().CommandsRequested += OnCommandsRequested;



private void OnCommandsRequested(SettingsPane sender,
    SettingsPaneCommandsRequestedEventArgs args)
{
    args.Request.ApplicationCommands.Add(new SettingsCommand("Display", "Display options", 
        _ => (new DisplaySettings() { DataContext = SettingsViewModel }).Show()));
    args.Request.ApplicationCommands.Add(new SettingsCommand("NewGame", "New game options", 
        _ => (new NewGameSettings() { DataContext = SettingsViewModel }).Show()));
}


콘텐츠 공유

공유 계약을 사용하면 앱이 데이터를 공유할 수 있으며, 사용자가 다른 앱으로 데이터를 보낼 수 있습니다. 예를 들어 사용자는 앱의 데이터를 메일 앱에 공유하여 새 메시지를 만들 수 있습니다.

주요 리소스:

Windows는 앱의 이미지 공유를 기본적으로 지원하며 Reversi에 추가 기능이 필요하지 않습니다.

데이터 바인딩

데이터 바인딩을 통해 UI 컨트롤이 표시하는 데이터에 UI 컨트롤을 연결하여 하나가 변경되면 나머지가 업데이트되도록 할 수 있습니다. 데이터 바인딩은 데이터 입력 형태에 일반적으로 사용되지만, 전체 UI를 구동하여 앱 논리에서 UI가 분리되게 하는 데도 사용할 수 있습니다.

주요 리소스:

Reversi는 데이터 바인딩을 사용하여 UI(또는 "보기" 계층)를 앱 논리(또는 "보기 모델" 계층)에 연결합니다. 이러한 계층화는 UI를 다른 코드와 분리하는 데 도움이 되며, MVVM(Model-View-ViewModel) 패턴이라고 합니다. Reversi에서 이 패턴을 사용하는 방법은 Reversi 앱 구조를 참조하세요. MVVM에 대한 간단한 소개는 Model-View-ViewModel 패턴 사용을 참조하세요.

Reversi의 바인딩 대부분은 코드 숨김이 몇몇 경우(예: Board.xaml.cs 파일)에 사용되기는 하지만 바인딩 태그 확장을 사용하여 XAML로 정의됩니다. 각 페이지는 페이지의 모든 요소가 바인딩에 대해 데이터 원본으로 사용하는 DataContext 속성을 설정합니다.

UI 업데이트

Reversi에서 데이터 바인딩은 UI를 구동합니다. UI 상호 작용은 데이터 원본 속성을 변경하며, 데이터 바인딩은 UI를 업데이트하여 이러한 변경에 응답합니다.

이러한 업데이트는 Reversi 보기 모델 클래스가 BindableBase 클래스를 상속하기 때문에 작동합니다. 이 클래스는 Common/BindableBase.cs 파일에 있으며, 표준 INotifyPropertyChanged 구현과 몇 개의 지원 메서드를 제공합니다. SetProperty 메서드는 단일 메서드 호출을 사용하여 속성의 백업 값과 바인딩된 UI를 업데이트합니다. OnPropertyChanged 메서드는 지정된 속성에 바인딩되는 UI를 업데이트합니다. 이 메서드는 업데이트 시기 제어에, 그리고 다른 속성에서 값을 가져오는 속성에 유용합니다.

다음 GameViewModel.cs의 코드는 SetPropertyOnPropertyChanged의 기본 사용을 보여 줍니다.


public State CurrentPlayer
{
    get { return _currentPlayer; }
    set
    {
        SetProperty(ref _currentPlayer, value);
        OnPropertyChanged("IsCurrentPlayerAi");
        OnPropertyChanged("IsPlayerOneAi");
        OnPropertyChanged("IsPlayerTwoAi");
        OnPropertyChanged("CurrentPlayerAiSearchDepth");
    }
}


값 변환

다른 속성에서 값을 가져오는 계산된 속성을 만들어 바인딩에 더 적합한 형태로 속성 값을 변환할 수 있습니다.

다음 GameViewModel.cs의 코드는 간단한 계산된 속성입니다. 이 속성에 바인딩된 UI는 이전 예제로부터의 OnPropertyChanged 호출에 일치시켜 업데이트됩니다.


public bool IsPlayerOneAi { get { return (int)PlayerOne > 0; } }


계산된 속성은 필요한 어떤 종류의 변환에 대해서도 만들기가 쉽지만 코드를 어수선하게 만들 수 있습니다. 따라서 일반적인 변환의 경우 변환 코드를 재사용 가능한 IValueConverter 구현에 넣는 것이 더 좋습니다. Reversi는 다양한 UI 요소를 표시하고 숨기는 바인딩에 대해 Common/Converters 폴더의 NullStateToVisibilityConverterBooleanToVisibilityConverter 클래스를 사용합니다.

다음 StartPage.xaml의 바인딩은 속성에 값이 있는지 여부에 따라 패널을 표시하거나 숨깁니다.


<StackPanel Visibility="{Binding GameViewModel, 
  Converter={StaticResource NullStateToVisibilityConverter}}">


다음 NewGameSettings.xaml의 바인딩은 ToggleSwitch 컨트롤의 상태에 따라 패널을 표시하거나 숨깁니다.


<StackPanel Orientation="Horizontal" 
  Visibility="{Binding IsOn, ElementName=PlayerOneSwitch, 
    Converter={StaticResource BooleanToVisibilityConverter}}">


관련 예제는 앱 바를 참조하세요.

명령

Button 동작은 가끔 코드 숨김 파일에서 Click 이벤트 처리기를 사용하여 구현됩니다. Reversi에서는 이러한 구현을 탐색 단추에만 사용하고 다른 단추에는 사용하지 않으며, 단추가 호출하는 UI가 아닌 코드에서 단추 UI를 분리합니다. 이렇게 하기 위해 Button.Command 속성은 ICommand 구현을 반환하는 보기 모델 속성에 바인딩됩니다.

Reversi 명령 속성의 형식은 DelegateCommand 또는 DelegateCommand<T>입니다. 이러한 클래스는 Common/DelegateCommand.cs 파일에 있으며, 재사용 가능한 표준 ICommand 구현을 제공합니다. 이러한 클래스를 사용하여 일회성 명령의 생성 과정을 간소화하고 필요한 코드를 단일 속성 구현에 국한시킬 수 있습니다.

다음 GameViewModel.cs의 코드는 사용자 지정 단추인 보드 공간에서 사용되는 이동 명령을 보여 줍니다. ?? 또는 "null 색인" 연산자는 필드 값이 null이 아닌 경우에만 반환됨을 나타냅니다. 그렇지 않으면 필드가 설정되고 새 값이 반환됩니다. 이는 단일 명령 개체가 만들어지고 처음 속성이 액세스되면 차후 모든 액세스에 대해 동일한 개체가 다시 사용된다는 의미입니다. 명령 개체는 MoveAsyncCanMove 메서드를 참조해서 DelegateCommand<ISpace>.FromAsyncHandler 메서드를 호출하여 초기화합니다. 이러한 메서드는 ICommand.ExecuteCanExecute 메서드 구현을 제공합니다.


public DelegateCommand<ISpace> MoveCommand 
{ 
    get 
    { 
        return _moveCommand ?? (_moveCommand = 
            DelegateCommand<ISpace>.FromAsyncHandler(MoveAsync, CanMove));
    } 
}


CanExecute 메서드는 단추의 활성 상태를 업데이트하기 위해 데이터 바인딩에 의해 호출됩니다. 그러나 명령 바인딩은 다른 바인딩의 변경 알림과 유사한 변경 알림을 사용합니다(UI 업데이트에서 설명). 다음 GameViewModel.cs의 코드는 UpdateView 메서드가 보기 모델 상태와 모델 상태를 동기화하고, 다음 이동을 계속하기 전에 각 명령에 대해 OnCanExecuteChanged를 호출하는 방법을 보여 줍니다.


private void UpdateView()
{
    SyncModelProperties();
    UpdateBoard();
    UndoCommand.RaiseCanExecuteChanged();
    RedoCommand.RaiseCanExecuteChanged();
    MoveCommand.RaiseCanExecuteChanged();
}


사용자 지정 종속성 속성

Reversi는 데이터 바인딩 업데이트를 사용하여 시각적 상태를 변경할 수 있도록 사용자 지정 컨트롤에 사용자 지정 종속성 속성을 사용합니다. 시각적 상태 및 애니메이션 전환은 VisualStateManager 클래스를 사용하여 XAML로 정의됩니다. 그러나 시각적 상태를 보기 모델 속성에 직접 바인딩하는 방법은 없습니다. 사용자 지정 종속성 속성은 보기 모델 속성에 바인딩할 대상을 제공합니다. 종속성 속성에는 필요한 VisualStateManager.GoToState 메서드를 호출하는 속성 변경 콜백이 포함됩니다.

다음 코드는 PlayerStatus 컨트롤이 코드 숨김을 사용하여 사용자 지정 종속성 속성을 보기 모델 속성에 바인딩하는 방법을 보여 줍니다. 속성 변경 콜백 메서드를 비롯하여 종속성 속성 중 하나만 여기에 표시됩니다. 콜백 및 OnApplyTemplate 메서드는 호출과 업데이트 메서드를 모두 다시 정의합니다. 그러나 OnApplyTemplate 호출은 첫 화면 출현에 대해 컨트롤을 초기화하므로 애니메이션 전환을 사용하지 않습니다.


public PlayerStatus()
{
    DefaultStyleKey = typeof(PlayerStatus);
    SetBinding(CurrentPlayerProperty, new Binding { 
        Path = new PropertyPath("CurrentPlayer") });
    SetBinding(IsClockShowingProperty, new Binding { 
        Path = new PropertyPath("Settings.IsClockShowing") });
    SetBinding(IsGameOverProperty, new Binding { 
        Path = new PropertyPath("IsGameOver") });
    SetBinding(WinnerProperty, new Binding { 
        Path = new PropertyPath("Winner") });
}

protected override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    UpdatePlayerState(false);
    UpdateClockState(false);
    UpdateGameOverState(false);
}

public bool IsClockShowing
{
    get { return (bool)GetValue(IsClockShowingProperty); }
    set { SetValue(IsClockShowingProperty, value); }
}

public static readonly DependencyProperty IsClockShowingProperty =
    DependencyProperty.Register("IsClockShowing", typeof(bool),
    typeof(PlayerStatus), new PropertyMetadata(true, IsClockShowingChanged));

private static void IsClockShowingChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
{
    (d as PlayerStatus).UpdateClockState(true);
}

private void UpdateClockState(bool useTransitions)
{
    GoToState(IsClockShowing ? "ClockShowing" : "ClockHidden", useTransitions);
}

private void GoToState(string state, bool useTransitions)
{
    VisualStateManager.GoToState(this, state, useTransitions);
}


비동기 코드

비동기 코드는 앱에서 오랜 시간이 걸리는 작업을 수행하는 동안 UI를 응답 가능 상태로 유지하는 데 도움이 됩니다.

주요 리소스:

Reversi는 비동기 코드를 사용하여 게임에서 이동을 수행합니다. 이동 애니메이션을 포함하여 각 이동을 완료하는 데 1초 이상 걸리며 AI 이동은 더 오랜 시간이 걸릴 수 있습니다. 그러나 UI는 항상 응답 가능 상태를 유지하며, 사용자 명령(예: 실행 취소)을 통해 진행 중인 이동이 취소됩니다.

GameViewModel.cs에서 가져온 이 코드는 Reversi에서 asyncawait 키워드, Task 클래스 및 취소 토큰을 사용하는 방법을 보여 줍니다. AsTask를 사용하여 Game 클래스의 Windows 런타임 비동기 코드와 통합됩니다. 자세한 내용은 다음 섹션을 참조하세요.


private async Task MoveAsync(ISpace move)
{
    var cancellationToken = GetNewCancellationToken();
    LastMoveAffectedSpaces = await Game.MoveAsync(move).AsTask(cancellationToken);
    if (cancellationToken.IsCancellationRequested) return;
    await OnMoveCompletedAsync(cancellationToken);
}



private async Task AiMoveAsync()
{
    var cancellationToken = GetNewCancellationToken();

    // Unlike the MoveAsync method, the AiMoveAsync method requires a try/catch 
    // block for cancellation. This is because the AI search checks for 
    // cancellation deep within a recursive, iterative search process
    // that is easiest to halt by throwing an exception. 
    try
    {
        // The WhenAll method call enables the delay and the AI search to 
        // occur concurrently. However, in order to retrieve the return 
        // value of the first task, both tasks must have the same signature,
        // thus requiring the delay task to have a (meaningless) return value.  
        var results = await Task.WhenAll(
            Game.GetBestMoveAsync(CurrentPlayerAiSearchDepth)
                .AsTask(cancellationToken),
            Task.Run(async () =>
            {
                await DelayAsync(MinimumTurnLength, cancellationToken);
                return (ISpace)null;
            })
        );

        // Perform the AI move only after both the 
        // search and the minimum delay have passed.
        LastMoveAffectedSpaces = await Game.MoveAsync(
            results[0]).AsTask(cancellationToken);
        if (cancellationToken.IsCancellationRequested) return;

        await OnMoveCompletedAsync(cancellationToken);
    }
    catch (OperationCanceledException)
    {
        System.Diagnostics.Debug.WriteLine("cancelled with exception");
    }
}


Windows 런타임 구성 요소 사용

Windows 런타임 구성 요소로 코드의 일부를 구현하면 해당 코드를 여러 앱, 여러 언어, 여러 플랫폼 또는 다른 언어로 다시 사용할 수 있습니다. 보다 쉽게 구성 요소를 다른 언어의 대체 구현으로 바꿀 수도 있습니다.

주요 리소스:

Reversi는 Windows 런타임 구성 요소로 해당 핵심 게임 논리를 구현하여 완전히 앱에서 이 논리를 분리합니다. 이렇게 하면 나중에 편리하게 확장하고 코드를 다시 사용할 수 있습니다. Reversi에는 또한 게임 엔진의 C++ 버전이 원래 C# 버전의 고성능 대체물로서 포함되어 있습니다. 자세한 내용은 Learn about the Reversi C++ 게임 엔진에 대해 알아보기를 참조하세요.

Game.cs에서 가져온 이 코드는 Reversi에서 Task 기반 비동기 코드(asyncawait 키워드 포함)를 사용하지만 Windows 런타임 비동기 인터페이스를 통해 결과를 표시하는 방법을 보여 줍니다. 또한 GameViewModel코드의 취소 토큰을 Game 클래스에서 이용하는 방법을 보여 줍니다.

이 예제 코드의 첫 번째 및 세 번째 메서드는 AsyncInfo.Run 메서드를 호출하여 IAsyncOperation<T>를 반환합니다. 이 경우 작업의 반환 값이 래핑되고 취소가 가능합니다. 두 번째 예제에서는 WindowsRuntimeSystemExtensions.AsAsyncAction 메서드를 호출하여 IAsyncAction을 반환합니다. 이 방법은 반환 값이 없으며 취소할 필요가 없는 작업에 유용합니다.


public IAsyncOperation<IList<ISpace>> MoveAsync(ISpace move)
{
    // Use a lock to prevent the ResetAsync method from modifying the game 
    // state at the same time that a different thread is in this method.
    lock (_lockObject)
    {
        return AsyncInfo.Run(cancellationToken => Task.Run(() =>
        {
            if (cancellationToken.IsCancellationRequested) return null;
            var changedSpaces = Move(move);
            SyncMoveStack(move);
            return changedSpaces;
        }, cancellationToken));
    }
}



public IAsyncAction AiMoveAsync(int searchDepth)
{
    return Task.Run(async () => 
    {
        // If it is the AI's turn and we're not at the end of the move stack,
        // just use the next move in the stack. This is necessary to preserve
        // the forward stack, but it also prevents the AI from having to search again. 
        var bestMove = Moves.Count < MoveStack.Count ? 
            MoveStack[Moves.Count] : await GetBestMoveAsync(searchDepth);
        await MoveAsync(bestMove);
    }).AsAsyncAction();
}

public IAsyncOperation<ISpace> GetBestMoveAsync(int searchDepth)
{
    if (searchDepth < 1) throw new ArgumentException(
        "must be 1 or greater.", "searchDepth");

    return AsyncInfo.Run(cancellationToken => Task.Run(() => 
    {
        return (ISpace)reversiAI.GetBestMove(Board, 
            CurrentPlayer == State.One, searchDepth, cancellationToken);
    }, cancellationToken));
}


관련 항목

Reversi 샘플 앱
XAML, C# 및 C++로 작성한 Windows 스토어 게임 Reversi
MVVM(Model-View-ViewModel) 패턴 사용
Reversi 샘플에서 Windows 스토어 앱 기능을 사용하는 방법 알아보기
Reversi 앱 구조 이해
Reversi C++ 게임 엔진에 대해 알아보기
C# 또는 Visual Basic을 사용하여 첫 Windows 스토어 앱 만들기
C# 또는 Visual Basic으로 작성한 Windows 런타임 앱용 로드맵
데이터 바인딩

 

 

표시:
© 2014 Microsoft