自己动手写游戏:Flappy Bird
START:最近闲来无事,看了看一下《C#开发Flappy Bird游戏》的教程,自己也试着做了一下,实现了一个超级简单版(十分简陋)的Flappy Bird,使用的语言是C#,技术采用了快速简单的WindowsForm,图像上主要是采用了GDI+,游戏对象的创建控制上使用了单例模式,现在我就来简单地总结一下。
一、关于Flappy Bird
《Flappy Bird》是由来自越南的独立游戏开发者Dong Nguyen所开发的作品,游戏中玩家必须控制一只小鸟,跨越由各种不同长度水管所组成的障碍,而这只鸟其实是根本不会飞的……所以玩家每点击一下小鸟就会飞高一点,不点击就会下降,玩家必须控制节奏,拿捏点击屏幕的时间点,让小鸟能在落下的瞬间跳起来,恰好能够通过狭窄的水管缝隙,只要稍一分神,马上就会失败阵亡。简单但不粗糙的8比特像素画面、超级马里奥游戏中的水管、眼神有点呆滞的小鸟和几朵白云,白天夜晚两种模式便构成了游戏的一切。玩家需要不断控制点击屏幕的频率来调节小鸟的飞行高度和降落速度,让小鸟顺利通过画面右方的管道缝隙。如果小鸟不小心擦碰到了管子的话,游戏便宣告结束。
二、游戏设计
2.1 总结游戏印象
玩过的Flappy Bird的童鞋们应该都对这款游戏有印象,现在我们来看看这款游戏的特点:
(1)这款游戏的画面很简单:一张背景图,始终就没有变过;
(2)这款游戏的对象只有俩:一个小鸟(有三种挥动翅膀的状态)以及一对管道(有管道向上和向下两个方向);
小鸟:①②③
管道:
2.2 总结设计思路
(1)万物皆对象
在整个游戏中,我们看到的所有内容,我们都可以理解为游戏对象;(在Unity中,GameObject即游戏对象)每一个游戏对象,都由一个单独的类来创建;在游戏中,总共只有两个游戏对象:小鸟和管道,那么我们就可以创建两个类:Bird和Pipe。但是,我们发现小鸟和管道都有一些共同的属性和方法,例如X,Y轴坐标,长度和宽度,以及绘制(Draw())和移动(Move())的方法,这时我们可以设计一个抽象类,将共有的东西封装起来,减少开发时的冗余代码,提高程序的可扩展性,符合面向对象设计的思路:
(2)计划生育好
在整个游戏中,我们的小鸟对象只有一个,也就是说在内存中只需要存一份即可。这时,我们想到了伟大的计划生育政策,于是我们想到了使用单例模式。借助单例模式,可以保证只生成一个小鸟的实例,即为程序提供一个全局访问点,避免重复创建浪费不必要的内存。
(3)对象的运动
在整个游戏中,小鸟会受重力默认向下坠落,而用户可以根据点击或按键盘Space键使小鸟向上飞,从图像呈现上其本质就是更改游戏对象在Y轴的位置,使其从下往上移动;而管道则会从屏幕右侧出现,从屏幕左侧消失,又从屏幕右侧出现,再从屏幕左侧消失,一直循环往复。可以看到,从图像呈现上期本质就是更改管道对象在X轴的位置,使其从右往左移动。
(4)设计流程图
在整个开发设计过程中,我们可以根据优先级设计开发流程,根据流程一步一步地实现整个游戏。
三、关键代码
3.1 设计抽象父类封装共有属性
/// <summary> /// 游戏对象基类 /// </summary> public abstract class GameObject { #region 01.构造函数及属性 public int X { get; set; } public int Y { get; set; } public int Width { get; set; } public int Height { get; set; } public GameObject(int x, int y) { this.X = x; this.Y = y; this.Width = this.Height = 0; } public GameObject(int x, int y, int width, int height) { this.X = x; this.Y = y; this.Width = width; this.Height = height; } #endregion #region 02.抽象方法 /// <summary> /// 抽象方法1:绘制自身 /// </summary> public abstract void Draw(Graphics g); /// <summary> /// 抽象方法2:移动自身 /// </summary> public abstract void Move(); #endregion #region 03.实例方法 public Rectangle GetRectangeleArea() { return new Rectangle(this.X, this.Y, this.Width, this.Height); } #endregion }
一切皆对象,这里封装了游戏对象小鸟和管道共有的属性,以及两个抽象方法,让小鸟和管道自己去实现。
3.2 设计单例模式减少对象创建
/// <summary> /// 小鸟对象单例模式类 /// </summary> public class SingleObject { private SingleObject() { } private static SingleObject singleInstance; public static SingleObject GetInstance() { if (singleInstance == null) { singleInstance = new SingleObject(); } return singleInstance; } public Bird SingleBird { get; set; } /// <summary> /// 添加游戏对象 /// </summary> /// <param name="parentObject">游戏对象父类</param> public void AddGameObject(GameObject parentObject) { if(parentObject is Bird) { SingleBird = parentObject as Bird; } } /// <summary> /// 绘制游戏对象 /// </summary> /// <param name="g"></param> public void DrawGameObject(Graphics g) { SingleBird.Draw(g); } }
这里借助单例模式使小鸟实例始终只有一个,实现上主要是将小鸟类和单例模式聚合。
3.3 设计重力辅助类使小鸟能够自动下落
(1)设计重力辅助类
/// <summary> /// 重力辅助类 /// </summary> public class Gravity { public static float gravity = 9.8f; /// <summary> /// s = 1/2*gt^2+vt /// </summary> /// <param name="speed">速度</param> /// <param name="second">时间</param> /// <returns>位移量</returns> public static float GetHeight(float speed, float time) { float height = (float)(0.5 * gravity * time * time) + speed * time; return height; } }
在Unity游戏引擎中给游戏对象增加一个刚体组件就可以使游戏对象受重力影响,但是在普通的程序中需要自己设计重力类使游戏对象受重力影响下落。这里使用中学物理的知识:求重力加速度的位移量;
(2)在定时器事件中使小鸟承受重力影响始终下落
private void GravityTimer_Tick(object sender, EventArgs e) { Bird singleBird = SingleObject.GetInstance().SingleBird; // Step1:获得小鸟下降的高度 float height = Gravity.GetHeight(singleBird.CurrentSpeed, singleBird.DurationTime * 0.001f); // singleBird.DurationTime * 0.001f => 将毫秒转换成帧 // Step2:获得小鸟下落后的坐标 int y = singleBird.Y + (int)height; // Step3:将新Y轴坐标赋给小鸟 int min = this.Size.Height - this.pbxGround.Height - 60; if (y > min) { // 限定小鸟不要落到地面下 y = min; } singleBird.Y = y; // Step4:使小鸟按照加速度下降 [ 公式:v=v0+at ] singleBird.CurrentSpeed = singleBird.CurrentSpeed + Gravity.gravity * singleBird.DurationTime * 0.001f; }
这里重点是将毫秒转换为帧,实现上是使DurationTime*0.001f使速度减慢;
3.4 设计碰撞检测方法使游戏能够终结
(1)Rectangle的IntersectsWith方法
在游戏界面中,任何一个游戏对象我们都可以视为一个矩形区域(Rectangle类实例),它的坐标是X轴和Y轴,它还有长度和宽度,可以轻松地确定一个它所在的矩形区域。那么,我们可以通过Rectangle的IntersectsWith方法确定两个Rectangle是否存在重叠,如果有重叠,此方法将返回 true;否则将返回 false。那么,在FlappyBird中主要是判断两种情况:一是小鸟是否飞到边界(屏幕的上方和下方),二是小鸟是否碰到了管道(向上的管道和向下的管道)。
(2)在定时器事件中循环判断小鸟是否碰到边界或管道
private void PipeTimer_Tick(object sender, EventArgs e) { // 移动管道 this.MovePipeLine(); // 碰撞检测 Bird bird = SingleObject.GetInstance().SingleBird; if (bird.Y == 0 || bird.Y == this.pbxGround.Height || bird.GetRectangeleArea() .IntersectsWith(pipeDown.GetRectangeleArea()) || bird.GetRectangeleArea() .IntersectsWith(pipeUp.GetRectangeleArea())) { // 暂停游戏 this.PauseGame(); if (MessageBox.Show("您已挂了,是否购买王胖子的滑板鞋继续畅玩?", "温馨提示", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { // 重新初始化游戏对象 this.InitialGameObjects(); // 重新开始游戏 this.RestoreGame(); } else { MessageBox.Show("您的选择是明智的,王胖子的滑板鞋太挫了!", "温馨提示", MessageBoxButtons.OK, MessageBoxIcon.Information); Environment.Exit(0); } } }
四、开发小结
从运行效果可以看出,此次DEMO主要完成了几个比较核心的内容:一是小鸟和管道的移动,二是小鸟和边界(最上方和最下方以及管道)的碰撞检测。当然,还有很多核心的内容没有实现,比如:计算通过的管道数量、游戏欢迎界面和结束界面等。希望有兴趣的童鞋可以去继续完善实现,这里提供一个我的Flappy Bird实现仅供参考,谢谢!
参考资料
赵剑宇,《C#开发史上最虐人游戏-Flappy Bird像素鸟》:http://bbs.itcast.cn/thread-42245-1-1.html
附件下载
SimpleFlappyBirdDemo:http://pan.baidu.com/s/1hqtcHIs