目录
Bug1:点击smile所处的位置会显示“No Way!”,按照正常逻辑,宝箱出现的位置与smile相同应该成功找到宝箱才对。
Bug2:点击笑脸出现“No Way” 和“Found Treasure”文字的叠加。这是因为代码中只是简单地向场景中添加Label,而显示新的Label时,原来的Label不会被清除。
Bug3:smile精灵和box精灵的碰撞矩阵太大,相隔一个图块的情况下,程序将其判定为碰撞。
Bug4:当距离危险区域只剩一个图块时,点击进入危险区域,不会显示危险区域提示。
Bug5:下方的“Click Map To Put Treasure”文字无法完全显示。
一、实验目的与要求
1. 理解A*寻路算法原理。
2. 进一步熟悉地图编辑器的使用。
3. 实现游戏中的人工智能。
二、实验内容与方法
1.完成游戏编译
成功编译并运行教材P200“游戏AI实例-迷宫寻宝”。
2.完成修改内容一
修改游戏代码,实现修改内容一,即修改窗口大小。
3.完成修改内容二
修改游戏代码,实现修改内容二,即增加DANGEROUS区域。
4.完成修改内容三
修改游戏代码,实现修改内容三,即增加按键监听。
5.完成修改内容四
修改游戏代码,实现修改内容四,即增加地图层次。
6.完成Bug修改
通过更改游戏代码的方式修复BUG,如:点击笑脸出现“No Way” 和“Found Treasure”的叠加。
7.添加配乐、音效
为游戏添加配乐、音效。
8. 游戏优化升级
自行发挥想象力,优化游戏功能。
三、实验步骤与过程
1. 完成游戏编译并修改游戏名称和窗口大小。
(1)创建游戏项目,详细步骤见我的实验1的文章。
(2)根据地图文件中显示的地图的宽度、高度、图块宽度和高度信息,计算出地图的宽为32 x 32=1024,高为20 x 32=640。如下图所示,aStar.h文件中定义的3个变量也证明了这一点。
(3) 在AppDelegate.cpp中,将游戏窗口大小改为1024x640。
static cocos2d::Size designResolutionSize = cocos2d::Size(1024, 700);
2. 添加DANGEROUS区域。
(1)使用Tiled打开地图文件,在地图中添加危险区域的图块,四个图块的编号分别是251、252、275和276。
(2)在aStar.h中定义两个新的变量DANGEROUS和NOT_DANGEROUS,表示方块是否为危险区域,然后在mapNode结构体中添加一个新的成员变量isdangerous,用于记录当前节点是否为危险区域。
#define DANGEROUS 7 //方格是否为危险区域
#define NOT_DANGEROUS 8
//定义节点结构:
struct mapNode
{
int status; //节点的状态标志
int isdangerous; //节点是否为危险区域
int xCoordinate; //节点的横、纵坐标
int yCoordinate;
int fValue; //节点的f值,g值,h值
int gValue;
int hValue;
mapNode* parent; //节点的父节点指针
};
(3)在MazeScene类的initMap函数中,遍历地图每一个图块时,先通过getTileGIDAt函数获取当前的图块编号,如果图块编号是251、252、275或276时,表示当前图块为危险区域,将当前图块的结构体mapNode的isdangerous的值设为DANGEROUS,否则设为NOT_DANGEROUS。
//依次扫描地图数组每一个单元
for (int i = 0; i< MAP_WIDTH; i++)
{
for (int j = 0; j< MAP_HEIGHT; j++)
{
int temp_isdangerous = NOT_DANGEROUS;
//危险区域
if (m_mapLayer->getTileGIDAt(Vec2(i, j)) == 251 || m_mapLayer->getTileGIDAt(Vec2(i, j)) == 252
|| m_mapLayer->getTileGIDAt(Vec2(i, j)) == 275 || m_mapLayer->getTileGIDAt(Vec2(i, j)) == 276
|| m_mapLayer->getTileGIDAt(Vec2(i, j)) == 169 || m_mapLayer->getTileGIDAt(Vec2(i, j)) == 170
|| m_mapLayer->getTileGIDAt(Vec2(i, j)) == 185 || m_mapLayer->getTileGIDAt(Vec2(i, j)) == 186)
{
temp_isdangerous = DANGEROUS;
//CCLOG("dangreous\n");
}
//若当前位置为墙体瓦片设置为不可通过
if (m_mapLayer->getTileGIDAt(Vec2(i, j)) == NOT_ACCESS_TILE
|| m_mapLayer->getTileGIDAt(Vec2(i, j)) == NOT_ACCESS_TILE1)
{
mapNode temp={NOT_ACCESS,temp_isdangerous, i, j, 0, 0, 0, nullptr };
m_map[i][j] = temp;
}
//否则设置为可以通过
else
{
mapNode temp={ ACCESS,temp_isdangerous, i, j, 0, 0, 0, nullptr };
m_map[i][j] = temp;
}
}
}
(4)在MazeScene类的移动路径moveOnPath函数中,定义一个变量dangerous_num记录危险区域在当前路径上的编号,当路径中出现危险区域时,立即记录该编号,否则为默认值-2,由于此时路径是从终点向起点遍历的,所以一定能够获取到smile第一次遇到危险区域时的编号。
//危险区域在当前路径上的编号
int dangerous_num = -2;
while (tempNode != nullptr)
{
//判断是1否存在危险区域
if (tempNode->isdangerous == DANGEROUS)
{
dangerous_num = loopNum;
}
path[loopNum].x = tempNode->xCoordinate;
path[loopNum].y = tempNode->yCoordinate;
loopNum++;
tempNode = tempNode->parent;
}
在遍历路径的结构体数组时,将结构体的编号和dangerous_num进行比较,如果相等表示存在危险区域,显示危险区的提示文字,并直接退出遍历。
(5)运行程序,当smile移动路线上有危险区域时,显示“Dangerous! You die!”文字,路径被危险区域阻挡,无法移动至新的区域。
3. 增加按键监听,当按下R键时,游戏重新开始。
(1)在MazeScene类的init函数中调用EventListenerKeyboard类的create函数定义变量listener,使用onKeyReleased函数为监听器添加键盘事件,当按下R按键时,调用replaceScene函数重新加载场景,最后调用addEventListenerWithSceneGraphPriority函数设置键盘监听优先级。
//增加R按键监听
auto listener = EventListenerKeyboard::create();
listener->onKeyReleased=[&](EventKeyboard::KeyCode code, Event* e)
{
switch (code)
{
case EventKeyboard::KeyCode::KEY_R:
Director::getInstance()->replaceScene(this->createScene());
break;
default:
break;
}
};
Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);
4. 添加地图层次,将迷宫地图设置为两层。
(1)使用Tiled设计一个1024x640的新地图,由于图块元素太多会让代码写起来更复杂,因此我让不可通行的区域用同一种图块,这样代码写起来比较简洁。
接着我新建一个图层覆盖在其上面,这个图层仅仅起到让地图更加美观的作用。
最后的地图效果如下图所示,图中白色的草为危险区域,绿色、黄色图块和桥都是可通行区域,水域和墙面都是不可通行区域。
(2)在MazeScene类中添加两个新的成员变量,一个用于记录当前游戏进入第几层的迷宫,另一个变量用于判断smile是否位于楼梯。
//位于第几层迷宫
int map_layer = 0;
//是否位于楼梯
bool is_starts_up = false;
(3)修改MazeScene类的init函数,当map_layer等于0时,加载第一层的地图,当map_layer等于1时,加载第二层的地图。
(4)修改MazeScene类的initMap函数,在危险区域和不可通过区域的判断条件中添加第二层的地图对应的图块编号。
//危险区域
if (m_mapLayer->getTileGIDAt(Vec2(i, j)) == 251 || m_mapLayer->getTileGIDAt(Vec2(i, j)) == 252
|| m_mapLayer->getTileGIDAt(Vec2(i, j)) == 275 || m_mapLayer->getTileGIDAt(Vec2(i, j)) == 276
|| m_mapLayer->getTileGIDAt(Vec2(i, j)) == 169 || m_mapLayer->getTileGIDAt(Vec2(i, j)) == 170
|| m_mapLayer->getTileGIDAt(Vec2(i, j)) == 185 || m_mapLayer->getTileGIDAt(Vec2(i, j)) == 186)
{
temp_isdangerous = DANGEROUS;
//CCLOG("dangreous\n");
}
//若当前位置为墙体瓦片设置为不可通过
if (m_mapLayer->getTileGIDAt(Vec2(i, j)) == NOT_ACCESS_TILE
|| m_mapLayer->getTileGIDAt(Vec2(i, j)) == NOT_ACCESS_TILE1)
{
mapNode temp={NOT_ACCESS,temp_isdangerous, i, j, 0, 0, 0, nullptr };
m_map[i][j] = temp;
}
(5)当点击的图块为楼梯时,应该不显示宝箱。在MazeScene类的onTouchBegan中,要为点击的地图单元添加宝箱精灵,通过getTileGIDAt函数获取点击的图块的编号,如果其编号为4即楼梯,就通过setVisible函数将宝箱精灵设为不可见,并将is_starts变量设为true。
//在当前地图单元添加宝箱精灵
auto box = this->getChildByTag(BOX_TAG);
box->setPosition(0.5 * UNIT + x * UNIT, m_visibleSize.height - 0.5 * UNIT - y * UNIT);
box->setVisible(true);
//终点为楼梯
if (m_mapLayer->getTileGIDAt(Vec2(x, y)) == 4)
{
box->setVisible(false);
is_starts_down = true;
}
(6)在MazeScene类的update函数中,通过intersectsRect函数判断smile是否到达宝箱的位置,如果到达宝箱的位置,通过is_starts的值判断目的地是否为楼梯,如果为楼梯,则根据map_layer的值判断进入下一层还是回到第一层,最后调用removeAllChildren函数将本类的所有子节点移除,并调用init函数重新初始化场景,这样就能加载新的地图。
5. 修改游戏中出现的Bug。
Bug1:点击smile所处的位置会显示“No Way!”,按照正常逻辑,宝箱出现的位置与smile相同应该成功找到宝箱才对。
(1)在MazeScene类的aStar函数中,通过判断OPEN表是否为空来判断寻路失败与否,但是当点击的位置正好就是smile所处的位置时,OPEN表也是空的,因此,此时要判断起点m_origin和终点destination的x,y坐标是否相同,如果相同,就不显示“No Way”文字。
//循环检验8个方向的相邻节点
while (checkNeighboringNodes(map, open, open->openNode, destination))
{
//从OPEN表中选取节点插入CLOSED表
insertNodeToClosedList(close, open);
//若OPEN表为空,表明寻路失败
if (open == nullptr)
{
//点击自身不显示no way
if (m_origin->xCoordinate != destination->xCoordinate
&& m_origin->yCoordinate != destination->yCoordinate)
Bug2:点击笑脸出现“No Way” 和“Found Treasure”文字的叠加。这是因为代码中只是简单地向场景中添加Label,而显示新的Label时,原来的Label不会被清除。
(1)在MazeScene类定义3个变量,分别表示3种情况下要显示的Label的TAG。
#define TIP_TAG 5
#define WAY_TAG 6
#define DANGEROUS_TAG 7
(2)在MazeScene类的init函数中,添加3种情况下要显示的文字,并且调用setVisible函数将Label设为不可见。
//预置找到宝箱的提示,将其设置为不可见
auto successTip = Label::createWithBMFont("fonts/futura-48.fnt", "Found Treasure!");
successTip->setPosition(m_visibleSize.width / 2, m_visibleSize.height / 2);
this->addChild(successTip, 2,TIP_TAG);
successTip->setVisible(false);
//预置屏幕中央显示寻路失败的提示
auto failedTip = Label::createWithBMFont("fonts/futura-48.fnt", "No Way!");
failedTip->setPosition(m_visibleSize.width / 2, m_visibleSize.height / 2);
this->addChild(failedTip, 2, WAY_TAG);
failedTip->setVisible(false);
//预置屏幕中央显示危险区的提示
auto dangerousTip = Label::createWithBMFont("fonts/futura-48.fnt", "Dangerous! You die!");
dangerousTip->setPosition(m_visibleSize.width / 2, m_visibleSize.height / 2);
this->addChild(dangerousTip, 2, DANGEROUS_TAG);
dangerousTip->setVisible(false);
(3)在要显示文字的时候,通过getChildByTag获取3个Label的实例,然后通过setVisible将要显示的Label设为可见,其他两个Label设为不可见。
//显示寻路成功的标签提示
auto tip1 = this->getChildByTag(TIP_TAG);
auto tip2 = this->getChildByTag(WAY_TAG);
auto tip3 = this->getChildByTag(DANGEROUS_TAG);
tip1->setVisible(true);
tip2->setVisible(false);
tip3->setVisible(false);
Bug3:smile精灵和box精灵的碰撞矩阵太大,相隔一个图块的情况下,程序将其判定为碰撞。
(1)这实际上是因为MazeScene类的update中判断smile精灵是否移动到box精灵位置的条件中使用了getBoundingBox获取的矩形,这个区域太大了,不符合要求。
(2)其实smile和box精灵的位置是根据地图图块像素和地图宽高计算出来的,因为是用的同样的计算方法,所以它们在每个相同图块中对应的实际位置也是相同的,只需要用getPosition函数获取其实际坐标进行比较即可。
//如果笑脸精灵达到宝箱位置
if ( smile->getPosition().equals( box->getPosition() ) )
Bug4:当距离危险区域只剩一个图块时,点击进入危险区域,不会显示危险区域提示。
(1)这实际上是因为在MazeScene类的moveOnPath函数中,结构体的是从loopNum-2开始扫描的,即直接扫描其下一个图块,而我的判断条件是j-1,当只差一个时,loopNum值为2,此时不满足危险区域条件,这是我代码设计的漏洞。
(2)如下图所示,让结构体的扫描从loopNum-1开始,这样smile移动的第一个位置变成其当前所处的位置,这样即使相差一个图块也能让j-1的判断条件生效了。
//从结构体数组尾部开始扫描
for (int j = loopNum - 1; j >= 0; j--)
Bug5:下方的“Click Map To Put Treasure”文字无法完全显示。
(1)将窗口高度改为700,这样就能让文字完全显示了。
(2)尝试运行程序,成功让文字完全显示。
6. 为游戏添加配乐和音效。
(1)将背景音乐和音效要使用的文件添加到Resources文件夹中。
(2)由于要让背景音乐在切换地图时不重新播放,要在MazeScene类的createScene方法中加载背景音乐并且循环播放,预加载3个音效。
//bgm
SimpleAudioEngine::getInstance()->preloadBackgroundMusic("boss1_bgm.wav");
SimpleAudioEngine::getInstance()->playBackgroundMusic("boss1_bgm.wav", true);
//音效
SimpleAudioEngine::getInstance()->preloadEffect("found.wav");
SimpleAudioEngine::getInstance()->preloadEffect("press.wav");
SimpleAudioEngine::getInstance()->preloadEffect("wrong.wav");
(3)接着添加开启和关闭背景音乐的按钮,创建关闭和开启音乐的Button的实例,将两个按钮的位置设置在右下角,然后调用setVisible函数将开启音乐按钮设置为不可见。
//按钮
Button* button_v = Button::create("Chapter11/v_btn.png","Chapter11/v_btn.png");
Button* button_nv = Button::create("Chapter11/nv_btn.png","Chapter11/nv_btn.png");
button_v->setScale(0.5);
button_nv->setScale(0.5);
button_v->setPosition(Vec2(Vec2(visibleSize.width - 20, 20)));
button_nv->setPosition(Vec2(Vec2(visibleSize.width - 20, 20)));
//隐藏
button_nv->setVisible(false);
scene->addChild(button_v, 113);
scene->addChild(button_nv, 113);
(4)添加触摸事件,当按下关闭音乐按钮时,调用setVisible函数将关闭音乐按钮设置为不可见,开启音乐按钮设置为可见,接着调用SimpleAudioEngine类的stopBackgroundMusic函数关闭背景音乐。
接着为开启按钮添加触摸事件,按下按钮时,将关闭音乐按钮设置为可见,开启音乐按钮设置为不可见,接着调用SimpleAudioEngine类的playBackgroundMusic函数播放背景音乐。
//关闭bgm
button_v->addTouchEventListener([button_v, button_nv](Ref* sender, Widget::TouchEventType type)
{
switch (type)
{
case cocos2d::ui::Widget::TouchEventType::BEGAN:
break;
case cocos2d::ui::Widget::TouchEventType::MOVED:
break;
case cocos2d::ui::Widget::TouchEventType::ENDED:
button_nv->setVisible(true);
button_v->setVisible(false);
SimpleAudioEngine::getInstance()->stopBackgroundMusic();
SimpleAudioEngine::getInstance()->stopAllEffects();
bgm = false;
break;
}
});
//开启bgm
button_nv->addTouchEventListener([button_v, button_nv](Ref* sender, Widget::TouchEventType type)
{
switch (type)
{
case cocos2d::ui::Widget::TouchEventType::BEGAN:
break;
case cocos2d::ui::Widget::TouchEventType::MOVED:
break;
case cocos2d::ui::Widget::TouchEventType::ENDED:
button_v->setVisible(true);
button_nv->setVisible(false);
SimpleAudioEngine::getInstance()->playBackgroundMusic("2D MUSIC LOOP.wav", true);
bgm = true;
break;
}
});
(5)在按钮的触摸事件中直接使用stopAllEffects函数无法关闭音效,因为音效不是一直播放的,只播放几秒钟的时间,因此我在aStar类中添加一个静态变量控制音效的开关,当关闭按钮的触摸事件触发时,其值为false,当开启按钮的触摸事件触发时,其值为true。
static bool bgm;
(6)添加“no way”和“dangerous”时的音效,在MazeScene类的aStar函数中会通过判断OPEN表是否为空来判断寻路是否成功,如果寻路失败则使用stopAllEffects函数停止正在播放的音效,然后根据静态bgm的值判断是否通过playEffect函数播放音效。
//音效
SimpleAudioEngine::getInstance()->stopAllEffects();
if (bgm)
{
SimpleAudioEngine::getInstance()->playEffect("wrong.wav", false);
}
在MazeScene类的moveOnPath函数中,判断路径中存在危险区域时,也要以同样的规则播放音效。
//判断是否存在危险区域
if (j - 1 == dangerous_num)
{
//音效
SimpleAudioEngine::getInstance()->stopAllEffects();
if (bgm)
{
SimpleAudioEngine::getInstance()->playEffect("wrong.wav", false);
}
(7)添加寻宝成功的音效,因为寻宝成功的文字是在smile到达宝箱位置时才显示,所以音效也要在此时播放。先在MazeScene类中添加一个成员变量is_found,用来判断是否播放寻宝成功的音效。
//音效
bool is_found = false;
MazeScene类的aStar函数寻路成功时,就要将is_found的值改为true。
在MazeScene类的update函数中,当smile到达宝箱位置时,要在bgm和is_found的值都为true的情况下才播放寻宝成功的音效,并将is_found的值重新改为false。
(8)添加鼠标点击的音效,在MazeScene类的onTouchBegan函数中根据bgm的值判断是否播放音效即可。
7. 优化游戏。
优化1:增加迷宫地图层数至4层和增加返回上一层的楼梯。
(1)在Tiled创建两个新的地图superMaze3.tmx和superMaze4.tmx,并用如下图所示的图块分别表示“返回上一层”和“进入下一层”,并且第一层地图只有向下的楼梯,第四层地图只有向上的楼梯。
(2)在MazeScene类的init函数中,根据map_layer的值判断加载哪一层的地图,并且每个地图的主图层都是“layer1”。
//载入地图,将放置到适当位置
TMXTiledMap* tileMap;
switch (map_layer)
{
case 0:
tileMap = TMXTiledMap::create("Chapter11/superMaze1.tmx");
break;
case 1:
tileMap = TMXTiledMap::create("Chapter11/superMaze2.tmx");
break;
case 2:
tileMap = TMXTiledMap::create("Chapter11/superMaze3.tmx");
break;
case 3:
tileMap = TMXTiledMap::create("Chapter11/superMaze4.tmx");
break;
default:
break;
}
tileMap->setPosition(0, m_visibleSize.height - MAP_HEIGHT * UNIT);
this->addChild(tileMap, 1);
//获取地图中的主图层
m_mapLayer = tileMap->getLayer("layer1");
//初始化地图数组
initMap();
(3)在MazeScene类添加成员变量is_starts_up和is_starts_down,用于判断smile是否位于楼梯图块。
//是否位于楼梯
bool is_starts_up = false;
bool is_starts_down = false;
(4)当点击的图块为楼梯时,应该不显示宝箱。在MazeScene类的onTouchBegan中,通过getTileGIDAt函数获取点击的图块的编号,如果其编号为4即向下楼梯,就通过setVisible函数将宝箱精灵设为不可见,并将is_starts_down变量设为true,如果编号为61,将is_starts_up变量设为true。
//终点为楼梯
if (m_mapLayer->getTileGIDAt(Vec2(x, y)) == 4)
{
box->setVisible(false);
is_starts_down = true;
//不得分
goal--;
}
else if (m_mapLayer->getTileGIDAt(Vec2(x, y)) == 61)
{
box->setVisible(false);
is_starts_up = true;
//不得分
goal--;
bgm = true;
}
(5)在MazeScene类的update函数中,通过intersectsRect函数判断smile是否到达宝箱的位置,如果到达宝箱的位置,通过is_starts_down和is_starts_dp的值判断目的地是否为楼梯,如果为向下楼梯,则进入下一层,map_layer值加一,如果为向上的楼梯,则返回上一层,map_layer值减一,最后调用removeAllChildren函数将本类的所有子节点移除,并调用init函数重新初始化场景,这样就能加载新的地图。
//判断是否到达楼梯
if (is_starts_down)
{
map_layer++;
//移除所有结点,重新初始化
this->removeAllChildren();
this->init();
is_starts_down = false;
}
else if (is_starts_up)
{
map_layer--;
//移除所有结点,重新初始化
this->removeAllChildren();
this->init();
is_starts_up = false;
}
优化2:显示当前smile位于哪一层地图。
(1)在MazeScene类的init函数中添创建一个Label的实例,用于显示地图层数,将map_layer+1的值显示在游戏左下角。
//显示地图层数和得分
auto layerTip = Label::createWithBMFont("fonts/futura-48.fnt", "Map Layer: "+Value(map_layer+1).asString());
layerTip->setPosition(130, 0.5 * (m_visibleSize.height - MAP_HEIGHT * UNIT));
layerTip->setScale(0.8);
this->addChild(layerTip, 1);
优化3:显示smile的得分,每次找到一个宝箱加一分。
(1)在MazeScene类中添加2个新的成员变量,一个用于记录当前的分数,一个用于显示当前的分数。
//得分
int goal = 0;
Label* goalTip;
(2)在MazeScene类的init函数中添创建一个Label的实例,用于显示当前得分,将goal的值显示在游戏右下角。
goalTip = Label::createWithBMFont("fonts/futura-48.fnt", "Goal: " + Value(goal).asString());
goalTip->setPosition(m_visibleSize.width-150, 0.5 * (m_visibleSize.height - MAP_HEIGHT * UNIT));
goalTip->setScale(0.8);
this->addChild(goalTip, 1);
因为得分会变化,所以要在MazeScene类的update函数中将新的goal的值赋予Label。
void MazeScene::update(float delta)
{
auto smile = this->getChildByTag(SMILE_TAG);
auto box = this->getChildByTag(BOX_TAG);
//更新得分
goalTip->setString("Goal: " + Value(goal).asString());
(3)在MazeScene类的aStar函数中,更新goal的值。当寻路成功时,goal值加一。
(4)当点击楼梯或者危险区域时,根据我设计的代码逻辑,也会将goal值加一,这显然错误的,因此,我在判断是否为楼梯和是否为危险区域的代码中添加一行“goal--”,这样就能让goal的值保持不变了。
四、实验结论或心得体会
在本次实验中,我成功运行了“迷宫寻宝”游戏,增加了DANGEROUS区域、1层新的迷宫地图和游戏配乐与音效,设计了进入下一层地图和返回上一层地图的机制,修改了5个bug。在这些基础上,我还优化了游戏,将地图层数增加到了4,添加了显示地图层数和得分的功能。通过本次实验,我熟练掌握了添加游戏配乐与音效的方法,学习了使用多个图层绘制地图的技巧。
本次实验比较简单,读懂代码之后就非常容易上手了,游戏的Bug比较少,修改起来非常简单。实验最复杂的部分是地图的绘制,如果想要设计比较美观的地图需要花费较长时间,设计地图可以使用多个图层,第一个图层可以作为代码逻辑判断的依据,在这一层规定哪些图块是可通过的,哪些是不可通过的,其他图层会覆盖第一层,因此这些图层的功能仅仅是美化地图,这样达到代码简洁,地图美观的效果,这个地图设计的技巧是我本实验最大的收获。