0.前言
我想通过编写一个完整的游戏程序方式引导读者体验程序设计的全过程。我将采用多种方式编写具有相同效果的应用程序,并通过不同方式形成的代码和实现方法的对比来理解程序开发更深层的知识。
了解我编写教程的思路,请参阅体现我最初想法的那篇文章中的“1.编程计划”和“2.已经编写完成的文章(目录)”:
学习编程从游戏开始——编程计划(目录) - lexyao - 博客园
我已经在下面这篇文章中介绍了使用LCL和FCL组件构建一个项目(pTetris)的过程,后续的使用Lazarus的文章中使用的例子都是以向这个项目添加新功能的方式表述的:
在Lazarus下的Free Pascal编程教程——用向导创建一个使用LCL和FCL组件的项目(pTetris) - lexyao - 博客园
在前面写的文章中我们已经构建了pTetris项目的框架,并逐步添加了一些功能。前面所有的工作只是搭建了一个工作场所,还没有涉及实质的工作内容。就像是我们要生产某一种产品,已经建造了厂房,但还没有安装生产设备,更别说生产产品了。在这一篇文章里,我们将探讨构建生产流水线的事情。
俄罗斯方块游戏中方块是核心。归根结底,就是创建方块、移动方块、消除方块。方块从产生到消亡的过程就是我们的生产流程,我们的编程工作就是围绕这个流程展开的。
在这篇文章里,我主要讲述以下几个方面的内容:
- 应用程序设计中模块划分的原则
- pTetris项目的模块划分
- pTetris模块功能规划
- pTetris模块化程序设计
- 结束语
1.应用程序设计中模块划分的原则
无论干什么工作,我们已经习惯了把大的整体划分成若干小块,这就是模块划分。
在程序设计中,整个程序是一个整体,但这个整体是由若干模块组成的。我们完成的工作有模块独立完成了,也有模块之间的协作。
如何划分模块,关系到管理的工作量。模块划分合理,可以减少模块之间的交叉活动,减少相互干扰,有利于生产效率的提高,也减少了管理的工作量。
具体的项目模块划分,有以工作场所为核心的,也有以产品部件为核心的。不存在哪一种划分方式更合理的问题,只有哪一种划分方法更适合。
2.pTetris项目的模块划分
具体到我们的pTetris项目,可以考虑按数据流程划分模块。
俄罗斯方块游戏的核心是方块。把方块的生命周期作为我们的数据流程,那么对方块的管理的各个环节都可以作为一个模块。
如果把方块作为我们的产品,那么按着方块的生命周期可以这样划分:方块生产->打包装入盒子->放入盒子队列->取出盒子,移动盒子->打开盒子,方块堆积->方块消除。
3.pTetris模块功能规划
有了模块划分,现在我们来规划每个模块的功能。
3.1 方块生产
把方块定义成一个类,生产方块就是构建这个类的实例。构建方块需要设置方块的属性:
- 方块的大小:默认是16x16像素。如果使用其他尺寸的图标,则需要改变方块的大小,这也涉及到方块队列和方块移动区方格的大小
- 方块的表面图案:方块的表面图案有两种形式,一种是使用颜色填充,一种是使用图标
- 使用颜色填充可分为单色填充、渐变色填充、四色填充
- 使用图标可以有指定一套图标、随机使用多套图标
- 同组的方块属性相同还是各自随机产生
- 盒子旋转时方块的图案是否跟着旋转
3.2 打包装入盒子
游戏中的方块是成组出现的,生产出的方块要打包成组,也就是将方块装入盒子。打包装入盒子需要考虑以下因素:
- 每组中方块的个数:通常都是4个方块一组,也有5个一组的。不过,5个一组的游戏难度特别大,可以考虑个方块个数从1到5随机出现
- 每组方块的排列方式:方块的排列方式在游戏中是随机出现的,方块排列的算法是打包装入盒子的核心工作
3.3 放入盒子队列
一般的游戏中都有一种选择方式:是否提示下一组方块的样式。在pTetris项目中,我们提出了更多的要求:形成一个方块组队列,具体可以考虑以下因素:
- 队列中盒子的数量:数量可以确定为0-4组,队列中盒子数为0则表示没有提示
- 队列要有先进先出的特性
- 方块生产、打包的订单是由队列需求提出的
3.4 取出盒子,移动盒子
游戏的核心是移动盒子,而这些动作都是由计时器操作的,这是游戏的调度中心。移动盒子需要考虑以下问题:
- 从队列中取出一组盒子:无论队列中是否有盒子,都要把队列作为盒子的唯一来源。
- 取出盒子后计时器操作盒子下落
- 在盒子移动的过程中要响应干扰盒子的命令:左右移动、旋转、加速下落、直接跌落、暂停移动
- 判断盒子是否可以执行移动:
- 到达边缘后不能继续向边上移动
- 靠在边上还有旋转的空间吗
- 下落动作是否收到已经堆积方块的阻挡
- 刚刚落地时是否还可以继续左右移动
- 停止移动后打开盒子,将方块交给堆积区
3.5 打开盒子,方块堆积
由于堆积的方块是按行消除的,而包装盒子要占据不同的行,这样就必须在消除之前打开盒子。什么时间打开盒子比较好呢?当然是在交接的时候。送货到达目的地后,包装的盒子就打开了。方块堆积后要考虑的问题:
- 放下所在的高度:如果放置高度与计分有关,那么就要考虑放置高度和放置方块的个数(计算基本得分及高度加成)
- 消除行:放下方块后是否填充了所在行的所有空格,满足消除的条件,如果满足消除的条件,就要交给方块消除处理
- 消除行后的移动:消除满行后,在其上的行要下移。
- 如果消除满行的高度对计分有影响,就要考虑消除行的顺序问题,一个是从上到下依次消除
- 如果同时消除多行对计分有影响,就要在消除之前进行满行统计
- 考虑到消除行影响计分的问题可以在堆积时只做统计,把消除方块的工作交给下一个模块去完成,或者统计工作也交给下一个模块
3.6 方块消除
方块消除的动作是在方块堆积时引起的,可以把消除融合在堆积动作中,但考虑到计分的需要,一个把消除行独立出来。消除行应考虑的问题:(计算消除加成)
- 消除行所在的高度
- 同时消除的行数
- 消除行时是否有动画显示
- 消除行后上方的方块下移
4.pTetris模块化程序设计
4.1 为每一个模块定义一个类
前面已经进行了模块划分,现在我们为每一个模块定义一个类。只是取一个名字,不添加任何成员。
在pTetrisUnit单元中添加以下定义类的代码:
//为每一个模块定义一个类 cxBox=class; //单个方块 cxBoxs=class; //一组方块,也就是将方块打包的盒子 cxBoxQueue=class; //方块队列,也就是在提示区显示的下一个进入移动去的盒子 cxBoxMove=class; //正在移动的盒子 cxBoxHeap=class; //堆积的方块 cxBoxDestroy=class; //销毁方块
在移动盒子有关的过程中,游戏的核心是移动盛满了方块的盒子,所以核心模块是cxBoxMove。我们以cxBoxMove为中心,按着数据的流程将分成三大块来编写程序的代码:
- 取得当前移动的盒子:这是cxBoxMove所有活动的开始,也算是前期准备工作
- 移动盒子的操作:这是游戏的核心活动,是程序与用户交流的过程,包括用户要进行的界面操作,cxBoxMove中响应用户操作需要的代码
- 交付盒子后的操作:移动盒子到达目的地后,整个游戏的人机交流结束了,但交付盒子后还需要对交付的盒子进行处理,包括消除满行、计算得分、调整速度等
4.2 取得当前移动的盒子
游戏的核心是移动盛满了方块的盒子,cxBoxMove模块取得当前移动的盒子的代码我们在下面这篇文章中讲述:
在Lazarus下的Free Pascal编程教程——以数据需求拉动程序运行的模块化程序设计方法 - lexyao - 博客园
4.3 移动盒子的操作
移动盒子的操作是按着用户玩游戏的方法来考虑的,一方面提供用户需要的操作方法。另一方面要编写代码响应用户的操作。用户操作的方法我们已经在界面设计中考虑了,响应用户操作的代码我们在下面这篇文章中讲述:
4.4 交付盒子后的操作
交付盒子后用户的操作就算是结束了,后面的代码需要表达什么附加的意义呢?我想有三方面的事情:
- 销毁盒子和方块,这是程序运行的需要,也是游戏的需要
- 用户关心的当然是计分,让用户有成就感,有争取更高荣誉的想法,这是让用户喜欢这款游戏的根本点
- 加速算法是重要的一个方面,通过加速提高难度,激发用户有争强好胜的情绪,也让游戏有结束的时候
满足这三方面要求的代码我们在下面这篇文章中讲述: