目录
1. 按照“英雄快跑实验指导”文件指引,尝试运行本次实验游戏
Bug1:改变窗口大小后,窗口中人物动画的位置和实际位置有偏差。
Bug3:下图所示的树的高度太高,角色无法跳到其上面。使用Tiled打开地图文件,将树改成合适的高度,这样角色就能跳上树顶了。
Bug4:在显示Gameover字样后,地图还会滚动,过一段时间,会显示Success字样,且与Gameover字样重合。
前言
这门课的所有实验都挺简单的,但是我估摸着混了个A+,不写白不写,本次实验可能会有我没有发现的错误或者仍需完善的内容,但整体上是没有问题的,得分也不低。
一、实验目的与要求
1.熟悉cocos2d-x开发环境。
2.了解cocos2d-x中二维游戏场景绘制方法。
3.掌握瓦片地图编辑器使用方法。
二、实验内容与方法
1.完成基本实验
按照“英雄快跑实验指导”文件指引,成功运行本次实验游戏。
2.修改游戏显示名称
通过修改游戏代码,使自己的学号替换原“MyGame”字样出现在标题栏左上角。
3.修改游戏地图
修改原有地图,将你的学号添加在背景中。
4.完成Bug修改
通过修改游戏代码的方式修复BUG,如:游戏结束响应Bug、树枝检测Bug。
5. 游戏优化
自行设计并优化游戏。
三、实验步骤与过程
1. 按照“英雄快跑实验指导”文件指引,尝试运行本次实验游戏
(1)进入D:\Cocos2d-x\cocos2d-x-3.17.2\tools\cocos2d-console\bin目录,在该目录打开Windows PowerShell,输入cocos new MyGame -p com.学号.edu -l cpp -d Hero_Running命令创建项目。
(2)将“英雄快跑”源代码和Resources中的文件,复制到本项目的Classes和Resources目录中,并将HelloWorldScene.cpp文件和HelloWorldScene.h文件删除,同时更新VS中的资源管理器列表。
(3)修改AppDelegate.cpp的内容,引用头文件MapScene.h,将创建场景的方法改为MapScene类中的createScene方法。
#include "MapScene.h"
// create a scene. it's an autorelease object
auto scene = MapScene::createScene();
// run
director->runWithScene(scene);
(4)尝试编译并运行程序,如下图所示,成功运行游戏。
2. 修改窗口大小和游戏显示名称
(1)在AppDelegate.cpp中,将变量designResolutionSize改为cocos2d::Size(960, 640),这样就能将窗口大小改为960x640。
static cocos2d::Size designResolutionSize = cocos2d::Size(960, 640);
(2)使用GLViewImpl类的createWithRect函数创建游戏窗口,将第一个参数改为学号,这样就能将游戏显示名称改为学号。
if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) || (CC_TARGET_PLATFORM == CC_PLATFORM_MAC) || (CC_TARGET_PLATFORM == CC_PLATFORM_LINUX)
glview = GLViewImpl::createWithRect("学号", cocos2d::Rect(0, 0, designResolutionSize.width, designResolutionSize.height));
#else
glview = GLViewImpl::create("学号");
#endif
director->setOpenGLView(glview);
}
3. 将学号添加到游戏背景中
(1)打开Resources目录中的地图文件MoveAndControl.tmx,在Tiled中编辑地图,在地图中添加我的学号。
(2)如果使用别的图集,需要添加一个新的图层,位于上方的图层会覆盖下方的图层,不然程序会出现错误,这是因为不同图集会出现相同的砖块类型。
4. 查找和修改游戏的BUG
Bug1:改变窗口大小后,窗口中人物动画的位置和实际位置有偏差。
(1)尝试用多种窗口尺寸进行测试,在1080x720尺寸下发现人物在碰到尖刺前就掉出地图,在360x640尺寸下发现人物能够站在尖刺上。尝试打印人物位置的砖块坐标,发现人物掉出地图的坐标是正确的,所以是人物动画的位置出现问题。
(2)在Config.h文件中,将变量PLAYER_WIDTH的值改为200,这个变量是MapSence.cpp中创建精灵帧函数中调用的参数,该参数决定使用的纹理块矩形的宽度,初始的窗口大小为480x360,PLAYER_WIDTH为100,而我将窗口大小改为960x640,将PLAYER_WIDTH改为200比较合适。
static int PLAYER_WIDTH = 200; // player img width
// 玩家跑动动画
Vector<SpriteFrame* >frameVector;
for(int i=0;i<4;i++)
{
auto spriteFrame = SpriteFrame::create(PLAYER_IMG_PATH[i], Rect(0,0,PLAYER_WIDTH,PLAYER_HEIGHT));
frameVector.pushBack(spriteFrame);
}
(3) 最后将玩家的初始位置设为(160,40),让玩家出现在窗口中合适的位置。
// 添加玩家
addPlayer(Vec2(160,40));
Bug2:有的砖块未添加碰撞检测,角色不能站在砖块上。
(1)角色应该能站在树顶和树枝上,而在游戏中,角色如果碰到下图所示的树顶和树叶,将会掉落。
(2)在Tiled选中地图中的图块,如下图所示,选中了树枝的图块,对应的砖块类型是148,因为Tiled是以0开始编号,而cocos2d是以1开始编号的,所以在cocos2d中,该图块的编号应该是149。
(3)在MapScene.cpp中,先获取角色脚的位置在地图中坐标,然后通过该坐标获取对应的砖块坐标,再通过砖块坐标获取其对应的砖块编号playerTiledID,当playerTiledID不为可站立的砖块时,角色下落。树叶图块的编号为99,100和101,树枝图块的编号为149和133,在该if语句中添加对应条件,角色就能站立在这些图块上。
// 不跳动时遇到非地面图块自动下落
if((int)(player_map_y / map->getTileSize().width) >= 0)
{
int playerTiledID = map->getLayer(MAP_BG_LAYER_NAME)->getTileGIDAt(Vec2((int)(player_map_x/map->getTileSize().width),(int)(map->getMapSize().height-1-player_map_y/map->getTileSize().height)));
if ((m_isJump == false)&&(m_jumpDir == Dir::STOP)&& (playerTiledID != 8) && (playerTiledID != 7)
&& (playerTiledID != 151) && (playerTiledID != 170) && (playerTiledID != 171) && (playerTiledID != 172)
&& (playerTiledID != 99) && (playerTiledID != 100) && (playerTiledID != 101)
&& (playerTiledID != 149) && (playerTiledID != 133)) {
m_isJump = true;
m_jumpDir = Dir::DOWN;
m_jumpSpeed = 2;
}
//log("%f %d",(player_map_x / map->getTileSize().width), playerTiledID);
}
(4) 在MapScene.cpp中,有角色跳跃操作的代码,跳跃分为两个阶段,向上跳和向下落,角色向下落时,遇到可以站立的图块,就应当停止下落。在下落过程中要获取角色脚下的地图块的编号,树叶图块的编号为99,100和101,树枝图块的编号为149和133,在if语句中,添加对应的条件,让角色遇到树叶和树枝时停止下落。
// 获取玩家脚下的地图块的编号
if (player_map_y/map->getTileSize().width >= 0) {
int tiledID = map->getLayer(MAP_BG_LAYER_NAME)->getTileGIDAt(Vec2((int)(player_map_x/map->getTileSize().width),(int)(map->getMapSize().height - 1 - player_map_y/map->getTileSize().height)));
if (tiledID == 8 || tiledID == 7 || tiledID == 151 || tiledID == 170 || tiledID == 171 || tiledID == 172
|| tiledID == 99 || tiledID == 100 || tiledID == 101 || tiledID == 149 || tiledID == 133) {
check = true;
player->setPositionY((int)(player_screen_y + player->getContentSize().height/2 - 12));
m_jumpSpeed = PLAYER_JUMP_SPEED;
m_jumpDir = Dir::STOP;
m_isJump = false;
break;
}
}
Bug3:下图所示的树的高度太高,角色无法跳到其上面。使用Tiled打开地图文件,将树改成合适的高度,这样角色就能跳上树顶了。
Bug4:在显示Gameover字样后,地图还会滚动,过一段时间,会显示Success字样,且与Gameover字样重合。
(1)在MapScene.cpp中,变量player_screen_y是角色的脚部在屏幕中的y位置,当变量小于-80时,代表角色掉出屏幕,此时调用gameOver函数显示Gameover字样。此时让变量m_gameOver变量为true,这个标志用来记录游戏是否结束。调用unscheduleUpdate函数停止计时器,让地图停止滚动,然后用removeChildByTag删除角色的结点。
if (player_screen_y <= -80) {
gameOver(); // 角色死亡
m_gameOver = true;
this->unscheduleUpdate();
this->removeChildByTag(PLAYER_TAG);
return;
}
(2)当地图静止时,角色位置开始移动,当角色位置超过窗口时,显示Success字样。添加一个判断条件,当m_gameOver为false才显示Success字样,m_gameOver为true表示角色已经掉出屏幕,m_gameOver为false表示游戏尚未结束,这样就不会让Success与Gameover字样同时显示了。
// 地图静止后移动角色
player->setPositionX(player->getPositionX() + m_runSpeed);
if(player->getPositionX() > visibleWidth && m_gameOver == false)
{
player->setPositionX(visibleWidth );
this->unscheduleUpdate();
this->removeChildByTag(PLAYER_TAG);
success();
return;
}
5. 优化游戏
(1)每次游戏结束后重新启动游戏都很麻烦,要关闭窗口重新运行,我尝试添加一个“重新开始”的按钮,点击后重启游戏。
在MapScene.h文件中声明restart函数,该函数用来添加一个按钮以重新启动游戏。
//重新开始游戏
void restart();
在restart函数中,使用Button类的Create函数创建一个按钮,按钮使用的图片如下图所示,将按钮的位置设置在Success或者Gameover字样的下方,然后为按钮设置触摸事件,当按下按钮时,导演类调用replaceScene函数启动一个新的场景,即重启游戏。
void MapScene::restart()
{
//创建按钮
auto buttonReplay = Button::create("btn-play-normal.png","btn-play-selected.png");
Size visibleSize = Director::getInstance()->getVisibleSize();
//设置位置
buttonReplay->setPosition(Vec2(Vec2(visibleSize.width / 2, visibleSize.height / 2-50)));
//添加监听
buttonReplay->addTouchEventListener([&](Ref* sender, Widget::TouchEventType type)
{
Director::getInstance()->replaceScene(MapScene::createScene());
m_gameOver = false;
});
this->addChild(buttonReplay, 12);
}
最后在Success函数和gameOver函数中调用restart函数,这样就能在游戏结束时显示重启游戏的按钮了。
void MapScene::success()
{
auto tips = Label::createWithBMFont(TIPS_FNT_PATH,GAME_SUCCESS);
Size visibleSize = Director::getInstance()->getVisibleSize();
tips->setPosition(Vec2(visibleSize.width/2, visibleSize.height/2));
this->addChild(tips,10);
m_gameOver = true;
restart();
}
游戏中的效果如下图所示,按下play按钮,游戏将会重新开始。
(2)尝试添加暂停/继续游戏的功能。
在MapScene.h文件中声明pause1函数,该函数用来添加一个暂停/继续按钮。
//暂停
void pause1();
在pause1函数中,创建两个按钮,分别表示暂停和继续,按钮的图片如下图所示。
将两个按钮的位置都设置在左上角,然后调用setVisible函数将“继续”按钮设为不可见。
void MapScene::pause1()
{
Button* buttonPause = Button::create("pause.png", "pause.png");
Button* buttonContinue= Button::create("continue.png", "continuee.png");
buttonPause->setScale(0.5);
buttonContinue->setScale(0.5);
Size visibleSize = Director::getInstance()->getVisibleSize();
buttonContinue->setPosition(Vec2(Vec2(30, visibleSize.height - 30)));
buttonPause->setPosition(Vec2(Vec2(30, visibleSize.height - 30)));
//隐藏
buttonContinue->setVisible(false);
//buttonPause->setVisible(false);
this->addChild(buttonPause, 13);
this->addChild(buttonContinue, 13);
为“暂停”按钮添加触摸事件,当按下按钮时,先要判断m_gameOver变量是否为false,因为游戏结束时不能使用暂停按钮。
调用pause函数暂停游戏,调用setVisible函数将“暂停”按钮设置为不可见,将“继续”按钮设置为可见,然后使用Label类的createWithBMFont函数创建“Pause”文字,让它显示在屏幕中间,调用setTag函数将该Label的Tag设为15。
//暂停事件
buttonPause->addTouchEventListener([buttonContinue, buttonPause,this](Ref* sender, Widget::TouchEventType type)
{
if (!m_gameOver)
{
Director::getInstance()->pause();
buttonContinue->setVisible(true);
buttonPause->setVisible(false);
//添加pause文字
auto labelPause = Label::createWithBMFont(TIPS_FNT_PATH, "Pause");
Size visibleSize = Director::getInstance()->getVisibleSize();
labelPause->setPosition(Vec2(visibleSize.width / 2, visibleSize.height / 2));
labelPause->setTag(15);
this->addChild(labelPause, 10);
}
});
为“继续”按钮添加触摸事件,当按下按钮时,调用setVisible函数将“暂停”按钮设置为可见,将“继续”按钮设置为不可见,通过removeChildByTag函数删除Tag为15的结点,即删除“Pause”文字,然后调用resume函数继续游戏。
//继续事件
buttonContinue->addTouchEventListener([buttonContinue, buttonPause,this](Ref* sender, Widget::TouchEventType type)
{
//删除pause文字
this->removeChildByTag(15);
buttonContinue->setVisible(false);
buttonPause->setVisible(true);
Director::getInstance()->resume();
});
}
游戏中的效果如下图所示,按下暂停按钮后游戏暂停,按下继续按钮后游戏继续。
(3)添加游戏难度设置的功能
每一帧都让地图位置向左移动3,当地图静止时,让人物x位置每一帧都加3,这是本游戏滚动地图和让角色移动的方式,因此只要改变这个移动的值,就能改变地图滚动和人物移动的速度。
在MapScene.h文件中声明m_runSpeed变量表示地图滚动和人物移动的速度,声明setting函数用来改变地图滚动和人物移动的速度。
int m_runSpeed=3;//滚动速度
//设置
void setting();
在MapScene.cpp的update函数中,获取地图的x位置,然后让该位置每帧减m_runSpeed,当地图的最右端显示在窗口右端时,让地图静止,将角色的x位置每帧加m_runSpeed。
// 滚动背景地图
int mapWidth = map->getMapSize().width*map->getTileSize().width;
int visibleWidth = Director::getInstance()->getWinSize().width;
this->getChildByTag(MAP_TAG)->setPositionX(this->getChildByTag(MAP_TAG)->getPositionX() - m_runSpeed);
if(map->getPositionX() < -(mapWidth-visibleWidth))
{
map->setPositionX(-(mapWidth-visibleWidth));
// 地图静止后移动角色
player->setPositionX(player->getPositionX() + m_runSpeed);
if(player->getPositionX() > visibleWidth && m_gameOver == false)
{
player->setPositionX(visibleWidth );
this->unscheduleUpdate();
this->removeChildByTag(PLAYER_TAG);
success();
return;
}
}
在setting函数中,使用Label类的createWithBMFont函数创建“Difficuty:Easy”文字,然后将其位置设置在右上角。
void MapScene::setting()
{
auto setting_title=Label::createWithBMFont(TIPS_FNT_PATH, "Difficulty: Easy");
Size visibleSize = Director::getInstance()->getVisibleSize();
setting_title->setPosition(Vec2(visibleSize.width-140, visibleSize.height -30));
setting_title->setScale(0.7);
int &runSpeed= m_runSpeed;
创建监听器,监听鼠标事件,使用getCursorX和getCursorY函数获取鼠标点击的位置,如果该位置在创建的文字的附近时,改变游戏难度,如果游戏难度为“Easy”,就将难度改为“Normal”,并将滚动速度m_runSpeed改为6,如果游戏难度为“Normal”,就将难度改为“Hard”,并将滚动速度m_runSpeed设为9,如果游戏难度为“Hard”,就将难度改为“Easy”。
//添加监听器
auto listener1 = EventListenerMouse::create();
listener1->onMouseDown = [setting_title, &runSpeed]( Event* event)
{
EventMouse *e=(EventMouse*)event;
if (e->getCursorX() >= setting_title->getPositionX() - 100 && e->getCursorX() <= setting_title->getPositionX() + 100
&& e->getCursorY() >= setting_title->getPositionY() - 30 && e->getCursorY() <= setting_title->getPositionY() + 30)
{
if (setting_title->getString() == "Difficulty: Easy")
{
setting_title->setString("Difficulty: Normal");
runSpeed = 6;
}
else if (setting_title->getString() == "Difficulty: Normal")
{
setting_title->setString("Difficulty: Hard");
runSpeed = 9;
}
else {
setting_title->setString("Difficulty: Easy");
runSpeed = 3;
}
return true;
}
};
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener1, setting_title);
this->addChild(setting_title, 11);
}
因为跳跃操作也添加了触摸事件,调整难度时点击屏幕会让角色跳一下,所以在触摸事件的函数中,先判断触摸的位置是否在难度设置字样附近,如果是角色就不进行跳跃,否则角色跳跃。
bool MapScene::onTouchBegan(Touch *touch, Event *unused_event)
{
Size visibleSize = Director::getInstance()->getVisibleSize();
Vec2 setting_title = Vec2(visibleSize.width - 140, visibleSize.height - 30);
if (touch->getLocation().x >= setting_title.x - 100 && touch->getLocation().x <= setting_title.x + 100
&& touch->getLocation().y >= setting_title.y - 30 && touch->getLocation().y <= setting_title.y + 30)
{
}
else {
if (!this->m_isJump) {
m_isJump = true;
m_jumpDir = Dir::UP;
}
return true;
}
}
游戏中的效果如下图所示,点击难度会更改难度,地图滚动速度和人物移动速度会改变。
四、实验结论或心得体会
在本次实验中,我成功运行了“英雄快跑”游戏,查找和修复了游戏的4个Bug,并添加了一些优化功能,包括重启游戏功能,难度调整功能和暂停/继续游戏功能。通过本次实验,我学习了“英雄快跑”游戏的代码内容,熟悉了cocos2d-x开发环境,了解了cocos2d-x中二维游戏场景绘制方法,以及熟练掌握了瓦片地图编辑器使用方法。
我初步接触了cocos2d的游戏代码,cocos2d有许多封装好的函数,使用起来非常方便,但这也表示要学习的知识也非常多。我在代码中使用了触摸事件和鼠标事件,复习了相应的知识,并且学习了许多新知识,如按钮,文字等,这让我以后学习cocos2d变得简单许多。
我在本次实验遇到了诸多困难,最难处理是人物动画效果和实际位置出现偏差这个Bug,我在网上查询了许多动画帧的资料,才修复了这个Bug。