一、游戏分析
相信各位读者都曾玩过一款叫做《打砖块》的小游戏,和贪吃蛇一样,也是一款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接口实现
- using System;
- namespace Breakout.DAL
- {
-
-
-
- public interface IDataAccessor
- {
-
-
-
-
- Int32[,] read();
-
-
-
- void write(Int32[,] mapData);
- }
- }
文本文档类型数据的读写类:
///TxtDataAccessor类实现
- using System;
- using System.IO;
- using System.Text;
-
- namespace Breakout.DAL
- {
-
-
-
- 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类实现
- using System;
- using System.Text;
- using System.Xml;
- using System.IO;
-
- namespace Breakout.DAL
- {
-
-
-
- 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类实现
- using System;
- using System.IO;
- using System.Collections.Generic;
-
- namespace Breakout
- {
-
-
-
- internal class Level
- {
-
-
-
- private static List m_levels = new List();
- private static Int32 m_index = -1;
-
-
-
-
- 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();
- }
- }
-
-
-
-
-
- public static String next()
- {
- if (++m_index > m_levels.Count - 1)
- {
- m_index = 0;
- }
-
- return m_levels[m_index];
- }
-
-
-
-
-
- public static String prev()
- {
- if (--m_index < 0)
- {
- m_index = 0;
- }
- return m_levels[m_index];
- }
-
-
-
-
-
- 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类实现
这是一个相对简单的精灵类,准确地说刚刚好满足这个游戏的需要,然而这是一个“潜力类”,还有非常多的特性等待你去总结,这里以简单为宗旨,能越简单地实现目标就用最简单的方法。简单地说明一下精灵类的move方法,此方法记录了精灵前一时刻和当前时刻的坐标,保存前一时刻坐标的目的是为了擦除精灵移动过程中留下的轨迹,当然,这只是现在处理精灵移动痕迹的方法。精灵移动方法很简单,就是精灵的坐标与速度的加成,而且还处理了精灵越界,防止出现意外的错误异常。精灵的移动与上章中贪吃蛇的移动有细微差别,上一章中,贪吃蛇的移动方法里不仅处理了蛇的移动,还处理了边界的碰撞问题;而在这一章的精灵中,移动就是纯粹移动,没有进行游戏中碰撞等的逻辑判断,而是把这样的判断延迟到游戏类中去,让游戏类中的游戏逻辑汇总处理这样的逻辑。这样的好处是,当修改游戏逻辑的时候,就能集中精力在游戏类上,而不必要修改相关的游戏对象类,当把游戏逻辑转移到脚本(LUA等)处理时,也减少对象与脚本之间的依赖性,总的来说,只需要关心游戏类和脚本语言的交互即可。
精灵类就是为演员们准备的,首先登场的是小球这个演员,有了精灵类这个基础,小球类的实现真是简单:
///Ball类实现
- using System;
- using CGraphics;
-
- namespace Breakout
- {
-
-
-
- internal class Ball : Sprite
- {
-
-
-
-
-
- public Ball(CPoint point, ConsoleColor color)
- : base(point, color)
- {
-
- }
-
-
-
-
-
-
-
- public Ball(Int32 x, Int32 y, ConsoleColor color)
- : base(x, y, color)
- {
-
- }
-
-
-
-
-
- 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类实现
- using System;
- using CGraphics;
-
- namespace Breakout
- {
-
-
-
- internal enum BoardState
- {
- Run,
- Stop
- }
-
-
-
-
- internal class Board : Sprite
- {
-
-
-
- private Int32 m_length;
-
-
-
- private BoardState m_state;
-
-
-
-
-
-
- public Board(CPoint point, Int32 len, ConsoleColor color)
- : base(point, color)
- {
- this.m_length = len;
-
- this.m_state = BoardState.Run;
- }
-
-
-
-
-
-
-
- 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;
- }
-
-
-
-
-
- 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类实现
- using System;
- using CGraphics;
-
- namespace Breakout
- {
-
-
-
- internal class Brick:Sprite
- {
-
-
-
- private Boolean m_bAlive;
-
-
-
- private CSize m_size;
-
- public Brick(Int32 x, Int32 y, ConsoleColor color)
- : base(x, y, color)
- {
- m_bAlive = true;
-
- 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;
- }
-
-
-
-
-
- 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());
- }
-
-
-
-
-
- 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结构
- public struct CRect
- {
-
-
-
-
-
-
- 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类实现
- using System;
- using CGraphics;
- using Breakout.DAL;
-
- namespace Breakout
- {
-
-
-
- internal sealed class Wall
- {
-
-
-
- public static Int32 OFFER_X = 3;
- public static Int32 OFFER_Y = 1;
-
-
-
- private Random m_random;
-
-
-
- private Brick[,] m_bricks;
-
-
-
- private Int32 m_count;
-
-
-
- private Int32 m_rows;
-
-
-
- private Int32 m_cols;
-
-
-
- private CDraw m_draw;
-
-
-
-
- public Wall()
- {
- m_draw = new CDraw();
- m_random = new Random();
- }
-
-
-
-
-
- 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++;
- }
- }
- }
- }
- }
-
-
-
-
-
- public Int32 getBrickCount()
- {
- return m_count;
- }
-
-
-
-
-
-
- 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));
- }
-
-
-
-
- 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);
- }
- }
- }
- }
- }
-
-
-
-
-
- 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);
- }
- }
- }
- }
-
-
-
-
-
- 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类实现
- using System;
- using CEngine;
- using CGraphics;
- using Breakout.DAL;
-
- namespace Breakout
- {
-
-
-
- public class BreakoutGame : CGame
- {
-
-
-
- private static Int32 WINDOW_WIDTH = 100;
-
-
-
- private static Int32 SCREEN_WIDTH = 39;
-
-
-
- private static Int32 SCREEN_HEIGHT = 35;
-
-
-
-
- private static Int32 BALL_X = 37;
- private static Int32 BALL_Y = SCREEN_HEIGHT - 5;
-
-
-
-
- private static Int32 BOARD_X = 15;
- private static Int32 BOARD_Y = SCREEN_HEIGHT - 1;
- private static Int32 BOARD_LEN = 8;
-
-
-
-
- private Ball ball;
-
-
-
- private Board board;
-
-
-
- private Wall wall;
-
-
-
- private Random random;
-
-
-
- private Int32 delayTime;
-
-
-
- private Int32 breakCount;
-
-
-
- private Int32 lives;
-
-
-
- private Int32 score;
-
-
-
- private String level;
-
-
-
- private Boolean selectedLevel;
-
-
-
-
- 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();
- }
-
-
-
-
-
- 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);
- }
-
-
-
-
-
- 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);
- }
-
-
-
-
-
- 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);
- }
- }
-
-
-
-
-
- protected override void gameKeyUp(CKeyboardEventArgs e)
- {
- board.setVelocityX(0);
- board.setState(BoardState.Stop);
- selectedLevel = false;
- }
-
-
-
-
- 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);
- }
- }
-
-
- delayTime = 2;
- }
- }
-
-
-
-
- private void boardRun()
- {
- Int32 leftX = Wall.OFFER_X+1;
- Int32 rightX = SCREEN_WIDTH - board.getLength() - Wall.OFFER_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);
- }
- }
-
-
-
-
- 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());
- }
-
-
-
-
-
- private Boolean isGamePass()
- {
- return breakCount == wall.getBrickCount();
- }
-
-
-
-
- private void nextLevel()
- {
- wall.erase(base.getDraw());
- wall.loadWall(new TxtDataAccessor(Level.next()));
- wall.draw(base.getDraw());
-
- level = Level.curr();
- }
-
-
-
-
- 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
四、结语
总算结束这一章的讲解了,文章的内容均属于我的个人想法,可能会出现不正确或者模糊的地方,如果大家有发现这些情况,请指点出来,你们的意见总是很宝贵的!这一章到此结束,期待下一个游戏的到来吧,下一个游戏将会是什么呢?我也不知道!(头脑风暴中@@@@)