本次教程内容:
- 地图对象的外部定义
- 异常处理机制
- 菜单控制的实现
我们在事件定义的结构上略做修改,把地图的基本参数定义也加进来,这样就可以完全取消主函数中逐个读取地图的硬编码。一张地图,既有属性,又有函数,封装起来,很有对象的感觉了。
设计结构做如下调整:
#snake
@mode=2
@x=40
@y=19
hitWall(){
jump(start);
}
load(){
init();
}
length(10){
jump(snake2,20);
}
为了简单起见,参数定义加一个特殊标记@开头。写起来明确,计算机读起来也方便。
我们设计了一个按名字存储这些参数的对象:
class ParmList{
map<string, string> data;
public:
void addParm(string aLine){
int i=0;
string str1, str2;
str1= readBefore(aLine, i, '=');
str2= aLine.substr(i+ 1);
setValue(str1, str2);
}
void setValue(string aKey, string aValue){
data.insert(make_pair(aKey, aValue));
}
string getValue(string aKey, string aDefault=""){
map<string, string>::iterator it = data.find(aKey);
if (it != data.end()){
return it->second;
} else {
return aDefault;
}
}
void clear(){
data.clear();
}
};
在内部使用了map来存储信息,实现了addParm函数,专门用于拆分这种“=”连接的参数定义形式。
一旦我们让客户来输入什么,有一件事就不得不做了,那就是异常处理。因为你不能假设客户能够输入格式正确的数据。我们的代码应当在客户输入了错误的数据时给出尽量明确的提示,而不是无声无息地崩溃掉。
c++的异常处理包括两个机制:
机制1:抛出异常
抛出异常就是非常简单的一个电话,我这里出错了。之所以用抛出,就是表现了本地不再做任何处理的意思。
机制2:捕获异常
捕获异常,通过两个语句来实现
try 语句(做好准备):我们预见到这里可能会抛出个异常,让我们提高警惕。
catch语句(接住异常):果然异常来了,让我们看看是什么,准备处理它。
我们看具体的代码:
异常抛出者(BasicMap类的loadMapFromFile函数):
void loadMapFromFile(string aFileName){
ifstream fin(aFileName.c_str());
if (fin) {
while (!fin.eof()){
string str1;
getline(fin, str1);
mapInfo.push_back(str1);
}
} else {
// 这里抛出异常
throw "Map <"+ aFileName+ "> not exists.";
}
fin.close();
}
让我们看看精灵在面对遇到异常时具体都在干什么?
精灵2(异常抛出者):(打电话)文件工作组么?请打开这个文件,把读取接口与fin房间中的对象对接,谢谢。
文件工作组:已办妥,请接收。
精灵2:什么?这就是你们干的活么?接口是空的?
文件工作组(电话已挂断)
精灵2:(打电话)喂,最高监听组么?我投诉!
最高督查组:这里是无人应答电话,听到滴声后请留言。滴。
精灵2:我这里遇到一个情况,我只能告诉你一句话:“文件不存在”。(throw)
(自言自语)这就是我知道的全部内容了,我已经仁至义尽了,其他事情我就都不管了,再见。
(挂掉电话,隐身)
异常捕获者(MapManager类的addMap函数):
BasicMap* addMap(string aName, int ax, int ay, int aMode){
aName= lowCase(aName);
if (mapList.count(aName)== 0){
BasicMap *pmap;
if (aMode== 1){
pmap= new MapRpg();
} else {
pmap= new MapSnake();
}
pmap->mm= this;
pmap->x= ax;
pmap->y= ay;
pmap->mode= aMode;
try {
// 准备在这里捕获异常
pmap->loadMap(aName, dataPath+ aName+ ".txt");
} catch (const string msg){
// 捕获到了字符串异常,直接显示并退出程序
cerr << msg << endl;
exit(1);
}
mapList.insert(make_pair(aName, pmap));
return pmap;
} else {
return mapList[aName];
}
}
让我们看看精灵在处理异常时是怎么做的。
精灵1(异常处理者):(自言自语)现在我们打算做一件有风险的事情,弄不好会出错,出错了会有人投诉,把最高督查的电话先接过来。(try)
精灵1:(施展魔法)地图对象的loadMap,创建分身!(精灵2出现,并得到执行代码)
精灵1:(打电话)精灵2,请按这个文件名,载入地图。
精灵2:好的,请稍....
(电话已经挂断了)
精灵1:(迷惑中)干好了么?这是啥情况?
这时,最高督查组电话响了。
精灵1:不忙接,我们看看电话留言是什么。
电话显示:收到一条文字信息。
精灵1:如果是文字信息的话,那就归我管。(catch)
(自言自语:不过我不关心它的内容)
精灵1:(打电话)控制台,我们收到一条文字信息,可能是个错误,请把它显示出来给用户看。
控制台:我们已经通过标准错误输出显示了。
精灵1:等等,那个“标准错误输出”是啥?
控制台:其实也就是显示在屏幕上了。
精灵1:OK,我希望用户已经明白出了什么问题。想让我们继续干活,就把该提供的东西弄正确再来!退出程序。
我希望通过上面的模拟对话过程,用户能明白异常处理的机制。
下面让我们来设计菜单的实现。菜单的实现,包括五部分内容:菜单的信息载入,菜单的触发,菜单显示,菜单的控制,菜单的隐藏。设计的重点是,每部分功能的职责放在哪里。
一、菜单的信息的载入:
1、仿照事件定义的脚本结构,设计出菜单定义的脚本结构。
#main
重新开始(){
jump(start);
}
退出(){
quit();
}
“#”后面是菜单的名字,然后每个类似函数的结构,函数名是菜单选项,而函数的内容是点击选项后执行的动作。
这套设计与事件定义几乎完全相同,读取逻辑也基本一致。同样,如果菜单有属性,也可以在函数定义的前面添加。
2、载入的行为,由MapManager执行。
3、菜单的存储,由MapManager负责
二、菜单的触发:
1、键盘增加对ESC的监听,通过escMap事件来触发菜单
2、事件的执行,只能由MapManager来执行
三、菜单的显示
1 一个方框,通过调用函数drawWindow来显示
2 方框内多个菜单条目,通过调用函数textOut来显示
3 当选条目的标记,通过设置背景色高亮的方式来表现
四、菜单的职责
1 打开菜单后,当前地图类不能继续接收键盘信息,也不再处理自动任务
2 键盘信息由MapManager直接转交Menu类处理
3 菜单有自己独立的键盘处理函数
4 菜单负责记录自己的事件,但不负责执行
5 菜单能够看见调用它的地图,菜单所有事件均通过地图来执行
五、菜单的控制
1 上下键切换当选条目,在菜单对象内操作,切换后显示刷新
2 ESC键无选项关闭菜单,触发escMenu事件,实际由MapManager来执行,通过所在地图局部刷新来实现
3 回车/空格键选择当前条目对应的事件,并执行脚本,具体谁来执行,根据动作来决定
drawWindow和textOut两个函数,作用分别是在屏幕上显示一个有边框的矩形区,和从某个位置显示文本。
这两个函数具有一定的工具性质,比如除了在现实菜单上用到之外,在显示对话,以及战斗画面上也能用到。
但它们与tools.cpp中的普适工具函数的性质还是有所不同的,又更加具体了一些。
我们把它们单独放在一个cpp文件中,它只能看到tools.cpp。
菜单类的实现,放在menu.cpp中,它可以看到basicmap.cpp,tools.cpp,share.cpp
原来的map.cpp可以上面这个两个源代码。
新的代码逻辑,用三个图进行描述:源代码关系图,核心类图,菜单操作时序图。
从菜单操作时序图看,信息流在很大程度上由MapManger和Map对象负责传递。这部分工作,实际上独立与地图的具体功能实现,最好能由一个专门的机制来集中处理。
另外,MapManger还负担了创建读取地图和菜单的工作,此外地图类也负责了地图信息的读取。这导致这两个类与外部文件的具体格式绑定得太紧密。如果能由一个专门的机制负责创建对象,而地图和管理器只负责运行业务。在结构上会显得更加清晰。
课程小结:
随着功能的不断增加,系统架构的早期设计方案已经面临巨大压力。如果这时候我们来看设计模式,就回发现其中很多模式正是为了解决这些问题而提出的。
欢迎加入编程教学讨论群:102494165
本教程每节课的源代码,统一下载地址
链接:https://pan.baidu.com/s/1q4aoYesre1PHaCoV8gkhDQ
提取码:8den