Windowsストア アプリ 作り方解説 XNA編 第1回 ~オリジナルのWindows Phone 7 アプリ~

マイクロソフトの田中達彦です。
本連載では、Windows Phone 7のXNAで開発したアプリをWindowsストア アプリとして移植していきます。

[オリジナルのアプリ]
ここで使用するアプリは、Shoot Evoというアプリです。
https://www.windowsphone.com/s?appid=9cde78dc-1edb-4051-9a50-d6d0f033e7f6

このShoot Evoというアプリは、日経ソフトウエア2012年3月号に記事として掲載したShootというゲームの進化版です。
記事については、下記ページをご参照ください。
https://blogs.msdn.com/b/ttanaka/archive/2012/01/24/2012-3-windows-phone-xna.aspx

このアプリを、Windows 8に移植していきます。

[Shoot Evoのソースコード]
XNAを使用した、オリジナルのソースコードです。

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;

// 加速度計用
//using Microsoft.Devices.Sensors;

using System.IO.IsolatedStorage;
using System.IO;

namespace ShootEvo
{
    /// <summary>
    /// 基底 Game クラスから派生した、ゲームのメイン クラスです。
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // 画像用のフィールド
        Texture2D Ball; // ボール
        Texture2D Goal; // ゴール
        Texture2D Monster; // モンスター

        // 座標用のフィールド
        Vector2 BallLocation; // ボールの描画位置
        Vector2 MonsterLocation; // モンスターの描画位置

        // 速さ用のフィールド
        Vector2 BallSpeed; // ボールの移動の速さ
        Vector2 MonsterSpeed; // モンスターの移動の速さ

        // フラグ類
        bool HitMonster; // ボールがモンスターに当たっているときtrue
        int WaitCounter; // 処理を一時中断するためのカウンター
        bool GoalIn; // ボールがゴールに入っているときにtrue
        bool HitGoal; // ボールがゴールポストに当たっているときにtrue

        // 加速度計用フィールドの定義
        //Accelerometer MyAccelerometer;

        // シュートした時間
        const int MaxShoot = 200;
        DateTime[] ShootTime = new DateTime[MaxShoot];
        bool[] GoalSuccess = new bool[MaxShoot];
        int CurrentShoot = 0;
        int Current60secGoals = 0;

        // ハイスコア
        int GoalHighScore60secGoals = 0;
        int TotalShoot = 0;
        int TotalGoal = 0;
        float CurrentShootPercent = 0;

        // フォント
        SpriteFont TextFont1;
        SpriteFont TextFont2;

        // 時間表示用
        DateTime GameStartTime;
        TimeSpan PassedTime;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            // 縦画面への変更
            graphics.SupportedOrientations = DisplayOrientation.Portrait;
            graphics.PreferredBackBufferHeight = 800;
            graphics.PreferredBackBufferWidth = 480;

            // 全画面モードへの変更
            // (電池の残量や時間を表示しない)
            graphics.IsFullScreen = true;

            // Windows Phone のフレーム レートは既定で 30 fps です。
            TargetElapsedTime = TimeSpan.FromTicks(333333);

            // ロック中のバッテリ寿命を延長する。
            InactiveSleepTime = TimeSpan.FromSeconds(1);
        }

        /// <summary>
        /// ゲームの開始前に実行する必要がある初期化を実行できるようにします。
        /// ここで、要求されたサービスを問い合わせて、非グラフィック関連のコンテンツを読み込むことができます。
        /// base.Initialize を呼び出すと、任意のコンポーネントが列挙され、
        /// 初期化もされます。
        /// </summary>
        protected override void Initialize()
        {
            // TODO: ここに初期化ロジックを追加します。
            // ジェスチャーの有効化
            TouchPanel.EnabledGestures = GestureType.Flick | GestureType.Tap;

            // 加速度計の使用
            //MyAccelerometer = new Accelerometer();
            //MyAccelerometer.Start();

            // フィールド類の初期化
            InitializeData();

            // シュートした時間の初期化
            for (int i = 0; i < MaxShoot; i++)
            {
                ShootTime[i] = new DateTime(0);
                GoalSuccess[i] = false;
            }

            // ゲーム開始時の時間
            GameStartTime = DateTime.Now;

            LoadData();

            base.Initialize();
        }

        private void InitializeData()
        {
            // 表示する画像の初期値
            BallLocation = new Vector2((480 - 60) / 2, 800 - 60);
            MonsterLocation = new Vector2((480 - 200) / 2, 200);

            // ボールの速さの初期化
            BallSpeed = new Vector2(0, 0);
            Random rnd = new Random();
            MonsterSpeed = new Vector2(rnd.Next(21) - 10, rnd.Next(11) - 5);

            // 各種フラグの初期化
            HitMonster = false;
            WaitCounter = 0;
            GoalIn = false;
            HitGoal = false;

        }

        /// <summary>
        /// LoadContent はゲームごとに 1 回呼び出され、ここですべてのコンテンツを
        /// 読み込みます。
        /// </summary>
        protected override void LoadContent()
        {
            // 新規の SpriteBatch を作成します。これはテクスチャーの描画に使用できます。
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: this.Content クラスを使用して、ゲームのコンテンツを読み込みます。
            // 画像データの読み込み
            Ball = Content.Load<Texture2D>("ball");
            Goal = Content.Load<Texture2D>("goal");
            Monster = Content.Load<Texture2D>("monster");

            TextFont1 = Content.Load<SpriteFont>("SpriteFont1");
            TextFont2 = Content.Load<SpriteFont>("SpriteFont2");
        }

        /// <summary>
        /// UnloadContent はゲームごとに 1 回呼び出され、ここですべてのコンテンツを
        /// アンロードします。
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: ここで ContentManager 以外のすべてのコンテンツをアンロードします。
        }

        /// <summary>
        /// ワールドの更新、衝突判定、入力値の取得、オーディオの再生などの
        /// ゲーム ロジックを、実行します。
        /// </summary>
        /// <param name="gameTime">ゲームの瞬間的なタイミング情報</param>
        protected override void Update(GameTime gameTime)
        {
            // ゲームの終了条件をチェックします。
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // TODO: ここにゲームのアップデート ロジックを追加します。
            // 経過時間の計算
            PassedTime = DateTime.Now - GameStartTime;

            // タッチパネルの状況を検知する
            while (TouchPanel.IsGestureAvailable)
            {
                GestureSample gs = TouchPanel.ReadGesture();

                // 画面がフリックされたかどうか
                if (gs.GestureType == GestureType.Flick)
                {
                    // もしボールが動いていないときは、フリックに合わせてボールを動かす
                    if (BallSpeed.X == 0 && BallSpeed.Y == 0)
                    {
                        BallSpeed.X = gs.Delta.X / 50;
                        BallSpeed.Y = gs.Delta.Y / 50;

                        // シュートの情報を記録
                        TotalShoot++;

                        CurrentShoot++;
                        if (CurrentShoot > MaxShoot)
                            CurrentShoot = 0;

                        ShootTime[CurrentShoot] = DateTime.Now;

                        break;
                    }
                }
                else if (gs.GestureType == GestureType.Tap && WaitCounter > 0)
                {
                    WaitCounter = 0;
                }
            }

            // ボールが止まった後、少し待つ
            if (WaitCounter > 0)
            {
                WaitCounter--;

                CurrentShootPercent = (float)TotalGoal / (float)TotalShoot * 100f;
                return;
            }

            // もしゴールしたりモンスターに当たったときに、データを初期化する
            if (HitGoal || HitMonster || GoalIn)
                InitializeData();

            // ボールの位置の計算
            BallLocation.X += BallSpeed.X;
            BallLocation.Y += BallSpeed.Y;

            // 加速度計の値によって、X座標を変更する
            //BallLocation.X += MyAccelerometer.CurrentValue.Acceleration.X * 10;

            // 乱数発生用のフィールド
            Random rnd = new Random();

            // 15分の1の確率で、モンスターの移動方向を変える
            if (rnd.Next(15) == 0)
            {
                // X方向の移動距離として-10から10の乱数、Y方向は-5から5の乱数を作成
                MonsterSpeed.X = rnd.Next(21) - 10;
                MonsterSpeed.Y = rnd.Next(11) - 5;
            }

            // モンスターの位置の計算
            MonsterLocation.X += MonsterSpeed.X;
            MonsterLocation.Y += MonsterSpeed.Y;

            // X座標が一定値を超えたときは移動方向を反転させる
            if (MonsterLocation.X < 30 || MonsterLocation.X > 450 - Monster.Width)
                MonsterSpeed.X = -MonsterSpeed.X;

            // Y座標が一定値を超えたときは移動方向を反転させる
            if (MonsterLocation.Y < 200 || MonsterLocation.Y > 400)
                MonsterSpeed.Y = -MonsterSpeed.Y;

            // 衝突判定用の長方形の定義
            Rectangle ballRect = new Rectangle((int)BallLocation.X, (int)BallLocation.Y,
                Ball.Width, Ball.Height);
            int margin = 15;
            Rectangle monsterRect = new Rectangle((int)MonsterLocation.X + margin, (int)MonsterLocation.Y + margin,
                Monster.Width - margin * 2, Monster.Height - margin * 2);

            // 2つの長方形が触れているかどうかの確認
            if (ballRect.Intersects(monsterRect))
            {
                // 2つの長方形が接触していたとき
                HitMonster = true;
                WaitCounter = 90; // 3秒間待つ

                GoalSuccess[CurrentShoot] = false;
                Calc60secShoot();

            }
            else if (BallLocation.Y < 120)
            {
                // ボールのY座標が120未満のときは、ゴールに入ったとみなす。
                // ただし、左右50ピクセル未満にボールがあったときは、
                // ゴールポストに当たったとみなす
                if (BallLocation.X < 50 || BallLocation.X > 430 - Ball.Width)
                {
                    HitGoal = true;
                    GoalSuccess[CurrentShoot] = false;
                    Calc60secShoot();
                }
                else
                {
                    GoalIn = true;
                    TotalGoal++;
                    GoalSuccess[CurrentShoot] = true;
                    Calc60secShoot();
                }

                WaitCounter = 90;
            }

            // ボールが画面からはみ出したときは、ゴールポストに当たった処理と
            // 同じ処理を行う
            if (BallLocation.X < 0 - Ball.Width || BallLocation.X > 480
                || BallLocation.Y > 800)
            {
                HitGoal = true;
                WaitCounter = 90;
            }

            base.Update(gameTime);
        }

        private void Calc60secShoot()
        {
            int i;
            TimeSpan min = new TimeSpan(0, 1, 0);
            Current60secGoals = 0;

            DateTime currentTime = DateTime.Now;

            for (i = 0; i < MaxShoot; i++)
            {
                if (GoalSuccess[i] && (currentTime.Subtract(ShootTime[i]) < min))
                    Current60secGoals++;
            }

            if (Current60secGoals > GoalHighScore60secGoals)
                GoalHighScore60secGoals = Current60secGoals;

            SaveData();

        }

        private void SaveData()
        {
            // このアプリケーションの分離ストレージの取得
            IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication();
            // data.txtというファイルを作る
            IsolatedStorageFileStream isfs = new IsolatedStorageFileStream("data.txt", FileMode.Create, isf);
            // ファイルに書き込むための準備をする
            StreamWriter writer = new StreamWriter(isfs);

            // ファイルに書き込む
            writer.WriteLine(TotalShoot.ToString());
            writer.WriteLine(TotalGoal.ToString());
            writer.WriteLine(GoalHighScore60secGoals.ToString());

            writer.Flush();
            // ファイルを閉じる
            writer.Close();
        }

        private void LoadData()
        {
            // このアプリケーションの分離ストレージの取得
            IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication();

            // 分離ストレージにdata.txtというファイルがあるかどうかの確認
            if (isf.FileExists("data.txt"))
            {
                // data.txtがある場合、そのファイルを開く
                IsolatedStorageFileStream isfs = new IsolatedStorageFileStream("data.txt", FileMode.Open, isf);
                // ファイルの内容を読み込む準備をする
                StreamReader reader = new StreamReader(isfs);

                // ファイルに記述されている文字列を読んで、int型に変換する
                TotalShoot = int.Parse(reader.ReadLine());
                TotalGoal = int.Parse(reader.ReadLine());
                GoalHighScore60secGoals = int.Parse(reader.ReadLine());

                // ファイルを閉じる
                reader.Close();
            }
        }

        /// <summary>
        /// ゲームが自身を描画するためのメソッドです。
        /// </summary>
        /// <param name="gameTime">ゲームの瞬間的なタイミング情報</param>
        protected override void Draw(GameTime gameTime)
        {
            // 画面の背景を深緑で描画する
            GraphicsDevice.Clear(Color.DarkGreen); // この行も変更

            // TODO: ここに描画コードを追加します。
            // 描画開始メソッドの呼び出し
            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);

            string s;

            s = PassedTime.Minutes.ToString("00") + ":" + PassedTime.Seconds.ToString("00");

            // 時間の描画
            if (PassedTime.TotalHours < 1)
                spriteBatch.DrawString(TextFont2, s, new Vector2(0, 780), Color.LightGreen);
            else if (PassedTime.TotalDays < 1)
                spriteBatch.DrawString(TextFont2, PassedTime.Hours.ToString("00") + ":" + s, new Vector2(0, 760), Color.LightGreen);
            else
                spriteBatch.DrawString(TextFont2, PassedTime.Days.ToString() + "day(s) " + PassedTime.Hours.ToString("00") + ":" + s, new Vector2(0, 760), Color.LightGreen);

            // ゴールの描画
            // ゴールしたとき、ゴールポストに当たっているときは色を変える
            if (GoalIn)
                spriteBatch.Draw(Goal, new Vector2(40, 0), Color.Yellow);
            else if (HitGoal)
                spriteBatch.Draw(Goal, new Vector2(40, 0), Color.Red);
            else
                spriteBatch.Draw(Goal, new Vector2(40, 0), Color.White);

            // モンスターの描画
            // ボールがモンスターに当たっているときは色を変える
            if (HitMonster)
                spriteBatch.Draw(Monster, MonsterLocation, Color.DarkRed);
            else
                spriteBatch.Draw(Monster, MonsterLocation, Color.White);

            // ボールの描画
            // ボールがモンスターに当たっているときは色を変える
            if (HitMonster)
                spriteBatch.Draw(Ball, BallLocation, Color.Red);
            else
                spriteBatch.Draw(Ball, BallLocation, Color.White);

            if (WaitCounter > 5)
            {
                if (TotalShoot > 1)
                    spriteBatch.DrawString(TextFont1, "Total Shoots : " + TotalShoot.ToString(), new Vector2(35, 300), Color.Aqua);
                else
                    spriteBatch.DrawString(TextFont1, "Total Shoot : " + TotalShoot.ToString(), new Vector2(35, 300), Color.Aqua);

                if (TotalGoal > 1)
                    spriteBatch.DrawString(TextFont1, "Total Goals : " + TotalGoal.ToString(), new Vector2(35, 350), Color.Aqua);
                else
                    spriteBatch.DrawString(TextFont1, "Total Goal : " + TotalGoal.ToString(), new Vector2(35, 350), Color.Aqua);

                spriteBatch.DrawString(TextFont1, "% of Success : " + CurrentShootPercent.ToString("0.00") + "%", new Vector2(35, 400), Color.Beige);

                spriteBatch.DrawString(TextFont1, "Goals in latest 1 minute:", new Vector2(5, 550), Color.LightCyan);

                if (Current60secGoals > 1)
                    spriteBatch.DrawString(TextFont1, Current60secGoals.ToString() + " Goals", new Vector2(170, 600), Color.LightCyan);
                else
                    spriteBatch.DrawString(TextFont1, Current60secGoals.ToString() + " Goal", new Vector2(190, 600), Color.LightCyan);

                if (GoalHighScore60secGoals > 1)
                    spriteBatch.DrawString(TextFont1, "High Score : " + GoalHighScore60secGoals.ToString() + " Goals", new Vector2(35, 650), Color.Beige);
                else
                    spriteBatch.DrawString(TextFont1, "High Score : " + GoalHighScore60secGoals.ToString() + " Goal", new Vector2(50, 650), Color.Beige);

            }

            // 描画終了メソッドの呼び出し
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

 

次回以降で、移植のポイントを順次説明していきます。

 

[前後の記事]
第2回 : Windows PhoneのXNAとWindowsストア アプリの違い
番外1 : Shoot Evo リリース1を公開
番外2 : Shoot Evoのソースコード / プロジェクト ファイルを公開

日本マイクロソフト
田中達彦