"リバーシ" での Windows ストア アプリ機能の使用

Applies to Windows only

リバーシのサンプルでは、XAML と C# を使った Windows ストア アプリの共通の機能を使います。ここでは、これらの機能をリバーシのサンプルで使う方法について説明し、主要機能のトピックへのリンクを示します。

ここでは、サンプル全体を理解する必要はありません。ただし、前提として、既に XAML と C#、そして各機能の基本を理解している、またはリンク先のトピックを読んで学ぶ用意があるものとします。アプリ開発の基本事項について詳しくは、「C# または Visual Basic を使った初めての Windows ストア アプリの作成」をご覧ください。

このサンプルの概要については、「Windows ストア ゲーム "リバーシ" (XAML、C#、C++)」をご覧ください。さまざまな機能の連携方法について詳しくは、「リバーシ アプリの構造について」をご覧ください。元の C# ゲーム エンジンを C++ に移植する方法については、「リバーシ C++ ゲーム エンジンについて」をご覧ください。

リバーシのサンプル アプリをダウンロードするか、またはソース コードを参照してください。

タイルとスプラッシュ画面

アプリのタイルとスプラッシュ画面は、アプリのユーザーが最初に目にする部分です。タイルとスプラッシュ画面を使って、魅力的なエントリ ポイントの提示やブランドの表示が可能です。基本はいたって簡単です。また、説明書に示されているように、複雑なことも実行できます。

主要リソース:

リバーシは、基本的なタイルとスプラッシュ画面のサポートのみ提供します。これには正方形タイルおよびワイド タイルとスプラッシュ画面が含まれます。次に縮小サイズを示します。

リバーシのタイルとスプラッシュ画面

イメージ ファイル名は Package.appxmanifest ファイルに設定されます。画像を複数のスケールで指定して、複数の画面サイズをサポートできます。この使用は簡単であり、他の実装は必要ありません。

アプリ バー

アプリ バーは、アプリ コマンドを配置する標準の場所を提供します。既定では、ユーザーは必要に応じてアプリ バーの表示と非表示を切り替えることができます。このため、使用頻度が少ないコマンドを配置するのに適しています。これにより、メイン UI をコンテンツとの直接操作に集中させることができます。

主要リソース:

リバーシには、アプリ バーに適したセカンダリ コマンド (時計の一時停止、動作の取り消し/やり直し) があります。アプリ バーは、通常のゲーム プレイ時には非表示になっています。画面の上端または下端をスワイプすると、表示と非表示が切り替わります。

リバーシのアプリ バー

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>


リバーシは、CommandBar コントロールと AppBarButton コントロールを使って、既定の動作とスタイルを取得します。ボタンの動作とその有効状態は、ボタンの Command プロパティにバインドされているビュー モデル コマンドによって提供されます (「コマンド」セクションをご覧ください)。

[再生] ボタンと [一時停止] ボタンは、1 つのトグル ボタンのように機能します。この効果を出すため、ボタンの Visibility プロパティは同じビュー モデル プロパティにバインドされています。どちらのバインドでも BooleanToVisibilityConverter を使いますが、一方のバインドには、バインドの効果が反転する ConverterParameter プロパティも設定します。これにより、各ボタンは、もう一方が表示されていない場合にのみ表示されるようになります。詳しくは、「データ バインディング」セクションをご覧ください。

トースト通知

トースト通知は、アプリで重要なイベントが発生すると、ユーザーに通知を出します。これは、他のアプリがアクティブになっているときでも行われます。

主要リソース:

リバーシでは、石を動かすのにしばらく時間がかかることがあります。待っている間に他のアプリに切り替えた場合、自分の番になると、トースト通知が表示されます。

リバーシのトースト通知

リバーシでは、トースト通知に必要な最低限のコードを使って、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);
}


設定ポップアップ

設定チャームは、アプリの設定への標準化されたアクセスを提供します。

主要リソース:

リバーシには設定ポップアップが 2 つあります。1 つは表示オプション用で、もう 1 つは新しいゲームのオプション用です。

リバーシの設定ポップアップ

App.xaml.cs から抜粋した次のコードは、リバーシで 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 には、アプリの画像の共有のサポートが組み込まれているため、リバーシに追加機能は必要ありません。

データ バインディング

データ バインディングにより、ある UI コントロールが変更されると別の UI コントロールが更新されるように、UI コントロールと表示データを関連付けることができます。データ バインディングはデータ入力フォームで一般的ですが、データ バインディングを使って UI 全体を制御したり、アプリのロジックと UI を分離させたりすることもできます。

主要リソース:

リバーシでは、データ バインディングを使って UI ("ビュー" レイヤー) をアプリのロジック ("ビュー モデル" レイヤー) に関連付けます。このレイヤーは Model-View-ViewModel (MVVM) パターンと呼ばれ、他のコードと UI を分離させるのに役立ちます。 リバーシでこのパターンを使う方法について詳しくは、「リバーシ アプリの構造」をご覧ください。MVVM の概要については、「Model-View-ViewModel (MVVM) パターンの使用」をご覧ください。

リバーシの大部分のバインドは、XAML で Binding マークアップ拡張を使って定義されますが、いくつかのケース (Board.xaml.cs ファイルなど) ではコード ビハインドが使われます。各ページでは、ページのすべての要素をバインドのデータ ソースとして使う DataContext プロパティが設定されます。

UI の更新

リバーシの UI はデータ バインディングによって制御されます。UI による対話式操作でデータ ソースのプロパティが変更されると、データ バインディングが UI を更新して変更に対応します。

これらの更新が機能するのは、リバーシのビュー モデル クラスが BindableBase クラスを継承するためです。このクラスは Common/BindableBase.cs ファイルにあり、標準の INotifyPropertyChanged 実装といくつかのサポート メソッドを提供します。SetProperty メソッドは、1 回のメソッド呼び出しでプロパティのバッキング値とバインドされた 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 実装に変換コードを配置することをお勧めします。リバーシでは、Common/Converters フォルダーの NullStateToVisibilityConverter クラスと BooleanToVisibilityConverter クラス (さまざまな UI 要素の表示と非表示を切り替えます) をバインドに使います。

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 イベント ハンドラーと一緒に実装されます。リバーシでは、ナビゲーション ボタンについてはこのとおりですが、他のボタンについては、ボタンが呼び出す非 UI コードとボタンの UI を分離させます。 これを行うには、ICommand 実装を返すビュー モデル プロパティに Button.Command プロパティをバインドします。

リバーシのコマンド プロパティは DelegateCommand タイプまたは DelegateCommand<T> タイプです。これらのクラスは Common/DelegateCommand.cs ファイル内にあり、再利用可能な標準の ICommand 実装を提供します。これらのクラスを使うことで、1 回使うだけのコマンドの作成が簡略化され、必要なコードを単一のプロパティ実装に収めることができます。

GameViewModel.cs から抜粋した次のコードは、カスタム ボタンであるボード領域で使われる移動コマンドを示しています。 ?? ("null 結合" 演算子) は、フィールド値が null ではない場合にのみフィールド値が返されることを意味します。それ以外の場合は、フィールドが設定され、新しい値が返されます。したがって、プロパティに初めてアクセスしたときに単一のコマンド オブジェクトが作成され、以降のすべてのアクセスでこの同じオブジェクトが再利用されます。コマンド オブジェクトは、MoveAsync メソッドと CanMove メソッドを参照して DelegateCommand<ISpace>.FromAsyncHandler メソッドを呼び出して初期化されます。これらのメソッドは、ICommand.Execute メソッドと CanExecute メソッドの実装を提供します。


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


カスタム依存関係プロパティ

リバーシでは、データ バインディング更新を使って表示状態を変更できるように、カスタム コントロールでカスタム依存関係プロパティを使います。表示状態と切り替え効果のアニメーションは、XAML で VisualStateManager クラスを使って定義されます。ただし、表示状態をビュー モデル プロパティに直接バインドする方法はありません。カスタム依存関係プロパティは、ビュー モデル プロパティへのバインドのターゲットを提供します。この依存関係プロパティには、必要な VisualStateManager.GoToState メソッド呼び出しを実行するプロパティ変更コールバックが含まれます。

次のコードは、PlayerStatus コントロールでコード ビハインドを使い、カスタム依存関係プロパティをビュー モデル プロパティにバインドする方法を示しています。ここでは、依存関係プロパティのうちの 1 つで、プロパティ変更コールバック メソッドが含まれるものを示しています。コールバックと 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 の応答性を保つことができます。

主要リソース:

リバーシでは、非同期コードを使って、ゲーム内の移動を実行します。各移動は、移動のアニメーションを含め、完了するまでに少なくとも 1 秒はかかります。AI の移動は、さらに時間がかかる場合があります。ただし、UI は常に応答できる状態であるため、ユーザー コマンド (元に戻すなど) で進行中の移動を取り消すことができます。

GameViewModel.cs から抜粋した次のコードは、リバーシで async キーワード、await キーワード、Task クラス、キャンセル トークンを使う方法を示しています。Game クラスで Windows ランタイム非同期コードと統合するために AsTask を使っていることに注目してください (詳しくは、次のセクションをご覧ください)。


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 ランタイム コンポーネントとして実装すると、そのコードをさまざまなアプリ、プラットフォーム、または言語で再利用することができます。また、コンポーネントを別の言語の別の実装に簡単に置き換えることもできます。

主要リソース:

リバーシでは、コア ゲーム ロジックを Windows ランタイム コンポーネントとして実装して、アプリから完全に切り離しています。これにより、今後の拡張性とコードの再利用をサポートすることが可能になりました。リバーシには、元の C# バージョンの高性能版として、ゲーム エンジンの C++ バージョンも付属しています。詳しくは、「リバーシ C++ ゲーム エンジンについて」をご覧ください。

Game.cs から抜粋した次のコードは、リバーシで Task ベースの非同期コード (async キーワードや await キーワードなど) を使うが、結果は Windows ランタイムの非同期インターフェイスを使って公開する方法を示しています。また、GameViewModel コードのキャンセル トークンを Game クラスで使う方法も示しています。

コード例の最初と 3 番目の方法では、AsyncInfo.Run メソッドを呼び出して、IAsyncOperation<T> を返します。これにより、タスクの戻り値をラップし、取り消しを実行できるようにします。2 番目の例では、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));
}


関連トピック

リバーシのサンプル アプリ
Windows ストア ゲーム "リバーシ" (XAML、C#、C++)
Model-View-ViewModel (MVVM) パターンの使用
"リバーシ" での Windows ストア アプリ機能の使用
リバーシ アプリの構造について
リバーシ C++ ゲーム エンジンについて
C# または Visual Basic を使った初めての Windows ストア アプリの作成
C# または Visual Basic を使った Windows ランタイム アプリのためのロードマップ
データ バインディング

 

 

表示:
© 2014 Microsoft