手游<<黄金矿工>>

黄金矿工

好久没有写点东西了,最近工作有一点忙,生活上事情也比较繁琐,趁着最近有点时间,写个小游戏供大家娱乐下!随便恭喜木木同学被挖去了新公司,祝工作顺利~

关于项目(代码下载地址在文章最下面点击GitHub链接)

项目说明:采用cocos2d-X3.12游戏引擎,基于C++开发,支持Android,iOS以及wp系统

开发工具:支持Xcode,eclipse,visual studio都可以,提前在机器上创建一个cocos2d-x的空工程,将Class文件删除,把下载好的代码Class文件拖入到工程,把res文件放入到项目的Resoures目录下运行就OK了~

辅助工具:Cocostudio

项目比较简单,适合有一定编程经验对游戏有兴趣想入门的同学.

游戏截图
游戏截图
游戏截图
粒子效果

首页场景

本游戏的布局基本都是cocostudio布局的,放一张在cocostudio中布局的样式图

cocostudio布局样式

通过CSLoder加载csb文件添加到对应的场景中展示,具体代码如下

auto mainCsb = CSLoader::createNode("csb文件名");
this->addChild(mainCsb);

Logo放大出现动画也在cocostudio中创建,通过CSLoader获取Timeline对象,播放指定的帧动画

    animation = CSLoader::createTimeline("Layer.csb");
    mainCsb->runAction(animation);
    // 播放指定的动画
    animation->gotoFrameAndPlay(0, 25, false);

云飘动动画以及人物吹口哨抖腿的动画都是通过代码实现的,当然也可以在cocostudio中制作骨骼动画,通过在工程中加载导出的js文件播放动画也可以,这里的动画比较简单,就直接通过代码实现了,如果项目中用到比较复杂的动画推荐采用加载js动画的方式.这里由于没有用到骨骼动画,就不做相应的介绍了,有兴趣的同学可以自己研究下~

由于游戏需要用到存储的数据比较小,这里对用户游戏数据持久化存储是采用UserDefault来进行.如果项目中需要存储的数据量比较庞大,建议使用数据库建表来进行存储,这样更方便数据的查询与管理.

在工程中通过一个单例UserDataManager来管理用户的数据,提供背景音乐是否静音,音效是否静音,用户账户金币以及当前游戏的关数四个成员变量.

UserDataManager *UserDataManager::getInstance()
{
   if (s_SharedUserDataManager == nullptr) {
       s_SharedUserDataManager = new UserDataManager();

       s_SharedUserDataManager->_musicMute = UserDefault::getInstance()->getBoolForKey(musicMuteKey, false);
       s_SharedUserDataManager->_soundMute = UserDefault::getInstance()->getBoolForKey(soundMuteKey, false);
       s_SharedUserDataManager->_allMoney = UserDefault::getInstance()->getIntegerForKey(userAllMoneyKey, 0);
       s_SharedUserDataManager->_stageNum = UserDefault::getInstance()->getIntegerForKey(userStageNumKey, 1);
   }

   return s_SharedUserDataManager;
}

监听按钮的点击事件,可以在cocostudio中对节点进行命名,然后在代码中通过下面方法获取对应name的节点

    // 获取startButton节点
    auto startBtn = static_cast<Button *>(Helper::seekWidgetByName(static_cast<Widget *>(mainCsb), "startButton"));

需要注意的是,按钮点击回调函数需要在.h文件提前声明,或者可以采用lambda表达式也可

  • 采用函数的回调写法
    // 添加按钮的事件

tartBtn->addTouchEventListener(CC_CALLBACK_2(MainLayer::startButtonTouch, this));

 - 采用lambda表达式写法
    ```c++
       startBtn->addTouchEventListener([=](Ref *sender, Widget::TouchEventType touchType){
        // button click callBack
    });
    ```
StartButton点击后会有两个逻辑:通过`UserDataManager`获取用户游戏关数
  - 用户没有游戏记录或者当前记录是游戏是第一关,直接进入游戏场景,开始游戏.
  - 用户有游戏记录并且关数大于1,直接进入商店场景.

###商店场景
同样也是通过采用cocostudio来进行布局的,效果如下
![商店场景cocostudio效果](http://upload-images.jianshu.io/upload_images/575247-208604c801777723.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

商店场景也比较简单,账户余额通过上文中的`UserDataManager`可以获取用户的金币数.

UserDataManager::getInstance()->getUserAllMoney();


商品采用Button来展示,这样可以获取玩家选择的当前商品,每一个商品只能购买一次,如果购买了商品后,对应商品上显示1的图标.商品描述通过点击商品,展示对应的商品作用描述.

```c++
auto csb = CSLoader::createNode("ShopScene.csb");
    this->addChild(csb);

    // 添加商品描述容器
    goodsDesVec.push_back(Value("炸药.购买以后,当抓到较重且金额不多的物品时,按下上方炸药即可炸毁物品,以便节省时间.功效为下一关"));
    goodsDesVec.push_back(Value("力量药水.购买以后,在下一关力量会增加,抓到物品后拉回速度会增加20%.功效为下一关"));
    goodsDesVec.push_back(Value("优质矿石.购买后在下一关中收购钻石的价格将变成原价格的3倍,但不保证下一关一定会有钻石~其效果为下一关"));
    goodsDesVec.push_back(Value("矿石收藏书.购买后下一关的矿石的价格将会是原有价格的3倍,其功效为下一关"));

    // 初始化商品描述Text
    goodsDesText = static_cast<Text *>(Helper::seekWidgetByName(static_cast<Widget *>(csb), "shopDetail"));
    Text *userMoney = static_cast<Text *>(Helper::seekWidgetByName(static_cast<Widget *>(csb), "userMoney"));
    userMoney->setString("$" + to_string(UserDataManager::getInstance()->getAllMoney()));

    // 获取购买按钮.并且添加按钮的点击事件
    Button *buyButton = static_cast<Button *>(Helper::seekWidgetByName(static_cast<Widget *>(csb), "buyButton"));
    buyButton->addTouchEventListener([=](Ref *sender, Widget::TouchEventType type){
        if (type == Widget::TouchEventType::ENDED) {
            int index = lastSelected->getTag() - 1;

            auto oneIV = buyOnes.at(index);
            if (oneIV->isVisible()) {
                return;
            }
            // 获取商品价格
            int price = 0;
            switch (index) {
                case 0:
                    price = kBombPrice;
                    break;
                case 1:
                    price = kPotionPrice;
                    break;
                case 2:
                    price = kDiamondsPrice;
                    break;
                case 3:
                    price = kStoneBookPrice;
                    break;
            }

            if (UserDataManager::getInstance()->getAllMoney() - payMoneyCount - price >= 0) {
                payMoneyCount += price;
                userMoney->setString("$" + to_string(UserDataManager::getInstance()->getAllMoney() - payMoneyCount));
                oneIV->setVisible(true);
            }
        }
    });

点击下一关,切换到游戏场景.

游戏场景

一样也是在cocostudio中布局,这里需要注意的是并不是采用一个csb文件就全部将节点添加完毕,这里分三块布局,如下图所示三块


顶部

level

钩子

游戏Layer提供一个快速创建场景的方法

// 参数分别为 是否购买了炸弹.力量药水.砖石升值书.石头收藏书.在商店的花销
static Scene *createScene(bool isBuyBomb, bool isBuyPotion, bool isBuyDiamonds, bool isStoneBook, int payMoney);

在上面函数中,创建游戏场景,给游戏场景添加刚体世界,用户后期进行碰撞的响应,代码如下

   Scene *scene = Scene::createWithPhysics();

    auto world = scene->getPhysicsWorld();
    world->setGravity(Vec2::ZERO);

    auto gameLayer = Game::create(isBuyBomb, isBuyPotion, isBuyDiamonds, isStoneBook, payMoney);
    scene->addChild(gameLayer);

    PhysicsBody *body = PhysicsBody::createEdgeBox(Size(kWinSizeWidth, kWinSizeHeight));
    body->setCategoryBitmask(10);
    body->setCollisionBitmask(10);
    body->setContactTestBitmask(10);

    Node *node = Node::create();
    node->setPosition(kWinSize * 0.5);
    node->addComponent(body);
    node->setColor(Color3B::RED);
    node->setTag(kWorldTag);
    scene->addChild(node);

    SpriteFrameCache::getInstance()->addSpriteFramesWithFile("res/Resources/level-sheet.plist");

    return scene;

在游戏Layer的init初始化方法中,通关用户当前关数获取对应的level.csb文件,然后添加到游戏的layer中.获取level.csb中的所有矿石节点,并且给每个矿石添加刚体body,以便后期进行与钩子的碰撞事件.

bool Game::init(bool isBuyBomb, bool isBuyPotion, bool isBuyDiamonds, bool isStoneBook, int payMoney)
{
    if (!Layer::init()) return false;

    auto csb = CSLoader::createNode("GameLayer.csb");
    this->addChild(csb, 10);

    this->isBuyPotion = isBuyPotion;
    this->isBuyDiamonds = isBuyDiamonds;
    this->isBuyStoneBook = isStoneBook;

    bompButton = static_cast<Button *>(Helper::seekWidgetByName(static_cast<Widget *>(csb), "BompButton"));
    bompButton->setVisible(isBuyBomb);
    bompButton->addTouchEventListener([=](Ref *ref, Widget::TouchEventType type){
        if (type == Widget::TouchEventType::ENDED) {
            // click bomp
            if (isOpenHook) {
                bompButton->setVisible(false);
                // 炸毁物品
                backSpeed = 10;

                isOpenHook = false;
                leftHook->setRotation(0);
                rightHook->setRotation(0);

                goldSprite->removeFromParent();
            }
        }
    });

    // 获取序列帧动画
    minerTimeLine = CSLoader::createTimeline("GameLayer.csb");
    this->runAction(minerTimeLine);

    auto hookCsb = CSLoader::createNode("Hook.csb");
    hookCsb->setPosition(kWinSizeWidth * 0.48, kWinSizeHeight * 0.856);
    this->addChild(hookCsb, 11);

    rope = static_cast<ImageView *>(Helper::seekWidgetByName(static_cast<Widget *>(hookCsb), "rope"));
    middleCircle = static_cast<Sprite *>(rope->getChildByTag(59));
    leftHook = static_cast<Sprite *>(middleCircle->getChildByTag(60));
    rightHook = static_cast<Sprite *>(middleCircle->getChildByTag(61));
    curPayMoney = payMoney;

    // 添加钩子刚体
    PhysicsBody *hookBody = PhysicsBody::createCircle(20);
    hookBody->setContactTestBitmask(10);
    hookBody->setCollisionBitmask(10);
    hookBody->setCategoryBitmask(10);
    middleCircle->addComponent(hookBody);
    circlePosition = middleCircle->getPosition();
    this->addButtonAction(csb);

    setUpText(static_cast<Widget *>(csb));

    timeCount = 60;

    // 添加碰撞事件
    auto physicsListener = EventListenerPhysicsContact::create();
    physicsListener->onContactBegin = CC_CALLBACK_1(Game::physicsBegin, this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(physicsListener, this);

    loadStageInfo();

    return true;
}

进入游戏场景后,首先展示关卡过关提示,通过用户当前关卡数计算出过关需要的金额.展示完毕后,正式开始游戏.

  • 添加点击屏幕点击事件
    // 添加点击事件
   auto listener = EventListenerTouchOneByOne::create();
   listener->onTouchBegan = CC_CALLBACK_2(Game::touchCallBack, this);
   _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
  • 开始钩子左右摆动动画
void Game::startShakeHookAnimation()
{
   float duration = 1;
   float angle = 65;
   rope->runAction(RepeatForever::create(Sequence::create(RotateTo::create(duration, angle), RotateTo::create(duration, 0), RotateTo::create(duration, -angle), RotateTo::create(duration, 0), NULL)));
}
  • 开启倒计时
schedule(CC_SCHEDULE_SELECTOR(Game::updateTime), 1, 59, 0);

当屏幕点击时,如果钩子是在摇摆阶段,停止钩子摇摆动画,开始伸长钩子动画

bool Game::touchCallBack(cocos2d::Touch *touch, cocos2d::Event *event)
{
    if (!ropeChangeing) {
        rope->pause();
        ropeChangeing = true;
        minerTimeLine->gotoFrameAndPlay(0, 105, true);
        schedule(CC_SCHEDULE_SELECTOR(Game::addRopeHeight), 0.025);
    }

    return false;
}

等待钩子碰撞的回调,如果碰撞后,不是场景的边缘则代表钩到了矿石

bool Game::physicsBegin(cocos2d::PhysicsContact &contact)
{
    if (contact.getShapeB()->getBody()->getNode()->getTag() != kWorldTag) {
        // 碰到金块, 打开钩子
        if (!isOpenHook) {
            this->pullGold(contact);
        }
    } else {
        this->backSpeed = 10;
    }

    this->unschedule(CC_SCHEDULE_SELECTOR(Game::addRopeHeight));
    this->schedule(CC_SCHEDULE_SELECTOR(Game::subRopeHeight), 0.025);

    return true;
}

碰撞后停止钩子伸长动画,执行钩子拉回函数

void Game::subRopeHeight(float dt)
{
    middleCircle->setPosition(circlePosition);

    ropeHeight -= backSpeed;
    if (ropeHeight <= 20) {
        ropeHeight = 20;
        minerTimeLine->pause();
        // 恢复原样, 继续摇摆
        rope->resume();
        this->unschedule(CC_SCHEDULE_SELECTOR(Game::subRopeHeight));
        ropeChangeing = false;

        if (isOpenHook) {
            isOpenHook = false;
            leftHook->setRotation(0);
            rightHook->setRotation(0);

            if (goldSprite != nullptr) {
                // 加分动画
                Label *scoreLabel = Label::create();
                scoreLabel->setColor(Color3B(50, 200, 0));
                scoreLabel->setSystemFontSize(25);
                scoreLabel->setString(to_string(goldSprite->score));
                scoreLabel->setPosition(rope->convertToWorldSpace(middleCircle->getPosition()));
                this->addChild(scoreLabel, 1000);

                curStageScore += goldSprite->score;
                auto spawn = Spawn::create(MoveTo::create(0.5, Vec2(allMoney->getPosition().x + 10, allMoney->getPosition().y)), Sequence::create(ScaleTo::create(0.25, 3), ScaleTo::create(0.25, 0.1), NULL), NULL);
                auto seque = Sequence::create(spawn, CallFuncN::create([=](Node *node){

                    scoreLabel->removeFromParent();
                    allMoney->setString(to_string(curStageScore + UserDataManager::getInstance()->getAllMoney() - curPayMoney));

                }),NULL);
                scoreLabel->runAction(seque);

                // 加分
                goldSprite->removeFromParent();
                goldSprite = nullptr;
            }
        }
    }

    //爆炸效果
    CCParticleSystem* particleSystem = CCParticleExplosion::create();
    particleSystem->setTexture(CCTextureCache::sharedTextureCache()->addImage("stars.png"));
    addChild(particleSystem);

    rope->setSize(Size(3, ropeHeight));
}

项目总结

项目写的比较匆忙,并且cocos2d-X更新到3.0+版本后,好多函数都弃用了...框架内部还是有许多Bug,当然我相信这难不倒大家的~

感觉光靠文字来讲述一个项目实在是太困难.希望大家还是参考工程代码,当遇到无法看懂或者不理解的时候参考下我写的Blog应该会更好一些.这个游戏项目说实话还是非常简单的,相信大家仔细研究下都可以实现的.

好久没用C++了T_T,有什么问题和不足之处大家同样还是可以留言.

以后我会分享一些有意思的小项目.希望朋友继续关注维尼的小熊.

代码下载地址(如果觉得有帮助,请点击Star★)

代码下载地址,记得Star★和Follow

小熊的技术博客

点击链接我的博客,欢迎关注

小熊的新浪微博

我的新浪微博,欢迎关注

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,519评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,029评论 4 62
  • hhhhh🤔
    syeaneko_5bd0阅读 79评论 0 0
  • 杂乱的工地无序肮脏, 从旁飘出淡淡清香。 下班的姑娘心事重重, 不禁驻足抬头张望。 白色的玉兰亭亭玉立, 已然开成...
    Yumi玉米粒阅读 880评论 2 6
  • 君27岁了,大事虽不成,但小事也算干成了几件。 今年是君妈最高兴也最烦闷的一年。老房子等着翻新,儿子说定了媳妇,现...
    月影伊人阅读 270评论 0 0