第二章教程13:细节决定实现方法


本次教程内容:

  • 解决问题的顺序
  • 控制喇叭发出声音
  • else语句
  • 颜色设置
  • 二进制原理
  • 论硬编码
  • 彩色地图显示

做一个从0到1的开发,既不能一味完善细节,也不能一味搭建架构。前者会让你的开发难以扩展,而后者会让你的开发过程变得枯燥。应当让它像小树一样长大,有枝有叶,逐步长粗长高。

听说地图已经可以测试了,小Pa非常高兴,专程过来与开发组开会测试和讨论。

测试之后,小Pa提出了几个问题:

  • 1、地图的颜色千篇一律的灰白色,看起来过于单调,能否加上色彩。比如小Pa希望,至少地图上的文字应该用不同的颜色来表示,英雄也希望用一个不同的颜色来表示。
  • 2、英雄撞墙的时候,如果能加上撞墙的声音的提示,用户才能更加明确这种情况下英雄不移动是游戏的设计,而不是出现了卡顿、死机等问题。
  • 3、最后一张地图,进入之后,能否加上掌声的效果。
  • 4、在最后一张地图中,希望英雄不能移动,点击任何键都可以退出了。
  • 5、无论如何也不能接受点击回车就进入下一地图的操作,必须得走到指定位置才能切换地图。

开发组听着小Pa提出的前几个问题,觉得她考虑得很好。等听到最后一个问题之后,立刻意识到,她毕竟仍然是一个不懂软件开发的人。这个测试,只是让你能够看到每个地图的效果,而不是最后的游戏体验测试。对于外行来说,有时很似乎难明白其中的差别。不过,既然开发组拿用户当免费的测试人员,对这一点也就只好忍一忍了。

解决问题的顺序,首先从快见效,对现有结构影响最小的那个问题入手。

这就是撞墙提示音的问题。
c++可以通过Beep指令调用系统喇叭发出声音,用于表达撞墙提示足够了。
Beep指令接受两个参数,第一个是声音的频率,第二个是声音持续的时间(毫秒数)。
下面这样的一组参数调整,听起来差不多就是撞墙的一个声音了。

Beep(200, 100);


修改点完全集中于我们已有的tryMove函数中,以前只是在可以移动时进行移动,现在我们在不能移动时发出声音。这里将引入一个新的语句else。
它只与if语句配合使用,其功能是当前一个if语句条件不成立时,执行else后面的语句。
完整代码如下:

    void tryMove(int ax, int ay){
        if (mapInfo[ay][ax]== ' ') {
           hideHero();
           x= ax;
           y= ay;
           showHero();
        } else {
            Beep(200, 100);
        }
    }


请注意一个细节:以前版本的代码中,我们把hideHero()和showHero()放在了判断之外。但现在由于有了撞击发声的延迟,如果继续这样布置语句的位置,一旦出现发声,在英雄不能移动时,会出现英雄短暂消失的视觉效果。为了避免这一问题,所以我们必须在确保可以移动时才去做英雄的隐藏和显示。

第二步,来解决地图显示颜色的问题。
控制台有改变颜色的功能,全光谱的颜色都可用,但同时出现在屏幕上的颜色只能有16种。最规范的做法,当然每张地图配一组不同的调色板,这样的地图效果可以达到最细腻。但考虑到控制台项目的本质,不把对画面效果的追求放在重点,所以我们只选取默认的16种颜色。
用这样一个函数,就可以设置下一次输出到屏幕的文本颜色。

void setColor(int aColor){
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), aColor);
}


为了这个函数工作,必须引入<windows.h>库
我们可以将setColor函数理解为对SetConsoleTextAttribute函数的一个封装。因为我们不关心更多的细节。
setColor函数只需传递一个参数,即颜色值。
为了弄清楚每个数字代表什么颜色,我们做一个小程序来测试。

# include <iostream>
//# include <conio.h>
# include <windows.h>

using namespace std;

void setColor(int aColor){
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), aColor);
}

int main(){
    for (int i=0; i< 16; i++){
        setColor(i);
        cout << "颜色" << i << endl; 
    }
    return 0;
}

运行效果是这样。

颜色0看不到,因为是黑色,与底色一样了。
从0到15的一个数字,可以用4位2进制来表现。
比如6是0110,而14是1110
配色的规律,我们可以理解为每一位二进制,代表一种颜色成分的有无。
从低到高,分别是蓝色成分,绿色成分,红色成分,高亮成分。
其中0是纯黑色,15是纯白色,默认的颜色是7号:暗白色。
大体来说是0~7为暗色,8~15为亮色。
但由于8号颜色是亮色的黑,而7号是暗色的白,所以7号的颜色反而显得比8号的颜色更亮。

二进制是计算机的基础,如果读者对此不甚清楚,这里值得用一点篇幅做一个扩展的介绍。

二进制

关于二进制,这里重点讲三个问题

  • 1、数字的表示法
  • 2、计算机为何采用二进制这种表示法
  • 3、二进制如何表现负数

数字的表示法

我们应当区分数字的值和数字的表示法是两个不同的概念。
比如当我们看到35这个数字,它首先是一个表示法,如果进一步明确了它用的表示法是10进制,我们就能够准确地知道它的值了。如果不借助任何进制,我们一样能够准确地表达出这个数字,只不过麻烦一点:它和下面的五角星个数相同。
★★★★★★★
★★★★★★★
★★★★★★★
★★★★★★★
★★★★★★★

为了能够简洁地表现这个值,我们就必须借用一种表示法。
常见的数字表示法是进制表示法,其特点是

  • 1、有若干个基本符号,每个符号代表一个数字值
  • 2、一个数字表示为若干位,同样的符号在不同的位置有不同的权重,其真正的值是它自己代表的值和权重的值相乘的结果
  • 3、数字的值就是每位和其权重值乘积的结果之和

所谓10进制,就是
右边第一位的权重为1,
第二位为10,
第三位为100,
……
依次类推;

比如16进制,就是
右边第一位的权重为1,
第二位为16,
第三位为256,
……
依次类推;

而二进制,则是:
右边第一位的权重为1,
第二位为2,
第三位为4,
……
依次类推;

比如代表上面五角星的个数的这个数字,用10进制表示为35,用16进制表示为23,用2进制表示则是100011。
为了清晰起见,可以严格地这样来表示(100011)2=(23)16=(35)10
顺便说一下,进制表示法并不是唯一的数字表示法。比如还有一种比较常见的罗马数字表示法,用于表示比较小的数字也很方便,时常见于钟表的表盘。
它的逻辑是不同的字母代表不同的数,以大数为准,左减右加。比如这个35,就可以表示为XXXV(X代表10,出现3次代表30,V代表5,出现在右边代表加)。原则上,任何一个符号的出现不能超过3次,所以想表示45就必须换一个思路,用XLV来表示(L代表50,X出现在L的左边代表-10,V出现在L的右边代表+5,结果是45)。使用这种数字表示法的人们口算能力想必是非常强的。
古代的韩信点兵,也可以理解为另一种数字表示体系,它只能表示不超过105的数字。第一位代表除3的余数,第二位代表除5的余数,第三位代表除7的余数。在这个表示体系中,35表示为002。
扩展一下,这个数字体系中
(70)10=(1)韩信
(21)10=(10)韩信
(15)10=(100)韩信
现在知道韩信点兵的计算口诀是怎么来的了么?

计算机为何采用二进制这种表示法

现在我们明确了二进制就是一种数字表示法。现在的电子计算机由于其物理实现的方式,使用二进制具有物理优势,这里不详细展开了。但以前有模拟计算机,以后会有生物计算机和量子计算机,对它们来说,使用二进制都未必是天然的。


二进制怎样表示负数
二进制表示负数的方法,最常用的是补码法。必须说明一点,不是只有二进制才有补码的概念,10进制一样可以用补码来表示负数。但补码有明显的局限,即所表示的数字范围受限。而且10进制是给人类看的,人类不在意多用一个符号“-”来代表负数,然后规定“减负等于加正”等一系列的负数运算规则。但计算机只有两个符号,不能在0/1之外再引进一个负号,而且计算机表示数字本来就受位数的局限,它不在意增加这个局限,最后从提高效率的角度来看计算机也不希望正数和负数有不同的计算规则。
为了理解2进制的补码,先让我们看一下10进制的补码。有补码就有了位数限制,为了简单起见,现在假设我们只能使用2位10进制的时候,如果都表示正数,我们可以表现的数字就是0~99。如果想表示负数,我们就规定0~49是整数,而用50到99代表负数-50~-1。就是用100加上这个负数所得到值来表示这个负数。
这样35仍然代表35,而-3就变成了97。
看看我们的运算:35+(-3)=32
现在的35+97=132,但我们只有两位,所以只取后两位,也就是32。
两者结果完全一样。
明白了原理你就知道,它不可能不一样。
明白了10进制的补码原理,二进制也就简单了。
比如当我们使用8位2进制的时候,如果用来表现无符号数(unsigned),那么能表示0~255范围内的数。如果用来表示有符号数,则我们规定0~127为正数,128~255为负数代表-128~-1,也就是用256加上这个负数所得的值来表示负数。
同样上面的例子:100011代表(35)10,11111101代表(-3)10,两者求和,舍去最高位后,(100011)2+(11111101)2=(100000)2
以上就是二进制的负数表示法。


二进制还可以用来表示小数,这里就不再做展开讲解了。让我们尽快回到如何显示彩色地图的问题上,以免小Pa等得着急。
虽然有了控制输出内容颜色的方法,但现有的显示地图的方式,是一次输出地图的一整行。这样的显示方式,是不可能在一行中出现不同颜色的。我们调整一下地图显示的实现方法,可以做到精确地控制每个字符的颜色。

只需在Map类中做一些修改,代码如下:

    void showMap(){
        system("cls");
        for (int y=0; y< mapInfo.size(); y++){
            for (int x= 0; x< mapInfo[y].size(); x+=2){
                gotoxy(x, y);
                string str1= mapInfo[y].substr(x, 2);
                setCharColor(str1);
                cout << str1;
            }
        }
    }
    void setCharColor(string aChar){
        if (aChar=="♀"){
            // 英雄用亮黄色 
            setColor(14);
        } else if (aChar=="█") {
            // 墙用白色 
            setColor(7);
        } else {
            // 文字用浅蓝色 
            setColor(11);
        }
    }
    void showHero(){
        gotoxy(x,y);
        setCharColor("♀");
        cout << "♀";
    }

setCharColor是一个新增的函数,它根据所显示的内容不同,设置不同的颜色。
比如,英雄用亮黄色,墙用白色,文字用浅蓝色。
这个函数里使用把具体的颜色和字符对应写入代码的方式,被称作“硬编码”。
硬编码是一个典型的特殊的解决方案,很显然这种硬编码的方式不可能提供一个一般的解决方案。
但对于编程的开发过程来说,是有其价值的。我们可以通过硬编码的代码来屏蔽复杂性以便其他的程序逻辑能够正常测试;同时硬编码的尝试能够具体地展现代码逻辑,有助于我们思考怎样提炼出更一般代码。但既然使用了硬编码,我们就不应在显示地图和英雄的时候各自设置,而是设置这样一个函数来统一设置颜色,方便以后的修改。
从语法的角度,我们可以看一下,多个if-else的连用是怎样的格式。

我们先从改动较小的函数说起,showHero,在显示之前,必须调用一个设置颜色的语句,讲英雄的显示符号作为参数传过去,我们注意到同样的一个“♀”被写了三次,这是一个警示,我们应当做一些什么事情来改变这种情况。否则如果我们忽然决定换一个英雄标识的时候,必须改动三处。这种现象,不应该存在于一个严谨的代码中,它应该进入我们的任务列表,尽快解决。

然后来看改动比较大的showMap函数。我们发现,showMap函数中可以调用定义在它后面的函数setCharColor,类函数之间的互相调用,不用考虑前后顺序的问题。
这里用了二重循环来显示地图。一个地图,本来就是一个二维的概念。为了明确起见,我们使用x和y作为循环变量,而没有使用一般的i,j作为循环变量。
指令x+=2,是指令x=x+2的缩写形式,它的意义就是避免重复输入两次变量名。对于代码来说,重复的东西越少越好。这就是所谓的干燥原则(DRY):Don't Repeat Yourself
为什么是x+=2,还是那个原因,中文字符一个等于英文字符的两个。
而substr是字符串对象的一个函数,取子串。针对字符串有许多操作函数,取子串是其中之一。我们后面会有专题讲解,这里先只看这一个取子串函数。它接收两个参数,一个是起始位置,一个是子串长度。
我们用c++编程应当尽快习惯所有的内部编号序列都是从0开始的,字符串也一样。
新方法显示地图,一次只显示两个字符(或者说一个中文字符)。每次用gotoxy指定它的位置,而用setCharColor指定它的颜色。endl回车标志的存在只是为了调整下次输出时光标的位置,而现在是每个字符的输出都重新确定位置,所以它也不必使用了。

最后的效果是这样(以map1为例)

课程小结:

本节课实现了小Pa所提的五个需求中的两个,当然中间我们用了一些篇幅来讲二进制的原理。
然后我们告诉小Pa,她提的需求太多了,不可能一次改完。小Pa表示理解——客户一般都会理解的。
在开发过程中,应该建立一个的任务列表,随时记录当前和后面的开发任务。因为我们经常会在开发功能A的时候忽然想起应当先实现功能B,而在实现功能B的时候又惊讶地发现不得不先实现功能C。如果不稍加记录,嵌套了几次之后,我们可能会忘记最开始的设计思想和架构思路。

本章的完整代码及附属工具的代码,请见这里
 

发布了24 篇原创文章 · 获赞 0 · 访问量 4568

猜你喜欢

转载自blog.csdn.net/xiaorang/article/details/104959422
今日推荐