第二章教程14:管理器夺权

本次教程内容:

  • 播放背景音乐
  • 键盘监听功能转移
  • 结构体
  • 融入事件机制
  • 数字与字符串互相转换

不同的需求如果能用统一的操作方法来实现,无论是客户还是程序员都会受益。

再回顾一下上节课中小Pa提出的5个需求:

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

针对需求中尚未解决的问题,开发组给小Pa提出两点建议:

  • 1、掌声效果可以加上,但应当由小Pa去找合适的掌声效果
  • 2、在最后一张地图中,英雄不能移动,未必是最好的选择,不如给英雄两个明确的移动方向:一个方向是再次游戏;另一个方向是退出游戏。这样,操作逻辑更加统一,而且增加再次游戏的功能,对用户更加友好。

小Pa愉快地接受了建议,回去寻找音效和修改地图。这样我们的工作组有时间来研究实现方法。

播放背景音乐是通过MCI机制,作为入门编程,我们无须对此机制有深入的了解。
源代码中附带了播放音乐用的audio_clip.cpp和audio_clip.h文件,我们知道如何在代码中引用它就好了。
现在的问题是,map.cpp文件本身已经越来越大了,它自身包括了两个类(地图类,地图管理器类),同时还附带了几个工具功能在其中(定位光标,隐藏光标,设置颜色),而且,马上还得增加一个播放音乐的功能。是时候再做一次文件级的功能重构了。
我们可以把所有工具性质的函数放到另外一个tools.cpp中。
其他3个函数,就是简单的代码移动。这里只重点讲解一下音乐播放的部分。完整代码,请见资源。

AudioClip gPlayer;
void playSound(string aFileName){
    gPlayer.load(aFileName);
    gPlayer.play();
}
void stopSound(){
    gPlayer.stop();
}

audio_clip中的播放功能是通过一个AudioClip类来实现的,这也是因为面向对象的思路大为普及的具体表现。但我们只是简单地取用其中播放音乐和停止音乐两个功能,所以我们用了这样一个机制,把它改为了两个功能函数。

下面就是一个代码重构的重点了。从功能分配的角度来说,具体功能应当下放到所须知识尽量少的位置。但如果在那个位置上的知识不足以支撑它行使整个功能,就可以考虑是否把功能的实现位置提升一级。
现在的键盘监控功能就处于这样的位置。
以前的键盘监控只是在本地图内行走,所以把它放在地图对象中是合适的。但现在地图的行走过程中会出现切换到其它地图的动作,而其它地图的信息,不是这个地图对象所能了解的,也不是它应该了解的。这就暗示了控制地图行走这一功能可能不宜继续留在地图类之中,而是应当把它提升一级,放到地图管理器中。

前面讲过,在面向对象的编程中,每个对象的知识越少越好,增加新知识最好能有一个合理的原因。

目前看,如果键盘监听提升到地图管理器中,地图管理器也必须增加一个新的知识,它必须知道当前显示的地图是哪个。才能准确地做出哪里可以移动的判断。考虑到地图跳转的动作由地图管理器来实现是合理的,所以目前看来,让它增加这个知识也是合理的。

另一个问题是,英雄的位置究竟记录在哪里合适?因为英雄只有一个,把它分别记录在每个地图上似乎有其不合理性,让我们把它也记录在地图管理器中。而每个地图上的(x,y)含义做一下调整,作为默认的入口位置。这样感觉更加合理一些,我们先暂时做这样的调整。

既然调整到了英雄的信息,我们还记得上次提到过,英雄的标记不应当多次以常数的形式出现在代码中。我们可以考虑设计一个结构体,把位置信息和标志统一保存起来。
这里顺便介绍一个c++中面向对象编程的一个方言:结构体struct,struct和class具有相同的含义,都是将变量和函数封装在一起。唯一的区别是struct所有的变量和函数触发指定,否则都是public,而class正好相反。从使用的习惯上,功能为主的对象一般用class,而变量为主的对象一般用struct。因为英雄信息中变量为主,操作还不多,所以我们把它定义为struct。代码如下:

扫描二维码关注公众号,回复: 10255684 查看本文章
struct HeroInfo{
    int x, y;
    string mark;
    int color;
    void moveTo(int ax, int ay){
        x= ax;
        y= ay;
    }
};

它包含四个变量,其中x,y是当前位置,mark是标记,color是英雄的颜色。考虑到英雄的颜色如果由每个具体地图对象的setCharColor函数来指定是不合理的,所以把英雄标记的颜色也提出来。目前只有一个操作moveTo,这个函数避免了移动英雄是对x,y分别赋值。
为了对mark和color赋值,我们建立一个MapManager的初始化构造函数。

    MapManager(){
        hero.mark= "♀";
        hero.color= 14;
    }


而在jumpMap的动作中,也有一些初始化的工作。

    void jumpMap(string aName){
        if (mapList.count(aName)== 1){
            currentMap= mapList[aName];
            currentMap.showMap();
            hero.x= currentMap.x;
            hero.y= currentMap.y;
            showHero();
        }
    }

由于英雄的信息移动到了地图管理器,showHero(), hideHero()函数也同样移动到了地图管理器,同样,由于tryMove函数涉及到修改英雄坐标的功能,也同样移动到了地图管理器,Map类的函数只剩3个。这是一次上级对下级的大规模夺权!
由于上级管理类总由办法获得下级的信息,所以上级类有一种天然的夺权倾向。一旦某个功能不容易处理时,就会被放到上级管理类,造成管理类过度膨胀。
所以在我们设计程序的时候,一旦有机会就应当尽量做出调整,以平衡这种天然趋势。

做了这样的调整后,地图的跳转成为了可能。
我们使用一种事件登记机制,来优雅地解决这个问题。
我们的事件包含两部分内容,一个是事件的触发位置,一个是事件的动作。
触发位置又有三个信息:地图名,x坐标,y坐标;事件的动作则是目标地图名。
希望读者看到这里,能够想到我们前面讲过的map容器。map容器提供一对一的映射关系。
这里我们可以把地图名+x坐标+y坐标的整体,形成一个键,而把目标地图名作为值。
事件列表数据的初始化,我们会使用硬编码,除了上节课所讲的原因之外,另一个原因是速度快,小Pa很快就会带着掌声音效和修改后的地图回来,我们希望在她回来后能尽快看到我们的代码效果。如果事后我们能想起来把这些硬编码做很好的重构,那就没有任何问题了。
相关代码如下:
事件列表定义

    map <string, string> eventList;


事件列表初始化,位于MapManager的构造函数中

        eventList.insert(make_pair("map1_40_19", "map2"));
        eventList.insert(make_pair("map2_40_1", "map3"));
        eventList.insert(make_pair("map3_40_19", "mapWin"));


增加一个事件检测函数testEvent

    void testEvent(int ax, int ay){
        string str1;
        str1= currentMap.name+ "_"+ int2str(ax)+ "_"+ int2str(ay);
        if (eventList.count(str1)== 1){
            string evt1= eventList[str1];
            jumpMap(evt1);
        }        
    }


在这个代码中,值得重点讲一下int2str函数。
将数字转换为字符串,或则它的反操作,把字符串转为数字,在很多数程序中是极为常用的功能。
不管c++有没有处理它的函数,但我们都来用新的逻辑实现一下。作为一个常用的工具函数,我们把它的实现放在tools.cpp中。
这里将用到另外一个库sstream(String Stream:字符串流)
正如在c++里处理文件时把文件当作和cin/cout一样输入输出流,它还可以把字符串也当作一种输入输出流。
转换的方式具有一定的通用性:首先定义个字符串流。
如果想把数字转成字符串,就把数字输出到流中,然后用这个流来读输入到一个字符串变量中。
如果想把字符串转成数字,就是相反的操作,先输出字符串到流中,然后再输入到数字变量。
具体代码见下:

string int2str(int aInt){
    stringstream res;
    res << aInt;
    string ret;
    res >> ret;
    return ret;
}

int str2int(string aStr){
    stringstream res;
    res << aStr;
    int ret;
    res >> ret;
    return ret;
}


用这个操作逻辑,可以很方便地将任何内容转成字符串,也可以从字符串中读取任何结构化的内容。

而testEvent在tryMove函数中当确认能够移动时,在移动后调用此函数。

    void tryMove(int ax, int ay){
        if (currentMap.mapInfo[ay][ax]== ' ') {
           hideHero();
           hero.moveTo(ax, ay);
           showHero();
           // 移动到新位置后,检测是否会触发事件
           testEvent(ax, ay); 
        } else {
            Beep(200, 100);
        }
    }
    


代码功能实现得差不多了,开发组的组长小Q一边测试迷宫,一边就等小Pa回来了。应该说这是小Q第一次真正走通小Pa设计的这几个迷宫。程序开发者不重视数据是一个常见的现象,但有些时候,不见到真实的数据,就不会发现代码中的问题。没想到,走到第三幅地图,眼看就可以到出口,获得胜利,没想到却怎么也走不出去了!

正在此时,小Pa走了进来。

小Pad动作其实很快,找到鼓掌的音效并不难。打开免费素材的爱给网的音效专区:http://www.aigei.com/sound/
搜索“鼓掌”,就出现了很多相关的音效素材。选择一些进行试听之后,她选择了其中一个。
修改地图,小Pa的设想是最后一张地图让英雄出现在屏幕中间,再规划出一条路,向左走可以再次启动游戏(其实也就是跳转到第一张地图);向右走可以退出游戏。
在程序员看来,这些都算不上什么工作量,但小Pa肯定不同意这个说法。

设计好之后,小Pa回来找开发组,准备进行新的一轮测试。没想到进门之后听到的第一句话就是:“小Pa,你的地图设计得有问题啊!”

这也不完全是推脱责任,地图处理代码其它地方行走正常,唯独这里出现问题,肯定说明地图有什么异常。
这个问题究竟是什么原因造成的,是谁的责任,应当怎样解决,请听下回分解。

课程小结:

客户的前一个需求满足后,她就必然会提出更多的需求。事件处理部分是让地图实现动态效果,也是RPG游戏的一个关键的情节载体。

本章的完整代码及附属工具代码的下载地址

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

猜你喜欢

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