从网上下了一点素材资源,外加自己ps一点资源,然后东拼西凑写了一个横版跑酷的小游戏
ps:csdn好不爽,无法传大点的gif,所以只好录了个短的gif,而且压缩之后凑合能看
预览
步骤
1 工程结构
开发环境
- win8.1
- vs2013
- cocos2dx 3.2
代码目录
游戏组成结构
主要有以下几个场景
void LoadingScene::loadingCallBack(Texture2D *texture) { loadedNum++; //此处的预加载帧动画指示用于实验性质 switch(loadedNum) { case 1: //预加载帧缓存纹理 SpriteFrameCache::getInstance()->addSpriteFramesWithFile("boy.plist",texture); loadProgress->setPercentage((float)loadedNum/totalNum*100); break; case 2: //预加载帧缓存纹理 SpriteFrameCache::getInstance()->addSpriteFramesWithFile("girl.plist",texture); loadProgress->setPercentage((float)loadedNum/totalNum*100); break; default: break; } if(loadedNum==totalNum) { //预加载帧动画 auto boyAnimation=Animation::create(); boyAnimation->setDelayPerUnit(0.1f); for(int i=1;i<=12;i++) { char str[100]={0}; sprintf(str,"boy%d.png",i); boyAnimation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName(str)); } AnimationCache::getInstance()->addAnimation(boyAnimation,"boyAnimation"); //预加载帧动画 auto girlAnimation=Animation::create(); girlAnimation->setDelayPerUnit(0.2f); for(int i=1;i<=8;i++) girlAnimation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName("girl"+std::to_string(i)+".png")); AnimationCache::getInstance()->addAnimation(girlAnimation,"girlAnimation"); ////预加载音乐和音效 SimpleAudioEngine::getInstance()->preloadBackgroundMusic("spring_music.wav"); SimpleAudioEngine::getInstance()->preloadBackgroundMusic("winter_music.mp3"); SimpleAudioEngine::getInstance()->preloadEffect("jump.wav"); SimpleAudioEngine::getInstance()->preloadEffect("point.mp3"); SimpleAudioEngine::getInstance()->preloadEffect("gameover.wav"); //加载完毕跳转到游戏场景 auto mainMenu=MainMenu::createScene(); TransitionScene *transition=TransitionFade::create(1.0f,mainMenu); Director::getInstance()->replaceScene(transition); } }
3 主菜单场景
添加一个菜单和对应的回调函数,分别有开始游戏、选项、关于等菜单项//添加菜单 auto newGameItem=MenuItemImage::create("newgameA.png","newgameB.png",CC_CALLBACK_1(MainMenu::menuStartCallback,this)); newGameItem->setPosition(Point(visibleOrigin.x+visibleSize.width/2,visibleOrigin.y+visibleSize.height/2)); auto optionItem=MenuItemImage::create("option_btn.png","option_btn.png",CC_CALLBACK_1(MainMenu::menuOptionCallback,this)); optionItem->setPosition(Point(visibleOrigin.x+visibleSize.width/2,visibleOrigin.y+visibleSize.height/2-60)); auto aboutItem=MenuItemImage::create("aboutA.png","aboutB.png",CC_CALLBACK_1(MainMenu::menuAboutCallback,this)); aboutItem->setPosition(Point(visibleOrigin.x+visibleSize.width/2,visibleOrigin.y+visibleSize.height/2-120)); auto menu=Menu::create(newGameItem,optionItem,aboutItem,NULL); menu->setPosition(Point::ZERO); this->addChild(menu,1);
4 游戏主场景
common头文件
//游戏关卡枚举 enum Level { SPRING,WINTER }; //游戏角色枚举 enum PlayerType { BOY,GIRL }; //游戏角色状态 enum PlayerState { RUN,SLIDE,JUMP }; //地图元素枚举 enum BlockType { LAND,//砖块 NPC,//怪物 STAR,//星星 TOOL,//工具 NONE //空 }; //场景间隔基本单位定为80,分辨率800*480,则排满是10*6个格子,便于作碰撞检测 const float BLOCK_SIZE=80.0f; const float PICKUP_SIZE=40.0f; const float PLAYER_RADIUS=50.0f; const float GRAVITY=-1500.0f; const float PLAYER_SPEED=700.0f;为了便于计算,将屏幕设计分辨率定位800*480,一个砖块或怪物的大小是80*80,一个拾取物的大小是40*40,这样计算坐标时只需要左整数倍数的偏移就可以了。
游戏里面设置了两种关卡,分别对应了春天和冬天的地图,春天的怪物是绿水灵,冬天的怪物是蓝蘑菇,(玩过冒险岛的都知道),同时还可以选择男、女两种角色。
都是用全局变量进行控制。
游戏角色player类
class Player:public Node { public: virtual bool init() override; CREATE_FUNC(Player); public: void run(); //主角奔跑 void jump(); //主角跳跃 void slide(); //主角滑行 PlayerState playerState; //角色状态 private: Sprite *playerSprite; //奔跑的角色精灵 Sprite *playerSpriteSlideJump; //滑行和起跳的角色精灵 Animate *playerAnim; Texture2D *jumpTexture; Texture2D *slideTexture; };游戏角色分男女,具有奔跑、跳跃、滑行三种状态
角色还有奔跑动画,同时还要为角色绑定刚体,需要受物理引擎作用
playerSprite=Sprite::create(playerTextureName); //此处必须初始化一张角色纹理,否则后面无法切换纹理 jumpTexture=Sprite::create(playerJumpTexureName)->getTexture(); //创建跳跃纹理 slideTexture=Sprite::create(playerSlideTextureName)->getTexture(); //创建滑行纹理 playerAnim=Animate::create(playerAnimation); this->addChild(playerSprite); auto playerBody=PhysicsBody::createBox(playerSprite->getContentSize()); //这里要用包围盒,如果用圆形的话会导致滚动 playerBody->setDynamic(true); playerBody->setContactTestBitmask(1); playerBody->setGravityEnable(true); playerBody->getShape(0)->setRestitution(0.0f); //设置刚体回弹力 this->setPhysicsBody(playerBody);
游戏移动地图类
主要是地图中的砖块和怪物等元素的无限滚屏移动
主要是地图中的砖块和怪物等元素的无限滚屏移动
首先,在初始化里面,手动设置初始地图,放置砖块、怪物、拾取物等等
//手动搭建地图,没有加入即时计算~ //1层 for(int i=0;i<10;i++) { if(i!=3&&i!=4&&i!=7&&i!=8) { //添加land auto block=Sprite::create(block_file); block->setPosition(BLOCK_SIZE/2+i*BLOCK_SIZE,BLOCK_SIZE/2+1*BLOCK_SIZE); this->addChild(block); block->setTag(LAND); //设置tag auto blockBody=PhysicsBody::createBox(block->getContentSize()); blockBody->setDynamic(false); blockBody->setContactTestBitmask(1); blockBody->getShape(0)->setRestitution(0); block->setPhysicsBody(blockBody); } } //2层 for(int i=0;i<10;i++) { if(i==2||i==5||i==6) { //添加怪物 auto npc=Sprite::create(npc_file); npc->setTag(NPC); npc->setPosition(BLOCK_SIZE/2+i*BLOCK_SIZE,BLOCK_SIZE/2+2*BLOCK_SIZE); auto npcBody=PhysicsBody::createBox(npc->getContentSize()); npcBody->setDynamic(false); npcBody->setContactTestBitmask(1); npcBody->getShape(0)->setRestitution(0); npc->setPhysicsBody(npcBody); this->addChild(npc); } if(i==3) { //添加land auto block=Sprite::create(block_file); block->setPosition(BLOCK_SIZE/2+i*BLOCK_SIZE,BLOCK_SIZE/2+1*BLOCK_SIZE); this->addChild(block); block->setTag(LAND); //设置tag auto blockBody=PhysicsBody::createBox(block->getContentSize()); blockBody->setDynamic(false); blockBody->setContactTestBitmask(1); blockBody->getShape(0)->setRestitution(0); block->setPhysicsBody(blockBody); } } //3层 for(int i=0;i<10;i++) { if(i!=0&&i!=3&&i!=4) { //添加星星 auto star1=Sprite::create(star_file); star1->setTag(STAR); star1->setPosition(PICKUP_SIZE/2+i*BLOCK_SIZE,BLOCK_SIZE/2+3*BLOCK_SIZE); auto starBody1=PhysicsBody::createBox(star1->getContentSize()); starBody1->setDynamic(false); starBody1->setContactTestBitmask(1); starBody1->getShape(0)->setRestitution(0.0f); star1->setPhysicsBody(starBody1); this->addChild(star1); auto star2=Sprite::create(star_file); star2->setTag(STAR); star2->setPosition(PICKUP_SIZE/2*3+i*BLOCK_SIZE,BLOCK_SIZE/2+3*BLOCK_SIZE); auto starBody2=PhysicsBody::createBox(star2->getContentSize()); starBody2->setDynamic(false); starBody2->setContactTestBitmask(1); starBody2->getShape(0)->setRestitution(0.0f); star2->setPhysicsBody(starBody2); this->addChild(star2); } } //4层 for(int i=0;i<10;i++) { if(i==3||i==4) { //添加land auto block=Sprite::create(block_file); block->setPosition(BLOCK_SIZE/2+i*BLOCK_SIZE,BLOCK_SIZE/2+4*BLOCK_SIZE); this->addChild(block); block->setTag(LAND); //设置tag auto blockBody=PhysicsBody::createBox(block->getContentSize()); blockBody->setDynamic(false); blockBody->setContactTestBitmask(1); blockBody->getShape(0)->setRestitution(0); block->setPhysicsBody(blockBody); } if(i==8) { auto star1=Sprite::create(star_file); star1->setTag(STAR); star1->setPosition(PICKUP_SIZE/2+i*BLOCK_SIZE,BLOCK_SIZE/2+3*BLOCK_SIZE); auto starBody2=PhysicsBody::createBox(star2->getContentSize()); starBody2->setDynamic(false); starBody2->setContactTestBitmask(1); starBody2->getShape(0)->setRestitution(0.0f); star2->setPhysicsBody(starBody2); this->addChild(star2); } if(i==6) { //添加道具 auto tool=Sprite::create(tool_file); tool->setTag(TOOL); tool->setPosition(PICKUP_SIZE/2+i*BLOCK_SIZE,BLOCK_SIZE/2+3*BLOCK_SIZE); auto toolBody=PhysicsBody::createBox(tool->getContentSize()); toolBody->setDynamic(false); toolBody->setContactTestBitmask(1); toolBody->getShape(0)->setRestitution(0.0f); tool->setPhysicsBody(toolBody); this->addChild(tool); } }每个物体都绑定了刚体,便于在物理世界做碰撞检测
然后需要启动无限滚屏
//启动调度器,地图滚屏 this->schedule(schedule_selector(GameMap::mapUpdate),0.01f);目前所采用的策略是党某个node消失在左侧是,重新设置其坐标到有边缘从右往左移动,不过,更好的方法是动态的随机生成地图结点,但是算法较复杂。
void GameMap::mapUpdate(float dt) { for(auto &node:this->getChildren()) { node->setPositionX(node->getPositionX()-3.0f); if(node->getPositionX()<=-node->getContentSize().width/2) node->setPositionX(node->getPositionX()+BLOCK_SIZE/2+10*BLOCK_SIZE); } }
游戏主场景类
这里有游戏最重要的逻辑代码。
一开始需要初始化物理引擎
auto *scene=Scene::createWithPhysics(); scene->getPhysicsWorld()->setGravity(Vec2(0,GRAVITY));
backGround1=Sprite::create(backGroundFile); backGround1->setAnchorPoint(Point::ZERO); backGround1->setPosition(Point::ZERO); this->addChild(backGround1,0); backGround2=Sprite::create(backGroundFile); backGround2->setAnchorPoint(Point::ZERO); backGround2->setPosition(Point::ZERO); this->addChild(backGround2,0);
void GameScene::backGroundUpdate(float dt) { backGround1->setPositionX(backGround1->getPositionX()-1.0f); backGround2->setPositionX(backGround1->getPositionX()+backGround1->getContentSize().width); if(backGround2->getPositionX()<=0.0f) backGround1->setPositionX(0.0f); }
//添加player player=Player::create(); player->setPosition(Point(visibleOrigin.x+2*BLOCK_SIZE,visibleOrigin.y+4*BLOCK_SIZE)); this->addChild(player,1);
添加地图
//设置地图,默认锚点在左下角 gameMap=GameMap::create(); gameMap->setPosition(visibleOrigin.x,visibleOrigin.y); this->addChild(gameMap,1);
添加两个控制按钮
//添加滑行和跳跃按钮的事件 score=0; //初始化分数 slideBtn=Sprite::create("slideButton.png"); auto slideBtnTexture1=Sprite::create("slideButton.png")->getTexture(); auto slideBtnTexture2=Sprite::create("slideButtonPress.png")->getTexture(); slideBtnTextures.pushBack(slideBtnTexture1); slideBtnTextures.pushBack(slideBtnTexture2); slideBtn->setScale(0.5); slideBtn->setPosition(Point(visibleOrigin.x+100,visibleOrigin.y+50)); this->addChild(slideBtn,2); jumpBtn=Sprite::create("jumpButton.png"); auto jumpBtnTexture1=Sprite::create("jumpButton.png")->getTexture(); auto jumpBtnTexture2=Sprite::create("jumpButtonPress.png")->getTexture(); jumpBtnTextures.pushBack(jumpBtnTexture1); jumpBtnTextures.pushBack(jumpBtnTexture2); jumpBtn->setScale(0.5); jumpBtn->setPosition(Point(visibleOrigin.x+visibleSize.width-100,visibleOrigin.y+50)); this->addChild(jumpBtn,2);
触摸检测,用于控制按钮
滑行时动态更换纹理
跳跃时给角色一个向上的速度,暂停动画,更换纹理
要注意的是,一开始只能二段跳,接了道具之后可以三段跳
bool GameScene::onTouchBegan(Touch *touch,Event *event) { auto touchPoint=touch->getLocation(); //检测是否触摸在按钮区域 if(slideBtn->getBoundingBox().containsPoint(touchPoint)) { slideBtn->setTexture(slideBtnTextures.at(1)); player->slide(); } if(jumpTimes<jumpTotal&&jumpBtn->getBoundingBox().containsPoint(touchPoint)) { if(isSound) SimpleAudioEngine::getInstance()->playEffect("jump.wav"); //播放跳跃音效 jumpBtn->setTexture(jumpBtnTextures.at(1)); player->jump(); jumpTimes++; } return true; } void GameScene::onTouchEnded(Touch *touch,Event *event) { auto touchPoint=touch->getLocation(); //判断是释放时是否在按钮区域 if(slideBtn->getBoundingBox().containsPoint(touchPoint)) { slideBtn->setTexture(slideBtnTextures.at(0)); player->run(); } if(jumpBtn->getBoundingBox().containsPoint(touchPoint)) { jumpBtn->setTexture(jumpBtnTextures.at(0)); } }
碰撞检测
bool GameScene::onContactBegin(const PhysicsContact &contact) { jumpTimes=0; //落回地面就将已跳跃次数清零 //获得被碰撞物体,getShapeA getShapeB要尝试一下 auto target=contact.getShapeA()->getBody()->getNode(); if(target->getTag()==STAR) { //碰到星星就涨分,星星消失 gameMap->moveNode(target); addscore(100); //拾取星星得100 } else if(target->getTag()==NPC&&target->getPositionY()+target->getContentSize().height/2<player->getPositionY()) //此处要用else if,只有当角色在怪物头上才能踩中 { gameMap->moveNode(target); addscore(150); //踩怪得150 } else if(target->getTag()==NPC&&target->getPositionY()+target->getContentSize().height/2>=player->getPositionY()) //如果角色正面遇到怪物就挂了 gameOver(); else if(target->getTag()==TOOL) { jumpTotal=3; auto toolIcon=Sprite::create("accelerate_state.png"); toolIcon->setPosition(Point(visibleOrigin.x+180,visibleOrigin.y+50)); this->addChild(toolIcon,2); target->removeFromParent(); //道具只出现一次,从parent里面删除 addscore(300); //道具300 } //落回地面恢复跑步状态 if(player->playerState==JUMP) player->run(); return true; }
碰撞检测的逻辑主要是
- 判断碰撞的是砖块则继续奔跑
- 判断碰撞的是怪物,并且是踩在投上,则消除怪物,得分
- 判断碰撞的是拾取物,则消除拾取物,得分,如果是道具的话则获得三段跳的能力
- 判断碰撞是正面遇到怪物,则游戏结束
游戏结束的逻辑
- 正面遇到怪物
- 落到屏幕底下
void GameScene::gameOver() { //游戏结束停止所有的调度器 gameMap->unscheduleAllSelectors(); this->unscheduleAllSelectors(); //播放游戏结束声音 if(isSound) SimpleAudioEngine::getInstance()->playEffect("gameover.wav"); //游戏结束出现菜单 visibleSize=Director::getInstance()->getVisibleSize(); visibleOrigin=Director::getInstance()->getVisibleOrigin(); auto gameOverPanel=Node::create(); auto overLabel=Sprite::create("gameover.png"); overLabel->setPosition(visibleOrigin.x+visibleSize.width/2,visibleOrigin.y+visibleSize.height/2+100); gameOverPanel->addChild(overLabel); auto backItem=MenuItemImage::create("back_to_menu.png","back_to_menu_press.png",[](Object *sender) { //用lambda表达式作为菜单函数回调 auto mainMenu=MainMenu::createScene(); TransitionScene *transition=TransitionFade::create(1.0f,mainMenu); Director::getInstance()->replaceScene(transition); }); auto backMenu=Menu::createWithItem(backItem); backMenu->setPosition(visibleOrigin.x+visibleSize.width/2,visibleOrigin.y+visibleSize.height/2-50); gameOverPanel->addChild(backMenu); gameOverPanel->setPositionY(visibleOrigin.y+visibleSize.height); this->addChild(gameOverPanel,3); //滑入gameover logo,注意node的锚点在左下角 gameOverPanel->runAction(MoveTo::create(0.5f,Vec2(visibleOrigin.x,visibleOrigin.y))); }
其他逻辑
都放到update这个函数里面进行
角色被阻挡后赶到原来的位置
//当角色被挡道之后跟上原来的位置 float step=2.0f; if(player->getPositionX()<2*BLOCK_SIZE) player->setPositionX(player->getPositionX()+step); if(player->getPositionX()>2*BLOCK_SIZE) player->setPositionX(player->getPositionX()-step);
void GameScene::addscore(float number) { if(isSound) SimpleAudioEngine::getInstance()->playEffect("point.mp3"); //播放得分音效 score+=number; scoreLabel->setString(String::createWithFormat("score: %d",score)->_string); }
5 设置场景
设置游戏关卡、游戏角色和声音,这里懒得加xml配置文件存储。
//春天 auto springItem=MenuItemImage::create("spring_icon.png","spring_icon_press.png",[](Object *sender) { //修改关卡 level=SPRING; textLevel->setString("level: Spring"); }); springItem->setPosition(visibleOrigin.x+visibleSize.width/4+35,visibleOrigin.y+visibleSize.height/4*3); //冬天 auto winterItem=MenuItemImage::create("winter_icon.png","winter_icon_press.png",[](Object *sender) { //修改关卡 level=WINTER; textLevel->setString("level: Winter"); }); winterItem->setPosition(visibleOrigin.x+visibleSize.width/4*3-35,visibleOrigin.y+visibleSize.height/4*3); //男生 auto boyItem=MenuItemImage::create("boy1.png","boy_jump.png",[](Object *sender) { //修改角色 playerType=BOY; textPlayer->setString("player: Boy"); }); boyItem->setPosition(visibleOrigin.x+visibleSize.width/2,visibleOrigin.y+visibleSize.height/2-100); //女生 auto girlItem=MenuItemImage::create("girl1.png","girl_jump.png",[](Object *sender) { //修改角色 playerType=GIRL; textPlayer->setString("player: Girl"); }); girlItem->setPosition(visibleOrigin.x+visibleSize.width/2+200,visibleOrigin.y+visibleSize.height/2-100); //声音开关 auto soundItem=MenuItemImage::create("sound_on.png","sound_off.png",[](Object *sender) { //修改声音 isSound=!isSound; textSound->setString(isSound?"sound: On":"sound: Off"); }); soundItem->setPosition(visibleOrigin.x+soundItem->getContentSize().width/2,visibleOrigin.y+soundItem->getContentSize().height/2); auto menu=Menu::create(backItem,springItem,winterItem,boyItem,girlItem,soundItem,NULL); menu->setPosition(Point::ZERO); this->addChild(menu);通过全局变量与其他场景交互
6 关于场景
//添加动态背景 auto backGround=Sprite::create("about.jpg"); backGround->setPosition(visibleOrigin.x+visibleSize.width/2,visibleOrigin.y+visibleSize.height/2); auto repeatanim=MoveBy::create(3.0f,Vec2(0,50)); backGround->runAction(RepeatForever::create(Sequence::create(repeatanim,repeatanim->reverse(),NULL))); this->addChild(backGround,0); //添加关于告示 auto aboutLabel=Sprite::create("aboutLabel.png"); aboutLabel->setPosition(visibleOrigin.x+visibleSize.width/2,visibleOrigin.y+visibleSize.height/2); this->addChild(aboutLabel,1); auto text=LabelBMFont::create("author:tashaxing\nE-mail:tashaxing@163.com\n\nWish you have fun!","bitmapFontChinese.fnt"); text->setPosition(visibleOrigin.x+visibleSize.width/2,visibleOrigin.y+visibleSize.height/2); this->addChild(text,2);一些简介
效果图
整个demo也做了蛮久的,很多图片和图标,配色都是我自己ps的,算是策划、美术和程序一人承担了。
源码下载
csdn:
跑酷游戏
github:
parkour