1. 程式人生 > >從無到有寫一個C#彈球小遊戲(一)

從無到有寫一個C#彈球小遊戲(一)

彈球遊戲也算是經典小遊戲之一,這次我試著自己寫一個彈球小遊戲。



新建專案:C#->win窗體應用程式->完成,開始寫程式碼。

第一步先想一想這個遊戲都有哪些要素:一個球,一個玩家控制的球拍,一系列磚塊。他們都需要有哪些性質呢?

球:在螢幕上做直線移動,碰到別的東西反彈。

而反彈是什麼呢,反彈就是改變速度方向。如果分解速度的話可以分解成x方向和y方向的速度,因為在這個遊戲中球只可能碰到矩形物體,所以不考慮斜面。球只可能碰到與座標軸平行的邊界。這裡說的座標軸是win視窗上的座標軸,向右為x軸正向,向左為y軸正向,視窗左上角是原點。

所以可以把反彈當作如果碰到與x軸平行的邊界,那麼與y軸平行的速度的分量變為原來的負值,如果碰到與y軸平行的邊界,那麼與x軸平行的速度的分量變為原來的負值。

球拍:球拍只可能隨著玩家的控制做左右移動,而且碰到移動的邊界的話就停止繼續往前走。

磚塊:磚塊靜止在場景中,如果被別的物體(球)碰撞就會消失。

因為C#是面向物件的,所以以上的三個東西就是三個class,而且顯而易見的是他們有一些共同的性質,所以可以由一個class派生出來。

他們有什麼共同的性質呢?

三個東西都得在場景中繪製出來,都可能與別的東西碰撞。

這就是一些性質:位置,邊界,繪製,碰撞。

所以基類就應該有這些欄位:位置資訊,碰撞資訊,邊界資訊。

這些方法:繪圖,運動,是否碰撞。

位置,碰撞,邊界三個東西說白了就是三個不同的矩形,只不過功能不太一樣。

位置矩形確定了這個物體應該在什麼地方畫,碰撞矩形確定了這個物體怎麼與別的東西碰撞。很多人認為這兩個矩形應該是一回事,實際上在彈球這個遊戲中這兩個矩形確實也是一回事,但是為了類的可移植性,還是同時保留這兩個資訊吧。事實上,我做這個練習是為了以後寫一個類似的“坦克大戰”小遊戲聯絡一下,畢竟剛開始學C#。

邊界其實就是一個移動範圍,確定了這個邊界之後,這個物體就只能在這個邊界範圍內移動。也許會有人覺得所有物體共用一個邊界就行了,何必每個物體儲存自己的邊界資訊呢,這不是浪費空間嗎?在這個遊戲中確實也是所有物體用同一個邊界資訊的,可是在別的地方,比如有一個小怪,預設的狀態是在城堡的門口巡邏,來來回回來來回回,這個時候這個資訊就有用了,這個小怪的邊界就是城堡門口的一片地方。

考慮到這三種資訊都是一個矩形,所以我自己寫了一個Bounds結構體來封裝這個矩形,方便操作,程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace 彈球
{
    /// <summary>
    /// 邊界:為簡化問題,場景中每個物體都有其矩形碰撞邊界
    /// </summary>
    public struct Bounds
    {
        /// <summary>
        /// 左上方的頂點X座標
        /// </summary>
        public int Left;
        /// <summary>
        /// 左上方的頂點Y座標
        /// </summary>
        public int Top;
        /// <summary>
        /// 寬度
        /// </summary>
        public int Right;
        /// <summary>
        /// 高度
        /// </summary>
        public int Bottom;

        /// <summary>
        /// 建構函式:設定頂點位置,長寬
        /// </summary>
        /// <param name="iX"></param>
        /// <param name="iY"></param>
        /// <param name="iWidth"></param>
        /// <param name="iHeight"></param>
        public Bounds(int iX = 0, int iY = 0, int iWidth = 0, int iHeight = 0)
        {
            Left = iX;
            Top = iY;
            Right = iX + iWidth;
            Bottom = iY + iHeight;
        }

        /// <summary>
        /// 建構函式:根據另一邊界資訊建立邊界
        /// </summary>
        /// <param name="Other"></param>
        public Bounds(Bounds Other)
        {
            Left = Other.Left;
            Top = Other.Top;
            Right = Other.Right;
            Bottom = Other.Bottom;
        }

        /// <summary>
        /// 設定頂點位置,長寬,經計算得到上下左右四個值
        /// </summary>
        /// <param name="iX"></param>
        /// <param name="iY"></param>
        /// <param name="iWidth"></param>
        /// <param name="iHeight"></param>
        public void SetBounds(int iX, int iY, int iWidth = 0, int iHeight = 0)
        {
            Left = iX;
            Top = iY;
            Right = iX + iWidth;
            Bottom = iY + iHeight;
        }

        /// <summary>
        /// 根據另一邊界設定邊界
        /// </summary>
        /// <param name="Other"></param>
        public void SetBounds(Bounds Other)
        {
            Left = Other.Left;
            Top = Other.Top;
            Right = Other.Right;
            Bottom = Other.Bottom;
        }

        /// <summary>
        /// 向右移動(引數為負時向左移動)
        /// </summary>
        /// <param name="iDistance"></param>
        public void MoveRight(int iDistance)
        {
            Left += iDistance;
            Right += iDistance;
        }

        /// <summary>
        /// /// <summary>
        /// 向下移動(引數為負時向上移動)
        /// </summary>
        /// <param name="distance"></param>
        /// </summary>
        /// <param name="iDistance"></param>
        public void MoveDown(int iDistance)
        {
            Top += iDistance;
            Bottom += iDistance;
        }

        /// <summary>
        /// 向iVvector所指的方向移動,距離為iSpeed
        /// </summary>
        /// <param name="iVector"></param>
        /// <param name="iSpeed"></param>
        public void Move(vector2D iVector, int iSpeed)
        {
            Left += (int)Math.Ceiling(iVector.X * iSpeed);
            Right += (int)Math.Ceiling(iVector.X * iSpeed);

            Top += (int)Math.Ceiling(iVector.Y * iSpeed);
            Bottom += (int)Math.Ceiling(iVector.Y * iSpeed);
        }
    }

    /// <summary>
    /// 列舉值方向,有五個可能的值:None, Left, Up, Right, Down
    /// </summary>
    enum Direction { None = 0, Left = 1, Up = 2, Right = 3, Down = 4 };

}

因為後main會涉及到物體移動的問題,而一個矩形移動至少會涉及到兩個值的改變,比如左右移動至少會改變left和right邊界,所以封裝好移動的幾個函式方便使用。

方向的列舉值有五個,但是在這個程式中只會用到三個(表示拍子的移動方向),程式碼的冗餘是挺令人不爽,但是考慮到”坦克大戰“中坦克的移動方向會有五個,所以也不難忍受。

上面有一個vector2D的結構體,這其實就是向量,因為球的速度是向量,包括x方向的和y方向的速度,所以我寫了這麼一個結構體如下:

    /// <summary>
    /// 二維向量,用來表示平面上的方向,只有方向,沒有大小
    /// </summary>
    public struct vector2D
    {
        /// <summary>
        /// 橫向的值
        /// </summary>
        private double x;

        /// <summary>
        /// 獲得橫向的值
        /// </summary>
        public double X
        {
            get { return x; }
        }

        /// <summary>
        /// 將橫向值取反
        /// </summary>
        public void NegateX()
        {
            x = -x;
        }

        /// <summary>
        /// 縱向的值
        /// </summary>
        private double y;

        /// <summary>
        /// 將縱向值取反
        /// </summary>
        public void NegateY()
        {
            y = -y;
        }

        /// <summary>
        /// 獲得縱向的值
        /// </summary>
        public double Y
        {
            get { return y; }
        }


        /// <summary>
        /// 建構函式:向量為單位長度,引數只代表方向
        /// </summary>
        /// <param name="iX"></param>
        /// <param name="iY"></param>
        public vector2D(double iX, double iY)
        {
            double length = Math.Sqrt(iX*iX + iY*iY);
            x = iX/length;
            y = iY/length;
        }

        /// <summary>
        /// 注意:向量為單位長度
        /// </summary>
        /// <param name="iX"></param>
        /// <param name="iY"></param>
        public void setValue(double iX, double iY)
        {
            double length = Math.Sqrt(iX * iX + iY * iY);
            x = iX / length;
            y = iY / length;
        }
    }

經過上面的鋪墊,那三個類的父類程式碼終於出來了,如下:

using System;
using System.Drawing;

namespace 彈球
{
    /// <summary>
    /// 放置在場景中所有物體的父類,包含位置邊界、碰撞邊界和移動邊界等基本資訊。
    /// </summary>
    abstract class Actor
    {
        public Actor()
        { }

        /// <summary>
        /// 位置資訊:繪圖時的資訊
        /// </summary>
        public Bounds PositionBounds;

        /// <summary>
        /// 碰撞資訊:物體與其他物體發生碰撞時的資訊
        /// </summary>
        public Bounds CollisionBounds;

        /// <summary>
        /// 移動邊界資訊:物體無論如何移動都不會移出此範圍
        /// </summary>
        public Bounds MoveBounds;

        /// <summary>
        /// 建構函式:設定Actor的位置
        /// </summary>
        /// <param name="iX"></param>
        /// <param name="iY"></param>
        public Actor(int iX, int iY)
        {
            PositionBounds.SetBounds(iX, iY);
            CollisionBounds.SetBounds(iX, iY);
        }

        /// <summary>
        /// Actor的繪圖函式,每個Actor有自己的繪圖函式,當要繪製它的時候呼叫這個函式
        /// </summary>
        /// <param name="g"></param>
        abstract public void Draw(Graphics g);

        abstract public void Update();

        /// <summary>
        /// 向右移動(引數為負時向左移動)
        /// </summary>
        /// <param name="distance"></param>
        public void MoveRight(int distance)
        {
            PositionBounds.MoveRight(distance);
            CollisionBounds.MoveRight(distance);
        }

        /// <summary>
        /// 向下移動(引數為負時向上移動)
        /// </summary>
        /// <param name="distance"></param>
        public void MoveDown(int distance)
        {
            PositionBounds.MoveDown(distance);
            CollisionBounds.MoveDown(distance);
        }

        /// <summary>
        /// 檢測是否與另一物體碰撞:
        /// </summary>
        /// <param name="Other"></param>
        /// <returns></returns>
        public Boolean IsCollisionDirectionWith(Actor Other)
        {
            return Actor.IsCollision(this, Other);
        }

        /// <summary>
        /// 靜態方法:檢測兩個Actor是否相碰撞,如果碰撞返回true
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static Boolean IsCollision(Actor a, Actor b)
        {
            if (a.CollisionBounds.Right < b.CollisionBounds.Left ||
                a.CollisionBounds.Bottom < b.CollisionBounds.Top ||
                b.CollisionBounds.Right < a.CollisionBounds.Left ||
                b.CollisionBounds.Bottom < a.CollisionBounds.Top)
            {
                return false;
            }
            else
            {
                return true;
            }
        }
    }
}