Microsoft Visual Web Developer 2008 Express Edition
 Silverlight 3

■Silverlightでゲーム作成 Prev  Top  Next

ドメイン

超レトロゲーム、スペースインベーターです。 あくまで Silverlight でゲーム作成する方法について検証するのが当サイトの目的なので、ゲームの内容は何でもいいんですけどね。

ゲーム制作する上で必要となるのはリアルタイムで画面の更新処理を行うことです。 DirectX と異なり Silverlight ではイベントドリブンでの実装となるのでタイマーイベントを使用して更新します。
あとキーボード入力もやります。

---Page2.xaml---


<navigation:Page x:Class="SilverlightTutrial.Page2" 
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
           xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
           xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
           mc:Ignorable="d"
           xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
           d:DesignWidth="640" d:DesignHeight="480"
           Title="Page2 Page">

  <navigation:Frame x:Name="frmPage2" JournalOwnership="OwnsJournal">
    <!-- タイマーイベントを開始 -->
    <Canvas x:Name="canControl" Loaded="StartTimer" xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
      <!-- 自機 -->
      <Rectangle x:Name="playerControl" Width="30" Height="30" Stroke="#333366" StrokeThickness="2" Fill="#f0f0f0" />
      <!-- キーボード入力を有効にするためのダミーコントロール -->
      <Button x:Name="button" FontSize="30" Content="START" Width="0" Height="0" Canvas.Left="220" Canvas.Top="220"  KeyDown="move_KeyDown" KeyUp="move_KeyUp" />
      <!-- ゲームオーバー画面などで表示するボタン.これはダミーでない -->
      <Button x:Name="nextButton" FontSize="30" Content="START" Width="200" Height="60" Canvas.Left="220" Canvas.Top="280" Click="start_Click" />
      <Button x:Name="endButton" FontSize="30" Content="EXIT" Width="200" Height="60" Canvas.Left="220" Canvas.Top="350" Click="end_Click"/>
      <!-- メッセージ用コントロール -->
      <TextBlock x:Name="Message" />
      <!-- デバック用コントロール -->
      <TextBlock x:Name="Debug" />
    </Canvas>
  </navigation:Frame>

</navigation:Page>

ゲーム画面のレイアウトです。タイマーのイベントハンドラは <Canvas> 内で設定します。 プレイヤーはここで設定しますが、敵キャラとかはマネージコード内で動的に設定するのでここでは作成しません。 というか数が多いのでマネージコードの for文内 で一気に作成した方が楽チン。

あと微妙なところはキーボード入力のためにダミーで設定しているボタンコントロールです。 最初のころ、キーボード入力を <Canvas> 内で設定したりしてみましたが、キーボード入力してもイベントが発生しませんでした。 調べたところ、フォーカスが当たっているコントロールに対してのみキー入力イベントが発生するようです。 MSDN

ではコントロールにフォーカスを当てるにはどうするのかというと
 1.IsEnabled が true に設定されていること。
 2.Visibility が Visible に設定されていること。
 3.フォーカスが Silverlight コンテンツ領域の外に完全に出ないこと。
 4.IsTabStop が true に設定されていること。
の4項目を満たす必要があるらしいです。
MSDN

<Canvas>についてこれらのパラメータを調べてみると、そもそも IsEnabled がメンバに含まれていませんでした。

そんなわけで、<Canvas> ではキー入力イベントを受け取れないっぽいので、別の方法を考えて 結局サンプルのとおりになりました。非表示にはできないのでサイズを 0 にして表示されないようにしています。

これが一般的な方法かは知りません。もっとスマートな方法があるのだろうか?

---Page2.xaml.cs---


using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Navigation;
using System.Windows.Browser;

namespace SilverlightTutrial
{
    public partial class Page2 : Page
    {
        // 処理モード
        int Mode;
        int enemyMoveMode;
        int enemyMoveCnt;

        // 自機の速度
        double PlayerSpeed;

        // キー入力判定
        bool RightKey;
        bool LeftKey;

        // 敵
        Rectangle[] enemyArray = null;

        // 自機弾
        Ellipse pShoot = new Ellipse();
        // 敵弾(10発まで同時表示)
        Ellipse[] eShootArray = new Ellipse[10];

        // コンストラクタ
        public Page2()
        {
            InitializeComponent();

            Initialize();

            this.canControl.Children.Add(pShoot);
            for (int i = 0; i < eShootArray.Length; i++)
                this.canControl.Children.Add(eShootArray[i]);
            for( int i=0; i<enemyArray.Length; i++ )
                this.canControl.Children.Add(enemyArray[i]);
        }

        private void Initialize()
        {
            Mode = 0;
            enemyMoveMode = 0;
            enemyMoveCnt = 0;
            PlayerSpeed = 0.0;
            RightKey = false;
            LeftKey = false;
            Point p;

            // メッセージ非表示
            Message.Visibility = Visibility.Collapsed;
            Canvas.SetZIndex(Message, 999);   // 大きいほど前面に表示される可能性が高くなる(微妙な言い回しだがMSDNにそう書いてある)

            nextButton.Visibility = Visibility.Collapsed;
            Canvas.SetZIndex(nextButton, 999);
            endButton.Visibility = Visibility.Collapsed;
            Canvas.SetZIndex(endButton, 999);

            // 弾を初期化する
            shootInitialize();

            // 敵を初期化する
            enemyArrayInitialize();

            // App1.Current.Host.Content.ActualWidth -> Silverlightコントロールの横幅
            p = new Point(App1.Current.Host.Content.ActualWidth * 0.5 - playerControl.Width / 2,
                App1.Current.Host.Content.ActualHeight - playerControl.Height - 10);
            // 自機の座標を初期位置に設定する
            Canvas.SetLeft(playerControl, p.X);
            Canvas.SetTop(playerControl, p.Y);
        }

        // ユーザーがこのページに移動したときに実行されます。
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
        }

        private void shootInitialize()
        {
            pShoot.Visibility = Visibility.Collapsed;
            pShoot.Width = 10.0;
            pShoot.Height = 10.0;
            pShoot.Stroke = new SolidColorBrush(Color.FromArgb(0xff, 0x0f, 0x0f, 0x0f));
            pShoot.StrokeThickness = 2;
            pShoot.Fill = new SolidColorBrush(Color.FromArgb(0xff, 0xff, 0x0f, 0x0f));
            Canvas.SetZIndex(pShoot, 997);

            for (int i = 0; i < eShootArray.Length; i++)
            {
                if( eShootArray[i] == null )
                    eShootArray[i] = new Ellipse();
                eShootArray[i].Visibility = Visibility.Collapsed;
                eShootArray[i].Width = 10.0;
                eShootArray[i].Height = 10.0;
                eShootArray[i].Stroke = new SolidColorBrush(Color.FromArgb(0xff, 0x0f, 0x0f, 0x0f));
                eShootArray[i].StrokeThickness = 2;
                eShootArray[i].Fill = new SolidColorBrush(Color.FromArgb(0xff, 0xff, 0xff, 0xf0));
                Canvas.SetZIndex(eShootArray[i], 998);
            }
        }

        private void enemyArrayInitialize()
        {
            const double offsetw = 80.0;
            const double offseth = 50.0;
            const double sleft = 20;
            const double stop = 30;

            int cnt = 0;
            double left = sleft;
            double top = stop;

            // 敵を適当に作成する
            if( enemyArray == null )
                enemyArray = new Rectangle[21];

            for (int i = 0; i < enemyArray.Length; i++)
            {
                if( enemyArray[i] == null )
                    enemyArray[i] = new Rectangle();
                enemyArray[i].Visibility = Visibility.Visible;
                enemyArray[i].Width = 30;
                enemyArray[i].Height = 30;
                enemyArray[i].Stroke = new SolidColorBrush(Color.FromArgb(0xff, 0x0f, 0x0f, 0x0f));
                enemyArray[i].StrokeThickness = 4;
                enemyArray[i].Fill = new SolidColorBrush(Color.FromArgb(0xff, 0x0f, 0x0f, 0xf0));

                Canvas.SetLeft(enemyArray[i], left);
                Canvas.SetTop(enemyArray[i], top);

                if (cnt < 6)
                {
                    left += offsetw;
                    cnt++;
                }
                else
                {
                    left = sleft;
                    top += offseth;
                    cnt = 0;
                }
            }
        }

        // タイマーの実装
        public void StartTimer(object o, RoutedEventArgs sender)
        {
            System.Windows.Threading.DispatcherTimer myDispatcherTimer = new System.Windows.Threading.DispatcherTimer();
//            myDispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 33); // 33 ミリ秒ごとにイベントを発生させるようにする
            myDispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 1000 / 60); // 1000 / 60 ミリ秒ごとにイベントを発生させるようにする
            myDispatcherTimer.Tick += new EventHandler(Each_Tick);      // タイマーのイベントハンドラーを設定
            myDispatcherTimer.Start();                                  // タイマー開始
        }

        // タイマーイベント処理
        public void Each_Tick(object o, EventArgs sender)
        {
            // キーボード入力を有効にするためになにがなんでもフォーカスを当てる。
            // ただし常にフォーカスを当てると、ボタンが表示されている画面でマウスクリックが反応しなくなるので注意
            if( Mode == 0 )
                button.Focus();

            switch (Mode)
            {
                case 0:  // 戦い中
                    playerUpdate();      // 自機の更新
                    enemyUpdate();       // 敵の更新
                    shootUpdate();       // 弾の更新
                    CollisionAll();      // 当たり判定
                    break;

                case 1:  // ゲームオーバー

                    break;

                case 2:  // クリア

                    break;
            }
        }

        // 自機の移動
        private void playerUpdate()
        {
            // キーボードの入力状態をチェックし、速度を計算する
            if (RightKey)
                PlayerSpeed += 2.0;
            if (LeftKey)
                PlayerSpeed -= 2.0;

            // 自機の位置を計算
            double left = Canvas.GetLeft(playerControl) + PlayerSpeed;

            // 自機がスクリーンからはみ出たら戻す
            if (left < 0.0)
                left = 0.0;
            if (left > App1.Current.Host.Content.ActualWidth - playerControl.Width)
                left = App1.Current.Host.Content.ActualWidth - playerControl.Width;

            // 座標を設定する
            Canvas.SetLeft(playerControl, left);

            // 減速
            PlayerSpeed *= 0.9;
        }

        // 敵の移動
        private void enemyUpdate()
        {
            switch (enemyMoveMode)
            {
                // 右に移動
                case 0:
                    enemyMoveCnt++;
                    if (enemyMoveCnt > 80)
                    {
                        enemyMoveCnt = 0;
                        enemyMoveMode++;
                    }
                    MoveEnemy(1.0, 0.0);
                    break;
                // 下に移動
                case 1:
                    enemyMoveCnt++;
                    if (enemyMoveCnt > 30)
                    {
                        enemyMoveCnt = 0;
                        enemyMoveMode++;
                    }
                    MoveEnemy(0.0, 1.0);
                    break;
                // 左に移動
                case 2:
                    enemyMoveCnt++;
                    if (enemyMoveCnt > 80)
                    {
                        enemyMoveCnt = 0;
                        enemyMoveMode++;
                    }
                    MoveEnemy(-1.0, 0.0);
                    break;
                // 下に移動
                case 3:
                    enemyMoveCnt++;
                    if (enemyMoveCnt > 30)
                    {
                        enemyMoveCnt = 0;
                        enemyMoveMode = 0;
                    }
                    MoveEnemy(0.0, 1.0);
                    break;
            }
        }

        private void MoveEnemy(double addX, double addY)
        {
            double x;
            double y;
            Random rnd = new Random();
            int j = 0;

            for (int i = 0; i < enemyArray.Length; i++)
            {
                if (enemyArray[i].Visibility == Visibility.Visible)
                {
                    x = Canvas.GetLeft(enemyArray[i]);
                    y = Canvas.GetTop(enemyArray[i]);
                    Canvas.SetLeft(enemyArray[i], x + addX );
                    Canvas.SetTop(enemyArray[i], y + addY);

                    if (rnd.Next(500) == 0)
                    {
                        // 弾発射
                        while( j < eShootArray.Length )
                        {
                            if (eShootArray[j].Visibility == Visibility.Collapsed)
                            {
                                Canvas.SetLeft(eShootArray[j], Canvas.GetLeft(enemyArray[i]) + enemyArray[i].Width / 2 - eShootArray[j].Width / 2);
                                Canvas.SetTop(eShootArray[j], Canvas.GetTop(enemyArray[i]) + enemyArray[i].Height / 2 - eShootArray[j].Height / 2);
                                eShootArray[j].Visibility = Visibility.Visible;                                
                                break;
                            }
                            j++;
                        }
                    }
                }
            }
        }

        // 弾の更新
        private void shootUpdate()
        {
            double left;
            double top;

            // 自機の弾の更新
            if (pShoot.Visibility == Visibility.Visible)
            {
                left = Canvas.GetLeft(pShoot);
                top = Canvas.GetTop(pShoot);
                top -= 10.0;
                if (top > 0)
                {
                    Canvas.SetLeft(pShoot, left);
                    Canvas.SetTop(pShoot, top);
                }
                else
                    pShoot.Visibility = Visibility.Collapsed;
            }

            // 敵の弾の更新
            for (int i = 0; i < eShootArray.Length; i++)
            {
                if (eShootArray[i].Visibility == Visibility.Visible)
                {
                    top = Canvas.GetTop(eShootArray[i]);
                    if (top <= App1.Current.Host.Content.ActualHeight)
                    {
                        Canvas.SetTop( eShootArray[i], top += 5.0 );
                    }

                    else
                        eShootArray[i].Visibility = Visibility.Collapsed;
                }
            }
        }

        // キーボード入力したときに発生するイベント
        private void move_KeyDown(object sender, KeyEventArgs e)
        {
            switch (e.Key)
            {
                // 右に移動
                case Key.Right:
                    RightKey = true;
                    break;
                // 左に移動
                case Key.Left:
                    LeftKey = true;
                    break;
                // 攻撃
                case Key.Up:
                    // 弾を非表示のときのみショットを撃てる
                    if (Mode == 0 && pShoot.Visibility == Visibility.Collapsed)
                    {
                        // 弾の座標を自機の座標から計算し、設定する
                        Canvas.SetLeft(pShoot, Canvas.GetLeft(playerControl) + playerControl.Width / 2);
                        Canvas.SetTop(pShoot, Canvas.GetTop(playerControl) - pShoot.Height);
                        pShoot.Visibility = Visibility.Visible;
                    }
                    break;
            }
        }

        // キーボード入力をやめたときに発生するイベント
        private void move_KeyUp(object sender, KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Right:
                    RightKey = false;
                    break;
                case Key.Left:
                    LeftKey = false;
                    break;
            }
        }

        private void CollisionAll()
        {
            // 自機の弾と敵との当たり判定

            double shootL = Canvas.GetLeft(pShoot);
            double shootR = Canvas.GetLeft(pShoot) + pShoot.Width;
            double shootT = Canvas.GetTop(pShoot);
            double shootB = Canvas.GetTop(pShoot) + pShoot.Height;

            double bodyL;
            double bodyR;
            double bodyT;
            double bodyB;

            for (int i = 0; i < enemyArray.Length; i++)
            {
                if (enemyArray[i].Visibility == Visibility.Visible)
                {
                    bodyL = Canvas.GetLeft(enemyArray[i]);
                    bodyR = Canvas.GetLeft(enemyArray[i]) + enemyArray[i].Width;
                    bodyT = Canvas.GetTop(enemyArray[i]);
                    bodyB = Canvas.GetTop(enemyArray[i]) + enemyArray[i].Height;

                    // 当たり判定チェック
                    if (!(shootR < bodyL ||
                          bodyR  < shootL ||
                          shootB < bodyT ||
                          bodyB  < shootT))
                    {
                        // ヒットしたので非表示にする。
                        enemyArray[i].Visibility = Visibility.Collapsed;
                        pShoot.Visibility = Visibility.Collapsed;
                        break;
                    }
                }
            }

            bool flg = false;
            for (int i = 0; i < enemyArray.Length; i++)
            {
                if (enemyArray[i].Visibility == Visibility.Visible)
                {
                    flg = true;
                    break;
                }
            }
            if (flg == false)
            {
                // クリア
                ChangeMode(2);
                return;
            }

            // 自機と敵の弾との当たり判定

            shootL = Canvas.GetLeft(playerControl);
            shootR = Canvas.GetLeft(playerControl) + playerControl.Width;
            shootT = Canvas.GetTop(playerControl);
            shootB = Canvas.GetTop(playerControl) + playerControl.Height;

            // 敵の弾の座標
            for (int i = 0; i < eShootArray.Length; i++)
            {
                if (eShootArray[i].Visibility == Visibility.Visible)
                {
                    bodyL = Canvas.GetLeft(eShootArray[i]);
                    bodyR = Canvas.GetLeft(eShootArray[i]) + eShootArray[i].Width;
                    bodyT = Canvas.GetTop(eShootArray[i]);
                    bodyB = Canvas.GetTop(eShootArray[i]) + eShootArray[i].Height;

                    // 当たり判定チェック
                    if (!(shootR < bodyL ||
                          bodyR  < shootL ||
                          shootB < bodyT ||
                          bodyB  < shootT))
                    {
                        // ヒットしたのでゲームオーバー
                        ChangeMode(1);
                        return;
                    }
                }
            }

            // 自機と敵との当たり判定            
            for (int i = 0; i < enemyArray.Length; i++)
            {
                if (enemyArray[i].Visibility == Visibility.Visible)
                {
                    bodyL = Canvas.GetLeft(enemyArray[i]);
                    bodyR = Canvas.GetLeft(enemyArray[i]) + enemyArray[i].Width;
                    bodyT = Canvas.GetTop(enemyArray[i]);
                    bodyB = Canvas.GetTop(enemyArray[i]) + enemyArray[i].Height;

                    // 当たり判定チェック
                    if (!(shootR < bodyL ||
                          bodyR  < shootL ||
                          shootB < bodyT ||
                          bodyB  < shootT))
                    {
                        // ヒットしたのでゲームオーバー
                        ChangeMode(1);
                        return;
                    }
                }
            }
        }

        private void ChangeMode(int pMode)
        {
            Mode = pMode;

            switch (Mode)
            {
                // ゲームオーバー
                case 1:
                    Message.Text = "ゲームオーバー";
                    Message.FontSize = 40;
                    Message.FontWeight = FontWeights.Bold;
                    Message.Width = App1.Current.Host.Content.ActualWidth;
                    Message.Height = 40;
                    Canvas.SetLeft(Message, 0);
                    Canvas.SetTop(Message, ( App1.Current.Host.Content.ActualHeight - Message.Height ) / 2 - 100);
                    Message.TextAlignment = TextAlignment.Center;   // 中央寄せ
                    Message.Foreground = new SolidColorBrush(Color.FromArgb(0xff, 0xff, 0, 0));
                    Message.Visibility = Visibility.Visible;

                    // ボタン表示
                    nextButton.Visibility = Visibility.Visible;
                    endButton.Visibility = Visibility.Visible;
                    break;

                // クリア
                case 2:
                    Message.Text = "クリア!!";
                    Message.FontSize = 40;
                    Message.FontWeight = FontWeights.Bold;
                    Message.Width = App1.Current.Host.Content.ActualWidth;
                    Message.Height = 40;
                    Canvas.SetLeft(Message, 0);
                    Canvas.SetTop(Message, (App1.Current.Host.Content.ActualHeight - Message.Height) / 2 - 100);
                    Message.TextAlignment = TextAlignment.Center;   // 中央寄せ
                    Message.Foreground = new SolidColorBrush(Color.FromArgb(0xff, 0x00, 0, 0xff));
                    Message.Visibility = Visibility.Visible;

                    // ボタン表示
                    nextButton.Visibility = Visibility.Visible;
                    endButton.Visibility = Visibility.Visible;
                    break;
            }
        }
        // つまらないのでやめる
        private void end_Click(object sender, RoutedEventArgs e)
        {
            // javascript によりウィンドウを閉じる
            HtmlPage.Window.Eval("var w=window.open('','_top'); w.close()");
        }

        // 仕方がないので再挑戦
        private void start_Click(object sender, RoutedEventArgs e)
        {
            Initialize();
        }
    }
}

長いですが、ほとんどがゲームのアルゴリズムです。

1.タイマーイベントについて
タイマーイベントは StartTimer メソッドで開始します。 33 ミリ秒ごとにイベントを発生させるようにするという中途半端な数字を設定しているのは Silverlight のフレームレートを 60FPS に設定しているからです。 (最初30FPSだったのを60FPSに修正しました。) フレームレートの設定は SilverlightTutrialTestPage.html で行っています。

2.キーボード入力イベントについて
キーボード入力で自機を移動させますが、 move_KeyDownメソッド 内で直接座標を更新しません。 これはキーボードのキーを押しっぱなしにしたときの入力速度が一定でないためです。 キーボードを押しっぱなしにするとわかりますが、最初遅くてしばらくたつと速く入力されるような動作をします。 そのため キーボード入力されたらフラグを ON にし、 あとは描画処理の方でこのフラグを参照し、ONだったら自機を動かすといった感じで実装します。 入力解除は move_KeyUpメソッド で行います。

3.座標系ついて
Silverlightの座標系の原点は左上です。 そのため、コントロールを +Y 方向に移動させようとすると、下に移動するので注意が必要です。もっとも一般的に2D系はこの座標系となってますけどね。

4.感想というか今後の宿題
Silverlightでゲームを作成した感想。まず遅い!!。というかまれに停止する。ハードウェア・アクセラレーションを使用することも可能らしいが、 ジオメトリ変換やアルファブレンドを使用していないこのサンプルでは無意味らしいです。残念。 ただこれとは別にきになる点が。どうもコントロールひとつ見た目を変更するたびに、すべてのコントロールを再表示しているのではないかと思われます。 DirectXの場合、まずバックバッファをクリアして、がしがしレンダリングして、最後にプライマリサーフェイスに転送して終了となります。 しかし、Silverlightではそういった処理をやってるように見えない(少なくともそうコーディングしてない)にもかかわらず、 問題なく表示されています。しかし、この予想が正しいとすると遅いのは当たり前。Silverlight でのゲーム制作は現実的ではないのかなあ。 とはいってもまだ断定するのは早い。もう少し、いろいろいじってみようと思います。業務系アプリで使えるかの検証もあるしな。
処理速度について考察は保留とします。確かにまれに停止したりはするんですが、今回作成したサンプルゲーム程度では たいした負荷にはならないでしょう、多分。


Prev  Top  Next
inserted by FC2 system