陳鍾誠

Version 1.0

C# 遊戲程式:XNA 遊戲引擎

XNA 遊戲程式架構

XNA 是微軟於 2007 年推出的遊戲設計引擎,以 C# 為主要語言。因此,您可以輕易的用 C# 在 Visual Studio 的 XNA 開發工具中撰寫 2D、3D 電腦遊戲。

XNA 所撰寫的遊戲可以在 MS. Windows 的電腦中,或者是 XBOX 遊戲機中使用,最近微軟更將 XNA 加入到 Window Phone 7 版中,讓手機也納入到 XNA 所支援的平台中。

大部分對 XNA 有興趣的人都會將焦點放在 3D 遊戲上,而這也正是 XNA 最強最好用的地方,用 XNA 寫 3D 遊戲其實相當簡單,其難度與 2D 遊戲差不多。

一個 XNA 的遊戲,是一個繼承 Microsoft.Xna.Framework.Game 這個類別的程式,其架構大致如下。

public class MyGame : Microsoft.Xna.Framework.Game 
{
        GraphicsDeviceManager graphics; // 繪圖裝置管理器
        GamePadState previousState; // XBOX 的按鈕狀態
#if !XBOX
        KeyboardState previousKeyboardState;      // 個人電腦的鍵盤狀態。
#endif
        GameObject gameObj = null;  // 模型物件
        SoundEffect sound = null; // 音效物件
        ...

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

        protected override void Initialize() // 初始化
        {
            camera = new Camera(this);
            this.Components.Add(camera);
            base.Initialize();
        }

        protected override void LoadContent()
        {
            gameObj.model = Content.Load<Model>("MyModel"); // 載入模型
            gameObj.scale = 0.1f; // 設定放大倍率
            gameObj.position = new Vector3(0.0f, 0.0f, -10.0f); // 設定起始位置
            ...
        }

        protected override void Update(GameTime gameTime)
        {
           // 1. 偵測鍵盤或滑鼠,根據輸入決定主角如何移動。
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            KeyboardState keyboardState = Keyboard.GetState();  //得到目前鍵盤每一個按鍵的狀況
            if (keyboardState.IsKeyDown(Keys.Space) && previousKeyboardState.IsKeyUp(Keys.Space))
            { 
               // 當空白鍵被按下時,...
            }

           // 2. 移動物件
           gameObj.position += gameObj.velocity;

           // 3. 播放聲音
           if (...)  explodeSound.Play();

           // 4. 呼叫基礎類別的 Update() 函數。
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            gameObj.Draw(camera);

            base.Draw(gameTime);
        }

        public void Draw(Model model)
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;

                    effect.World =
                        Matrix.CreateFromYawPitchRoll(
                        rotation.Y,
                        rotation.X,
                        rotation.Z) *

                        Matrix.CreateScale(scale) *

                        Matrix.CreateTranslation(position);

                    effect.Projection = camera.projection;
                    effect.View = camera.view;
                }
                mesh.Draw();
            }
        }
    }

在以上的程式架構中,MyGame : Microsoft.Xna.Framework.Game 這行程式宣告 MyGame 繼承 Xna.Framework.Game 這個類別,這是所有 XNA 程式共同的起點。透過這樣的繼承關係,MyGame 就只要實作 XNA 遊戲需要實作的部份,其餘的邏輯就由 Xna.Framework 幫我們處理掉了,於是、我們就只要實作以下的幾個函數,就能讓 XNA 遊戲運作起來了。

        protected override void Initialize() // 初始化
        ...

        protected override void LoadContent() // 載入資源
        ...

        protected override void Update(GameTime gameTime) // 更新畫面
        ... 

        protected override void Draw(GameTime gameTime) // 繪製畫面

通常,在 XNA 遊戲程式中,我們會在 Initialize() 進行初始化動作 (物件建立、參數設定、….),然後在 LoadContent() 函數當中載入資源 (像是 2D 的圖檔或 3D 的模型檔等)。然後、整個 XNA 程式就開始運作了,XNA 引擎會不斷的呼叫 Update 動作 (通常每秒六十次),以變更新遊戲畫面,而我們所要作的工作,就只剩下決定要如何畫圖,以及鍵盤滑鼠被按下時應該作甚麼事就行了。

如果讀者想進一步理解 XNA 的 2D 或 3D 遊戲,以下是筆者用 C# 所撰寫的幾個 XNA 遊戲程式,請讀者直接參考這些內容學習 XNA 遊戲程式的寫法。

XNA 的 2D 打飛碟遊戲

專案下載:

本節將介紹如何用微軟的 XNA 遊戲引擎設計一個 2D 的打飛碟遊戲。在該遊戲中,共有 3 隻飛碟會亂飛,玩家的符號是一個砲台,您必須發射砲彈打飛碟,並且一邊閃避飛碟,每打死一台飛碟就會加一分,一但被飛碟撞到後就會死亡,導致遊戲結束,最後看看你能得到多少分,下圖顯示了遊戲的一個擷取畫面。

設計方法:XNA 遊戲的主要架構包含載入 (LoadContent)、更新 (Update) 、繪出 (Draw) 等三個動作,以下是本遊戲中的主要程式片斷。

資料結構

    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        Sprite playerSprite;
        const int MAX_MISSILES = 5, MAX_ENEMIES = 3;
        Sprite[] missileSprites = new Sprite[MAX_MISSILES];
        Sprite[] enemySprites = new Sprite[MAX_ENEMIES];
        Vector2 screenSize;
        Texture2D playerTexture, enemyTexture, missileTexture;
        KeyboardState previousKeyboardState = Keyboard.GetState();
        bool isGameOver = false;
        Random random = new Random();
        SpriteFont font;
        int score = 0;

載入資源 (圖檔+字型)

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here
            screenSize = new Vector2(graphics.GraphicsDevice.Viewport.Width, graphics.GraphicsDevice.Viewport.Height);
            playerTexture = Content.Load<Texture2D>("player");
            enemyTexture = Content.Load<Texture2D>("enemy");
            missileTexture = Content.Load<Texture2D>("missile");

            playerSprite = new Sprite(playerTexture, new Vector2(300, 400));
            playerSprite.size.Y = 140;
            for (int i = 0; i < MAX_MISSILES; i++)
            {
                missileSprites[i] = new Sprite(missileTexture, new Vector2(0, 0));
                missileSprites[i].isAlive = false;
            }
            for (int i = 0; i < MAX_ENEMIES; i++)
            {
                enemySprites[i] = new Sprite(enemyTexture, randomEnemyXY());
                enemySprites[i].velocity = new Vector2(random.Next(-3, 3), random.Next(-3, 3));
                enemySprites[i].isAlive = false;
            }

            font = Content.Load<SpriteFont>("SpriteFont");
            base.LoadContent();
        }

        public Vector2 randomEnemyXY() 
        {
           int enemyX = random.Next(0, (int) (screenSize.X-100) );
           int enemyY = random.Next(0);
           return new Vector2(enemyX, enemyY);
        }

更新邏輯模型

        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // TODO: Add your update logic here
            if (isGameOver) return;
#if !XBOX
            KeyboardState keyboardState = Keyboard.GetState();
            if (keyboardState.IsKeyDown(Keys.Left))  playerSprite.velocity = new Vector2(-5, 0);
            if (keyboardState.IsKeyDown(Keys.Right)) playerSprite.velocity = new Vector2(5, 0);
            if (keyboardState.IsKeyDown(Keys.Up)) playerSprite.velocity = new Vector2(0, -5);
            if (keyboardState.IsKeyDown(Keys.Down)) playerSprite.velocity = new Vector2(0, 5);
            if (keyboardState.IsKeyDown(Keys.Space) && previousKeyboardState.IsKeyUp(Keys.Space))
            {
                float x = playerSprite.position.X + playerTexture.Width/2 - missileTexture.Width/2;
                float y = playerSprite.position.Y;
                fireMissile(x, y);
            }
            previousKeyboardState = keyboardState;
#endif
            Rectangle playerRect = new Rectangle((int)playerSprite.position.X + 20, (int)playerSprite.position.Y + 20,
                                                 playerTexture.Width-40, playerTexture.Height/2);
            foreach (Sprite enemy in enemySprites) 
            {
                if (!enemy.isAlive)
                {
                    enemy.isAlive = true;
                    enemy.position = randomEnemyXY();
                }

                enemy.Move(screenSize);
                Rectangle enemyRect = new Rectangle((int) enemy.position.X+20, (int) enemy.position.Y+20, 
                                                    enemyTexture.Width-40, enemyTexture.Height-40);
                if (enemyRect.Intersects(playerRect))
                    isGameOver = true;

                foreach (Sprite missile in missileSprites)
                {
                    if (missile.isInside(screenSize))
                        missile.Move(screenSize);
                    else
                        missile.isAlive = false;

                    if (missile.isAlive)
                    {
                        Rectangle missileRect = new Rectangle((int) missile.position.X, (int) missile.position.Y, 
                                                              missileTexture.Width, missileTexture.Height);
                        if (missileRect.Intersects(enemyRect))
                        {
                            enemy.isAlive = false;
                            score ++;
                        }
                    }
                }
            }
            
            playerSprite.Move(screenSize);

            base.Update(gameTime);
        }

        public void fireMissile(float x, float y)
        {
            foreach (Sprite missile in missileSprites)
            {
                if (!missile.isAlive)
                {
                    missile.isAlive = true;
                    missile.position = new Vector2(x, y);
                    missile.velocity = new Vector2(0, -5);
                    return;
                }
            }
        }

繪出 - 更新畫面

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here
            spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
            playerSprite.draw(spriteBatch);
            foreach (Sprite enemy in enemySprites)
                enemy.draw(spriteBatch);
            foreach (Sprite missile in missileSprites)
            {
                if (missile.isAlive)
                    missile.draw(spriteBatch);
            }
            spriteBatch.DrawString(font, String.Format("Score:{0}", score), new Vector2(10, 10), Color.Black);
            if (isGameOver)
                spriteBatch.DrawString(font, "Game Over!", new Vector2(screenSize.X / 2, screenSize.Y / 2), Color.Black);
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }

透過以上程式,我們示範了如何使用 XNA 設計一個簡單的小遊戲 『打飛碟』。透過這樣的範例,應該可以呈現出 XNA 的 2D 遊戲設計主要架構。

XNA 的 3D 遊戲架構

在 3D 的世界裏,每個物件都有其 3D 座標,而且最後還是必須要透過某個相機將整個視角畫面呈現在 2D 的螢幕上,因此 XNA 的 3D 遊戲設計牽涉到遊戲物件 (Game Object) 與照相機 (Camera) 等物體。

通常 XNA 程式裏的每個模型都會有對應的物件,如果把這些物件抽象化,那就會抽象出下列 GameObject 結構,於是我們就只要繼承這個物件,然後為每個物件的 model 填入適當的模型就行了。

    // 遊戲模型物件
    class GameObject
    {
        public Model model = null;
        public Vector3 position = Vector3.Zero;
        public Vector3 rotation = Vector3.Zero;
        public float scale = 1.0f;
        public Vector3 velocity = Vector3.Zero;
        public bool alive = false;

        public BoundingSphere boundingSphere()
        {
            BoundingSphere sphere = model.Meshes[0].BoundingSphere;
            sphere.Center = position;
            sphere.Radius *= scale;
            return sphere;
        }

        public void Draw(Camera camera)
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;

                    effect.World =
                        Matrix.CreateFromYawPitchRoll(
                        rotation.Y,
                        rotation.X,
                        rotation.Z) *

                        Matrix.CreateScale(scale) *

                        Matrix.CreateTranslation(position);

                    effect.Projection = camera.projection;
                    effect.View = camera.view;
                }
                mesh.Draw();
            }
        }
    }

在遊戲當中,除了模型物件之外,還有一個很特殊的物件,那就是像機,這個物件的邏輯較為獨特,幾乎有固定的寫法,以下是一個基本的像機物件之寫法。

    // 相機物件
    class Camera : Microsoft.Xna.Framework.GameComponent
    {
        public Matrix view;         //  視覺矩陣
        public Matrix projection =        //  投影矩陣
                 Matrix.CreatePerspectiveFieldOfView(    
                                  MathHelper.PiOver4,  // 視角 45度
                                  1.333f, // 螢幕 寬高比
                                  1,      // 最近的Z軸截點
                                  10000);  // 最遠的Z軸截點 

        public Vector3 FrontVector = new Vector3(0, 0, -1); //相機的 往前向量
        public Vector3 Position = new Vector3(0.0f, 2.0f, 20.0f); // 相機的位置
        public float Yaw = 0.0f, Pitch = 0.0f, Roll = 0.0f;
        public float YawDelta = 0.2f, PitchDelta = 0.2f, RollDelta = 0.2f;
        public float MoveDelta = 200;   // 前後走 的 增減量

        public Camera(Game game)
            : base(game)
        {
            // TODO: Construct any child components here

        }

        //更新 相機 的位置
        public void UpdateCamera(Vector3 Position, float Yaw, float Pitch, float Roll)
        {
            //攝影機一開始是 朝向 負 Z 軸 看著(0, 0, -1) 的 
            Vector3 LookAt = Position + Vector3.TransformNormal(FrontVector,
                                            Matrix.CreateFromYawPitchRoll(Yaw, Pitch, Roll)); 

            // 根據 相機的位置 相機要看到的點 相機上方的向量 得到 視覺矩陣
            view = Matrix.CreateLookAt(Position,  // 相機的位置
                                       LookAt,    // 相機要看到的點
                                       Vector3.Up); // 相機上方的向量
        }

        public override void Initialize()
        {
            base.Initialize();
        }

        public override void Update(GameTime gameTime)
        {
            // TODO: Add your update code here
            float elapsedTime = (float) gameTime.ElapsedGameTime.TotalSeconds;

            KeyboardState newState;  // 宣告一個KeyboardState 結構的變數
            newState = Keyboard.GetState();  //得到目前鍵盤每一個按鍵的狀況
            if (newState.IsKeyDown(Keys.Right)) Yaw -= YawDelta * elapsedTime; //右鍵按下
            if (newState.IsKeyDown(Keys.Left)) Yaw += YawDelta * elapsedTime;  //左鍵按下
            if (newState.IsKeyDown(Keys.Up)) Pitch += PitchDelta * elapsedTime;
            if (newState.IsKeyDown(Keys.Down)) Pitch -= PitchDelta * elapsedTime;
            UpdateCamera(Position, Yaw, Pitch, Roll);

            base.Update(gameTime);
        }
    }

XNA 的 3D 打飛碟遊戲

程式碼:

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.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using System.Diagnostics;


namespace _3D_FighterGame
{   
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        static void Main(string[] args)
        {
            using (Game1 game = new Game1())
            {
                game.Run();
            }
        }
    }    
    
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        GameObject launcherBase = new GameObject();
        GameObject launcherHead = new GameObject();
        Model enemyModel = null, missileModel = null;
        List<GameObject> enemies = new List<GameObject>();
        List<GameObject> missiles = new List<GameObject>();
        Camera camera = null;
        Random random = new Random(7);
        SoundEffect explodeSound = null, fireMissileSound = null;

        GamePadState previousState;
#if !XBOX
        KeyboardState previousKeyboardState;
#endif

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

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
            camera = new Camera(this);
            this.Components.Add(camera);

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here
            camera = new Camera(this);

            launcherBase.model = Content.Load<Model>("Models\\launcher_base");
            launcherBase.scale = 0.1f;
            launcherBase.position = new Vector3(0.0f, 0.0f, -10.0f);

//            launcherHead.model = Content.Load<Model>("Models\\launcher_head");
//            launcherHead.scale = 0.2f;
//            launcherHead.position = launcherBase.position + new Vector3(0.0f, 20.0f, -120.0f);

            enemyModel = Content.Load<Model>("Models\\enemy");
            missileModel = Content.Load<Model>("Models\\missile");
            explodeSound = Content.Load<SoundEffect>("Sound\\explosion");
            fireMissileSound = Content.Load<SoundEffect>("Sound\\missilelaunch");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        protected void newEnemy()
        {
            GameObject e = new GameObject();
            e.model = enemyModel;
            e.position = new Vector3(random.Next(-5, 5) * 50.0f, random.Next(-5, 5) * 50.0f, -2000.0f);
            e.velocity = new Vector3(0, 0, 2.0f);
            e.scale = 0.03f;
            enemies.Add(e);
        }

        protected void newMissile()
        {
            Matrix rotationMatrix = Matrix.CreateFromYawPitchRoll(camera.Yaw, camera.Pitch, camera.Roll);
            
            GameObject m = new GameObject();
            m.model = missileModel;
            m.position = launcherBase.position + Vector3.Transform(Vector3.Forward * 10, rotationMatrix);
            m.scale = 10.0f;
            // m.velocity = new Vector3(0, 0, -10.0f);
            m.velocity = Vector3.Transform(Vector3.Forward * 10, rotationMatrix);
            missiles.Add(m);
            fireMissileSound.Play();
        }

        protected void getLuncherBasePosition()
        {
            Matrix rotationMatrix = Matrix.CreateFromYawPitchRoll(camera.Yaw, camera.Pitch, camera.Roll);
            launcherBase.position = camera.Position + Vector3.Transform(Vector3.Forward * 50, rotationMatrix); //  + Vector3.Down * 10
            launcherBase.rotation.Y = camera.Yaw;
            launcherBase.rotation.X = camera.Pitch;
            launcherBase.rotation.Z = camera.Roll;
//            Trace.WriteLine(launcherBase.position);
        }

        double lastEnemyDelay = 0.0f;
        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            getLuncherBasePosition();

            KeyboardState keyboardState = Keyboard.GetState();  //得到目前鍵盤每一個按鍵的狀況
            if (keyboardState.IsKeyDown(Keys.Space) && previousKeyboardState.IsKeyUp(Keys.Space))
            {
                newMissile();
            }

            double elapsedTime = gameTime.ElapsedGameTime.TotalSeconds;
            lastEnemyDelay += elapsedTime;
            if (lastEnemyDelay >= 1)
            {
                if (enemies.Count < 10)
                {
                    newEnemy();
                    lastEnemyDelay = 0.0f;
                }
            }

            GameObject[] eList = enemies.ToArray();
            foreach (GameObject e in eList) 
            {
                if (e.position.Z >= 0)
                    enemies.Remove(e);
                else
                    e.position += e.velocity;
            }

            GameObject[] mList = missiles.ToArray();
            foreach (GameObject m in mList)
            {
                if (m.position.Z <= -3000)
                    missiles.Remove(m);
                else
                    m.position += m.velocity;
            }

            // TODO: Add your update logic here
            camera.Update(gameTime);

            previousKeyboardState = keyboardState;

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            // TODO: Add your drawing code here
            DrawGameObject(launcherBase);
//          DrawGameObject(launcherHead);
            foreach (GameObject e in enemies)
                DrawGameObject(e);
            foreach (GameObject m in missiles)
                DrawGameObject(m);

            base.Draw(gameTime);
        }

        void DrawGameObject(GameObject gameobject)
        {
            foreach (ModelMesh mesh in gameobject.model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;

                    effect.World =
                        Matrix.CreateFromYawPitchRoll(
                        gameobject.rotation.Y,
                        gameobject.rotation.X,
                        gameobject.rotation.Z) *

                        Matrix.CreateScale(gameobject.scale) *

                        Matrix.CreateTranslation(gameobject.position);

                    effect.Projection = camera.projection;
                    effect.View = camera.view;
                }
                mesh.Draw();
            }
        }
    }

    class GameObject
    {
        public Model model = null;
        public Vector3 position = Vector3.Zero;
        public Vector3 rotation = Vector3.Zero;
        public float scale = 1.0f;
        public Vector3 velocity = Vector3.Zero;
        public bool alive = false;
    }
    
    /// <summary>
    /// This is a game component that implements IUpdateable.
    /// </summary>
    public class GameComponent_Grid : Microsoft.Xna.Framework.DrawableGameComponent
    {
        public int gridSize = 12; // 每邊有 幾格
        public float gridScale = 1.0f; // 每格 的 寬
        public Color gridColor = new Color(0xFF, 0xFF, 0xFF, 0xFF);   // 格線 的顏色 內定是黑色

        VertexBuffer vertexBuffer; // 頂點緩衝區
        VertexDeclaration vertexDeclaration; // 頂點格式 (每個頂點 包含什麼內容)
        BasicEffect effect; // 產出時會用到的 效果
        int vertexCount;  // 頂點 數目
        GraphicsDevice device; //繪圖設備

        public Matrix world = Matrix.Identity; // 世界 觀測 投影 矩陣

        public Matrix view = Matrix.CreateLookAt(new Vector3(0.0f, 20.0f, 20.0f),
                                                      Vector3.Zero,
                                                      Vector3.Up);

        public Matrix projection = Matrix.CreatePerspectiveFieldOfView(
                                                 MathHelper.ToRadians(45.0f),
                                                 1.333f, 1.0f, 10000.0f);

        public GameComponent_Grid(Game game)
            : base(game)
        {
            // TODO: Construct any child components here
            this.device = game.GraphicsDevice;
        }

        /// <summary>
        /// Allows the game component to perform any initialization it needs to before starting
        /// to run.  This is where it can query for any required services and load content.
        /// </summary>
        public override void Initialize()
        {
            // TODO: Add your initialization code here
            effect = new BasicEffect(device, null); // 效果

            vertexCount = (gridSize + 1) * 4; // 每邊的頂點數 比 每邊的格數 多一,共有四個邊

            // 每個頂點 包含 位置 和 顏色 ,先空出 vertexCount 個 頂點
            VertexPositionColor[] vertices = new VertexPositionColor[vertexCount];

            float length = (float)gridSize * gridScale; // 邊長 等於 格數 乘以 格寬
            float halfLength = length * 0.5f;  // 半邊長 因為是要 以原點為中心 左右 各半

            int index = 0; // 頂點 索引

            // 定義頂點位置  頂都是 躺在 X Z 平面上
            for (int i = 0; i <= gridSize; ++i)
            {
                vertices[index++] = new VertexPositionColor(
                    new Vector3(-halfLength, 0.0f, i * gridScale - halfLength),
                    gridColor); // 左邊的頂點

                vertices[index++] = new VertexPositionColor(
                    new Vector3(halfLength, 0.0f, i * gridScale - halfLength),
                    gridColor); // 右邊的頂點

                vertices[index++] = new VertexPositionColor(
                    new Vector3(i * gridScale - halfLength, 0.0f, -halfLength),
                    gridColor); // 上緣的頂點

                vertices[index++] = new VertexPositionColor(
                    new Vector3(i * gridScale - halfLength, 0.0f, halfLength),
                    gridColor); // 下緣的頂點
            }

            // 建立 頂點緩衝區
            vertexBuffer = new VertexBuffer(device, vertexCount *  VertexPositionColor.SizeInBytes,
                                             BufferUsage.WriteOnly);
            // 將 頂點資料 複製入 頂點緩衝區 內
            vertexBuffer.SetData<VertexPositionColor>(vertices);

            // 頂點格式
            vertexDeclaration = new VertexDeclaration(device, VertexPositionColor.VertexElements);

            base.Initialize();
        }

        /// <summary>
        /// Allows the game component to update itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
            // TODO: Add your update code here

            base.Update(gameTime);
        }

        public override void Draw(GameTime gameTime)
        {
            
            // 效果 三大矩陣 設定
//            effect.World = world;
//            effect.View = view;
//            effect.Projection = projection;

            effect.VertexColorEnabled = true;  // 使用 頂點顏色 效果

            device.VertexDeclaration = vertexDeclaration; // 頂點格式
            device.Vertices[0].SetSource(vertexBuffer, 0, VertexPositionColor.SizeInBytes); // 頂點來源

            effect.Begin(); // 效果 開始

            foreach (EffectPass CurrentPass in effect.CurrentTechnique.Passes)
            {
                CurrentPass.Begin();
                device.DrawPrimitives(PrimitiveType.LineList, 0, vertexCount / 2); // 兩兩畫線 所以只有 vertexCount/2 條線
                CurrentPass.End();
            }
            effect.End();
        }
    }    
}

從 XNA 到 Unity3D

雖然 XNA 已經是成熟的技術,但是卻不支援像 Window Form 或 WPF 這樣所示即所得的開發方式,還好現在已經有一款稱為 Unity 遊戲開發軟體,可以讓您用所視即所得的方式開發 3D 的遊戲程式。Unity 是一款強大的工業級跨平台遊戲引擎 (Win PC, XBox, PS, Wii,Android……),具有很棒的視覺化開發環境,並且支援 C# 與 JavaScript 等開發語言,並且有免費版可以下載。

目前也已經有一些中文書介紹 Unity 的程式設計,以下是這些書的網址:

參考文獻