Cocos2d-x

怎样制作基于Cocos2d-x的SLG游戏-第6章

原创: @涵紫任珊

怎样制作基于Cocos2d-x的SLG游戏-第1章
怎样制作基于Cocos2d-x的SLG游戏-第2章
怎样制作基于Cocos2d-x的SLG游戏-第3章
怎样制作基于Cocos2d-x的SLG游戏-第4章
怎样制作基于Cocos2d-x的SLG游戏-第5章
怎样制作基于Cocos2d-x的SLG游戏-第6章
怎样制作基于Cocos2d-x的SLG游戏-第7章

前篇教程中我们已经创建好了播种界面,而本章我们将继续对单独的瓦片进行操作,完成在土地上播种的功能,同时我们将添加其他类似的界面来操作瓦片,如:清理界面(可以将树木、房屋等等从地图中清除),收获界面(收获成熟的农作物)等等。

再次说明,由于模拟经营类游戏的操作多样性,本系列教程将不会尽善尽美的实现整个SLG游戏的所有功能,这里只选取其中有代表的一些功能来实现,有兴趣的同学欢迎在本游戏的基础上进行扩展。本游戏最终将要实现的功能是这样的:

  1. 点击商品项和其他障碍物所在层(这里统称第2层)上的瓦片,将出现一个清理界面,该界面上有一把叉子,玩家点击叉子可将点击的瓦片从地图中移除,不过点击土块系瓦片除外。
  2. 点击空土块瓦片将出现上节中创建的播种界面,玩家选取播种层中的农作物选项,就可以在土块上栽种相应的种子。
  3. 种子种下去的同时,该处瓦片上将会生成一个计时面板,用于纪录种子多久后成熟。如果该处瓦片再次处于未选中状态,那这个计时面板将会隐藏起来,直到种子完全成熟后该处的计时界面才会被销毁。
  4. 种子成熟后,瓦片上的农作物将会变为相应的成熟作物。如果这时选中这些成熟的作物,那么将出现一个收获界面。收取作物后瓦片又将变为什么都没有的空土块。

以上就是对单独瓦片进行操作的全部功能,后面我们将会一一实现。

播种

上一章已经创建好播种界面,所以我们接下来首先来实现播种这项功能。
1、 根据游戏功能需求,定义如下的一个枚举类型来标示玩家所点击的瓦片类型:

typedef enum // 定义瓦片类型。如果需要扩展,可这这里定义更多类型。
{
    GROUD = 1,
    GROUD_CROP = 2,
    CROP_HARVEST,
    OTHER
} TileType;

2、 接着在玩家开始触碰屏幕的地方(既onTouchesBegan函数中)判断所触碰瓦片的类型。如下,在onTouchesBegan函数的if (gid != 0)中添加以下一段代码:

    switch (gid)
    {
        case 9: // 触碰到的是空土块
            tileType = TileType::GROUD;
            break;
        // 触碰到的是种了农作物幼苗的土块
        case 18:  
        case 20:
        case 22:
            tileType = TileType::GROUD_CROP;
            break;
        // 触碰到种植有成熟农作物的土块
        case 19:
        case 21:
        case 23:
            tileType = TileType::CROP_HARVEST;
            break;
        // 其他情况,也就是可以被清除的其他瓦片类型
        default:
            tileType = TileType::OTHER;
            break;
        }

以上代码中的gid对应了下图中的瓦片项。
iso-test-128

3、 当玩家松开手指的时候,创建播种界面。
在onTouchesEnded函数的if(press)中添加如下的代码:

    if(tileType == GROUD)
    {
        auto panel = SeedChooseLayer::create();
        panel->setPosition(screenPos);
        this->addChild(panel, 10, SEEDPANEL_TAG);
    }

其中SEEDPANEL_TAG是播种界面的标示,我们可以用它来判断播种界面是否存在,是否移除该界面等操作。其他界面也同样有着自己的标示,如下所示:

#define SEEDPANEL_TAG           888
#define HARVESTPANEL_TAG        889
#define REMOVEPANEL_TAG         890

4、 创建播种界面后,我们就该播种了。
不过首先请打开瓦片地图文件,我们来整理下瓦片层:在最底层的地图层上只放置草地瓦片,在草地层之上的层用来放置障碍物和商品项,最上层置空,用来设置提醒项。同时,修改之前的图层名字,规范代码逻辑(不然以后自己都看不懂自己写的什么),如下图所示:
p2
打开游戏主场景GameScene,添加update函数,并实现它。如下所示:

void GameScene::update(float dt)
{
    // 通过标示得到场景中的子节点
     auto seedPanel = this->getChildByTag(SEEDPANEL_TAG);
    // 判断瓦片是否为空土块,并且是否已经创建好了播种界面
    if(tileType == GROUD && seedPanel!= NULL)
    {
        // 得到选择的农作物类型
        auto type = ((SeedChooseLayer*)seedPanel)->getCurrType();
        // 根据农作物类型种植相应的农作物
        switch (type)
        {
            case WHEAT:
                map->getLayer("goodsLayer")->setTileGID(18, touchObjectPos);
                this->removeChild(seedPanel);
                break;
            case CORN:
                map->getLayer("goodsLayer")->setTileGID(20, touchObjectPos);
                this->removeChild(seedPanel);
                break;
            case CARROT:
                map->getLayer("goodsLayer")->setTileGID(22, touchObjectPos);
                this->removeChild(seedPanel);
                break;            
            default:
                break;
        }
    }
}

不要忘了在init函数中加上scheduleUpdate(),这样才会每一帧都调用update()。

5、 当玩家触摸到屏幕其他地方时,一定要把已有的播种界面清除,以确保场景中最多只有一个播种界面。所以回到onTouchesBegan函数中,清除已有播种界面。

    auto seedPanel = this->getChildByTag(SEEDPANEL_TAG);
    if(seedPanel){
        this->removeChild(seedPanel);
    }

现在运行我们的代码,就可以像下图一样在土地上播种庄稼了。

p3

清理瓦片

接下来我们创建一个清理界面,用来移除除土块以外的其他瓦片。它同播种界面原理一样,其定义如下:

class RemoveLayer: public Layer
{
public:
    virtual bool init() override;
    bool onTouchBegan(Touch *touch, Event *event);
    CREATE_FUNC(RemoveLayer);
    CC_SYNTHESIZE(bool, remove, Remove);

private:
    Sprite* fork;
};

该界面只有一个叉子供玩家选择,所以我们用布尔类型的变量来标示玩家是否选中它。其事件回调函数代码如下:

bool RemoveLayer::onTouchBegan(Touch *touch, Event *event)
{
    auto target = static_cast(event->getCurrentTarget());

    Point locationInNode = target->convertTouchToNodeSpace(touch);
    Size size = target->getContentSize();
    Rect rect = Rect(0, 0, size.width, size.height);

    if (rect.containsPoint(locationInNode))
    {
        target->setOpacity(180);
        // 判断是否选中叉子
        if (target == fork)
        {
            remove = true;
        }
        return true;
    }
    return false;
}

实现了清理界面后,按照创建和销毁播种界面的思路把清理界面添加到游戏场景中,既在onTouchesEnded中创建清理面板,在onTouchesBegan中销毁该清理面板,并在update函数中检测更新:

    if(NULL != removePanel && tileType == OTHER)
    {
        if(((RemoveLayer*)removePanel)->getRemove() == true)
        {
            map->getLayer("goodsLayer")->removeTileAt(touchObjectPos);
            this->removeChild(removePanel);
        }
    }

这样一来,我们就可以移除地图中的看不顺眼的瓦片了。

收获

运行游戏,当前效果如下图所示:

p4

此时点击长有幼苗的土块,将会出现一个记录庄稼成熟时间的面板出现,当时间到时,庄稼将会成熟。这个时候如果再点击庄稼,玩家就可以所获它了。

由于计时面板的创建有别于其他小界面的创建,所以这里我们先跳过这一步,先来创建收获面板。
收获界面同清理界面基本是一致的,它将会在庄稼成熟的时候被创建,其定义如下:

class HarvestLayer: public Layer
{
public:
    virtual bool init() override;
    bool onTouchBegan(Touch *touch, Event *event);
    CREATE_FUNC(HarvestLayer);
    CC_SYNTHESIZE(bool, harvest, Harvest);

private:
    Sprite* harvestSprite;
};

变量harvest用来标示玩家是否选中收获项,HarvestLayer的实现很简单,这里就不赘述了。

本章所用资源已更新,点此可进行下载,文章中的源代码将在本系列教程结束时上传,敬请期待。

怎样制作基于Cocos2d-x的SLG游戏-第5章

原创: @涵紫任珊

怎样制作基于Cocos2d-x的SLG游戏-第1章
怎样制作基于Cocos2d-x的SLG游戏-第2章
怎样制作基于Cocos2d-x的SLG游戏-第3章
怎样制作基于Cocos2d-x的SLG游戏-第4章
怎样制作基于Cocos2d-x的SLG游戏-第5章
怎样制作基于Cocos2d-x的SLG游戏-第6章
怎样制作基于Cocos2d-x的SLG游戏-第7章

上一章中,我们实现了商品的拖动操作,把商店的商品拖动到了地图上,但本章将实现拖动商品的校对检测,同时将实现瓦片的触摸控制(移动、销毁),播种、除草、收获。

前面我们拖动商品到什么地方,它就跟着移动到什么地方,如下图网格里的示意图所示。但一般情况下,为了更加形象的体现出瓦片地图这一特性,一般都不能让待入列的瓦片那些肆掠的移动,所以接下来我们将实现拖动商品项的细节调整。

p1

优化拖动操作

首先,先在头文件中设置两个变量用于记录鼠标/手指当前和在此之前的移动坐标(这个坐标指地图坐标)。

    Vec2 currPos;
    Vec2 perPos;

接着修改moveCheck函数,如下代码所示:

void GameScene::moveCheck(Vec2 position, int tag)
{
    auto mapSize = map->getMapSize();
    auto tilePos = this->convertTotileCoord(position);    
    canBliud = false; 
    // 1
    perPos = currPos;

    if( tilePos.x >= 0 && tilePos.x <= mapSize.width - 1 && tilePos.y >= 0 && tilePos.y <= mapSize.height - 1)
    {
        currPos = tilePos;
        int gid = map->getLayer("2")->getTileGIDAt(tilePos);
        if (gid == 0){
            buyTarget->setTexture(move_textures[tag]);           
            canBliud = true;
        }
        else
        {
            buyTarget->setTexture(move_textures_en[tag]);
            canBliud = false;
        }
        // 2
        auto screenPos = this->convertToScreenCoord(tilePos);
        buyTarget->setPosition(screenPos);     
        // 3
        if(perPos != currPos){
            map->getLayer("3")->removeTileAt(perPos);
            map->getLayer("3")->setTileGID(17, currPos);
        }
    }
    // 4
    else{
        buyTarget->setPosition(position);
        buyTarget->setTexture(move_textures_en[tag]);
        map->getLayer("3")->removeTileAt(perPos);
        canBliud = false;
    }   
}
  1. 给之前坐标赋值,让它等于当前坐标.
  2. 把当前地图坐标转换为屏幕坐标,同时设置商品项的位置到这个屏幕坐标上。这里convertToScreenCoord函数将完成这一转换。这样一来,screenPos坐标就将会是固定的一些值。注意:不要忘了删掉SpriteCallback函数中设置商品项位置的函数段。
  3. 鼠标/手指移动到哪里,就在那里的瓦片上设置一个可以标识它的记号,同时当移动到别的地方,要删除之前位置上的记号。
  4. 当tilePos超出地图范围时,让商品项跟着鼠标/手指的移动而移动,同时移除perPos位置上的记号。

convertToScreenCoord函数中的数学公式其实就是convertTotileCoord函数中数学原理的一个反推公式,其代码如下:

Vec2 GameScene::convertToScreenCoord(Vec2 position)
{
    auto mapSize = map->getMapSize();
    auto tileSize = map->getTileSize();
    auto tileWidth = map->getBoundingBox().size.width / map->getMapSize().width;
    auto tileHeight = map->getBoundingBox().size.height / map->getMapSize().height;

    auto variable1 = (position.x + mapSize.width / 2 - mapSize.height) * tileWidth * tileHeight ;
    auto variable2 = (-position.y + mapSize.width / 2 + mapSize.height) * tileWidth * tileHeight ;

    int posx = (variable1 + variable2) / 2 / tileHeight;
    int posy = (variable2 - variable1) / 2 / tileWidth;

    return Point(posx, posy);
}

最后还缺什么啦,我们来用手指想一下。对的,一般当拖动商品项到一个“不空”的瓦片上或地图之外的区域时,将会提示此处不可放。所以,接下来我们的工作来加这段提示的代码吧。

在SpriteCallback方法的if( canBliud == true ){}函数段后加上如下代码:

    else{
        // 得到放手时鼠标/手指的屏幕坐标,这个坐标是相对于地图的。所以计算它时应该要考虑到地图的移动和缩放。
        auto endPos =Vec2((widget->getTouchEndPos().x - bgOrigin.x)/bgSprite->getScale(), (widget->getTouchEndPos().y - bgOrigin.y)/bgSprite->getScale());
        // 把上面得到的屏幕坐标转换围地图坐标
        auto coord = convertTotileCoord( endPos);
        // 再把地图坐标转换为固定的一些屏幕坐标
        auto screenPos = this->convertToScreenCoord(coord);
        // 创建提醒项,把它设置在screenPos处
        auto tips = Sprite::create("tip.png");
        tips->setPosition(screenPos);
        bgSprite->addChild(tips);
        // 让提醒项出现一段时间后移除它
        tips->runAction(Sequence::create(DelayTime::create(0.1f),                                                CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, tips)),                                                NULL));
    }

运行你的游戏,你会发现它的效果如下图所示:

p2

对单独的瓦片进行操作

商品项的拖动效果已经近乎完美,接下来是该对单独的瓦片进行操作了。经营模式游戏中,当玩家触碰到地图瓦片时,将可以进行相应的操作,比如:触碰到树木将可以砍掉它;触碰到土地将可以播撒小麦、土豆、玉米等农作物,如果上成熟状态可以收获它们;触碰到建筑物时将可以修整,拆除它等等。如果玩家长按某个瓦片,我们还能移动它。

反正在这类游戏中对瓦片的操作是非常繁琐的,接下来我们就来一步一步的攻克它吧。最终的目标是做一个类似下图(《全面农场》)的效果。

p3 p4 p5

接下来我们需要先判断玩家的触碰是长按还是短按,然后在考虑其他的问题。

判断短按长按

Cocos2d-x中是没有长按事件的触发机制的,所以如果想实现长按的检测,需要自己实现。本教程中通过schedule来实现长按操作,其原理如下:

当玩家触碰到屏幕上的瓦片(除了草地,既瓦片地图的最底层)时,开始计时。如果在计时过程中发生了触摸移动或者异常中断了触摸响应,那么就取消本次计时。在计时过程中,如果达到预定时间,那么则执行相应的函数。最后在结束本次触摸时(抬起鼠标/手指),结束计时。

根据以上阐述的原理,我们需要一个变量来判断是否继续计时,所以请先在头文件中定义如下的变量:

bool press;
Vec2 touchObjectPos;

其中touchObjectPos由于记录触摸到的瓦片的地图坐标。

接下来找到onTouchesBegan函数,在函数中添加如下的一段代码:

    if(touches.size() == 1)
    {
        auto touch = touches[0];
        auto screenPos = touch->getLocation();       
        auto mapSize = map->getMapSize();       
        Vec2 pos;
        pos.x = (screenPos.x - bgOrigin.x)/bgSprite->getScale();
        pos.y = (screenPos.y - bgOrigin.y)/bgSprite->getScale();
        auto tilePos = this->convertTotileCoord(pos);        
        if( tilePos.x >= 0 && tilePos.x <= mapSize.width - 1 && tilePos.y >= 0 && tilePos.y <= mapSize.height - 1){
            int gid = map->getLayer("2")->getTileGIDAt(tilePos);
            // 如果瓦片地图的"2"层上tilePos处存在其他瓦片,则执行以下代码。
            if (gid != 0)
            {
                touchObjectPos = tilePos;
                map->getLayer("3")->setTileGID(17, tilePos);
                this->schedule(schedule_selector(GameScene::updatePress), 2);
                // longPress在开始按下的时候为true,如果移动,取消,则为false,在抬起的时候如果变量为true,那么执行schedule中的updatePress函数
                press = true;
            }
        }
    }

schedule的原理我就不废话了,不清楚的同学可以查看官方文档调度器(scheduler)一文。updatePress函数如下:

void GameScene::updatePress(float t)
{
    // 取消计时
    this->unschedule(schedule_selector(GameScene::updatePress));
    if(press)
    {
        log("是长按");
        map->getLayer("3")->removeTileAt(touchObjectPos);
        press = false;
    }
}

接下来在onTouchesMoved函数中添加如下代码:

    press = false;
    map->getLayer("3")->removeTileAt(touchObjectPos);
    this->unschedule(schedule_selector(GameScene::updatePress));

最后添加一个用于出来结束触摸响应的onTouchesEnded事件函数,其代码如下所示:

void GameScene::onTouchesEnded(const std::vector&touches, Event  *event)
{
    this->unschedule(schedule_selector(GameScene::updatePress));

    Size winSize = Director::getInstance()->getWinSize();
    if(touches.size() == 1)
    {
        auto touch = touches[0];
        auto screenPos = touch->getLocation();
        if(press)
        {
            log("是短按。此处应该创建相应的操作面板了");

            map->getLayer("3")->removeTileAt(touchObjectPos);
            press = false;
        }
    }
}

创建操作面板

对于一个模拟经营类游戏来说,操作面板之多,所以本教程就不大动干戈挨个的创建了,象征性的例举几个例子就行,毕竟这只是一篇教程,而不是商业项目,所以感兴趣的同学请跟着教程思路自己去拓展一下。

下面以播种面板为例,创建一个播种面板SeedChooseLayer,它继承于Layer,其定义如下所示:

// 定义农作物类型
typedef enum
{
    WHEAT = 0,
    CORN = 1,
    CARROT,
    NOTHING
} CropsType;

class SeedChooseLayer: public Layer
{
public:
    virtual bool init() override; 
    // 重载触摸回调函数
    bool onTouchBegan(Touch *touch, Event *event);
    CC_SYNTHESIZE(CropsType, currType, CurrType);// 选中的作物类型
    CREATE_FUNC(SeedChooseLayer);

private:
    Sprite* wheat;
    Sprite* corn;
    Sprite* carrot;
};

对去播种面板层的触摸响应,这里和地图的响应略有不同,它是单点触摸事件,所以以上重载的是用于出来单点事件的onTouchBegan函数(触摸点击开始事件)。

接下来,我们来实现SeedChooseLayer的各个方法。如下是init的实现。

bool SeedChooseLayer::init()
{
    if (!Layer::init())
    {
        return false;
    }
    currType = CropsType::NOTHING;

    auto bgSprite = Sprite::create("chooseBg.png");
    bgSprite->setAnchorPoint(Vec2(1, 0));
    this->addChild(bgSprite);

    wheat = Sprite::create("corn.png");
    wheat->setAnchorPoint( Point(0, 0));
    wheat->setPosition(Point(0, 0));
    bgSprite->addChild(wheat);

    corn = Sprite::create("wheat.png");
    corn->etPosition(Point(bgSprite->getContentSize().width/2, bgSprite->getContentSize().height/2));
    bgSprite->addChild(corn);

    carrot = Sprite::create("carrot.png");
    carrot->setAnchorPoint( Point(1, 1));
    carrot->setPosition(Point(bgSprite->getContentSize().width, bgSprite->getContentSize().height));
    bgSprite->addChild(carrot);

    // 创建事件监听器,OneByOne表示单点
    auto touchListener = EventListenerTouchOneByOne::create();
    // 设置是否向下传递触摸,true表示不向下触摸。
    touchListener->setSwallowTouches(true);
    touchListener->onTouchBegan = CC_CALLBACK_2(SeedChooseLayer::onTouchBegan, this);

    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, wheat);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener->clone(), corn);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener->clone(), carrot);
    return true;
}

在init函数中,我们初始化了播种面板中种子的布局,并且创建绑定了触摸事件。这里要注意:在面板中,我们设置了三个农作物选项供玩家选择,每一个选项都要处理触摸响应事件。所以,绑定需要绑定到每个选项上。然而当我们再次使用事件监听器的时候,需要使用 clone() 方法重新克隆一个,因为每个监听器在添加到事件调度器中时,都会为其添加一个已注册的标记,这就使得它不能够被添加多次。

绑定好触摸事件后,接下来需要实现具体的触摸回调。在onTouchBegan函数中我们要做的是:当玩家触碰到某选项时,重新设置其透明度,向玩家表明该项是被选中的;并且确定其选择的作物类型,方便我们后面代码的获取和使用。其代码如下图所示:

bool SeedChooseLayer::onTouchBegan(Touch *touch, Event *event)
{
    auto target = static_cast(event->getCurrentTarget());   // 1
    Point locationInNode = target->convertTouchToNodeSpace(touch);   // 2
    // 3
    Size size = target->getContentSize();
    Rect rect = Rect(0, 0, size.width, size.height);   
    // 4
    if (rect.containsPoint(locationInNode))
    {
        target->setOpacity(180);
        // 5
        if (target == wheat)
        {
            currType = CropsType::WHEAT;
        }else if(target == corn)
        {
            currType = CropsType::CORN;
        }else if(target == carrot)
        {
            currType = CropsType::CARROT;
        }else{
            currType = CropsType::NOTHING;
        }
        return true;
    }
    return false;
}
  1. 返回触摸事件当前作用的目标节点。
  2. 把touch对象中保存的屏幕坐标转换到GL坐标,再转换到目标节点的本地坐标下。
    在Node对象中有几个函数可以做坐标转换。convertToNodeSpace方法可以把世界坐标转换到当前node的本地坐标系中;convertToWorldSpace方法可以把基于当前node的本地坐标系下的坐标转换到世界坐标系中;convertTouchToNodeSpace这个函数可以把屏幕坐标系转换到GL坐标系,再转换到父节点的本地坐标下。
  3. 计算目标节点的矩形区域。
  4. 判断触碰点在不在目标节点的矩形区域内,即判断是否被选中。
  5. 根据选择的目标确定作物的类型。

以上我们就已经实现了播种界面的创建了,最后在onTouchesEnded函数中判断为短按的地方加上如下代码就可以实现播种界面的显示了。

panel = SeedChooseLayer::create();
panel->setPosition(screenPos);
this->addChild(panel, 10, SEEDPANEL_TAG);

效果如下图所示:

p6

本章资源已上传,点此可进行下载,文章中的源代码将在本系列教程结束时上传,敬请期待。

怎样制作基于Cocos2d-x的SLG游戏-第4章

原创: @涵紫任珊

怎样制作基于Cocos2d-x的SLG游戏-第1章
怎样制作基于Cocos2d-x的SLG游戏-第2章
怎样制作基于Cocos2d-x的SLG游戏-第3章
怎样制作基于Cocos2d-x的SLG游戏-第4章
怎样制作基于Cocos2d-x的SLG游戏-第5章
怎样制作基于Cocos2d-x的SLG游戏-第6章
怎样制作基于Cocos2d-x的SLG游戏-第7章

本章让我们回顾一下上一章所做的事情,有点忘了!呵呵,对了,就是下图的效果:

result

上一章,我们通过Cocos Studio编辑器简单的制作了一个能够自动适应多分辨率的UI,而在本章,我们将教会大家如何拖动滚动层中的商品选项到地图中,效果如下图所示:

result1

这里,我们第一步需要做的事是,把滚动层中的物品换成我们需要的商品,然后再完成拖动操作(把选中的商品拖动到TMX地图中空白的(没有其他障碍瓦片的)位置处)。下面就一起开始跟着做吧。

设置商品项

在Cocos Studio中,每个商品的信息都如下左图所示,包含了选项背景、商品描述、价格等等一些相同的属性。

item

所以在程序中,我们可以通过遍历滚动层中所有的子项来获得各属性的值,并重新设置它。这样一来,我们先在程序中定义如下的一些变量:

// 商品图样
const char* shop_textures[8] =
{
    "shopItem/Item1.png", "shopItem/Item2.png", "shopItem/Item3.png", "shopItem/Item4.png", "shopItem/Item5.png", "shopItem/Item6.png", "shopItem/Item1.png", "shopItem/Item2.png"
};

// 拖动过程中,选中项正常的纹理图,这个正常是能被添加在拖动位置
const char* move_textures[8] =
{
    "shopItem/moveItem1.png", "shopItem/moveItem2.png", "shopItem/moveItem3.png", "shopItem/moveItem4.png", "shopItem/moveItem5.png", "shopItem/moveItem6.png", "shopItem/moveItem1.png", "shopItem/moveItem2.png"
};

// 拖动过程中,选中项不正常的纹理图
const char* move_textures_en[8] =
{
    "shopItem_en/Item1.png", "shopItem_en/Item2.png", "shopItem_en/Item3.png", "shopItem_en/Item4.png", "shopItem_en/Item5.png", "shopItem_en/Item6.png", "shopItem_en/Item1.png", "shopItem_en/Item2.png"
};

// 商品描述
const char* shop_info[8] =
{
    "土地", "破树", "烂树", "烂草", "破屋", "破地", "婆婆", "反正破",
};

// 所值价格
const int shop_money[8] =
{
    20, 40, 60, 100, 120, 99, 50, 200,
};

再在initUI函数中添加如下的一段代码:

auto shop_scrollView = dynamic_cast(panel_shop->getChildByName("scrollview_shop"));

for (int i = 0; i < shop_scrollView->getChildren().size(); ++i)
    {
        Layout* shop_layout = static_cast(shop_scrollView->getChildren().at(i));
        shop_layout->setTag(SHOP_ITEM_LAYOUT_TAG + i);

        ImageView* buy_Sprite = static_cast(shop_layout->getChildByName("shopitem"));
        buy_Sprite->loadTexture(shop_textures[i]);

        TextField* info = static_cast(shop_layout->getChildByName("info"));
        info->setText(shop_info[i]);

        TextField* money = static_cast(shop_layout->getChildByName("money_image")->getChildByName("money"));
        money->setText(std::to_string(shop_money[i]));
    }

这样我们就可以设置好每个商品项了。再次运行程序,就会是下面这样的效果:

setItem

是不是很简单,哈哈,接下来继续看看怎样拖到商品项里的商品。

拖到操作

在Cocos Studio中,每个商品项中的“shopitem”是一个可交互的ImageView(前面我们自己设置的),我们可以通过给它绑定一个TouchEvent回调,让它像按钮一样可以处理更多不同的响应事件。

所以,回到initUI函数,为buy_Sprite绑定一个回调函数:

buy_Sprite->addTouchEventListener(CC_CALLBACK_2(GameScene::SpriteCallback, this));

其中,SpriteCallback回调方法和之前的menuShopCallback方法类似,需要根据TouchEvent事件的类型(按下、移动、抬起、取消),进行相应的逻辑处理。

拖动操作大概的过程是,按住某个商品,把它往游戏场景中拖,如果拖到的地方,该处的某个地图层上又没有其他的障碍物“瓦片”(我们把tmx地图的每一块图块都叫做瓦片),那我们放手时就在这里生成一个对应的瓦片商品。

具体的实现如下所示:

void GameScene::SpriteCallback(cocos2d::Ref* pSender, Widget::TouchEventType type)
{
    Size winSize = Director::getInstance()->getWinSize();
    // 获得所选择的Widget,和它的父Widget(也就是商品项)   
    Widget* widget = static_cast(pSender);
    Widget* parent = static_cast(widget->getParent());
    // 得到商品项的标记
    int tag = parent->getTag();

    // 根据TouchEventType类型进行逻辑处理
    switch (type)
    {
        // 按下手指/鼠标时,放大选中得商品
        case Widget::TouchEventType::BEGAN:
            widget->runAction( EaseElasticInOut::create( ScaleTo::create(0.1f, 1.5), 0.2f));
            break;

        case Widget::TouchEventType::MOVED:
            // 滑动手指/鼠标时,如果购物滚动面板是弹出状态,就先把它收起来 
            if(comeOut == true)
            {
                panel_shop->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(-panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                shop_btn->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(- panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                comeOut = false;
            }
            // 如果buyTarget为空,就先创建它
            if( buyTarget== NULL ){                
                buyTarget = Sprite::create(move_textures[tag - SHOP_ITEM_LAYOUT_TAG]);
                buyTarget->setAnchorPoint(Vec2(0.5f, 0));
                 // 把buyTarget添加到bgSprite上,这样buyTarget的位置就是相对于bgSprite了。
                bgSprite->addChild(buyTarget, 10);
            }
            // 移动buyTarget 
            else{                          
                Vec2 pos;
                // 因为buyTarget的位置是相对于地图的,所以我们需要考虑到它的移动和缩放。
                pos.x = (widget->getTouchMovePos().x - bgOrigin.x)/bgSprite->getScale();
                pos.y = (widget->getTouchMovePos().y - bgOrigin.y)/bgSprite->getScale();

                buyTarget->setPosition(pos);
                // 检测是否可以创建商品,这个后面会讲解。
                moveCheck(pos, tag - SHOP_ITEM_LAYOUT_TAG);
            }            
            break;
        // 抬起  
        case Widget::TouchEventType::ENDED:
            // 还原放大的widget
            widget->runAction( EaseElasticInOut::create(ScaleTo::create(0.1f, 1), 0.2f));
            // 移除buyTarget
            if(buyTarget != NULL)
            {
                buyTarget->removeFromParent();
                buyTarget= NULL;
            }            
            canBliud = false;
            break;
        // 取消触摸
        case Widget::TouchEventType::CANCELED:
            // 还原放大的widget
            widget->runAction( EaseElasticInOut::create(ScaleTo::create(0.1f, 1), 0.2f));
            // 生成瓦片
            if( canBliud == true )
            {
                // 得到放手时位置
                auto endPos =Vec2((widget->getTouchEndPos().x - bgOrigin.x)/bgSprite->getScale(), (widget->getTouchEndPos().y - bgOrigin.y)/bgSprite->getScale());
                // 在convertTotileCoord( endPos)位置上设置(生成)一个GID为9 + tag - SHOP_ITEM_LAYOUT_TAG的瓦片,这对应着滚动层中的商品,如下图所示。
                map->getLayer("2")->setTileGID(9 + tag - SHOP_ITEM_LAYOUT_TAG, convertTotileCoord( endPos));
                canBliud = false;
            }
            // 移除buyTarget
            if(buyTarget != NULL)
            {
                buyTarget->removeFromParent();
                buyTarget= NULL;
            }  
            // 弹出购物滚动面板          
            if(comeOut == false)
            {
                panel_shop->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                shop_btn->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                comeOut = true;
            }
            break;
        default:
            break;
    }
}

以上代码在触摸到可交互的商品时,先获取到它父节点的标记,这样我们就知道它是哪种商品了。再根据事件类型,进行以下的逻辑处理:

  • 触摸开始时,为所选择的商品(widget)添加一个放大的缓动效果,证明它是被选中了的。当然如果触摸开始到结束的时间太短,是不会有这个效果的。
  • 拖到widget时,先像在menuShopCallback中一样,收起弹出的购物滚动面板;然后创建一个临时的购买目标项(buyTarget),让它跟着手指的移动而移动,在此同时,检测它目前所在的地图位置上是否可以创建一个商品,关于这一点,后面会更详细的说明。
  • 正常完成触摸后,还原放大的widget,同时移除buyTarget。
  • 非正常取消触摸时,同样先还原放大的widget,移除buyTarget,弹出购物滚动面板。然后最重要的一点:在地图上“创建”选中的商品。这点我们也留到后面说明。

这里TouchEventType::ENDED在正常触摸完后,手指离开屏幕得时候触发,而TouchEventType::CANCELED则是在非正常触摸完结束时触发,比如手指没正常离开时候来了个电话,或者滑动过程中滑出了Widget范围抬起手指。

另外,地图图块已更新为:

iso-test-128

不用在意最后两个瓦片和前面的相同,这是本人的原因,因为实在是找不到资源了,做一点算一点吧,有机会再改。

上述检测以及在地图上生成选中商品的过程,都需要我们把Cocos2d-x坐标转化为瓦片地图对应的地图坐标。因为只要这样我们才能获得TMX地图层中每块瓦片的信息。关于瓦片地图的详细介绍请参考瓦片地图一文。

下面是把屏幕坐标转换为地图坐标的一段函数的实现:

Vec2 GameScene::convertTotileCoord(Vec2 position)
{
    // 得到瓦片地图的瓦片尺寸,对于本游戏是(30 * 30)
    auto mapSize = map->getMapSize();
    // 计算当前缩放下,每块瓦片的长宽
    auto tileWidth = map->getBoundingBox().size.width / map->getMapSize().width;
    auto tileHeight = map->getBoundingBox().size.height / map->getMapSize().height;
    // 把position转换为瓦片坐标,确保得到的是整数
    int posx = mapSize.height - position.y / tileHeight + position.x / tileWidth - mapSize.width / 2;
    int posy = mapSize.height - position.y / tileHeight - position.x / tileWidth + mapSize.width / 2;

    return Point(posx, posy);
}

convertTotileCoord方法将返回一个45度地图的坐标(这个坐标可能是超出正常取值范围的,它未经约束),其中参数position必须是一个相对于bgSprite(也就是瓦片地图 map)的位置值,因为地图的位置可能会有滚动和缩放的变化;参数tag是传入的标记值。

convertTotileCoord方法的注释已给出,其中最难理解的应该是将屏幕坐标转换为瓦片地图坐标的数学公式,其数学原理请阅读:www.gandraxa.com/isometric_projection.aspx

检测(moveCheck)的实现:

void GameScene::moveCheck(Vec2 position, int tag)
{
    auto mapSize = map->getMapSize();
    // 将position转化为地图坐标
    auto tilePos = this->convertTotileCoord(position);

    // canBliud是用于判断是否可生成瓦片的变量
    canBliud = false;

    // 约束tilePos的范围。如果tilePos在正确取值范围内(菱形内)
    if( tilePos.x >= 0 && tilePos.x <= mapSize.width - 1 && tilePos.y >= 0 && tilePos.y<= mapSize.height - 1)
    {
        // 前半段map->getLayer("2")是取得地图中名称为“2”的图层,而getTileGIDAt(tilePos)则是得到tilePos坐标上瓦片的GID标示。(GID标示为0时,表示该处没有任何的瓦片。)
        int gid = map->getLayer("2")->getTileGIDAt(tilePos);
        // 该处没有其他障碍瓦片时
        if (gid == 0) 
        {    // 设置拖动过程中正常的buyTarget纹理
            buyTarget->setTexture(move_textures[tag]);
            canBliud = true;
        }
        // 该处有障碍瓦片时,把buyTarget设置为move_textures_en中颜色偏红的纹理
        else{
            buyTarget->setTexture(move_textures_en[tag]);
            canBliud = false;
        }
    }
    // 如果位置在地图以外(四个角),同样把buyTarget设置为move_textures_en中颜色偏红的纹理
    else{
        buyTarget->setTexture(move_textures_en[tag]);
        canBliud = false;
    }
}

如果一切正常,此时我们运行游戏已经可以把购物面板中的商品无付费的拖动到场景中了,当然细节上还没考虑太多,后面再慢慢考虑。

但这儿还有一个问题,就是随着地图的移动和缩放,你会发现有时手指移动的位置和商品的位置不一致,就像下图这样:

err

找了半天,发现产生这个Bug的原因还是bgOrigin的问题。所以,果断回到onTouchesMoved函数,把下面一段代码:

    // 更新原点位置
    if( pos.x >= bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x
        || pos.x <= -bgSpriteCurrSize.width + winSize.width + bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x)
    {
        diff.x = 0;
    }
    if( pos.y >= bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y
        || pos.y <= -bgSpriteCurrSize.height + winSize.height + bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y)
    {
        diff.y = 0;
    }
    bgOrigin += diff;

修改为了:

    Vec2 off = pos - currentPos;        
    bgOrigin += off;

这样代码逻辑才更加严谨,是之前疏忽了。
好的,这章就算完成了,下面给一张清晰的游戏效果图。

result

点击这里下载资源。

怎样制作基于Cocos2d-x的SLG游戏-第3章

原创: @涵紫任珊

怎样制作基于Cocos2d-x的SLG游戏-第1章
怎样制作基于Cocos2d-x的SLG游戏-第2章
怎样制作基于Cocos2d-x的SLG游戏-第3章
怎样制作基于Cocos2d-x的SLG游戏-第4章
怎样制作基于Cocos2d-x的SLG游戏-第5章
怎样制作基于Cocos2d-x的SLG游戏-第6章
怎样制作基于Cocos2d-x的SLG游戏-第7章

经过前面两章的学习,我们已经可以掌握了地图的创建和加载,并且可以对它进行缩放,移动等基本的操作了。那么在本章,我们将通过Cocos Studio编辑器快速地制作一款能自动适应多分辨率的UI系统,享受一下Cocos2d-x的GUI系统和Cocos Studio所带来的便捷。

一直以来,如何快速地制作UI是开发游戏中不得不遭遇的一个问题,Cocos2d-x一路经历了1.0时期的固定位置和2.0的相对位置,一直到现在3.0中全新的GUI机制。尽管3.0版本引擎中引进了全新的,且功能强大的GUI机制,但如果要手动编码实现相关GUI的显示和布局问题,这还是将会是一件特别繁琐的事情。好在Cocos Studio对Cocos2d-x新的GUI系统进行了全面的支持,我们可以很方便的利用它来实现UI系统的设计。

result

本章的目标是实现如上图所示的效果(高清截图臣妾做不到,猿媛们请见谅)。选择左下角的购物车按钮会弹出一个购物滚动列表,下面就让我们一起来学习吧。

制作UI界面

开始之前,首先还是简单介绍下Cocos Studio,扫扫盲。Cocos Studio是一套基于Cocos2d-x的免费游戏开发工具集,它能帮助开发者快速创建游戏资源,将大部分繁琐的游戏开发工作使用编辑器来快速制作,进一步帮助游戏开发者减短开发周期、提高开发效率。

由于各个版本的Cocos Studio功能上略有不同,所以开发项目时最好采用对应的版本进行学习。本教程中采用的是Windows版本的Cocos Studio 1.5.0.1,其下载地址可在http://cn.cocos2d-x.org/download中找到。

下载安装好Cocos Studio以后,运行应用程序你将发现Windows版本Cocos Studio中核心的四个编辑器:UI编辑器、动画编辑器、场景编辑器和数据编辑器,它们分别用于处理游戏中的UI界面、动画资源、游戏场景和游戏数据。但因为我们只需要使用Cocos Studio来完成UI界面的制作,所以打开Cocos Studio后,选择UI Editor(UI编辑器)项,新建一个UI工程。下面就让我们一起来创建UI界面吧。

1)首先,因为我们的UI资源是按着1136

640的分辨率来制作的,所以先在快捷菜单栏中设置画布大小为1136

640。如下:

p2

2)接着选择 文件->导入资源 导入所需的资源文件,也可直接将资源拷入UI工程的Resources文件夹目录下,然后刷新资源面板。本教程所使用的资源已共享,可点击这里下载。

3)此时对象结构面板中只有如下的一个根节点。

p3

我们需要设置一下根节点的属性,如下图所示:

p4

其中,Cocos Studio的子控件布局的方式有四种,在图中依次排开,它们分别是:

  1. 绝对布局:子控件的位置由其坐标决定,其优点是布局灵活性大,缺点是在做全屏多分辨率的情况下不太适用。
  2. 相对布局:子控件的位置由其相对于父控件的纵横两个方向的位置决定,它还允许子控件相对于父控件的位置做偏移。
  3. 线性横向布局:线性布局的一种,子控件在父控件上呈平行结构依次排列。
  4. 线性纵向布局:线性布局的一种,子控件在父控件上呈垂直结构依次排列。

因为我们需要制作一款能自动适应多分辨率的界面,所以绝对布局是不可行的,这里需要把子控件布局设置为相对布局,也就是上图中所选择的第二个布局选项。

再者,去掉“交互”项的√勾选,因为UI将位于地图背景的上层,如果我们勾选了交互项,那么地图背景层的触摸事件就将遭到拦截,我们就无法对它做拖动和缩放操作了。交互项的意思简单点说就是此控件是否拦截输入、触摸等消息。

最后,将背景颜色调为无颜色,并勾选“自适应分辨率”项,这样整个根节点大小就会随着屏幕大小的变化而变化了。

4)接下来向场景中拖入一个图片控件,以它为例,我们来看看如何设置子控件的属性,如下图所示:

p5

因为我们需要让图片的大小随着分辨率的变化而变化,所以把尺寸的模式从Auto修改为了Custom,并把图片的尺寸设为它原始尺寸大小(280

82)。这样在1136

640的分辨率下就显示了原始大小。

勾选百分比选框,这样在其他分辨率下,比如480

320下,图片显示的尺寸就变成了((480 / 1136

280)

(320 / 640

82))的大小,保证了显示效果的一致性。但需要说明的是,如果非1136 * 640比例的分辨率下,图片难免会出现拉伸。

将名字属性修改为便于识别的名称,养成良好的习惯。

在控件布局中选择其横向布局为左边,纵向布局为上边,这样图片就会紧贴根节点的左上角了。调整边缘属性可改变图片相对于左上角的位置坐标。

5)同样的方法添加其他控件,注意层级关系,并一定要勾选百分比项。

6)下面我们来制作按下购物车按钮所弹出的商店部分。

首先,向场景中拖入一层容器,命名为panel_shop,控件布局时让它停靠在购物车按钮(button_shop)的左侧,因为shop_button本身就在屏幕的最左下角,所以新建的panel_shop位于屏幕渲染区域之外的。如下图所示:

p6

向panel_shop中添加一个滚动层控件,设置其属性,如下图所示:

p7

这里我们把滚动层的大小设置为了panel_shop的2/3,仅仅时为了后面在程序中好控制它们的显示。把滚动层的布局方式设置为线性纵向布局,同时勾选“交互”项,这样做的原因为了让我们能滚动该滚动层。滚动区域的大小应该比滚动层本身大,这样才能让滚动层滚动起来。所以我们把滚动区域的高调整为800。

接着在滚动层内创建一个如下的panel,并为其添加如下的子控件。

p8

快速复制上图控件并调整位置,清理滚动层的背景颜色,将滚动层的颜色设置为“无颜色”,将panel_shop层的颜色设为白色。

7)这时,改变画布大小试试,如果一切正常,那么这个画布里的几乎所有内容都会根据当前的分辨率进行适配。

你也看到啦,我们说的是几乎,那还有什么不能进行适配啦?比如:字体大小,滚动层滚动区域大小等等,它们对并勾选“自适应分辨率”项是不敏感的,这让人有点头疼,这可能是Cocos Studio存在的一个bug吧。所以为了避免一些不必要的问题,我们把画布大小改为程序中所设置的design resolution,既480*320,同时把滚动层滚动区域大小调整为一个比较合适的大小。

p9

制作完成后勾选滚动层的“裁剪”属性,这样滚动层超出自身区域的内容将不会显示。

8)打开导出对话框,选择导出路径,按默认配置导出资源。

代码中加载

Cocos2d-x 使用UI框架的步骤:

1)Cocos2d-x 导入Cocos Studio的UI编辑器文件(json和资源)
2)编码获取/创建控件,绑定控件
3)使用控件做逻辑处理:比如,按钮、ScrollView等等控件的响应事件。

打开GameScene.cpp添加头文件

#include "cocostudio/CCSGUIReader.h"
#include "ui/CocosGUI.h"

using namespace ui;

编写initUI函数,先简单初始化下游戏UI,如下列代码所示:

void GameScene::initUI()
{
    playerLayout = static_cast(cocostudio::GUIReader::getInstance()->widgetFromJsonFile("Ui/Ui_1.json"));
    addChild(playerLayout, 10);

    // 获取商店层和购物车按钮
    panel_shop = dynamic_cast(playerLayout->getChildByName("panel_shop"));   
    shop_btn = dynamic_cast

按钮是要有响应函数的,又由于它是Widget的一个子类,所以采用的TouchEvent的回调方式。这里所说的TouchEvent响应主要是使用在Widget上的,可以将其看做是函数回调的一个扩展,为更多的响应处理提供可能。下面是按钮回调的实现:

void GameScene::menuShopCallback(cocos2d::Ref* pSender, Widget::TouchEventType type)
{
    Size winSize = Director::getInstance()->getWinSize();

    switch (type)
    {
        case Widget::TouchEventType::BEGAN:  // 按下按钮
            // 为shop_btn添加一个缩放效果
            shop_btn->runAction( EaseElasticInOut::create(Sequence::create( ScaleBy::create(0.1f, 2),ScaleBy::create(0.2f, 0.5f), NULL), 0.5f));
            break;
        case Widget::TouchEventType::MOVED:  // 移动按钮
            break;
        case Widget::TouchEventType::ENDED:  // 放开按钮
            if(comeOut == false)  // 弹出
            {
                // 为panel_shop和shop_btn添加一个弹性的移动效果,移动的距离是(panel_shop->getContentSize().width / 3 * 2, 0)个单位。
                panel_shop->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                shop_btn->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                comeOut = true;
            }
            else if(comeOut == true) // 缩回
            {
                panel_shop->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(-panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                shop_btn->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(- panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                comeOut = false;
            }
            break;

        case Widget::TouchEventType::CANCELED:  // 取消点击
            break;

        default:
            break;
    }

}

效果图:

p10

当商店层和购物车按钮弹出时,如果触碰到了它们范围之外的区域,它们应该缩回。所以,在onTouchesBegan函数中需要添加如下的一段代码。

void GameScene::onTouchesBegan(const std::vector&touches, Event  *event)
{
    Size winSize = Director::getInstance()->getWinSize();
    if(comeOut == true)
    {
        panel_shop->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(-panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
        shop_btn->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(- panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
        comeOut = false;
    }
}

好的,现在我们已经简单的实现了UI的显示了。下一章我们将替换滚动层中的商品项和信息,并拖动商品到地图。

怎样制作基于Cocos2d-x的SLG游戏-第2章

原创: @涵紫任珊

怎样制作基于Cocos2d-x的SLG游戏-第1章
怎样制作基于Cocos2d-x的SLG游戏-第2章
怎样制作基于Cocos2d-x的SLG游戏-第3章
怎样制作基于Cocos2d-x的SLG游戏-第4章
怎样制作基于Cocos2d-x的SLG游戏-第5章
怎样制作基于Cocos2d-x的SLG游戏-第6章
怎样制作基于Cocos2d-x的SLG游戏-第7章

上一章中,我们使用TiledMap制作了一张简单的地图,并把它加入到了程序中,紧接着本章将实现地图的双指缩放和单指移动功能。

是不是觉得这一功能很接地气,呵呵,其实在很多类似的大地图背景游戏中这是非常常见和必要的一项功能,玩家可以通过滑动屏幕实现地图的滚动预览,同时可以通过两个手指的拉伸和聚拢实现地图背景的放大缩小。下面,就跟着我们一起来实现吧。

p1

双指缩放,单指拖动的实现

Cocos2d-x中有自己的一套事件分发机制,如果你还不是很清楚,可先阅读Cocos2d-x事件分发机制一文。

在Cocos2d-x 3.x 中,实现触摸响应的一般流程如下:

  1. 重载触摸回调函数
  2. 创建并绑定触摸事件
  3. 实现触摸回调函数

具体实现如下:

1、首先,在GameScene.h文件中声明成员函数。

virtual void onTouchesBegan(const std::vector &touches, cocos2d::Event *event);
virtual void onTouchesMoved(const std::vector &touches, cocos2d::Event *event);

2、在GameScene.cpp文件的init函数中创建并绑定触摸事件。

// 1 创建一个事件监听器
auto listener = EventListenerTouchAllAtOnce::create();
// 2 绑定触摸事件
listener->onTouchesBegan = CC_CALLBACK_2(GameScene::onTouchesBegan, this);// 触摸开始时触发
listener->onTouchesMoved = CC_CALLBACK_2(GameScene::onTouchesMoved, this);// 触摸移动时触发
// 3 添加监听器
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, bgSprite);
  1. 在使用触摸事件时,我们首先需要创建一个事件监听器,事件监听器包含了触摸事件、键盘响应事件、加速记录事件、鼠标响应事件和自定义事件。其中的触摸监听类型触摸事件又分为 EventListenerTouchOneByOne(单点触摸) 和 EventListenerTouchAllAtOnce(多点触摸) 两种。
  2. 让监听器绑定事件处理函数。上面绑定的onTouchesBegan和onTouchesMoved分别响应的是触摸点击开始事件和移动事件。与之相关的还有onTouchEnded和onTouchCancelled两个事件处理函数,但目前我们的游戏还不需要(也有可能不会用到),所以这里就不用实现了。
  3. 监听器创建完成后需要把它绑定给_eventDispatcher事件分发器,_eventDispatcher 是 Node 的属性,通过它我们可以统一管理当前节点(如:场景、层、精灵等)的所有事件分发情况。
    将事件监听器 listener 添加到事件调度器_eventDispatcher中有两种方法,即如下的两个函数:
    void addEventListenerWithSceneGraphPriority(EventListener* listener, Node* node);              
    void addEventListenerWithFixedPriority(EventListener* listener, int fixedPriority);
    

    两者的主要区别在于它们加入到事件分发器中的优先级的差异。其中的使用 addEventListenerWithSceneGraphPriority 方法添加的事件监听器优先级固定为0;而使用 addEventListenerWithFixedPriority 方法添加的事件监听器的优先级则可以自己设置,但不可以设置为 0,因为这个是保留给 SceneGraphPriority使用的。

3、最后在GameScene.cpp文件中实现触摸回调函数
一旦玩家开始触碰屏幕,我们的程序就会开始调用相应的触摸事件处理函数来处理相应的逻辑,所以现在我们就可以来完成这部分的逻辑了。

实现中有以下几个需要注意的问题:

  • 需要判断触碰是单点还是多点,如果是多点,那么就缩放;是单点,就拖动。
  • 节点缩放的参考点默认是其锚点。显然,对于一个大地图背景来说,如果不实时改变它的锚点位置和本身位置,那它的缩放必然不会按照选取的区域进行缩放,必然会出现类似下图的情况。

p2

(上图中,背景图片的锚点在蓝点的位置,当我们想放大红圈所圈的那棵树时,如果只是简单的改变背景的放大倍率,那一定会出现上图的第二种情况(目标会向右上角偏移);但如果我们把背景的锚点和位置都设置到目标处,那就会像第三中情况一样,得到一个比较好的放大效果。)

  • 当缩放到一定程度,如缩小到与可视区域一样时,为了避免出现空白的区域,我们需要做一些处理。同时地图不能无止境的放大或缩小,需要有一定的范围来约束。比如,放大到它本身的4倍时,应该停止放大。
  • 拖动地图移动时,地图不能移出可视区域,这里需要做边界控制。

掌握了这些注意事项以后,现在我们就可以开始具体的行动了。

首先,在GameScene.h中定义如下的变量:

Sprite* bgSprite;
Vec2 bgOrigin;

bgSprite是地图背景,需要缩放和移动的对象都是其子节点,这样我们就可以通过操作它来实现缩放和移动了。bgOrigin用于记录bgSprite的初始原点位置。

接着,我们跳转到GameScene.cpp的init()方法,修改之前添加地图背景的方法,同时初始化bgOrigin。如下代码所示:

mapLayer = Layer::create();
this->addChild(mapLayer,-1);
 
bgSprite = Sprite::create("2.jpg");
bgSprite->setAnchorPoint(Vec2::ZERO);
bgSprite->setPosition(Vec2::ZERO),
bgOrigin = Vec2(Vec2::ZERO);
mapLayer->addChild(bgSprite);
 
auto treeSprite = Sprite::create("1.png");
treeSprite->setAnchorPoint(Vec2::ZERO);
treeSprite->setPosition(Vec2::ZERO),
treeSprite->setScale(2);
bgSprite->addChild(treeSprite, 2);
 
auto map = TMXTiledMap::create("mymap8.tmx");
map->setAnchorPoint(Vec2::ZERO);
map->setPosition(Vec2::ZERO),
bgSprite->addChild(map, 1);

因为对层而言,它相比于其他的节点来说,其锚点、位置、大小都不好控制,所以我们需要通过另外的节点(比如这里的bgSprite)来执行后面的缩放和移动等动作。

最后压轴来了,实现触摸事件的处理函数如下:

void GameScene::onTouchesMoved(const std::vector&touches, Event  *event)
{
    auto winSize = Director::getInstance()->getWinSize();
    if(touches.size() > 1)        // 多点进行缩放
    {
        // 得到当前两触摸点
        auto point1 = touches[0]->getLocation();
        auto point2 = touches[1]->getLocation();
        // 计算两点之间得距离
        auto currDistance = point1.distance(point2);
        // 计算两触摸点上一时刻之间得距离
        auto prevDistance = touches[0]->getPreviousLocation().distance(touches[1]->getPreviousLocation());
        // 两触摸点与原点的差向量,pointVec1和pointVec2是相对于bgSprite的位置
        auto pointVec1 = point1  - bgOrigin;
        auto pointVec2 = point2  - bgOrigin;
        // 两触摸点的相对中点
        auto relMidx = (pointVec1.x + pointVec2.x) / 2 ;
        auto relMidy = (pointVec1.y + pointVec2.y) / 2 ;
        // 计算bgSprite的锚点
        auto anchorX = relMidx / bgSprite->getBoundingBox().size.width;
        auto anchorY = relMidy / bgSprite->getBoundingBox().size.height;
        // 相对屏幕的中点
        auto absMidx = (point2.x + point1.x) / 2 ;
        auto absMidy = (point2.y + point1.y) / 2 ;
 
        // 缩放时,为了避免出现空白的区域,需要做以下的边界处理。        
        // 当bgSprite快要进入到屏幕时,修改bgSprite的位置(既absMidx和absMidy)。
        if(  bgOrigin.x > 0)
        {
            absMidx -= bgOrigin.x;
        }
        if( bgOrigin.x < -bgSprite->getBoundingBox().size.width + winSize.width )
        {
            absMidx +=  -bgSprite->getBoundingBox().size.width + winSize.width - bgOrigin.x;
        }
        if( bgOrigin.y > 0 )
        {
            absMidy -= bgOrigin.y;
        }
        if( bgOrigin.y < -bgSprite->getBoundingBox().size.height + winSize.height )
        {
            absMidy +=  -bgSprite->getBoundingBox().size.height + winSize.height - bgOrigin.y;
        }
        // 重设bgSprite锚点和位置
        bgSprite->setAnchorPoint(Vec2(anchorX, anchorY));
        bgSprite->setPosition(Vec2(absMidx, absMidy));
        // 根据两触摸点前后的距离计算缩放倍率
        auto scale = bgSprite->getScale() * ( currDistance / prevDistance);
        // 控制缩放倍率在1~4倍之间,最小倍率不能太小,不让背景将不能填充满整个屏幕。
        scale = MIN(4,MAX(1, scale));
        bgSprite->setScale(scale);
        // 更新原点位置
        bgOrigin = Vec2(absMidx, absMidy) - Vec2(bgSprite->getBoundingBox().size.width * anchorX, bgSprite->getBoundingBox().size.height * anchorY) ;
    }
    else if(touches.size() == 1)        // 单点进行移动
    {
        // 单点时,touches中只有一个Touch对象,所以通过touches[0]就可以得到触摸对象
        auto touch = touches[0];
        // 计算滑动过程中的滑动增量
        auto diff = touch->getDelta();       
        // 得到当前bgSprite的位置
        auto currentPos = bgSprite->getPosition();
        // 得到滑动后bgSprite应该所在的位置
        auto pos = currentPos + diff;
        // 得到此刻bgSprite的尺寸
        auto bgSpriteCurrSize = bgSprite->getBoundingBox().size;
 
        //边界控制,约束pos的位置
        pos.x = MIN(pos.x, bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x);
        pos.x = MAX(pos.x, -bgSpriteCurrSize.width + winSize.width + bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x);
        pos.y = MIN(pos.y, bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y);
        pos.y = MAX(pos.y, -bgSpriteCurrSize.height + winSize.height + bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y);
        // 重设bgSprite位置
        bgSprite->setPosition(pos);
 
        // 更新原点位置
        if( pos.x >= bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x
           || pos.x <= -bgSpriteCurrSize.width + winSize.width + bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x)
        {
            diff.x = 0;
        }
        if( pos.y >= bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y
           || pos.y <= -bgSpriteCurrSize.height + winSize.height + bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y)
        {
            diff.y = 0;
        }
        bgOrigin += diff;
    }
}

以上就是onTouchesMoved函数的实现方法了,原理已在注释中解释清楚,所以我想理解起来已经不会很难。下面给出一张示意图帮助大家理解:

p3

下图是缩放过程中刚好出现空白的区域时的图形示意图:

p4

此时空白的区域的宽等于 -bgSprite->getBoundingBox().size.width + winSize.width - bgOrigin.x,所以我们把背景的位置向右移动-bgSprite->getBoundingBox().size.width + winSize.width - bgOrigin.x个单位就可以避免这种情况的出现。

代码中有一点需要注意的是,在缩放过程中,bgSprite的尺寸不断变化的,所以计算起锚点或进行边界处理时,一定要用它缩放后的尺寸宽高来计算,而不能是它本身的宽高。 所以代码中计算bgSprite的尺寸我们用getBoundingBox函数来获得经过缩放和旋转之后的外框盒大小,而不用getContentSize函数来获得节点原始的大小。

iOS端多点触碰默认是关闭的,所以需要在AppController.mm 程序启动回调中启用多点触摸才可以,具体方法是在以下的函数段后加入[eaglView setMultipleTouchEnabled:YES];

如下所示:

CCEAGLView *eaglView = [CCEAGLView viewWithFrame: [window bounds]
                                 pixelFormat: kEAGLColorFormatRGBA8
                                 depthFormat: GL_DEPTH24_STENCIL8_OES
                          preserveBackbuffer: NO
                                  sharegroup: nil
                               multiSampling: NO
                             numberOfSamples: 0];
 
[eaglView  setMultipleTouchEnabled:YES];

总的来说,要想很好的实现这一功能不是容易的,以上就是我们实现了的一种方法,虽然细节上还有一些问题,也未在真机上测试,但还是希望能对大家的学习有所帮助。如果你有更好的方法实现,也可以提出来,大家一起进步学习。

下章将讲解如何利用CocoStudio来制作一个支持多分辨率的UI系统。

怎样制作基于Cocos2d-x的SLG游戏-第1章

原创: @涵紫任珊

怎样制作基于Cocos2d-x的SLG游戏-第1章
怎样制作基于Cocos2d-x的SLG游戏-第2章
怎样制作基于Cocos2d-x的SLG游戏-第3章
怎样制作基于Cocos2d-x的SLG游戏-第4章
怎样制作基于Cocos2d-x的SLG游戏-第5章
怎样制作基于Cocos2d-x的SLG游戏-第6章
怎样制作基于Cocos2d-x的SLG游戏-第7章

在前两年《QQ农场》比较流行的时候,想必大家都曾被它虐过千百遍吧。如今,虽然《QQ农场》的热潮已经告一段落,但随着手机游戏的日益发展,更多的农场手游开始兴起,比如同为腾讯旗下的《全民农场》,它便是以其更丰富的内容、更贴近现实的社交互动玩法,将QQ农场取而代之。所以竟然大家都这么热爱农场游戏,那我们接下来就来自己制作一款类似的SLG游戏吧。

SLG游戏既策略模拟类游戏,它可以分很多类型,像农场游戏这样的模拟经营游戏只是其中的一种。农场游戏可以让玩家扮演一个农场的经营者,玩家通过购买种子,耕种、浇水、施肥、除草、收获果实,最后出售给市场来实现整个经营过程,它让玩家在经营农场的同时也可以感受到“作物养成” 带来的乐趣。

本款游戏我们将使用目前最新的Cocos2d-x 3.2引擎,同时结合Tiledmap以及Cocos Studio来制作。所以,在此之前请大家先下载好引擎,TiledMap编辑器和Cocos Studio编辑器。

创建项目

如果要说到环境搭建,那又该是一篇长篇大论了,所以这里我就不说了,不清楚的同学到网上问度娘吧,会有很多不同系统不同版本搭建的结果的。

Cocos2d-x创建项目的方式一直都在不停的改动,所以这里我觉得很有必要给大家介绍下如何创建项目。

创建3.2的项目其实很简单,打开终端(Windows是cmd)进入到引擎文件夹目录,然后输入以下命令就可以创建。

cocos new SLG -p com.cocos2dx.rs -l cpp -d /Users/cocos2d-x/workspace/cocos2dx/cocos2d-x-3.2/projects

如下图:

p1

说明:

  • new:new后是项目名
  • -p :-p后是包名
  • -l :-l后是语言(cpp指c++)
  • -d :-d后是项目生成路径

如果一切无误,那等待几分钟以后你就可以在给定的目录下找到新建的项目了。打开项目工程后,下面我们就可以开始游戏的制作了。

前期准备

打开工程后,运行程序,你会发现它不是一个空的项目,在Classes和Resource文件夹下Cocos2d-x已经给出了一些实质性的东西,当然这些不是一定都有用的,它们存在的目的是为了给我们展示一个典型的Cocos2d-x的例子。

除了AppDelegate.h 和 AppDelegate.cpp文件,这两个文件夹下其他的东西都是可被删除的(不过在删除之前,可以先看一下HelloWorld类,了解下它的类结构、类方法,以便对Cocos2d-x进行初步的学习,也方便初学者依葫芦画瓢再写一个类似的场景)。AppDelegate类是创建项目时自动生成的一个类,它控制着游戏的生命周期,是Cocos2d-x游戏的通用入口文件,类似于一般 Windows 工程中main函数所在的文件。

打开AppDelegate.cpp文件,在游戏加载期的最后一个applicationDidFinishLaunching()函数中我们可以设置第一个启动的游戏场景,如下:

auto scene = GameScene::createScene();
director->runWithScene(scene);

GameScene是我们新建的一个游戏场景,下面会讲解。

分辨率适配

在我的游戏中,首先第一件事还是做分辨率适配,这是个恒古不变的定律。现如今市场中各种屏幕尺寸和分辨率的移动设备层出不穷,为了更好地适应这些设备,游戏的分辨率适配是十分有必要的。

在农场游戏中,很最要的一点就是实现大地图背景的拖动、放大缩小等操作,所以可想而知,我们的地图不是全部都显示在屏幕内的,也就是说,我们不能像之前的方式那样把整个显示内容做适配,我们应该留有一定的边距供玩家拖动。如下图所示:

p2

同样是在applicationDidFinishLaunching函数中,我们添加如下一段代码对游戏做分辨率适配,以便它能更好的适应不同的运行环境。

glview->setDesignResolutionSize(480.0f, 320.0f, ResolutionPolicy::FIXED_HEIGHT);
std::vector searchPath;
searchPath.push_back("H_1920");
FileUtils::getInstance()->setSearchPaths(searchPath);
director->setContentScaleFactor(1440.0f / 320.0f);

分辨率适配的原理,建议大家阅读一下:Cocos2d-x 多分辨率适配完全解析这篇文章,虽然它不是针对最新版Cocos2dx引擎,但它还是能很清楚的告诉你分辨率适配的原理和方法。

还有要说明的一点是,我们的游戏地图的高为1920,分辨率适配时则只设为了1440,意思就是说,我们本该全在屏幕内的内容留出了4分之一的高度在屏幕外。

编辑游戏地图

模拟经营游戏中,游戏地图多为拼接而成,这里我们用瓦片地图编辑器(Tiled Map Editor)来制作游戏的地图,它可以把编辑后的地图文件保存为TMX格式的文件,能被Cocos2d-x很好的支持。瓦片地图(Tile Map)不但生成简单,而且可以灵活的用于引擎中。关于瓦片地图的介绍可参考瓦片地图一文。

接下来我们开始创建地图。

运行TiledMap编辑器,新建一个地图文件。填写如下对话框:

p3

在地图方向选项内,可以选择正常、45度(传说中的2.5D)和45度交错,这里我们选择45度。接下来需要设置地图大小,这里的数值是指有多少格tile元件,并不是像素,这里我们选择30×30的地图。

最后是确定tile元件的大小,根据美工提供的地面元件大小设置,这个教程里,我们使用128×64的大小。

在地图大小一栏中,你可以看到最终的地图大小为3840 * 1920。

点击确认之后,你可能已经发现了,这个游戏地图它是菱形的,如下图所示。

p4

这里你可能会想,为什么要菱形的啦,选择45度交错建一个接近矩形的不行吗?呵呵,其实这样也是可以的,只不过啦,Cocos2d-x引擎中默认是不支持45度交错的,如果需要在引擎中加载这种交错的地图必须自己修改引擎代码(看到这,是不是整个人都不好了),而且在修改过后还不能正确的得到地图的大小,需要自己编写计算大小的公式代码。鉴于这一点,我在想,难道《全名农场》、《请叫我海盗》等游戏地图的四个角都不能被点击操作都是因为这个原因吗? 哈哈,就当是我想多了吧。其实菱形就菱形吧,其他的游戏也都这样,可以在菱形地图的下层贴一层背景来掩盖这一现象。

接下来,我们还是回到正题开始拼接地图吧。选择地图-》新图块,然后填写如下所示的对话框。

p5

选择浏览按钮,将准备好的图块文件载入编辑器。接着设置图块的宽度和高度(默认情况下是一个tile元件的大小,但),根据图块文件中图块的大小来设置。边距、间距什么的,可不做修改,0就好。

最后选中相应的图块,拖动到渲染区拼一个理想的地图。

p6

暂时我们就只简单的拼一个地图就可以了,后面再根据游戏需要,设置一些必要的对象和属性。

加载地图资源

新建一个GameScene场景,加载游戏地图。不过在此之前,请把编辑好的tmx和图块文件拷贝到Resource文件夹下。GameScene的结构和HelloWorld差不多,照着HelloWorld类依葫芦画瓢就可以建一个。你可以先看看它是如何实现的,再实现自己的类。这里就不多说了。

在Cocos2d-x中使用TMX,有以下流程供你参考:

  1. 首先用地图编辑器编辑你的地图,导出成TMX 格式。
  2. 将导出的TMX 文件和相关图片放在工程的Resoure文件夹下。
  3. 使用Cocos2d-x中TMXTileMap 类的create方法创建地图对象,TMX 文件的解析是引擎内部完成的,所以我们不需要担心。TMXTileMap 是Node 的子类,因此只要添加到场景中即可。
  4. 通过TMXTileMap,可以获得其他相关对象,比如单个瓦片(属Sprite类),比如对象组(ObjectGroup类),比如层(TMXLayer类)等;你可以通过TMXLayer类修改,删除或者添加某个网格位置的瓦片,这样可以动态的修改地图了,你还可以进行其他的操作,相关的API 我们后面使用到了再做讲解。

在GameScene类的init函数中添加如下的代码创建游戏地图:

mapLayer = LayerColor::create(Color4B(78,127,41,255));
this->addChild(mapLayer,-1);
 
auto map = TMXTiledMap::create("mymap4.tmx");
mapLayer->setContentSize(map->getContentSize());
mapLayer->addChild(map, 10);
 
auto treeSprite = Sprite::create("1.png");
treeSprite->setAnchorPoint(Vec2(0, 0));
treeSprite->setPosition(Vec2(0, 0));
treeSprite->setScale(2);
mapLayer->addChild(treeSprite, 11);

代码中新建了一个带颜色的背景层,背景层的尺寸等于加载的TMX地图大小。接着把地图和如下的一个遮盖层(其实就是为了防止菱形的地图看起来那么突兀而添加的一层)依次添加到层上。

p7

运行程序,你将看到如下图所示的游戏场景:

p8

本教程的美术资源不是很完善,大家就将就看吧,见谅!

下章我们将实现游戏背景的单指拖动,双指缩放的功能,以及利用Cocos Studio来制作一个支持多分辨率的UI系统。

Cocos2d-x塔防游戏_贼来了11——完善游戏

原创: @涵紫任珊

Cocos2d-x塔防游戏_贼来了1——基础知识储备
Cocos2d-x塔防游戏_贼来了2——地图的创建加载
Cocos2d-x塔防游戏_贼来了3——进攻的敌人
Cocos2d-x塔防游戏_贼来了4——创建炮塔
Cocos2d-x塔防游戏_贼来了5——触摸响应
Cocos2d-x塔防游戏_贼来了6——触摸响应2
Cocos2d-x塔防游戏_贼来了7——数据管理与碰撞检测
Cocos2d-x塔防游戏_贼来了8——批量添加敌人
Cocos2d-x塔防游戏_贼来了9——关卡数据
Cocos2d-x塔防游戏_贼来了10——选择关卡
Cocos2d-x塔防游戏_贼来了11——完善游戏

现在回头看看,关于塔防系列的教程,前面我们已经拖拖拉拉的写了也有10篇之多了,是不是都不想听我啰嗦了。

虽然这个贼来了游戏还有很多功能(比如炮塔的升级、移除、数值系统)都还没有实现,但我们还是要止步于此了,感兴趣的同学可以自己继续研究拓展。毕竟这仅仅只是一篇教程,我们也只是为了教大家使用Cocos2d-x更方便的制作游戏,而不是在做一个商业项目,所以感兴趣的同学可以自己继续拓展。

在本章里,我们将完善游戏代码,添加更多的游戏场景、音乐音效、粒子效果以及预加载功能,下面就一起来看看吧。

音乐音效

Cocos2d-x声音的接口主要有三个API:

  1. SimpleAudioEngine:提供一个最简单的声音控制API。如果你仅仅是要播放一些简单的背景音乐,那我们推荐你选用该API。
  2. CDAudioManager:一个封装了AVAudioPlayer对象,且非常基础、简单的API。它可以播放MP3,IMA4或者AAC格式的声音。
  3. CDSoundEngine:基于OpenAL的声音引擎。它可以控制多达32种通道的声音,且功能强大,能完全取代CDAudioManager和SimpleAudioEngine。

因为本游戏只需要简单的播放一些声音,所以这里我们选择了最简单、最普遍的SimpleAudioEngine。 SimpleAudioEngine非常简单,通过下面的几个API就可以满足本游戏声音的播放。

SimpleAudioEngine::getInstance()->preloadBackgroundMusic(FileUtils::getInstance()->fullPathForFilename("XX").c_str() );
SimpleAudioEngine::getInstance()->preloadEffect(FileUtils::getInstance()->fullPathForFilename("XX" ).c_str());
 
SimpleAudioEngine::getInstance()->playBackgroundMusic(FileUtils::getInstance()->fullPathForFilename("XX").c_str(), true);
SimpleAudioEngine::getInstance()->playEffect(FileUtils::getInstance()->fullPathForFilename("XX").c_str(), false);

游戏中声音有两种,一种是背景音乐,另一种是音效,所以播放它们需要用不同的API。

还有一点要提的是,在使用SimpleAudioEngine之前需要包含它的头文件。如下所示:

#include "SimpleAudioEngine.h" 
using namespace CocosDenshion;

预加载功能

游戏资源预加载可以提升用户体验,比如当一个游戏场景的内容过于庞大时,预加载可以缓解游戏资源加载时的卡顿现象。

预加载的实现我们需要做的是,在第一个场景load完的时候,把下一个场景所用的资源提前加载到缓冲,这样切换到下个场景时就可以直接读取缓存资源了,玩家也就不用等很久了。

一般情况下,我们需要预加载的资源有图片和声音两种,在Cocos2d-x的cpp-tests例子里本身就有一个预加载图片的例子(TextureCacheTest)。它的原理是用一个变量来记录已加载的资源数目,当加载好一个资源时,这个变量值就加1,当这个变量值等于总资源数时,意味着我们已经加载了所有的文件,这时就可以跳转到下一个场景了。预加载声音的原理和它差不多,都需要先加入缓存,下面我们还是直接看看代码是怎样实现的吧。

void LoadingResScene::loadResources()
{
    SimpleAudioEngine::getInstance()->preloadBackgroundMusic(FileUtils::getInstance()->fullPathForFilename("sound/music.mp3").c_str() );
    numberOfLoadedRes++;
    SimpleAudioEngine::getInstance()->preloadEffect(FileUtils::getInstance()->fullPathForFilename("sound/dead.wav" ).c_str());
    numberOfLoadedRes++;
    SimpleAudioEngine::getInstance()->preloadEffect(FileUtils::getInstance()->fullPathForFilename("sound/button.wav").c_str() );
    numberOfLoadedRes++;
    SimpleAudioEngine::getInstance()->preloadEffect(FileUtils::getInstance()->fullPathForFilename("sound/comeout.wav").c_str() );
    numberOfLoadedRes++;
    SimpleAudioEngine::getInstance()->preloadEffect(FileUtils::getInstance()->fullPathForFilename("sound/shoot.wav").c_str() );
    numberOfLoadedRes++;
    SimpleAudioEngine::getInstance()->preloadEffect(FileUtils::getInstance()->fullPathForFilename("sound/tip.wav").c_str() );
    numberOfLoadedRes++;
    SimpleAudioEngine::getInstance()->preloadEffect(FileUtils::getInstance()->fullPathForFilename("sound/dead.mp3").c_str() );
    numberOfLoadedRes++;
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile("Play.plist");
    numberOfLoadedRes++;
 
    Director::getInstance()->getTextureCache()->addImageAsync("playbg.png", CC_CALLBACK_1(LoadingResScene::loadingCallBack, this));
    Director::getInstance()->getTextureCache()->addImageAsync("playbg1.png", CC_CALLBACK_1(LoadingResScene::loadingCallBack, this));
    ........................
}

preloadBackgroundMusic和preloadEffect方法是用来缓存音乐音效的。numberOfLoadedRes就是前面提到过的那个记录已加载资源数目的变量,每加载一个资源,numberOfLoadedRes值就加1。loadingCallBack是加载图片后的回调函数,在它的函数体中同样递增numberOfLoadedRes的值。

在贼来了游戏中,我们在加载资源的同时用了一根进度条来显示加载进度。进度条的创建前面章节已经讲过,所以就此跳过,接下来,我们来更新load进度条,代码如下:

void LoadingResScene::logic(float dt)
{
    float percent = (float)numberOfLoadedRes / (float)totalOfLoadedRes * 100;
 
    progressBar->setPercentage(percent);
    if (numberOfLoadedRes == totalOfLoadedRes)
    {
        transitionScene();
        SimpleAudioEngine::getInstance()->playBackgroundMusic(FileUtils::getInstance()->fullPathForFilename("sound/music.mp3").c_str(), true);
        SimpleAudioEngine::getInstance()->setBackgroundMusicVolume(0.1f);
    }
}

logic方法将时时更新进度条进度,它会在加载完所有的资源后跳转到下一个场景。logic方法用如下的定时器启动:

schedule(schedule_selector(LoadingResScene::logic));

LoadingResScene场景效果图:

p1

其他界面

除了前面部分文章中提到的各个关卡场景外,我们还为游戏添加了一些其他界面,如菜单界面,成功界面,失败界面,读取关卡信息界面(在每关游戏开始之前)。下面是整个游戏的流程图,其中就包含了各个界面。

p2

菜单界面(UIScene):

p3

在菜单界面中,有一个绕按钮旋转的粒子效果,关于它的实现原理感兴趣的童鞋可以跳转到使用Cocos2d-x实现微信“天天爱消除”炫耀button特效中查看。

关卡信息界面(LevelInfoScene):

p4

关卡信息界面是玩家选择了关卡后进入的一个界面,设想中的关卡信息界面将读取当前关卡的关卡信息,但这里我们只读取了关卡当前的得分情况。

成功界面(SuccessfulScene):

p5

失败界面(FailedScene):

p6

失败界面中加入粒子特效和按钮的弹入效果,粒子效果有粒子编辑器制作产生,而弹入效果请查看源码。

总结

到目前为止,尽管本游戏的功能还不完善,代码上也不是很完美,但我们的塔防系列游戏还是就此结束了。感谢大家一直以来的支持,希望本系列教程对大家学习Cocos2d-x有所帮助。同时欢迎大家扩展游戏功能或斧正文章代码。

在此还要提醒大家的是,由于之前提交过几次未修正的游戏源码,所有请大家一定要在 https://github.com/iTyran/thiefTD 这里下载最终版的代码,再次谢谢支持!

Cocos2dx 3.2 横版过关游戏Brave学习笔记(八)

作者: douxt

好长时间没更新了,这次准备加入敌人的攻击,将敌人波数设置为三波,最后一波会有个小boss。
打完第三波就会出现胜利画面。如果玩家死亡就出现失败界面。

这里采用比较简单的方法控制敌人,即每隔一段时间,让敌人进行攻击,如果不在范围内,则向玩家移动。
在MainScene的init中:

    this->schedule(schedule_selector(MainScene::enemyMove), 3);

这里是每隔3秒让敌人进行一次动作。

用到的函数:

void MainScene::enemyMove(float dt)
{
    for(auto enemy: _enemys)
    {
        if("dead" != enemy->getState()&&_player && "dead" != _player->getState())
        {
            if(_player->isInRange(enemy))
            {
                if("attacking" != enemy->getState())
                {
                    enemy->attack();
                    _player->beHit(enemy->getAttack());
                    this->updateHealth();
                }
            }else
            {
                enemy->walkTo(_player->getBestAttackPosition(enemy->getPosition()));
            }
        }
    }

}

还增加了更新玩家血量的代码,具体可以下载源代码参考。

然后玩家会死亡,对象会被释放,之前没考虑这个情况,所以很可能会出现指针指向无效对象的情况,以前用到_player的地方需要添加相应的检查。

在MainScene中增加变量记录关卡总数和当前关卡,两个vector来记录每一关每个怪的类型和位置:

    int _maxLevel;
    int _level;
    std::vector<std::vector<Player::PlayerType>> _enemyTypes;
    std::vector<std::vector<Vec2>> _enemyPositions;

并在initLevel中进行初始化:

void MainScene::initLevel()
{
    _level = 0;
    _maxLevel = 2;
    std::vector<Player::PlayerType> types;
    types.push_back(Player::ENEMY1);
    types.push_back(Player::ENEMY2);
    _enemyTypes.push_back(types);

    types.clear();
    types.push_back(Player::ENEMY1);
    types.push_back(Player::ENEMY2);
    types.push_back(Player::ENEMY2);
    _enemyTypes.push_back(types);

    types.clear();
    types.push_back(Player::ENEMY1);
    types.push_back(Player::ENEMY1);
    types.push_back(Player::ENEMY2);
    types.push_back(Player::BOSS);
    _enemyTypes.push_back(types);

    std::vector<Vec2> positions;
    positions.push_back(VisibleRect::center());
    positions.push_back(VisibleRect::right() - Vec2(200, 0));
    _enemyPositions.push_back(positions);

    positions.clear();
    positions.push_back(VisibleRect::center());
    positions.push_back(VisibleRect::right() - Vec2(200, 0) + Vec2(0, 200));
    positions.push_back(VisibleRect::right() - Vec2(200, 0) - Vec2(0, 200));
    _enemyPositions.push_back(positions);

    positions.clear();
    positions.push_back(VisibleRect::center() + Vec2(0, 200));
    positions.push_back(VisibleRect::center() - Vec2(0, 200));
    positions.push_back(VisibleRect::right() - Vec2(200, 0) + Vec2(0, 200));
    positions.push_back(VisibleRect::right() - Vec2(200, 0) - Vec2(0, 200));
    _enemyPositions.push_back(positions);

}

然后将addEnemy替换为新的函数:

void MainScene::addEnemyByLevel(int level)
{
    if(level > _maxLevel)
    {
        return;
    }

    auto types = _enemyTypes[level];
    auto positions = _enemyPositions[level];

    for(int ii=0; ii< types.size();ii++)
    {
        addOneEnemy(types[ii], positions[ii]);
    }
}

注意,Player中的类型中添加了一个新类型Boss。然后还需要增加变量来记录关卡总数目,目前的关卡等。
如果玩家死亡,弹出一个新界面,提示玩家游戏结束,并有一个按钮可以返回开始场景。
如果打完第三波,也会弹出类似界面,提示玩家胜利。

void MainScene::enemyDead(Ref* obj)
{
    auto player= dynamic_cast<Player*>(obj);
    if(Player::PlayerType::PLAYER == player->getPlayerType())
    {
        _player = nullptr;
        auto layer = GameOverLayer::create();
        layer->setText("You Died!");
        this->addChild(layer,10000);
    }
    else
    {
        _enemys.eraseObject(player,true);
        log("onEnemyDead:%d", _enemys.size());
        if(_enemys.size() == 0)
        {
            if(_level == 2)
            {
                auto layer = GameOverLayer::create();
                layer->setText("You Win!");
                this->addChild(layer,10000);
                return;
            }
            showNextLevelItem();
        }
    }
}

新增加了一个GameOverLayer。

代码提交了一下,名称为:"add enemy attack, boss, lose if die, win if clear level."




有些地方不太流畅,感觉判断触摸敌人的那块需要改进。 代码应该还存在着不少不完善的地方,希望发现的人能帮忙指出,谢谢!

Cocos2dx 3.2 横版过关游戏Brave学习笔记(七)

作者: douxt

这次还是按照原版教程来,补上游戏开始界面,画面滚动等等。

感觉MainScene::init里的代码有点乱,所以先整理一下。

bool MainScene::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
     //load frames into cache
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile("image/role.plist","image/role.pvr.ccz");
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile("image/ui.plist","image/ui.pvr.ccz");

    addRoles();
    addUI();
    addListener();
    addObserver();
    return true;
}

看上去好多了,运行了一下发现角色被背景挡住了,设置一下zOrder,使背景-角色-UI的zOrder依次递增。
另外发现可以用Director的方法实现暂停。

游戏开始界面

新建一个scene,名叫StartScene,并将AppDelegate.cpp中的MainScene改为StartScene。

#ifndef __StartScene__
#define __StartScene__
#include "cocos2d.h"

USING_NS_CC;

class StartScene : public Layer
{
public:
    bool init();
    static Scene* createScene();
    void onStart(Ref* obj);
    CREATE_FUNC(StartScene);
};

#endif

可以在这里载入缓存,添加按钮并响应。

#include "StartScene.h"
#include "MainScene.h"
#include "VisibleRect.h"
#include "CustomTool.h"

bool StartScene::init()
{
    if(!Layer::init())
        return false;
    log("StartLayer::init");

    SpriteFrameCache::getInstance()->addSpriteFramesWithFile("image/role.plist","image/role.pvr.ccz");
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile("image/ui.plist","image/ui.pvr.ccz");
    auto background = Sprite::create("image/start-bg.jpg");
    background->setPosition(VisibleRect::center());
    this->addChild(background);

    auto item = CustomTool::createMenuItemImage("start1.png", "start2.png", CC_CALLBACK_1(StartScene::onStart,this));
    auto menu = Menu::createWithItem(item);
    this->addChild(menu);
    return true;
}

Scene* StartScene::createScene()
{
    auto scene = Scene::create();   
    auto layer = StartScene::create();
    scene->addChild(layer);
    return scene;
}

void StartScene::onStart(Ref* obj)
{
    log("StartLayer::onStart");
    auto scene = MainScene::createScene();
    Director::getInstance()->replaceScene(scene);
}

另外使原来PauseLayer中的Home按钮实现切换到StartScene的作用:

void PauseLayer::home(Ref* obj)
{
    _eventDispatcher->removeEventListener(_listener);
    auto main = (MainScene*)this->getParent();
    main->onTouchResume();
    this->removeFromParentAndCleanup(true);
    auto start = StartScene::createScene();
    Director::getInstance()->replaceScene(start);
}

运行了一下,出错了,貌似忘了把listener什么的在onExit里注销掉。还忘了在点击home之后也继续一下,否则进入关卡后还是暂停状态。当然这些错误没有白犯,多犯几次就能熟练应对了。

画面滚动

下面加入过场,即敌人死光后,会出现一个GO>>>的提示,点击就会进入下个小关,有新的敌人,消灭后还可以继续切换……
建立一个容器容纳敌人,每死一个就将其移除,死光则显示Go图标。
在Player中,敌人死亡,发送一个消息通知MainScene.,在状态机回调函数onDead的回调函数func中:

if(_type != PLAYER)
    NotificationCenter::getInstance()->postNotification("enemyDead",this);

MainScene接收消息

    NotificationCenter::getInstance()->addObserver(this, callfuncO_selector(MainScene::enemyDead),"enemyDead",nullptr);

收到后会做出反应:

void MainScene::enemyDead(Ref* obj)
{
    auto player= (Player*)obj;
    _enemys.eraseObject(player,true);
    log("onEnemyDead:%d", _enemys.size());
    if(_enemys.size() == 0)
        showNextLevelItem();
}

在addUI里加入这个GO图标:

    auto goItem = CustomTool::createMenuItemImage("go.png", "go.png", CC_CALLBACK_1(MainScene::gotoNextLevel,this));
    goItem->setVisible(false);
    goItem->setTag(2);
    goItem->setPosition(VisibleRect::right().x - goItem->getContentSize().width/2, VisibleRect::center().y);
    _menu = Menu::create(pauseItem, debugItem, goItem, NULL);
    _menu->setPosition(0,0);
    this->addChild(_menu, 20);

并加入回调函数:

void MainScene::gotoNextLevel(Ref* obj)
{
    auto goItem = this->_menu->getChildByTag(2);
    goItem->setVisible(false);
    goItem->stopAllActions();

    _background->move("left",_player);
}

要进行背景滚动,实现一个Background类

#ifndef __Background__
#define __Background__
#include "cocos2d.h"

USING_NS_CC;

class Background : public Layer
{
public:
    bool init();
    CREATE_FUNC(Background);
    void move(const char* direction, Sprite* withSprite);
    void onMoveEnd();
private:
    bool _isMoving;
    Sprite* _spriteA;
    Sprite* _spriteB;
};


#endif

滚动完毕,发消息通知MainScene再次放置怪物。
发送消息:Background::onMoveEnd()

    NotificationCenter::getInstance()->postNotification("backgroundMoveEnd");

接受消息:MainScene

    NotificationCenter::getInstance()->addObserver(this, callfuncO_selector(MainScene::backgroundMoveEnd),"backgroundMoveEnd",nullptr);

响应:

void MainScene::backgroundMoveEnd(Ref* obj)
{
    addEnemy();
    log("adding enemy...");
}

现在原教程里的功能就基本上实现了。


然后提交了一下, 名称为"Note 7 Finish Basic Tutorial"

下一步要做的事情比较明显。
即让敌人主动进攻玩家,玩家死亡显示失败界面。
以及设置过关目标,例如打败某个小boss,然后显示胜利界面。

Cocos2dx 3.2 横版过关游戏Brave学习笔记(六)

作者: douxt

使用物理引擎

可以使用Box2D或者Chipmunk物理引擎,Box2D功能较强,Chipmunk被内置到引擎里,使用方便。
我们就先用这个内置的。主要用物理引擎来检测是否发生接触事件。当然,如果只是检测碰撞,其实不一定非要用物理引擎。

把MainScene的createScene函数修改一下:

Scene* MainScene::createScene()
{
    // init with physics
    auto scene = Scene::createWithPhysics();
    auto layer = MainScene::create();
    //set physics world
    layer->setPhysicsWorld(scene->getPhysicsWorld());
    scene->addChild(layer);
    return scene;
}

即创建场景时就可以指定附带物理引擎效果。将物理世界传给MainScene备用。
MainScene增加setPhysicsWorld函数,用于设置一个PhysicsWorld*类型的私有变量。

然后Player类在初始化的时候设置一下body以及碰撞和接触的条件,不应发生碰撞,但需要检测到接触事件。原文里用到了Sensor,可能是版本不一样我没有发现怎么设置sensor,所以用监听接触事件代替碰撞。

auto size = this->getContentSize();
    auto body = PhysicsBody::createBox(Size(size.width/2, size.height));
    body->setCollisionBitmask(0);
    body->setContactTestBitmask(1);
    this->setPhysicsBody(body);

因为图片帧中会包含大量空白,getContentSize()获得的矩形框覆盖了过多的区域,需要根据精灵实际显示大小进行适当裁剪。

这个时候运行一下,发现角色们直接开始往下掉了……原来默认重力不为0.
可以在MainScene::onEnter里将其设置为0:

void MainScene::onEnter()
{
    Layer::onEnter();
    // set gravity to zero
    _world->setGravity(Vec2(0, 0));
}

另外再额外增加按钮来切换是否显示调试用的框框。

    auto debugItem = MenuItemImage::create(
                                        "CloseNormal.png",
                                        "CloseSelected.png",
                                        CC_CALLBACK_1(MainScene::toggleDebug, this));
    debugItem->setScale(2.0);
    debugItem->setPosition(Vec2(VisibleRect::right().x - debugItem->getContentSize().width - pauseItem->getContentSize().width ,
        VisibleRect::top().y - debugItem->getContentSize().height));
 
    _menu = Menu::create(pauseItem, debugItem, NULL);
    _menu->setPosition(0,0);
    this->addChild(_menu);

然后在MainScene中增加对物理接触的监听。

    _listener_contact = EventListenerPhysicsContact::create();
    _listener_contact->onContactBegin = CC_CALLBACK_1(MainScene::onContactBegin,this);
    _listener_contact->onContactSeperate = CC_CALLBACK_1(MainScene::onContactSeperate,this);
    _eventDispatcher->addEventListenerWithFixedPriority(_listener_contact, 10);

对应的监听函数为:

bool MainScene::onContactBegin(const PhysicsContact& contact)
{
    auto playerA = (Player*)contact.getShapeA()->getBody()->getNode();
    auto playerB = (Player*)contact.getShapeB()->getBody()->getNode();
    auto typeA = playerA->getPlayerType();
    auto typeB = playerB->getPlayerType(); 
    if(typeA == Player::PlayerType::PLAYER)
    {
        // only one player so ShapeB must belong to an enemy        
        log("contact enemy!");
        playerB->setCanAttack(true);
    }
    if(typeB == Player::PlayerType::PLAYER)
    {
        // only one player so ShapeA must belong to an enemy        
        log("contact enemy!");
        playerA->setCanAttack(true);
    }
    return true;
}
 
void MainScene::onContactSeperate(const PhysicsContact& contact)
{
    auto playerA = (Player*)contact.getShapeA()->getBody()->getNode();
    auto playerB = (Player*)contact.getShapeB()->getBody()->getNode();
    auto typeA = playerA->getPlayerType();
    auto typeB = playerB->getPlayerType(); 
    if(typeA == Player::PlayerType::PLAYER)
    {
        // only one player so ShapeB must belong to an enemy        
        log("leave enemy!");
        playerB->setCanAttack(false);
    }
 
    if(typeB == Player::PlayerType::PLAYER)
    {
        // only one player so ShapeA must belong to an enemy        
        log("leave enemy!");
        playerA->setCanAttack(false);
    }
}

这里的重复代码过多,应该有办法优化一下。contact发生时,回调用onContactBegin,参数为 contact,里面包含了两个参加接触的Shape,ShapeA和ShapeB。
如果ShapeA是玩家,那B一定是敌人,设置一下让敌人可被攻击。如果ShapeB是玩家,那A就是敌人。

这时我发现教程好像省略了些东西。现在应该补充一下了。
敌人应该会响应触摸,然后发出一个clickEnemy消息,MainScene收到消息后,会判断敌人是否可被攻击,如果可被攻击,玩家执行attack Event,敌人执行beHit Event。
玩家会播放攻击帧动画,敌人会播放被击中帧动画。

这就需要把状态机的状态完善一下,在状态中加入"beingHit"状态, 并添加相应的用于转换的Event。在FSM::init()中

bool FSM::init()
{
    this->addState("walking",[](){cocos2d::log("Enter walking");})
        ->addState("attacking",[](){cocos2d::log("Enter attacking");})
        ->addState("dead",[](){cocos2d::log("Enter dead");})
        ->addState("beingHit",[](){cocos2d::log("Enter beingHit");});
 
    this->addEvent("walk","idle","walking")
        ->addEvent("walk","attacking","walking")
        ->addEvent("attack","idle","attacking")
        ->addEvent("attack","walking", "attacking")
        ->addEvent("die","idle","dead")
        ->addEvent("die","walking","dead")
        ->addEvent("die","attacking","dead")
        ->addEvent("stop","walking","idle")
        ->addEvent("stop","attacking","idle")
        ->addEvent("walk","walking","walking")
        ->addEvent("beHit","idle","beingHit")
        ->addEvent("beHit","walking","beingHit")
//        ->addEvent("beHit","attacking","beingHit") can attacking be stoped by beHit?
        ->addEvent("die","beingHit","dead")
        ->addEvent("stop","beingHit","idle")
        ->addEvent("stop","idle","idle");
 
    return true;
}

状态机如何设置也是个问题。 例如是否应该允许从attacking转移到walking? 我们允许从walking转移到attacking,这样就有可能在attacking时瞬间转移到walking,然后又可以进入attacking了。这似乎是个bug,除非你想让玩家拼手速。

另外每个态的回调函数也要写好,在Player::initFSM()中

void Player::initFSM()
{
    _fsm = FSM::create("idle");
    _fsm->retain();
    auto onIdle =[&]()
    {
        log("onIdle: Enter idle");
        this->stopActionByTag(WALKING);
        auto sfName = String::createWithFormat("%s-1-1.png", _name.c_str());
        auto spriteFrame = SpriteFrameCache::getInstance()->getSpriteFrameByName(sfName->getCString());
        this->setSpriteFrame(spriteFrame);
    };
    _fsm->setOnEnter("idle",onIdle);
 
    auto onAttacking =[&]()
    {
        log("onAttacking: Enter Attacking");
        auto animate = getAnimateByType(ATTACKING);
        auto func = [&]()
        {
            this->_fsm->doEvent("stop");
        };
        auto callback = CallFunc::create(func);
        auto seq = Sequence::create(animate, callback, nullptr);
        this->runAction(seq);
    };
    _fsm->setOnEnter("attacking",onAttacking);
     
    auto onBeingHit = [&]()
    {
        log("onBeingHit: Enter BeingHit");
        auto animate = getAnimateByType(BEINGHIT);
        auto func = [&]()
        {
            this->_fsm->doEvent("stop");
        };
        auto wait = DelayTime::create(0.6f);
        auto callback = CallFunc::create(func);
        auto seq = Sequence::create(wait,animate, callback, nullptr);
        this->runAction(seq);
    };
    _fsm->setOnEnter("beingHit",onBeingHit);
 
    auto onDead = [&]()
    {
        log("onDead: Enter Dead");
        auto animate = getAnimateByType(DEAD);
        auto func = [&]()
        {
            log("A charactor died!");
            NotificationCenter::getInstance()->postNotification("ENEMY_DEAD",nullptr);
            this->removeFromParentAndCleanup(true);
        };
        auto blink = Blink::create(3,5);
        auto callback = CallFunc::create(func);
        auto seq = Sequence::create(animate, blink, callback, nullptr);
        this->runAction(seq);
        _progress->setVisible(false);
    };
    _fsm->setOnEnter("dead",onDead);
}

onIdle函数除了停止行走帧动画外,还需将精灵帧设置到初始帧。
beingHit里可以延迟一点时间,等到刀砍下来在播放被击中帧动画。
死亡之后尸体可以闪烁几下在消失不迟。

然后我们让敌人响应触摸事件并发送消息,Player中:

    _listener = EventListenerTouchOneByOne::create();
    _listener->setSwallowTouches(true);
    _listener->onTouchBegan = CC_CALLBACK_2(Player::onTouch,this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(_listener,this);

响应函数为:

bool Player::onTouch(Touch* touch, Event* event)
{
    if(_type == PLAYER)
        return false;
 
    log("Player: touch detected!");
    auto pos = this->convertToNodeSpace(touch->getLocation());
    auto size = this->getContentSize();
    auto rect = Rect(size.width/2, 0, size.width, size.height);
    if(rect.containsPoint(pos))
    {
        NotificationCenter::getInstance()->postNotification("clickEnemy",this);
        log("enemy touched!");
        return true;
    }
    log("enemy not touched!");
    return false;
}

其中判断了触摸是否触碰到敌人,这里检测的比较随意,可能触摸敌人附近也会被认为触摸到了敌人。 此处应该进一步改良。

在MainScene中,接收上面发出的消息:

NotificationCenter::getInstance()->addObserver(this, callfuncO_selector(MainScene::clickEnemy),"clickEnemy",nullptr);

接受消息的回调函数:

void MainScene::clickEnemy(Ref* obj)
{
    log("click enemy message received!");
    auto enemy = (Player*)obj;
    if(enemy == nullptr)
    {
        log("enemy null");
        return;
    }
    if(enemy->isCanAttack())
    {
        _player->attack();
        enemy->beHit(_player->getAttack());
    }    
    else
    {
        _player->walkTo(enemy->getPosition());
    }
}

当时忽视了一个问题,如果玩家正在攻击,那么再次点击,敌人还是会瞬间扣血。所以应该加个判断,只有玩家不处于攻击状态时才允许攻击。

给Player增加生命值,最大生命值,攻击力属性,并可以用函数获取/设定。
然后上面的函数:

void Player::attack()
{
    _fsm->doEvent("attack");
}
 
void Player::beHit(int attack)
{
    _health -= attack;
    if(_health <= 0)     {         _health = 0;         this->_progress->setProgress((float)_health/_maxHealth*100);
        _fsm->doEvent("die");
        return;
    }
    else
    {
        this->_progress->setProgress((float)_health/_maxHealth*100);
        _fsm->doEvent("beHit");
    }
}

可以看出把状态机设置好,然后在适当时候触发事件即可。

这一次主要是关于物理引擎碰撞检测,但同时也涉及了触摸事件捕捉,事件的发放与接收,状态机的使用,等等。
现在终于可以把怪杀死了。


有个明显问题,刀没碰到怪就把怪打死了。这说明碰撞检测的盒子设置还需要进一步的精细化。

然后我提交了一个新版本,名为"Note 6 Physics contact, State Machine, etc."

?>