UI 最前線

XNA でのカラー スクロール

Charles Petzold

コード サンプルのダウンロード

Charles Petzold私が出版物向けに初めて作成した Windows プログラムの 1 つは COLORSCR ("カラー スクロール") という名前で、MSDN マガジンの前身である『Microsoft Systems Journal』の 1987 年 5 月号に掲載されました。

このプログラムは学習に役立つことが多く、これまで長年にわたり、他の API やフレームワーク用に書き換えてきました。このプログラムは単純で、3 つのスクロール バーまたはスライダーの赤、緑、青の値をそれぞれ操作してカスタム カラーを作成するだけですが、レイアウトやイベント処理など、非常に重要な作業を伴います。機能的にも、単純で意味のないデモというわけではありません。色選択ダイアログ ボックスを作成することがあれば、このプログラムを足掛かりに作業を始めるとよいでしょう。

今月のコラムでは、Windows Phone 7 に実装されている XNA Framework を使用してカラー スクロール プログラムを作成する方法を紹介します。完成したプログラムを図 1 に示します。

image: The Xnacolorscroll Program

図 1 Xnacolorscroll プログラム

XNA は、ゲーム プログラミング用の Microsoft .NET Framework ベースのマネージ コードですが、Microsoft.Xna.Framework 名前空間を検索しても、クラスの中にはスクロール バーやスライダー、さらにはボタンさえもありません。XNA は、とにかく、そのようなフレームワークではないのです。プログラムにスライダーが必要であれば、そのためにスライダーを作成しなければなりません。

XNA カラー スクロール プログラムの作成方法を紹介するのは、単に学習のために演習を行うことだけが目的ではありません。実際に意図するところはもっと大きいのです。

ご存知のように、Windows Phone 7 は Silverlight と XNA の両方をサポートしています。通常、特定のアプリケーションに対しては、どちらを使用するかはすぐに判断できます。ただし、アプリケーションのグラフィック要件に Silverlight ではとても対応できないが、XNA を使用するにはボタンやスライダーなどの使い慣れたクラスがないのでためらわれることがあります。

その場合、恐れる必要はないというのが今回のテーマです。単純なコントロールを XNA で作成することは、おそらく考えているよりも簡単です。

今月のコラムは、Windows Presentation Foundation (WPF) プログラマーと Silverlight プログラマー向けの XNA プログラミング入門と考えることもできます。私自身、WPF や Silverlight の特定の概念を今回の作業に応用しているため、XNA カラー スクロール プログラムでも WPF や Silverlight の知識は有用です。

ダウンロード可能なソース コードはすべて、XnaColorScroll という Visual Studio ソリューションにまとめています。このソリューションには、Windows Phone 7 用の XnaColorScroll プロジェクトと、このプログラムのために作成した 4 つの XNA "コントロール" を含む Petzold.Xna.Controls というライブラリがあります (言うまでもないことですが、このソリューションを Visual Studio に読み込むには、Windows Phone 開発ツールがインストールされている必要があります)。どうぞご自由にこれらのコントロールを盗んで、独自のコントロールを追加し、オリジナルの XNA コントロール ライブラリを作成してください。

Game とコンポーネント

WPF、Silverlight、または Windows フォーム プログラムのメイン クラスを Window、Page、または Form から派生するように、XNA プログラムのメイン クラスは Game から派生します。XNA プログラムは、この Game の派生クラスを使用して、ビットマップ、テキスト、および 3D グラフィックを画面に描画します。

プログラムを個別の機能エンティティに分けて編成できるように、XNA では "コンポーネント" の概念をサポートしています。XNA の概念の中ではおそらくこれがコントロールに最も近いものです。コンポーネントには、GameComponent の派生クラスと DrawableGameComponent の派生クラスの 2 種類があります。DrawableGameComponent 自体は GameComponent から派生したもので、名前がこの 2 つの違いを表しています。この演習では、この両方の基本クラスを使用します。

Game クラスでは、GameComponentCollection 型の Components というプロパティを定義します。Game によって作成されるコンポーネントは、すべてこのコレクションに追加されます。それにより、Game 自体が受け取るさまざまなオーバーライドされたメソッドに対する呼び出しと同じ呼び出しをコンポーネントが受け取るようにします。DrawableGameComponent は、Game が描画するものの上に描画します。

XnaColorScroll プログラムの分析を始めましょう。まずは、Game の派生クラスではなく、GameComponent の派生クラスから説明します。図 2 は、TextBlock というコンポーネントのソース コードです。これは、WPF や Silverlight の TextBlock とほぼ同じように使用されます。

図 2 TextBlock Game コンポーネント

public class TextBlock : DrawableGameComponent
{
  SpriteBatch spriteBatch;
  Vector2 textOrigin;

  public TextBlock(Game game) : base (game)
  {
    // Initialize properties
    this.Color = Color.Black;
  }

  public string Text { set; get; }
  public SpriteFont Font { set; get; }
  public Color Color { set; get; }
  public Vector2 Position { set; get; }
  public HorizontalAlignment HorizontalAlignment { set; get; }
  public VerticalAlignment VerticalAlignment { set; get; }

  public Vector2 Size
  {
    get
    {
      if (String.IsNullOrEmpty(this.Text) || this.Font == null)
          return Vector2.Zero;

      return this.Font.MeasureString(this.Text);
    }
  }

  protected override void LoadContent()
  {
    spriteBatch = new SpriteBatch(this.GraphicsDevice);
    base.LoadContent();
  }

  public override void Update(GameTime gameTime)
  {
    if (!String.IsNullOrEmpty(Text) && Font != null)
    {
      Vector2 textSize = this.Size;
      float xOrigin = (int)this.HorizontalAlignment * textSize.X / 2;
      float yOrigin = (int)this.VerticalAlignment * textSize.Y / 2;
      textOrigin = new Vector2((int)xOrigin, (int)yOrigin);
    }
    base.Update(gameTime);
  }

  public override void Draw(GameTime gameTime)
  {
    if (!String.IsNullOrEmpty(Text) && Font != null)
    {
      spriteBatch.Begin();
      spriteBatch.DrawString(this.Font, this.Text, this.Position, this.Color,
                            0, textOrigin, 1, SpriteEffects.None, 0);
      spriteBatch.End();
    }
    base.Draw(gameTime);
  }
}

図 2 で、コンストラクターの後のパブリック プロパティに注目してください。これらのプロパティは、テキスト自体、テキストのフォント、色、画面との相対位置を示します。HorizontalAlignment と VerticalAlignment の 2 つの追加のプロパティにより、テキストの位置として、テキスト文字列の端または中央を指定できます。XnaColorScroll では、これらのプロパティは、スライダーの中央にテキストを配置する場合に便利です。

このクラスには、取得専用の Size プロパティもあります。Size プロパティは、特定のフォントで表示されるテキストのサイズを返します (XNA Vector2 構造体は、しばしば、2 次元の位置とサイズのほか、ベクトルを表すために使用されます)。

TextBlock コンポーネントは、Update メソッドと Draw メソッドの両方をオーバーライドします。これは、描画可能なコンポーネントでも、Game の派生クラスでも通常の処理です。この 2 つのメソッドはプログラムの "ゲーム ループ" を構成し、ビデオ ディスプレイと同じレートで呼び出されます (Windows Phone 7 の場合は 1 秒間に 30 回)。Draw メソッドは、画面がリフレッシュされるたびに呼び出されることを除いては、WM_PAINT メッセージや OnPaint オーバーライドのようなものと考えることができます。Update メソッドは各 Draw 呼び出しの前に呼び出され、すべての必要な計算を実行し、Draw メソッドを準備するために使用されます。

前述のとおり TextBlock には、パフォーマンスを改善できる余地が大いにあります。たとえば、Size プロパティの値をキャッシュすることや、textOrigin フィールドに影響するプロパティが変更された場合に textOrigin フィールドの再計算のみを実行することが考えられます。このようなパフォーマンスの改善は、プログラムのすべての Update および Draw 呼び出しの実行にかかる時間が 0.03 秒を超えているために、プログラムの動作が遅くなっている場合には、優先的に取り組む必要があります。

コンポーネントと子コンポーネント

図 1 からわかるように、XnaColorScroll には 6 つの TextBlock コンポーネントがありますが、3 つのスライダーもあります。このスライダーは、おそらくもう少しやっかいです。

何年も前であれば、1 クラスだけでスライダー全体のコード作成を試みていたかもしれません。しかし、WPF および Silverlight のコントロール テンプレートの経験からすると、Slider コントロールを作成する最適な方法は、Thumb コントロールと RepeatButton コントロールを組み合わせることではないかと思い当りました。XNA のスライダー コンポーネントも、同様の子コンポーネントから作成することが、理に適っているように思えます。

Game クラスでは、GameComponentCollection 型の Components プロパティにコンポーネントを追加することで、子コンポーネントをクラス自体に追加します。ここで大きな疑問がわいてきました。GameComponent には、子コンポーネントを追加するための Components プロパティを設定できるのでしょうか。

残念ながら、できません。ただし、コンポーネントは親の Game クラスにアクセスでき、コンポーネントが自分の子コンポーネントのインスタンスを必要とする場合は、その追加コンポーネントも Game クラスの Components コレクションに追加できます。これを正しい順序で行うと、DrawOrder プロパティを操作する必要さえありません。DrawOrder プロパティは、DrawableGameComponent によって定義されるプロパティで、コンポーネントが表示される順序を指定する場合に便利です。既定では、Game クラスの Draw メソッドが最初に呼び出され、次に Components コレクション内の順序に従ってコンポーネントのすべての Draw メソッドが呼び出されます。

では、始めましょう。TextBlock と同様に、Thumb クラスも DrawableGameComponent から派生します。タッチ処理を除く Thumb クラス全体を図 3 に示します。Thumb では、WPF と Silverlight の Thumb コントロールと同様に、3 つのイベントを定義します。

図 3 Thumb クラスのほぼすべてのコード

public class Thumb : DrawableGameComponent
{
  SpriteBatch spriteBatch;
  Texture2D tinyTexture;
  int? touchId;

  public event EventHandler<DragEventArgs> DragStarted;
  public event EventHandler<DragEventArgs> DragDelta;
  public event EventHandler<DragEventArgs> DragCompleted;

  public Thumb(Game game) : base(game)
  {
    // Initialize properties
    this.Color = Color.White;
  }

  public Rectangle Position { set; get; }
  public Color Color { set; get; }

  protected override void LoadContent()
  {
    spriteBatch = new SpriteBatch(this.GraphicsDevice);
    tinyTexture = new Texture2D(this.GraphicsDevice, 1, 1);
    tinyTexture.SetData<Color>(new Color[] { Color.White });

    base.LoadContent();
  }

  ...

  public override void Draw(GameTime gameTime)
  {
    spriteBatch.Begin();
    spriteBatch.Draw(tinyTexture, this.Position, this.Color);
    spriteBatch.End();

    base.Draw(gameTime);
  }
}

図 2 の TextBlock コンポーネントでは、Position プロパティは Vector2 型の点でした。Thumb では、Position プロパティは四角形オブジェクトで、Thumb の場所だけではなく、四角形のサイズも定義します。Thumb の外観は、Position プロパティによって指定された場所とサイズで表示される、白い 1 x 1 ピクセルのビットマップのみです (1 ピクセルのビットマップを使用して特定のサイズに拡大するのは、私のお気に入りの XNA のプログラミング手法の 1 つです)。

Thumb ではタッチ入力を処理しますが、移動は行いません。Thumb の DragDelta イベントを処理し、新しい Position プロパティを設定するのは、スライダー コンポーネントです。

Thumb 以外にも、スライダーには Thumb の両側に 1 つずつ、合計 2 つの RepeatButton コンポーネントがあります。WPF と Silverlight の RepeatButton コントロールと同様に、この 2 つのコンポーネントを指で押し続けると、一連の Click イベントが生成されます。

今月のコラムの XNA スライダーでは、RepeatButton コンポーネントが画面の特定の領域を占めていますが、実際には非表示です (中央の細い縦のトラックは、スライダー自体によって描画されています)。つまり、RepeatButton は DrawableGameComponent ではなく、GameComponent から派生できます。図 4 は、タッチ処理を除く、RepeatButton コントロール全体のコードです。

図 4 RepeatButton クラスのほぼすべてのコード

public class RepeatButton : GameComponent
{
  static readonly TimeSpan REPEAT_DELAY = TimeSpan.FromMilliseconds(500);
  static readonly TimeSpan REPEAT_TIME = TimeSpan.FromMilliseconds(100);

  int? touchId;
  bool delayExceeded;
  TimeSpan clickTime; 

  public event EventHandler Click;

  public RepeatButton(Game game) : base(game)
  {
  }

  public Rectangle Position { set; get; }

  ...

}

これで、スライダー コンポーネントをビルドする準備ができました。適度に単純さを保つように、ここでは方向を縦のみにしています。スライダーには、Minimum、Maximum、Value などの一般的なパブリック プロパティがあります。また、スライダーでは ValueChanged イベントも定義します。スライダーは Initialize のオーバーライドで、3 つの子コンポーネントを作成 (およびこれらをフィールドとして保存) し、イベント ハンドラーを設定して、Game プロパティからアクセスできる親の Game クラスの Components コレクションにそれらのハンドラーを追加します。

public override void Initialize()
{
  thumb = new Thumb(this.Game);
  thumb.DragDelta += OnThumbDragDelta;
  this.Game.Components.Add(thumb);

  btnDecrease = new RepeatButton(this.Game);
  btnDecrease.Click += OnRepeatButtonClick;
  this.Game.Components.Add(btnDecrease);

  btnIncrease = new RepeatButton(this.Game);
  btnIncrease.Click += OnRepeatButtonClick;
  this.Game.Components.Add(btnIncrease);

  base.Initialize();
}

XNA の Game 派生クラスと GameComponent 派生クラスでは、クラス コンストラクター、Initialize メソッドのオーバーライド、および LoadContent メソッドのオーバーライドの 3 か所で初期化を行う機会があり、どの状況でどれを使用すべきか少しわかりにくいかもしれません。名前が示すように LoadContent はコンテンツ (フォントやビットマップなど) の読み込みに非常に適していて、GraphicsDevice オブジェクトの存在に依存するその他のすべての初期化にはこれを使用します。

個人的には、上記のコード スニペットのように、Initializeの オーバーライド中にコンポーネントを作成して、Components コレクションに追加する方法が好みです。XnaColorScroll では、Game 派生クラスでも 6 つの TextBlock コンポーネントと 3 つのスライダー コンポーネントを Initialize のオーバーライドで作成しています。基本クラスの Initialize メソッドが最後に呼び出されるときに、子コンポーネントのすべての Initialize メソッドが呼び出されます。次に、各スライダーの Initialize メソッドによって、Thumb と 2 つの RepeatButton コンポーネントが作成されます。この方法によって、確実に、これらのコンポーネントのすべてが、適切な表示順序で Components コレクションに追加されます。

タッチ入力を処理する

ほとんどの Windows Phone 7 プログラムではユーザー入力の主な手段はマルチタッチであり、XNA も例外ではありません (XNA プログラムではキーボード入力も可能です。また、電話機の加速度計を入力デバイスに使用することもできます)。

XNA プログラムでは、Update オーバーライド中にタッチ入力が取得されます。タッチ イベントはありません。すべてのタッチ入力はポーリングされ、TouchPanel クラスからアクセスできます。TouchPanel.GetState 静的メソッドは、Pressed、Moved、および Released アクティビティと位置情報を示す TouchLocation オブジェクト、および複数の指を識別するための整数の ID 番号から成る、低レベルのタッチ入力を提供します。TouchPanel.ReadGesture メソッド (このプログラムでは未使用) は、より高度なジェスチャ サポートを提供します。

初めてゲーム コンポーネントの作業に着手したとき、各コンポーネントで独自にタッチ入力を取得して、必要な入力は維持し、他は無視できると考えました。しかし、これはあまりうまく行かないようでした。行儀のよい子供たちのように分けあうのではなく、ゲーム クラスとコンポーネントがどれもタッチ入力を奪い合うようになりました。

そこで、タッチ入力の別の処理システムを考えることにしました。Game クラスのみが TouchPanel.GetState を呼び出します。次に、各 TouchLocation オブジェクトを、ProcessTouch というメソッドによって子コンポーネントに渡します。これにより、そのコンポーネントが入力を最初に取得できます。TouchLocation オブジェクトを利用するコンポーネントは ProcessTouch から true を返し、その特定の TouchLocation オブジェクトの最後までさらに処理を進めます (ProcessTouch から true を返すのは、WPF や Silverlight で RoutedEventArgs の Handled プロパティを true に設定するようなものです)。

XnaColorScroll では、次のように、メインの Game クラスの Update メソッドが TouchPanel.GetState を呼び出し、すべてのタッチ入力を取得してから、各スライダー コンポーネントの ProcessTouch メソッドを呼び出します。

TouchCollection touches = TouchPanel.GetState();

foreach (TouchLocation touch in touches)
{
  for (int primary = 0; primary < 3; primary++)
    if (sliders[primary].ProcessTouch(gameTime, touch))
      break;
}

ProcessTouch から true が返されると、各 ProcessTouch オブジェクトの後続の処理がどのように停止されるかに注目してください。

次に、Slider コントロールの ProcessTouch メソッドが、2 つの RepeatButton コンポーネントと Thumb コンポーネントの ProcessTouch メソッドを呼び出します。

public bool ProcessTouch(GameTime gameTime, TouchLocation touch)
{
  if (btnIncrease.ProcessTouch(gameTime, touch))
    return true;

  if (btnDecrease.ProcessTouch(gameTime, touch))
    return true;

  if (thumb.ProcessTouch(gameTime, touch))
    return true;

  return false;
}

これらのコンポーネントのいずれか (または Game クラス自体) が、固有のタッチ入力を実行する場合、まず各子コンポーネントの ProcessTouch が呼び出されます。その後、子コンポーネントによって処理されなかった TouchLocation オブジェクトが、クラス固有の用途に必要かどうかを確認できます。実際、この方法では、最前面に表示されている子コンポーネントに、タッチ入力への最初のアクセスを許可でき、通常はこのような動作が求められます。この処理は、WPF や Silverlight に実装されているイベント ルーティング処理によく似ています。

RepeatButton も Thumb も、コンポーネントの Position の四角形が指定する領域内に最初に指が触れた場合は、タッチ入力を処理します。この場合、コンポーネントはタッチ ID を保存することでその指を "キャプチャ-" します。この ID での他のすべてのタッチ入力は、この指が離れるまで、その特定のコンポーネントに帰属する入力になります。

イベントを伝達する

Thumb コンポーネントと RepeatButton コンポーネントのタッチ処理は、まさしく Slider コントロールの要ですが、興味があれば独学で学べるものです。Thumb コンポーネントはサーフェイスで指の動きを検出すると DragDelta イベントを生成し、RepeatButton コンポーネントはタップまたは押したままの状態を検出すると、Click イベントを生成します。

どちらのイベントも親のスライダー コンポーネントによって処理され、それに従って Value プロパティが調節されます。それを受けて、スライダーから ValueChanged イベントが生成されます。新しい Value に基く Thumb コンポーネントと 2 つの RepeatButton コンポーネントの Position プロパティの設定も、スライダーによって処理されます。

Game クラスは 1 つのイベント ハンドラーで、3 つのスライダー コンポーネントの ValueChanged イベントをまとめて処理するため、メソッドでは 3 つの TextBlock コンポーネントの新しい値と新しい色の設定だけが処理されます。

void OnSliderValueChanged(object sender, EventArgs args)
{
  txtblkValues[0].Text = sliders[0].Value.ToString("X2");
  txtblkValues[1].Text = sliders[1].Value.ToString("X2");
  txtblkValues[2].Text = sliders[2].Value.ToString("X2");

  selectedColor = new Color(sliders[0].Value, 
                            sliders[1].Value, 
                            sliders[2].Value);
}

これで、Game クラスの Draw オーバーライドには 2 つの仕事だけが残されました。1 つはコンポーネントの背景となる黒の四角形を描画すること、もう 1 つは右側に新しい selectedColor を使用して四角形を描画することです。どちらの仕事でも 1 x 1 ピクセルのビットマップを使用して、画面の半分のサイズに拡大します。

Silverlight よりも機能的?

最近、Silverlight バージョンの Windows Phone 7 用カラー スクロール プログラムも作成しましたが、そのプログラムでは、1 度に 1 つのスライダーしか操作できませんでした。ただし、XnaColorScroll プログラムを電話機に配置すれば、3 つの Slider コントロールのすべてを個別に操作できます。この違いを非常に興味深く感じています。Silverlight のような高度なインターフェイスは開発を容易にしますが、往々にして、何かを犠牲にする必要があるということが、このことからもわかります。この概念は、ノーフリーランチ定理 (TANSTAAFL: There ain’t no such thing as a free lunch) と呼ばれるものだと思います。

XNA は、ある意味で非常に低レベルな言語のため、開拓時代の西部を彷彿させるかもしれません。しかし、少しばかりコンポーネント (他のコンポーネントから作成されたコンポーネントも含む) を熟慮して使用し、イベント ルーティング処理をまねることで、XNA といえども手なずけて、Silverlight と同様に (おそらくそれ以上に) 活用できます。

Charles Petzold は MSDN マガジンの記事を長期にわたって担当している寄稿編集者です。新しく執筆した『Programming Windows Phone 7』(Microsoft Press、2010 年) は、bit.ly/cpebookpdf (英語) から無料でダウンロードできます。

この記事のレビューに協力してくれた技術スタッフの Shawn Hargreaves に心より感謝いたします。