從無到有寫一個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;
}
}
}
}