您的位置:首页 > 编程语言 > C#

C#游戏编程:《控制台小游戏系列》之《八、爆破七色砖实例》

2013-01-26 20:37 309 查看

一、游戏分析

相信各位读者都曾玩过一款叫做《打砖块》的小游戏,和贪吃蛇一样,也是一款80后皆知的经典小游戏。游戏虽然简单但极富挑战性,这章的内容就是制作一款类打砖块的小游戏,继续说明和熟悉游戏框架的使用。和前几个小游戏不同,本章的小游戏有了一些进步,最明显的是有了关卡的概念,游戏不再是单调的一关,你可以自己设计自己所想要的关卡,这一定程度上提高了游戏的可玩性。但是这个游戏和前面的一样,也是一个DEMO游戏,游戏还不是完整的,至少这个游戏就没有开始界面和信息保存等特征,目的有二:第一,为了减少代码篇幅,显得更清晰简单;第二,界面的实现与具体游戏的逻辑关联不大,把焦点放到游戏的主要部分而不是次要的,更能清晰地让读者理解游戏运行的逻辑。
让我们把焦点放到这个游戏上,这款游戏继承了原始打砖块游戏的优良传统,又有我们定制的各种特征,山寨中不失创新,更难能可贵的是:我们是在控制台实现,用一个个字符绘制出来的,最后实现的画面效果平滑、操作灵活。玩过打砖块游戏的读者都知道,游戏中主要的对象有撞击砖块的小球、有反弹小球的挡板和被爆破的砖墙,当然,砖墙就是一个个砖块组合而成的。游戏的对象非常清晰,对它们更详细的分析,并建立相关的类,加上游戏规则和游戏逻辑,游戏自然而然的就出来了。
说到游戏规则,让我们看看我们自己的打砖块的游戏规则:

■游戏开始小球有99条生命(吃了唐僧肉),得分为0分,关卡默认为第一关。
■按空格键开始/暂停游戏,开始游戏小球在挡板上方下落,玩家需要控制挡板把小球接住,这过程中小球的反弹方向和速度可能会发生3种情况:第一,挡板速度为0,即挡板不动,小球按正常方向反弹;第二,挡板在运动中,小球的反弹速度得到加成;第三,小球会得到一个随机的反弹值,挡板如果运动中就速度加成。

■小球反弹方向与挡板运动方向相反。

■小球撞中砖块,砖块消失,玩家得分10分,小球Y方向相反运动,这过程中可能会发生连撞砖块情况,当然,这款游戏没有对此进行特殊奖励,只是加快通关罢了。

■小球与边界的碰撞,除了下边界外,小球与其他边界的碰撞都发生反弹动作。

■小球死亡,小球下落到挡板以下,减一条生命,当得分大于20分时,扣减20分。

■通关规则,小球撞掉所有砖块,进入下一关,得分增加100分。

■游戏结束,小球生命为0,游戏结束,显示结束画面。

■按F1切换上一关,按F2切换下一关。

游戏规则建立以后,就要考虑游戏中的对象了,需要为它们建立模型:



挡板
从图中可以看到,挡板就是一条长为N宽为1有N个小方块组成的长方行板块状(当然,在我们这里可以称为一个N*1的矩形),挡板固定在游戏界面的底端,只可以左右方向移动。

小球
从图中可以看到,小球用一个圆形字符表示,小球可以在游戏屏幕区域自由反弹。
砖块
从图中可以看到,砖块是由4*2的方块字符组成的,然而,在游戏中,我们把它们当作一个整体,也即是一块砖块。
砖墙
也就是砖块的集合。
砖墙的数据不是硬编码到代码中,否则每次添加一个新的关卡就要重新编译项目了,我们把表示砖墙的数据存储在磁盘中的文本文档或者是xml文件上,这样当添加新关卡或者想修改关卡数据的时候就只需要修改程序外部数据文件即可。两种存储方法为:
文本文档存储法:
文件名约定:Level+编号.txt
砖墙数据表示:(0表示空,非0表示砖块,其中5表示砖的行数,7表示砖的列数,列数不能超过7)



XML文档表示法:
文件名约定:Level+编号.xml

砖墙数据表示:(0表示空,非0表示砖块,Rn列数不能超过7)



为了能够使游戏在通关之后能够进入下一关,我们还需要一个关卡列表,记录关卡播放的顺序,关卡列表中的关卡数据是用一个名为LevelList的文本文档记录,其中存放的关卡项只存储与关卡文件关联的关卡名,文件类型后缀忽略,当然,关卡顺序可以随意调整,但要确保每个关卡名都存在与之对应的关卡文件:



注意:以上关卡文件和关卡列表文件均放在游戏可执行文件路径下的Level文件夹下!

二、游戏类图分析

游戏需求等分析清楚之后,我们把相关对象关联起来并分析,这里用UML静态类图表示(我画的图可能不是最准确的,但这就是我目前的想法,还希望你们的指点):



类图分析:

游戏对象:
■精灵类(Sprite)
一个精灵表示游戏中的一个对象,也可以说是“参与”游戏中的一个演员,这是一个抽象类,封装有具体演员共有的属性和方法信息,比如演员的位置,演员的颜色,演员的速度方向等,所有的演员还有移动和绘制本身等功能。总的来说,这个类为其他演员类提供一个模板,要想成为“演员”就必须具有这样的特征,一定程度上提高了代码的重用性,减少冗余。

■小球类(Ball)
小球是这个游戏的“演员”,所以它继承了精灵类,拥有了精灵的所有特征,然而小球这个演员与其他演员外表不同,它有自己的渲染实现。

■挡板类(Board)
挡板这个“演员”也继承了精灵类,也有着精灵的特征。

■砖块类(Brick)
这里的砖块有人会说它不是一个演员,最多只能算一个道具,听起来不错,不过因情况来考虑了,这里为了减少代码的重复编写,又考虑到砖块的特征与精灵十分接近,我们也让它继承精灵类,把它看作是一个不会动的演员(挂了?)。

■砖墙类(Wall)
砖墙由砖块组合而成,是一对多的关系。

数据访问接口:

■数据读写接口(IDataAccessor)

为考虑到数据的存储形式的多样性,这里提供数据的读写接口,提供给具体读写类实现。

■文本数据读写类(TxtDataAccessor)

实现文本文档类型数据读写。

■XML数据读写类(XmlDataAccessor)

实现xml类型数据读写。

游戏类:

■游戏类(BreakoutGame)

实现游戏的相关逻辑并呈现游戏画面。

■关卡类(Level)

实现关卡的切换。

四、游戏实现

有句话说得好:不打无准备之仗。在游戏实现之前,我们先实现游戏需要的辅助类,在这个游戏中,辅助的类包括数据的读写类和关卡类,我们先实现它们,以便提供给后期使用,而不必在写游戏逻辑过程中又考虑回来编写这些辅助类。
首先实现数据的读写:
///IDataAccessor接口实现

[csharp] view
plaincopyprint?

using System;

namespace Breakout.DAL

{

/// <summary>

/// 数据存取接口

/// </summary>

public interface IDataAccessor

{

/// <summary>

/// 读取数据

/// </summary>

/// <returns></returns>

Int32[,] read();

/// <summary>

/// 写入数据

/// </summary>

void write(Int32[,] mapData);

}

}

文本文档类型数据的读写类:
///TxtDataAccessor类实现

[csharp] view
plaincopyprint?

using System;

using System.IO;

using System.Text;

namespace Breakout.DAL

{

/// <summary>

/// TXT数据访问类

/// </summary>

public class TxtDataAccessor : IDataAccessor

{

private String fileName;

public TxtDataAccessor(String fileName)

{

this.fileName = fileName;

}

public Int32[,] read()

{

Int32 rows = 0;

Int32 cols = 0;

Int32 index = 0;

Int32[,] mapData = null;

try

{

StreamReader sr = new StreamReader(fileName);

String[] data = sr.ReadLine().Split(',');

rows = Int32.Parse(data[0]);

cols = Int32.Parse(data[1]);

mapData = new Int32[rows, cols];

while (!sr.EndOfStream)

{

String[] values = sr.ReadLine().Split(',');

for (Int32 c = 0; c < cols; c++)

{

mapData[index, c] = Int32.Parse(values[c]);

}

index++;

}

return mapData;

}

catch (FileNotFoundException e)

{

throw e;

}

}

public void write(Int32[,] mapData)

{

if (mapData == null)

return;

try

{

Int32 rows = mapData.GetUpperBound(0) + 1;

Int32 cols = mapData.GetUpperBound(1) + 1;

StreamWriter sw = new StreamWriter(fileName, false);

sw.WriteLine(rows.ToString() + "," + cols.ToString());

for (Int32 r = 0; r < rows; r++)

{

StringBuilder strb = new StringBuilder();

for (Int32 c = 0; c < cols; c++)

{

strb.Append(mapData[r, c].ToString() + ",");

}

sw.WriteLine(strb.Remove(strb.Length - 1, 1).ToString());

}

sw.Close();

}

catch (Exception e)

{

throw e;

}

}

}

}

xml类型数据读写类实现为:
///XmlDataAccessor类实现

[csharp] view
plaincopyprint?

using System;

using System.Text;

using System.Xml;

using System.IO;

namespace Breakout.DAL

{

/// <summary>

/// XML数据访问类

/// </summary>

public class XmlDataAccessor : IDataAccessor

{

private String fileName;

public XmlDataAccessor(String fileName)

{

this.fileName = fileName;

}

public Int32[,] read()

{

try

{

Int32[,] mapData = null;

XmlDocument xmlDoc = new XmlDocument();

xmlDoc.Load(fileName);

XmlNodeList nodelist = xmlDoc.SelectSingleNode("Map").ChildNodes;

if (nodelist[0].InnerText != "")

{

Int32 cols = nodelist[0].InnerText.Split(',').Length;

mapData = new Int32[nodelist.Count, cols];

for (Int32 r = 0; r < nodelist.Count; r++)

{

for (Int32 c = 0; c < cols; c++)

{

mapData[r, c] = Int32.Parse(nodelist[r].InnerText.Split(',')[c]);

}

}

}

return mapData;

}

catch (FileNotFoundException e)

{

throw e;

}

}

public void write(Int32[,] mapData)

{

if (mapData == null)

return;

try

{

FileInfo file = new FileInfo(fileName);

if (file.Exists)

{

file.Delete();

}

if (mapData != null)

{

XmlWriterSettings setting = new XmlWriterSettings();

setting.Indent = true;

setting.IndentChars = " ";

using (XmlWriter writer = XmlWriter.Create(fileName, setting))

{

writer.WriteStartElement("Map");

for (Int32 r = 0; r <= mapData.GetUpperBound(0); r++)

{

StringBuilder strb = new StringBuilder();

for (Int32 c = 0; c <= mapData.GetUpperBound(1); c++)

{

strb.Append(mapData[r, c].ToString() + ",");

}

writer.WriteElementString("R" + (r + 1).ToString(), strb.Remove(strb.Length - 1, 1).ToString());

}

writer.WriteEndElement();

writer.Flush();

}

}

}

catch (FileNotFoundException e)

{

throw e;

}

}

}

}

关卡类主要是实现关卡列表的播放,切换游戏关卡:
///Level类实现

[csharp] view
plaincopyprint?

using System;

using System.IO;

using System.Collections.Generic;

namespace Breakout

{

/// <summary>

/// 关卡类

/// </summary>

internal class Level

{

/// <summary>

/// 关卡列表

/// </summary>

private static List<String> m_levels = new List<String>();

private static Int32 m_index = -1;

/// <summary>

/// 初始化关卡列表

/// </summary>

static Level()

{

try

{

StreamReader sr = new StreamReader(@"Level\LevelList.txt");

if (sr != null)

{

while (!sr.EndOfStream)

{

String level = sr.ReadLine();

if (level.IndexOf("Level") != -1)

{

m_levels.Add(@"Level\" + level + ".txt");

}

}

sr.Close();

sr = null;

}

}

catch (Exception e)

{

Console.WriteLine(e.Message);

Console.ReadLine();

}

}

/// <summary>

/// 下一关

/// </summary>

/// <returns></returns>

public static String next()

{

if (++m_index > m_levels.Count - 1)

{

m_index = 0;

}

return m_levels[m_index];

}

/// <summary>

/// 上一关

/// </summary>

/// <returns></returns>

public static String prev()

{

if (--m_index < 0)

{

m_index = 0;

}

return m_levels[m_index];

}

/// <summary>

/// 当前关卡

/// </summary>

/// <returns></returns>

public static String curr()

{

String level = m_levels[m_index];

Int32 dashIndex = level.LastIndexOf(@"\");

level = level.Substring(dashIndex + 1, level.Length - dashIndex - 1);

level = level.Substring(0, level.LastIndexOf('.'));

return level;

}

}

}

万事开头难,辅助类已经实现完毕,以后就可以为我们所用了 ,转移焦点到游戏主要对象上,这才是我们编写游戏的核心内容,从现在开始,你就是一个翻译官:把UML类图翻译为代码,首先实现的是精灵类:
///Sprite类实现

[csharp] view
plaincopyprint?

using System;

using CGraphics;

namespace Breakout

{

/// <summary>

/// 精灵类

/// </summary>

internal abstract class Sprite

{

#region 字段

/// <summary>

/// 精灵颜色

/// </summary>

protected ConsoleColor m_color;

/// <summary>

/// 精灵位置

/// </summary>

protected CPoint m_position;

/// <summary>

/// 精灵上一时刻位置

/// </summary>

protected CPoint m_oldPoint;

/// <summary>

/// 精灵X轴方向速度

/// </summary>

protected Int32 m_vx;

/// <summary>

/// 精灵Y轴方向速度

/// </summary>

protected Int32 m_vy;

#endregion

#region 构造函数

/// <summary>

/// 构造函数

/// </summary>

/// <param name="point"></param>

/// <param name="color"></param>

public Sprite(CPoint point, ConsoleColor color)

{

this.m_position = point;

this.m_color = color;

this.m_vx = 0;

this.m_vy = 0;

}

/// <summary>

/// 构造函数

/// </summary>

/// <param name="x"></param>

/// <param name="y"></param>

/// <param name="color"></param>

public Sprite(Int32 x, Int32 y, ConsoleColor color)

{

this.m_position = new CPoint(x, y);

this.m_color = color;

this.m_vx = 0;

this.m_vy = 0;

}

#endregion

#region get set functions

public void setColor(ConsoleColor color)

{

this.m_color = color;

}

public ConsoleColor getColor()

{

return this.m_color;

}

public void setPosition(CPoint point)

{

this.m_position = point;

}

public void setPosition(Int32 x, Int32 y)

{

this.m_position = new CPoint(x, y);

}

public CPoint getPosition()

{

return m_position;

}

public void setVelocityX(Int32 vx)

{

this.m_vx = vx;

}

public Int32 getVelocityX()

{

return this.m_vx;

}

public void setVelocityY(Int32 vy)

{

this.m_vy = vy;

}

public Int32 getVelocityY()

{

return this.m_vy;

}

#endregion

#region 虚拟/抽象方法

/// <summary>

/// 精灵移动

/// </summary>

public virtual void move()

{

//保存上一时刻精灵灵位置

this.m_oldPoint = this.m_position;

//更新这一时刻精灵位置

Int32 dx = this.m_position.getX() + m_vx;

Int32 dy = this.m_position.getY() + m_vy;

dx = dx < 0 ? 0 : dx;

dy = dy < 0 ? 0 : dy;

this.m_position.setX(dx);

this.m_position.setY(dy);

}

/// <summary>

/// 绘制精灵

/// </summary>

/// <param name="draw"></param>

public abstract void draw(CDraw draw);

#endregion

}

}

这是一个相对简单的精灵类,准确地说刚刚好满足这个游戏的需要,然而这是一个“潜力类”,还有非常多的特性等待你去总结,这里以简单为宗旨,能越简单地实现目标就用最简单的方法。简单地说明一下精灵类的move方法,此方法记录了精灵前一时刻和当前时刻的坐标,保存前一时刻坐标的目的是为了擦除精灵移动过程中留下的轨迹,当然,这只是现在处理精灵移动痕迹的方法。精灵移动方法很简单,就是精灵的坐标与速度的加成,而且还处理了精灵越界,防止出现意外的错误异常。精灵的移动与上章中贪吃蛇的移动有细微差别,上一章中,贪吃蛇的移动方法里不仅处理了蛇的移动,还处理了边界的碰撞问题;而在这一章的精灵中,移动就是纯粹移动,没有进行游戏中碰撞等的逻辑判断,而是把这样的判断延迟到游戏类中去,让游戏类中的游戏逻辑汇总处理这样的逻辑。这样的好处是,当修改游戏逻辑的时候,就能集中精力在游戏类上,而不必要修改相关的游戏对象类,当把游戏逻辑转移到脚本(LUA等)处理时,也减少对象与脚本之间的依赖性,总的来说,只需要关心游戏类和脚本语言的交互即可。

精灵类就是为演员们准备的,首先登场的是小球这个演员,有了精灵类这个基础,小球类的实现真是简单:
///Ball类实现

[csharp] view
plaincopyprint?

using System;

using CGraphics;

namespace Breakout

{

/// <summary>

/// 小球类

/// </summary>

internal class Ball : Sprite

{

/// <summary>

/// 构造函数

/// </summary>

/// <param name="point"></param>

/// <param name="color"></param>

public Ball(CPoint point, ConsoleColor color)

: base(point, color)

{

}

/// <summary>

/// 构造函数

/// </summary>

/// <param name="x"></param>

/// <param name="y"></param>

/// <param name="color"></param>

public Ball(Int32 x, Int32 y, ConsoleColor color)

: base(x, y, color)

{

}

/// <summary>

/// 小球呈现

/// </summary>

/// <param name="draw"></param>

public override void draw(CDraw draw)

{

if (m_oldPoint != m_position)

{

//擦除颜色必须指定缺省符号

draw.setDrawSymbol(CSymbol.DEFAULT);

//擦除旧画面

draw.fillRect(m_oldPoint.getX()>>1, m_oldPoint.getY(), 1, 1, draw.getBackcolor());

//绘制新画面

draw.drawText("●", m_position.getX(), m_position.getY(), m_color);

}

}

}

}

这个小球类是不是非常之简单呢,细心的读者可能会发现到,绘制小球为什么不统一用绘制矩形的方法绘制小球,而是用绘制字符串这个函数?答案是:如果看过我前面的文章的就会知道这个绘制字符串与绘制矩形在X轴坐标上有细微差别,不指定字符串绘制区域的绘制字符串函数坐标X按每字符来计算,其他的绘制方法是按每字来计算,这样的好处是小球拥有了比按字计算多双倍的屏幕运动空间,小球的运动会更加自然和准确。
小球演员登场之后势必轮到它的好搭档,挡板演员闪亮登场,挡板类也很简单,唯一扩展的是挡板有了它的运行状态,其目的只是为了避免挡板的重绘导致的闪烁(如果你想拥有闪烁着的挡板,不防把这个状态去掉,代码显得更为清晰简单)。
///Board类实现

[csharp] view
plaincopyprint?

using System;

using CGraphics;

namespace Breakout

{

/// <summary>

/// 挡板状态枚举

/// </summary>

internal enum BoardState

{

Run,

Stop

}

/// <summary>

/// 挡板类

/// </summary>

internal class Board : Sprite

{

/// <summary>

/// 长度

/// </summary>

private Int32 m_length;

/// <summary>

/// 状态

/// </summary>

private BoardState m_state;

/// <summary>

/// 构造函数

/// </summary>

/// <param name="point"></param>

/// <param name="color"></param>

public Board(CPoint point, Int32 len, ConsoleColor color)

: base(point, color)

{

this.m_length = len;

this.m_state = BoardState.Run;

}

/// <summary>

/// 构造函数

/// </summary>

/// <param name="x"></param>

/// <param name="y"></param>

/// <param name="color"></param>

public Board(Int32 x, Int32 y, Int32 len, ConsoleColor color)

: base(x, y, color)

{

this.m_length = len;

this.m_state = BoardState.Run;

}

public Int32 getLength()

{

return this.m_length;

}

public void setState(BoardState state)

{

this.m_state = state;

}

/// <summary>

/// 挡板呈现

/// </summary>

/// <param name="draw"></param>

public override void draw(CDraw draw)

{

if (m_oldPoint != m_position && this.m_state == BoardState.Run)

{

if (m_oldPoint.getY() != 0)

{

//擦除颜色必须指定缺省符号

draw.setDrawSymbol(CSymbol.DEFAULT);

//擦除旧画面

draw.fillRect(m_oldPoint.getX() - 1, m_oldPoint.getY(), m_length + 2, 1, draw.getBackcolor());

}

draw.setDrawSymbol(CSymbol.RECT_SOLID);

//绘制新画面

draw.fillRect(m_position.getX(), m_position.getY(), m_length, 1, m_color);

}

}

}

}

砖块作为“睡着了的演员”,也是继承精灵类的,实现起来也非常简单:

///Brick类实现

[csharp] view
plaincopyprint?

using System;

using CGraphics;

namespace Breakout

{

/// <summary>

/// 砖块类

/// </summary>

internal class Brick:Sprite

{

/// <summary>

/// 砖块状态

/// </summary>

private Boolean m_bAlive;

/// <summary>

/// 砖块尺寸

/// </summary>

private CSize m_size;

public Brick(Int32 x, Int32 y, ConsoleColor color)

: base(x, y, color)

{

m_bAlive = true;

//宽4高2

m_size = new CSize(4,2);

}

public void setAlive(Boolean alive)

{

this.m_bAlive = alive;

}

public Boolean getAlive()

{

return this.m_bAlive;

}

public CSize getSize()

{

return this.m_size;

}

/// <summary>

/// 擦除砖块

/// </summary>

/// <param name="draw"></param>

public void erase(CDraw draw)

{

draw.setDrawSymbol(CSymbol.DEFAULT);

draw.fillRect(m_position.getX(), m_position.getY(), m_size.getWidth(), m_size.getHeight(), draw.getBackcolor());

}

/// <summary>

/// 绘制砖块

/// </summary>

/// <param name="draw"></param>

public override void draw(CDraw draw)

{

if (m_bAlive)

{

draw.setDrawSymbol(CSymbol.RECT_SOLID);

draw.fillRect(m_position.getX(), m_position.getY(), m_size.getWidth(), m_size.getHeight(), m_color);

}

}

}

}

砖块类有了它自己的成员函数,用于擦除它本身,当小球与之碰撞时,就执行这个函数。
俗话说:地上本来没有路,走的人多了,便有了路!我们的游戏也一样,游戏本身没有墙,砖头叠多了,便有了墙。很形象吧!砖墙就是砖块组合而成的,从上面的UML类图可以看到,砖墙类依赖于数据访问接口,显然它需要从这个接口得到组成砖墙的砖块数据。除此之外,砖墙类提供一个函数来判断墙是否已经损坏,也即是有砖块被打落,判断的理由为砖块的外接矩形(它本身大小)与小球的外接矩形是否相交,当相交时则证明小球与砖墙发生碰撞,相应的砖块就被打落。为了能判断两个矩形是否相交,我们扩展CRect结构,增加一个判断矩形相交的函数:

///扩展CRect结构

[csharp] view
plaincopyprint?

public struct CRect

{

//略

/// <summary>

/// 两矩形是否相交

/// </summary>

/// <param name="rcCheck"></param>

/// <returns></returns>

public Boolean collisionWith(CRect rcCheck)

{

return m_x <= rcCheck.getX() && rcCheck.getX() <= (m_x + m_width) &&

m_y <= rcCheck.getY() && rcCheck.getY() <= (m_y + m_height);

}

//略

}

下图说明了小球与砖块已经发生碰撞,黄色圈为砖头与小球的外接矩形:



分析好了砖墙类,我们就可以实现它了,也非常简单:
///Wall类实现

[csharp] view
plaincopyprint?

using System;

using CGraphics;

using Breakout.DAL;

namespace Breakout

{

/// <summary>

/// 砖墙类

/// </summary>

internal sealed class Wall

{

/// <summary>

/// 砖墙相对于工作区的偏移量

/// </summary>

public static Int32 OFFER_X = 3;

public static Int32 OFFER_Y = 1;

/// <summary>

/// 随机数

/// </summary>

private Random m_random;

/// <summary>

/// 砖块

/// </summary>

private Brick[,] m_bricks;

/// <summary>

/// 砖块数量

/// </summary>

private Int32 m_count;

/// <summary>

/// 行

/// </summary>

private Int32 m_rows;

/// <summary>

/// 列

/// </summary>

private Int32 m_cols;

/// <summary>

/// 绘图对象

/// </summary>

private CDraw m_draw;

/// <summary>

/// 构造函数

/// </summary>

public Wall()

{

m_draw = new CDraw();

m_random = new Random();

}

/// <summary>

/// 加载砖墙数据

/// </summary>

/// <param name="idata"></param>

public void loadWall(IDataAccessor idata)

{

Int32[,] data = idata.read();

if (data != null)

{

m_rows = data.GetUpperBound(0) + 1;

m_cols = data.GetUpperBound(1) + 1;

m_bricks = new Brick[m_rows, m_cols];

m_count = 0;

for (Int32 i = 0; i < m_rows; i++)

{

for (Int32 j = 0; j < m_cols; j++)

{

if (data[i, j] != 0)

{

Brick brick = new Brick(j, i, (ConsoleColor)m_random.Next(0, 16));

Int32 ix = j * (brick.getSize().getWidth() + 1);

Int32 iy = i * (brick.getSize().getHeight() + 1);

brick.setPosition(ix + OFFER_X, iy + OFFER_Y);

m_bricks[i, j] = brick;

m_count++;

}

}

}

}

}

/// <summary>

/// 获取砖数量

/// </summary>

/// <returns></returns>

public Int32 getBrickCount()

{

return m_count;

}

/// <summary>

/// 砖墙毁坏

/// </summary>

/// <param name="point"></param>

/// <returns></returns>

public Boolean destroy(CPoint point)

{

point.setX(point.getX()>>1);

for (Int32 i = 0; i < m_rows; i++)

{

for (Int32 j = 0; j < m_cols; j++)

{

if (m_bricks[i, j] != null)

{

if (m_bricks[i, j].getAlive())

{

CRect brickRect = new CRect(m_bricks[i, j].getPosition(), m_bricks[i, j].getSize());

if (brickRect.collisionWith(new CRect(point, 1, 1)))

{

m_bricks[i, j].setAlive(false);

m_bricks[i, j].erase(m_draw);

return true;

}

}

}

}

}

return false;

}

public Boolean destroy(Int32 x, Int32 y)

{

return destroy(new CPoint(x, y));

}

/// <summary>

/// 重置砖墙

/// </summary>

public void reset()

{

for (Int32 i = 0; i < m_rows; i++)

{

for (Int32 j = 0; j < m_cols; j++)

{

if (m_bricks[i, j] != null)

{

if (!m_bricks[i, j].getAlive())

{

m_bricks[i, j].setAlive(true);

}

}

}

}

}

/// <summary>

/// 擦除砖墙

/// </summary>

/// <param name="draw"></param>

public void erase(CDraw draw)

{

for (Int32 i = 0; i < m_rows; i++)

{

for (Int32 j = 0; j < m_cols; j++)

{

if (m_bricks[i, j] != null)

{

m_bricks[i, j].erase(draw);

}

}

}

}

/// <summary>

/// 绘制砖墙

/// </summary>

/// <param name="draw"></param>

public void draw(CDraw draw)

{

for (Int32 i = 0; i < m_rows; i++)

{

for (Int32 j = 0; j < m_cols; j++)

{

if (m_bricks[i, j] != null)

{

m_bricks[i, j].draw(draw);

}

}

}

}

}

}

砖墙类实现就是如此,万事具备只欠东风,这股风来自我们的游戏类,有了它,万物才能焕发出生机,才能互相建立关系。游戏类处理的是游戏的逻辑,然而这个游戏的运行逻辑就是前面所提及的游戏规则,只要根据规则编写代码即可,代码注释非常详细,这样就不多说了,下面是游戏类的实现:

///BreakoutGame类实现

[csharp] view
plaincopyprint?

using System;

using CEngine;

using CGraphics;

using Breakout.DAL;

namespace Breakout

{

/// <summary>

/// 打砖块游戏类

/// </summary>

public class BreakoutGame : CGame

{

/// <summary>

/// 工作区宽度

/// </summary>

private static Int32 WINDOW_WIDTH = 100;

/// <summary>

/// 游戏屏幕宽度

/// </summary>

private static Int32 SCREEN_WIDTH = 39;

/// <summary>

/// 游戏屏幕高度

/// </summary>

private static Int32 SCREEN_HEIGHT = 35;

/// <summary>

/// 小球初始位置

/// </summary>

private static Int32 BALL_X = 37;

private static Int32 BALL_Y = SCREEN_HEIGHT - 5;

/// <summary>

/// 挡板初始位置、大小

/// </summary>

private static Int32 BOARD_X = 15;

private static Int32 BOARD_Y = SCREEN_HEIGHT - 1;

private static Int32 BOARD_LEN = 8;

/// <summary>

/// 小球类

/// </summary>

private Ball ball;

/// <summary>

/// 挡板类

/// </summary>

private Board board;

/// <summary>

/// 砖墙

/// </summary>

private Wall wall;

/// <summary>

/// 随机数

/// </summary>

private Random random;

/// <summary>

/// 小球运行速度延迟

/// </summary>

private Int32 delayTime;

/// <summary>

/// 爆破砖块数量

/// </summary>

private Int32 breakCount;

/// <summary>

/// 小球生命

/// </summary>

private Int32 lives;

/// <summary>

/// 得分

/// </summary>

private Int32 score;

/// <summary>

/// 第几关

/// </summary>

private String level;

/// <summary>

/// 选择关卡状态

/// </summary>

private Boolean selectedLevel;

/// <summary>

/// 游戏初始化

/// </summary>

protected override void gameInit()

{

setTitle("控制台小游戏之——爆破七色砖v1.0");

setUpdateRate(20);

setCursorVisible(false);

//控制台尺寸

Console.WindowWidth = WINDOW_WIDTH;

Console.WindowHeight = SCREEN_HEIGHT;

//执行一次重绘

update();

this.random = new Random();

this.ball = new Ball(BALL_X, BALL_Y, ConsoleColor.White);

this.board = new Board(BOARD_X, BOARD_Y, BOARD_LEN, ConsoleColor.Yellow);

this.wall = new Wall();

//加载砖墙

wall.loadWall(new TxtDataAccessor(Level.next()));

//绘制墙

wall.draw(getDraw());

//游戏数据

lives = 99;

score = 0;

level = Level.curr();

}

/// <summary>

/// 游戏重绘

/// </summary>

/// <param name="e"></param>

protected override void onRedraw(CPaintEventArgs e)

{

base.onRedraw(e);

//绘制静态界面

CDraw draw = e.getDraw();

draw.setDrawSymbol(CSymbol.RHOMB_SOLID);

draw.drawRect(SCREEN_WIDTH + 1, 0, (WINDOW_WIDTH >> 1) - SCREEN_WIDTH - 1, SCREEN_HEIGHT, ConsoleColor.DarkYellow);

draw.drawText("生命:", (SCREEN_WIDTH << 1) + 4, 4, ConsoleColor.Blue);

draw.drawText("得分:", (SCREEN_WIDTH << 1) + 4, 6, ConsoleColor.Blue);

draw.drawText("FPS:", (SCREEN_WIDTH << 1) + 4, 8, ConsoleColor.Blue);

draw.fillRect(SCREEN_WIDTH, 17, (WINDOW_WIDTH >> 1) - SCREEN_WIDTH, 1, ConsoleColor.DarkYellow);

draw.drawText("操作:空格键开始发球,方向键控制挡板左右移动;F1键切换上一关,F2键切换下一关。",

SCREEN_WIDTH+3,19,6,10,ConsoleColor.DarkGreen);

draw.setDrawSymbol(CSymbol.RICE);

draw.fillRect(0,0,3,SCREEN_HEIGHT,ConsoleColor.DarkCyan);

draw.fillRect(SCREEN_WIDTH-2, 0, 3, SCREEN_HEIGHT, ConsoleColor.DarkCyan);

draw.drawText("By:007 阿理\nD-Zone Studio", SCREEN_WIDTH + 3, 31, 13, 10, ConsoleColor.DarkGray);

}

/// <summary>

/// 游戏渲染

/// </summary>

/// <param name="draw"></param>

protected override void gameDraw(CDraw draw)

{

//绘制小球

ball.draw(draw);

//绘制挡板

board.draw(draw);

//绘制动态数据

draw.drawText(level, (SCREEN_WIDTH <<1) + 9, 2, ConsoleColor.Magenta);

draw.drawText(lives.ToString(), (SCREEN_WIDTH << 1) + 10, 4, ConsoleColor.White);

draw.drawText(score.ToString(), (SCREEN_WIDTH << 1) + 10, 6, ConsoleColor.White);

draw.drawText(getFPS().ToString(), (SCREEN_WIDTH <<1) + 10, 8, ConsoleColor.Green);

}

/// <summary>

/// 键盘按下

/// </summary>

/// <param name="e"></param>

protected override void gameKeyDown(CKeyboardEventArgs e)

{

if (e.getKey() == CKeys.Left)

{

board.setVelocityX(-1);

board.setState(BoardState.Run);

}

else if (e.getKey() == CKeys.Right)

{

board.setVelocityX(1);

board.setState(BoardState.Run);

}

else if (e.getKey() == CKeys.Space)

{

ball.setVelocityY(1);

}

else if (e.getKey() == CKeys.F1)

{

if (!selectedLevel)

{

prevLevel();

selectedLevel = true;

}

}

else if (e.getKey() == CKeys.F2)

{

if (!selectedLevel)

{

nextLevel();

selectedLevel = true;

}

}

else if (e.getKey() == CKeys.Escape)

{

setGameOver(true);

}

}

/// <summary>

/// 键盘释放

/// </summary>

/// <param name="e"></param>

protected override void gameKeyUp(CKeyboardEventArgs e)

{

board.setVelocityX(0);

board.setState(BoardState.Stop);

selectedLevel = false;

}

/// <summary>

/// 小球运动

/// </summary>

private void ballRun()

{

if (--delayTime <= 0)

{

//小球开始运动

ball.move();

//小球与砖块碰撞检测

if (wall.destroy(ball.getPosition()))

{

ball.setVelocityY(1);

score += 10;

breakCount++;

}

//小球与左边界碰撞检测

if (ball.getPosition().getX() <= Wall.OFFER_X<<1)

{

ball.setPosition((Wall.OFFER_X << 1) + 2, ball.getPosition().getY());

ball.setVelocityX(-ball.getVelocityX());

}

//小球与右边界碰撞检测

if (ball.getPosition().getX() >= (SCREEN_WIDTH - Wall.OFFER_X) << 1)

{

ball.setPosition((SCREEN_WIDTH - Wall.OFFER_X) << 1, ball.getPosition().getY());

ball.setVelocityX(-ball.getVelocityX());

}

//小球与上边界碰撞检测

if (ball.getPosition().getY() <= Wall.OFFER_Y)

{

ball.setVelocityY(1);

}

//小球与挡板碰撞检测

if (ball.getPosition().getY() == SCREEN_HEIGHT - 2 &&

ball.getPosition().getX() >> 1 >= board.getPosition().getX() &&

ball.getPosition().getX() >> 1 <= board.getPosition().getX() + board.getLength())

{

Int32 ix = random.Next(-2, 3);

ball.setVelocityX(ix - board.getVelocityX());

ball.setVelocityY(-1);

}

//小球落到地面

else if (ball.getPosition().getY()> SCREEN_HEIGHT-1)

{

reset();

if (score > 20)

{

score -= 20;

}

if (lives > 0)

{

lives--;

}

else

{

setGameOver(true);

}

}

//延迟时间,建议采取这样的方式延迟对象的运动,而不是修改游戏更新率setUpdateRate

delayTime = 2;

}

}

/// <summary>

/// 挡板运动

/// </summary>

private void boardRun()

{

Int32 leftX = Wall.OFFER_X+1;

Int32 rightX = SCREEN_WIDTH - board.getLength() - Wall.OFFER_X;

//判断X轴方向是否可以移动

if (board.getPosition().getX() >= leftX && board.getPosition().getX() <= rightX)

{

if (board.getVelocityX() != 0)

{

board.move();

}

}

//矫正挡板位置防止嵌入障碍物

else if (board.getPosition().getX() < leftX)

{

board.setPosition(leftX, board.getPosition().getY());

board.setVelocityX(0);

}

//矫正挡板位置防止嵌入障碍物

else if (board.getPosition().getX() > rightX)

{

board.setPosition(rightX, board.getPosition().getY());

board.setVelocityX(0);

}

}

/// <summary>

/// 重置小球和挡板

/// </summary>

private void reset()

{

ball.setPosition(BALL_X, BALL_Y);

ball.setVelocityX(0);

ball.setVelocityY(0);

board.setPosition(BOARD_X, BOARD_Y);

board.setState(BoardState.Run);

getDraw().setDrawSymbol(CSymbol.DEFAULT);

getDraw().fillRect(3, SCREEN_HEIGHT - 1,SCREEN_WIDTH-5, 1, getDraw().getBackcolor());

}

/// <summary>

/// 是否通关

/// </summary>

/// <returns></returns>

private Boolean isGamePass()

{

return breakCount == wall.getBrickCount();

}

/// <summary>

/// 下一关

/// </summary>

private void nextLevel()

{

wall.erase(base.getDraw());

wall.loadWall(new TxtDataAccessor(Level.next()));

wall.draw(base.getDraw());

level = Level.curr();

}

/// <summary>

/// 上一关

/// </summary>

private void prevLevel()

{

wall.erase(base.getDraw());

wall.loadWall(new TxtDataAccessor(Level.prev()));

wall.draw(base.getDraw());

level = Level.curr();

}

}

}

游戏类的核心在于小球和挡板的运行逻辑,处理了边界碰撞、小球与砖块碰撞、小球与挡板碰撞的相关逻辑,代码非常简单,就不详说了,需要注意的还有切换关卡的函数,它先擦除旧砖墙,然后加载新的关卡,最后把新墙绘制到界面上。最后让我们欣赏下我们的劳动成果:











效果是不是非常棒呢!关卡由玩家自己创造,你只需要小小修改关卡文件数据,就有一幅全新的关卡,记得要把它添加到关卡列表!这里还提示一下:如果想要不同关卡的难度不同,比如小球速度的快慢,挡板的大小还有砖块需要砸几下才能打破,也可以采取外部文件配置参数的方法来实现,不同关卡附带一个参数文件即可。

关卡自由创作(有时间再写一个这个游戏的控制台版本关卡编辑器):



试玩链接:http://download.csdn.net/detail/hwenycocodq520/4644673

四、结语

总算结束这一章的讲解了,文章的内容均属于我的个人想法,可能会出现不正确或者模糊的地方,如果大家有发现这些情况,请指点出来,你们的意见总是很宝贵的!这一章到此结束,期待下一个游戏的到来吧,下一个游戏将会是什么呢?我也不知道!(头脑风暴中@@@@)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: