弹球游戏也算是经典小游戏之一,这次我试着自己写一个弹球小游戏。
新建项目: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 弹球
{
///
/// 边界:为简化问题,场景中每个物体都有其矩形碰撞边界
///
public struct Bounds
{
///
/// 左上方的顶点X坐标
///
public int Left;
///
/// 左上方的顶点Y坐标
///
public int Top;
///
/// 宽度
///
public int Right;
///
/// 高度
///
public int Bottom;
///
/// 构造函数:设置顶点位置,长宽
///
///
///
///
///
public Bounds(int iX = 0, int iY = 0, int iWidth = 0, int iHeight = 0)
{
Left = iX;
Top = iY;
Right = iX + iWidth;
Bottom = iY + iHeight;
}
///
/// 构造函数:根据另一边界信息创建边界
///
///
public Bounds(Bounds Other)
{
Left = Other.Left;
Top = Other.Top;
Right = Other.Right;
Bottom = Other.Bottom;
}
///
/// 设置顶点位置,长宽,经计算得到上下左右四个值
///
///
///
///
///
public void SetBounds(int iX, int iY, int iWidth = 0, int iHeight = 0)
{
Left = iX;
Top = iY;
Right = iX + iWidth;
Bottom = iY + iHeight;
}
///
/// 根据另一边界设置边界
///
///
public void SetBounds(Bounds Other)
{
Left = Other.Left;
Top = Other.Top;
Right = Other.Right;
Bottom = Other.Bottom;
}
///
/// 向右移动(参数为负时向左移动)
///
///
public void MoveRight(int iDistance)
{
Left += iDistance;
Right += iDistance;
}
///
/// ///
/// 向下移动(参数为负时向上移动)
///
///
///
///
public void MoveDown(int iDistance)
{
Top += iDistance;
Bottom += iDistance;
}
///
/// 向iVvector所指的方向移动,距离为iSpeed
///
///
///
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);
}
}
///
/// 枚举值方向,有五个可能的值:None, Left, Up, Right, Down
///
enum Direction { None = 0, Left = 1, Up = 2, Right = 3, Down = 4 };
}
因为后main会涉及到物体移动的问题,而一个矩形移动至少会涉及到两个值的改变,比如左右移动至少会改变left和right边界,所以封装好移动的几个函数方便使用。
方向的枚举值有五个,但是在这个程序中只会用到三个(表示拍子的移动方向),代码的冗余是挺令人不爽,但是考虑到”坦克大战“中坦克的移动方向会有五个,所以也不难忍受。
上面有一个vector2D的结构体,这其实就是矢量,因为球的速度是矢量,包括x方向的和y方向的速度,所以我写了这么一个结构体如下:
///
/// 二维向量,用来表示平面上的方向,只有方向,没有大小
///
public struct vector2D
{
///
/// 横向的值
///
private double x;
///
/// 获得横向的值
///
public double X
{
get { return x; }
}
///
/// 将横向值取反
///
public void NegateX()
{
x = -x;
}
///
/// 纵向的值
///
private double y;
///
/// 将纵向值取反
///
public void NegateY()
{
y = -y;
}
///
/// 获得纵向的值
///
public double Y
{
get { return y; }
}
///
/// 构造函数:向量为单位长度,参数只代表方向
///
///
///
public vector2D(double iX, double iY)
{
double length = Math.Sqrt(iX*iX + iY*iY);
x = iX/length;
y = iY/length;
}
///
/// 注意:向量为单位长度
///
///
///
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 弹球
{
///
/// 放置在场景中所有物体的父类,包含位置边界、碰撞边界和移动边界等基本信息。
///
abstract class Actor
{
public Actor()
{ }
///
/// 位置信息:绘图时的信息
///
public Bounds PositionBounds;
///
/// 碰撞信息:物体与其他物体发生碰撞时的信息
///
public Bounds CollisionBounds;
///
/// 移动边界信息:物体无论如何移动都不会移出此范围
///
public Bounds MoveBounds;
///
/// 构造函数:设置Actor的位置
///
///
///
public Actor(int iX, int iY)
{
PositionBounds.SetBounds(iX, iY);
CollisionBounds.SetBounds(iX, iY);
}
///
/// Actor的绘图函数,每个Actor有自己的绘图函数,当要绘制它的时候调用这个函数
///
///
abstract public void Draw(Graphics g);
abstract public void Update();
///
/// 向右移动(参数为负时向左移动)
///
///
public void MoveRight(int distance)
{
PositionBounds.MoveRight(distance);
CollisionBounds.MoveRight(distance);
}
///
/// 向下移动(参数为负时向上移动)
///
///
public void MoveDown(int distance)
{
PositionBounds.MoveDown(distance);
CollisionBounds.MoveDown(distance);
}
///
/// 检测是否与另一物体碰撞:
///
///
///
public Boolean IsCollisionDirectionWith(Actor Other)
{
return Actor.IsCollision(this, Other);
}
///
/// 静态方法:检测两个Actor是否相碰撞,如果碰撞返回true
///
///
///
///
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;
}
}
}
}