Cocos2d-x

怎样制作基于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."

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

作者: douxt

这次我们要为游戏增加一些UI元素。

添加血条

原版教程中用了ProgressTimer来实现血条,这个基本可以照搬过来。
ProgressTimer是什么呢?cpp-tests里面的 ActionsProgressTest 里面有若干示例,例如:


以及



我们所需要的比较接近第二种情况,即一个横向的血条,从左到右血量从0%变化到100%。

用一个空血槽图片的Sprite做背景,上面放一个ProgressTimer, 通过设置ProgressTimer的进度来控制血条的长短。
建立一个Progress类来实现:
头文件:

#ifndef __Progress__
#define __Progress__
#include "cocos2d.h"
USING_NS_CC;
 
class Progress : public Sprite
{
public:
    bool init(const char* background, const char* fillname);
    /*
    the inputs are SpriteFrame Names.
    they should be loaded into SpriteFrameCache before calling this.
    */
    static Progress* create(const char* background, const char* fill);
 
    void setFill(ProgressTimer* fill){_fill=fill;}
 
    void setProgress(float percentage){_fill->setPercentage(percentage);}
 
private:
    ProgressTimer* _fill;
};
#endif

其实现为:

#include "Progress.h"
 
bool Progress::init(const char* background, const char* fillname)
{
    this->initWithSpriteFrameName(background);
    ProgressTimer* fill = ProgressTimer::create(Sprite::createWithSpriteFrameName(fillname));
    this->setFill(fill);
    this->addChild(fill);
 
    fill->setType(ProgressTimer::Type::BAR);
    fill->setMidpoint(Point(0,0.5));
    fill->setBarChangeRate(Point(1.0, 0));
    fill->setPosition(this->getContentSize()/2);
    fill->setPercentage(100);
    return true;
}
 
Progress* Progress::create(const char* background, const char* fillname)
{
    Progress* progress = new Progress();
    if(progress && progress->init(background,fillname))
    {
        progress->autorelease();
        return progress;
    }
    else
    {
        delete progress;
        progress = NULL;
        return NULL;
    }
}

init函数需要传入两个SpriteFrameName,所以UI资源需要提前载入:

SpriteFrameCache::getInstance()->addSpriteFramesWithFile("image/ui.plist","image/ui.pvr.ccz");

代码中设置了ProgressTimer的参数,值得注意的有:
setType:设置类型为ProgressTimer::Type::BAR,血条自然是条状类的。
setMidpoint:设置血条的起点为(0,0.5),即左侧的中间点。
setBarChangeRate:设置变化率为(1,0),即在x轴变化率为1,y轴不变化。
setPercentage:设置血条填充率为100,即满血状态。

然后在MainScene::init中加入如下代码:

_progress = Progress::create("player-progress-bg.png","player-progress-fill.png");
    _progress->setPosition(VisibleRect::left().x + _progress->getContentSize().width/2, VisibleRect::top().y - _progress->getContentSize().height/2);
    this->addChild(_progress);

每次需要设置坐标的时候,要通过Director获取窗口尺寸以及原点坐标,有些繁琐,可以利用cpp-tests中的VisibleRect类来简化输入,将源文件和头文件拷贝至Classes下,加入工程,包含头文件即可使用。

运行程序可以看到玩家的血条了。

然后我们还要给敌人增加血条。可以在Player类中添加,但是这样岂不是把玩家头上也加上血条了?
没关系,可以让玩家头上的血条默认隐藏。

在Player中增加私有变量_progress,并在init中进行初始化,增加bool型变量_isShowBar 用来通过角色类型判断是否显示。
Player::init中添加:

    auto size = this->getContentSize();
    _progress = Progress::create("small-enemy-progress-bg.png","small-enemy-progress-fill.png");
    _progress->setPosition( size.width*2/3, size.height + _progress->getContentSize().height/2);
    this->addChild(_progress);
    if(!_isShowBar)
    {
        _progress->setVisible(false);
    }

运行程序便可以看到玩家和敌人的血条都显示了。

增加暂停界面

在右上角增加一个暂停按钮,触摸按钮会暂停游戏,并弹出暂停菜单。从暂停菜单,可以返回开始界面(目前还没有),以及继续游戏。

先从简单的做起,先给界面增加一个暂停的按钮好了。
因为UI资源已经载入SpriteFrameCache,所以我们可以从这里面直接提取SpriteFrame来建立MenuItemImage 对象。但是MenuItemImage好像没有直接用spriteFrame的创建函数,不妨自己写个函数,简化操作。具体实现请下载代码参考。在MainScene::init中加入:

    auto pauseItem = CustomTool::createMenuItemImage("pause1.png", "pause2.png", CC_CALLBACK_1(MainScene::onTouchPause,this));
    pauseItem->setPosition(VisibleRect::right().x - pauseItem->getContentSize().width/2, 
                            VisibleRect::top().y - pauseItem->getContentSize().height/2);

下面定义并实现一下函数MainScene::onTouchPause:

void MainScene::onTouchPause(Ref* sender)
{
    _player->pause();
    _enemy1->pause();
    _enemy2->pause();
    auto layer = PauseLayer::create();
    this->addChild(layer,100);
}

原版教程里一个display.pause()就实现了暂停。
我现在暂时用手动暂停所有角色的方法来模拟。
有暂停就有恢复:

void MainScene::onTouchResume()
{
    _player->resume();
    _enemy1->resume();
    _enemy2->resume();
}

注意:当时第一次写,用的这个暂停方法不合适,尤其是当角色死亡之后对象被回收,这时再这样暂停会引发错误。更好的方法是用Director自带的pause和resume方法。
以后的版本里改正了这个问题。

上面的onTouchPause中创建了一个新的PauseLayer,并通过设置zOrder保证其覆盖在前面。
其实头文件如下:

#ifndef __PauseLayer__
#define __PauseLayer__
#include "cocos2d.h"
 
USING_NS_CC;
 
class PauseLayer : public LayerColor
{
public:
    bool init();
    CREATE_FUNC(PauseLayer);
 
    void addUI();
    void addTouch();
    void home(Ref* obj);
    void back(Ref* obj);
private:
    EventListenerTouchOneByOne* _listener;
};
 
#endif

实现为:

#include "PauseLayer.h"
#include "VisibleRect.h"
#include "CustomTool.h"
//#include "StartScene.h"
#include "MainScene.h"
 
bool PauseLayer::init()
{
    if(!LayerColor::init())
        return false;
 
    this->initWithColor(Color4B(162, 162, 162, 128));
 
    addUI();
    addTouch();
    return true;
}
 
void PauseLayer::addUI()
{
    auto background = Sprite::createWithSpriteFrameName("pause-bg.png");
    background->setPosition(VisibleRect::center());
    this->addChild(background);
 
    auto homeItem = CustomTool::createMenuItemImage("home-1.png","home-2.png",
                                                    CC_CALLBACK_1(PauseLayer::home,this));
 
    auto resumItem = CustomTool::createMenuItemImage("continue-1.png","continue-2.png",
                                                    CC_CALLBACK_1(PauseLayer::back,this));
 
    auto bgSize = background->getContentSize();
    homeItem->setPosition(bgSize.width/3, bgSize.height/2);
    resumItem->setPosition(bgSize.width*2/3,bgSize.height/2);
    auto menu = Menu::create(homeItem, resumItem, NULL);
    menu->setPosition(VisibleRect::leftBottom());
 
    background->addChild(menu);
 
}
 
void PauseLayer::addTouch()
{
    _listener = EventListenerTouchOneByOne::create();
    _listener->onTouchBegan =[&](Touch* touch, Event* event)
    {
        log("PauseLayer::addTouch");
        return true;
    };
    _listener->setSwallowTouches(true);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(_listener, this);
}
 
void PauseLayer::home(Ref* obj)
{
    _eventDispatcher->removeEventListener(_listener);
    this->removeFromParentAndCleanup(true);
    //auto start = StartLayer::createScene();
    //Director::getInstance()->replaceScene(start);
}
 
void PauseLayer::back(Ref* obj)
{
    _eventDispatcher->removeEventListener(_listener);
    auto main = (MainScene*)this->getParent();
    this->removeFromParentAndCleanup(true);
    main->onTouchResume();
}

PauseLayer继承了LayerColor类,通过initWithColor(Color4B(162, 162, 162, 128))实现了一种透明的灰色背景,并在此之上添加了一个精灵模拟弹出窗口,并在窗口上添加了两个按钮,分别是Home按钮,目前功能还没实现,还有一个Resume按钮,按了之后会继续游戏。
另外PauseLayer还将触屏事件进行了拦截,使其不被下层的对象接收到。

运行一下,效果为:



提交一下代码,名为"Note 5 add UI"

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

作者: douxt

前几天我把笔记写的差不多了,突然觉得可以发到论坛和大家分享一下,感觉学习cocos2d的新人很多,能相互学习一下还是挺好的。

在发第一个帖子的时候出了点问题,有些文字莫名其妙的成了斜体……我查了一下,原来是代码中有数组用了下标i,这个[ i],被论坛识别为斜体标记了....晕

刚发现上次写的代码存在问题,即在角色正在行走中,如果再次点击,PlayAnimationForever会再次运行,出现了多个动画同时播放的情况。如果想要播放的和正在进行的动画相同,不应该停止然后在重新开始,因为这样会导致不连贯。所以我加了检查,通过getActionByTag检查是否正在进行所需的action,如果正在进行,则不做任何操作。好像原版也有这个问题。

不同的帧动画不应该同时播放,所以在播放一个帧动画之前,可以通过stopActionByTag把所有帧动画都停止掉。

另外,我移除了私有变量_seq,增加了一个枚举用于标记动作Tag。在walkTo中,用检查Tag方法来防止动作的重复进行。之前用Tag停止动作失败的原因,很有可能是不同的动作用了相同的Tag。

接下来我提交一下代码,名为"fix animation superposition" 。哈哈,好像提交代码上瘾了啊。

游戏中的状态机设计

Quick-Cocos2d-x内置了对状态机的支持,所以这里的状态机就要自己想办法了,初步的想法是设计一个状态机对象,然后让Player类持有一个状态机对象。当然也可以让Player继承状态机对象……不过我们先考虑用组合的方法把。

状态机的必备构件:
1.状态(State)
这里的状态有  idle,walking,attacking, dead 等。
先假设他们是互斥的。虽然一边walking一边attacking也是可能的。
2.事件(Event)
可以理解为指令,即要求满足一定条件的状态机改变状态到指定态。
例如
{name="walk", from="idle", to="walking"}
如果令状态机执行这个事件,则当其处于idle状态时,会变化至walking态。
所以状态机对象需要保存所有状态,以及所有的事件,以供使用。
3.动作(Action)
例如在进入dead状态后,角色需要播放dead动画,并移除自身。
每个状态都要提供一个函数如onIdleEnter,在进入这个态时调用,当然也可为空。
按理说退出一个状态也应该调用一个函数,如onIdleExit,不过我们暂时可以不用这个。

状态和事件是否需要单独设计class?如果是class是否要继承Ref?纠结了半天,也写了下Event类和State类,感觉直接用字符串表示状态也是可行的。所以果断删了,直接用字符串。

set<string> _states;用这个保存所有的状态,这里不应该有两个状态名字相同。
map<string, map<string, string>> _events; 用于保存所有的事件,形式为<eventName, <from, to>>
map<string, function<void()>> _onEnters;  保存每个态的回调函数,如果不为空就在进入状态时调用这个函数。
这个函数做什么用呢?当然是状态转换后的行为控制了。例如_onEnters["idle"]可以负责停止所有帧动画的播放。
_onEnters["dead"]让角色播放死亡动画,然后处理后事等等。
然后还需要保存当前状态,前一个状态。

折腾了半天,看了网上的资料,发现状态机也可以挺复杂,也参考了别人的简易状态机,还有状态机的数学语言定义等等……又发现了C++里的map容器可以用unordered_map,他的性能测试,set容器用法,map插入内容的方法……总算弄出一个能用的。

头文件如下:

#ifndef __FSM__
#define __FSM__
 
#include "cocos2d.h"
 
class FSM :public cocos2d::Ref
{
public:
 
    bool init();
    //Create FSM with a initial state name and optional callback function
    static FSM* create(std::string state, std::function<void()> onEnter = nullptr);
     
    FSM(std::string state, std::function<void()> onEnter = nullptr);
    //add state into FSM
    FSM* addState(std::string state, std::function<void()> onEnter = nullptr);
    //add Event into FSM
    FSM* addEvent(std::string eventName, std::string from, std::string to);
    //check if state is already in FSM
    bool isContainState(std::string stateName);
    //print a list of states
    void printState();
    //do the event
    void doEvent(std::string eventName);
    //check if the event can change state
    bool canDoEvent(std::string eventName);
    //set the onEnter callback for a specified state
    void setOnEnter(std::string state, std::function<void()> onEnter);
private:
    //change state and run callback.
    void changeToState(std::string state);
private:
    std::set _states;
    std::unordered_map<std::string,std::unordered_map<std::string,std::string>> _events;
    std::unordered_map<std::string,std::function<void()>> _onEnters;
    std::string _currentState;
    std::string _previousState;
};
 
#endif

现在不妨做个测试,可以先写到init里。

bool FSM::init()
{
    this->addState("walking",[](){cocos2d::log("Enter walking");})
        ->addState("attacking",[](){cocos2d::log("Enter attacking");})
        ->addState("dead",[](){cocos2d::log("Enter dead");});
 
    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");
 
    this->doEvent("walk");
    this->doEvent("attack");
    this->doEvent("eat");
    this->doEvent("stop");
    this->doEvent("die");
    this->doEvent("walk");
    return true;
}

在MainScene::init中加入:

auto fsm = FSM::create("idle",[](){cocos2d::log("Enter idle");});

运行输出如下:


复制代码
  1. FSM::doEvent: doing event walk
  2. FSM::changeToState: idle -> walking
  3. Enter walking
  4. FSM::doEvent: doing event attack
  5. FSM::changeToState: walking -> attacking
  6. Enter attacking
  7. FSM::doEvent: cannot do event eat
  8. FSM::doEvent: doing event stop
  9. FSM::changeToState: attacking -> idle
  10. Enter idle
  11. FSM::doEvent: doing event die
  12. FSM::changeToState: idle -> dead
  13. Enter dead
  14. FSM::doEvent: cannot do event walk

第一个walk Event成功,idle -> walking
第二个attack Event成功,walking -> attacking
第三个eat Event失败,因为我们没有定义eat Event
第四个stop Event成功,attacking -> idle
第五个die Event 成功,idle -> dead
第六个walk Event失败,这也是我们期望的,因为死了之后不应该还能行走。

下面应该考虑在player中使用FSM, 可以新建一个私有成员持有一个实例。
在尝试过程中出了点故障,好久才搞定,原来是FSM create之后我没有retain,访问出问题了。
既然要retain,那就别忘了release。

我们先把以前的walkTo改变一下,让他用状态机来实现。

void Player::walkTo(Vec2 dest)
{
    std::function<void()> onWalk = CC_CALLBACK_0(Player::onWalk, this, dest);
    _fsm->setOnEnter("walking", onWalk);
    _fsm->doEvent("walk");
}

即现在是委托"walking"状态的回调函数来进行动作,回调函数是由另一个函数Player::onWalk bind得到的。
这个函数如下:

void Player::onWalk(Vec2 dest)
{
    log("onIdle: Enter walk");
    this->stopActionByTag(WALKTO_TAG);
    auto curPos = this->getPosition();
 
    if(curPos.x > dest.x)
        this->setFlippedX(true);
    else
        this->setFlippedX(false);
 
    auto diff = dest - curPos;
    auto time = diff.getLength()/_speed;
    auto move = MoveTo::create(time, dest);
    auto func = [&]()
    {
        this->_fsm->doEvent("stop");
    };
    auto callback = CallFunc::create(func);
    auto seq = Sequence::create(move, callback, nullptr);
    seq->setTag(WALKTO_TAG);
    this->runAction(seq);
    this->playAnimationForever(0);
}

这个函数和原来的walkTo基本一样除了:

auto func = [&]()
    {
        this->_fsm->doEvent("stop");
    };

这里的回调函数会使用状态机,将角色回到idle状态,而idle的回调函数会停止播放动画。
另外在上面的代码中有一句:
->addEvent("walk","walking","walking");
这个的作用是允许在从walking状态转换到walking状态,当点击屏幕时,walk的目的发生变化,即使在walking中也应该即刻改变目标。

现在的情况好像和之前一样,不一样的是现在用的是状态机。

然后 我做了一个名为Note 4 的commit.

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

作者: douxt



原版的第二篇包含了动画和触摸响应。动画部分我费了好大工夫才搞定,所以就先把那部分当作第二篇了。另外我发现原来的教程基本上不会大段贴代码,只是把道理讲清楚,代码可以自己下载了慢慢看。我觉得这样很好,但有时候用代码说话比描述更清晰。

为了方便进行,代码做了些小改动,如将HelloWorld改名为Main,player,enemy1改为私有变量_player,_enemy1,将USING_NS_CC从HelloWorld的cpp文件移动到h文件等等,具体细节可以在:
https://github.com/douxt/Brave_cpp 查看


点击 commits 可以查看所有历史版本以及改动


点击<>便可以查看并下载这个版本的代码。

触摸响应

下一步给MainScene这个Layer添加触摸响应。
先从简单的开始,即触摸屏幕后,player角色会边播放行走动画,并移动到点击的屏幕位置。

给MainScene增加一个EventListenerTouchOneByOne* 类型的_listener_touch
给MainScene.h增加触摸响应函数:

bool onTouchBegan(Touch* touch, Event* event);
private:
EventListenerTouchOneByOne* _listener_touch;

并在 MainScene::init 里增加:

    _listener_touch = EventListenerTouchOneByOne::create();
    _listener_touch->onTouchBegan = CC_CALLBACK_2(MainScene::onTouchBegan,this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(_listener_touch, this);

触摸响应函数实现为:

bool MainScene::onTouchBegan(Touch* touch, Event* event)
{
    Vec2 pos = this->convertToNodeSpace(touch->getLocation());
    _player->walkTo(pos);
//    log("MainScene::onTouchBegan");
    return true;
}

这里调用了Player类的walkTo函数,还没定义,所以在Player头文件和源文件中定义一下。

void Player::walkTo(Vec2 dest)
{
    //stop current moving action, if any.
    if(_seq)
        this->stopAction(_seq);
     
    auto curPos = this->getPosition();
 
    //flip when moving backward
    if(curPos.x > dest.x)
        this->setFlippedX(true);
    else
        this->setFlippedX(false);
 
    //calculate the time needed to move
    auto diff = dest - curPos;
    auto time = diff.getLength()/_speed;
    auto move = MoveTo::create(time, dest);
    //lambda function
    auto func = [&]()
    {
        this->stopAllActions();
        _seq = nullptr;
    };
    auto callback = CallFunc::create(func);
    _seq = Sequence::create(move, callback, nullptr);
 
    this->runAction(_seq);
    this->playAnimationForever(0);
}

我曾经想用stopActionTag函数来停止指定的动作,不知为何竟然没成功。这里我索性将动作保存到私有变量_seq中,初始化为nullptr, 并在移动结束的回调中将其赋值为nullptr。
这样如果_seq不为空,则运动正在进行,需要把这个动作停下,否则可能出现多个MoveTo叠加的情况,这不是想要的结果。

然后通过现在位置和目标位置横坐标的比较,决定是否在X方向反转角色。接着根据动作计算所需时间。然后创建MoveTo动作,并定义了一个回调函数func,这是个lambda函数,具体资料可以在网上查到。

然后建立回调动作,将移动和回调组合为序列,然后运行这个序列,并开始播放这个角色的第一个动画walk.

现在运行一下,发现角色会向着点击的方向移动,移动到目的地后,动画会停止播放。

现在的版本为:

https://github.com/douxt/Brave_cpp/tree/51b653b01b5427d985e10483499487d450af31c2

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

作者: douxt



动画

这次我们要让人物动起来。
首先要将精灵帧缓存中的帧组合成动画,然后将这些动画放到动画缓存中。
需要播放的时候直接从缓存中拿出来用即可。


目前这个项目里用到的帧的格式为:  player1-1-1.png, 即:角色-动作编号-动画帧编号.png
对于player1,有walk,attack,dead,hit,skill动作,每个动作的帧数可以不同,这里分别为,4,4,4,2,4
对于enemy1,有walk,attack,dead,hit,帧数分别为3,3,3,2
相关信息可以在Resources下的image/role.plist里看到。

对于不同的角色类型,加载动画时略参数略有不同,现在我们给Player类增加一些参数,记录这些不同之处,并在初始化函数中,根据角色类型的不同来给这些参数赋值。
首先建立一个字符串,记录动画的前缀,如PLAYER类型的角色_name值为"player1"。
还需要一个整数记录动作的数目,一个字符串数组记录动作的名称,以及一个整数数组记录每个动作的帧数。

在类Player的末端加入:

private:
    PlayerType _type;  
    std::string _name;
    int _animationNum; 
    std::vector _animationFrameNum;
    std::vector _animationNames;

然后修改InitWithPlayerType函数,使之如下:

bool Player::initWithPlayerType(PlayerType type)
{
    std::string sfName = "";
    _type = type;
    int animationFrameNum[5] ={4, 4, 4, 2, 4};
    int animationFrameNum2[5] ={3, 3, 3, 2, 0};
 
    //setup according to PlayerType
    switch(type)
    {
    case PlayerType::PLAYER:
        sfName = "player1-1-1.png";
        _name = "player1";
        _animationNum = 5;
        _animationFrameNum.assign(animationFrameNum, animationFrameNum + 5);
        break;
    case PlayerType::ENEMY1:
        sfName = "enemy1-1-1.png";
        _name = "enemy1";
        _animationNum = 4;
        _animationFrameNum.assign(animationFrameNum2, animationFrameNum2 + 5);
        break;
    case PlayerType::ENEMY2:
        sfName = "enemy2-1-1.png";
        _name = "enemy2";
        _animationNum = 4;
        _animationFrameNum.assign(animationFrameNum2, animationFrameNum2 + 5);
        break;
    }
    this->initWithSpriteFrameName(sfName);
    std::string animationNames[] = {"walk", "attack", "dead", "hit", "skill"};
    _animationNames.assign(animationNames, animationNames + 5);
    //load animation
    this->addAnimation();
    return true;
}

现在给Player类增加载入动画函数Player::addAnimation(),相同动画的载入只需要进行一次,所以载入前先确定所需动画还没有被载入。

void Player::addAnimation()
{
    // check if already loaded
    auto animation = AnimationCache::getInstance()->getAnimation(String::createWithFormat("%s-%s",_name.c_str(),
                        _animationNames[0])->getCString());
    if(animation)
        return;
     
    for(int i1=0; i1<_animationNum; i1++)     {         auto animation = Animation::create();         animation->setDelayPerUnit(0.2f);
        //put frames into animation
        for(int j = 0; j< _animationFrameNum[i1] ; j++)         {             auto sfName =String::createWithFormat("%s-%d-%d.png",_name.c_str(), i1+1, j+1)->getCString();
            animation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName(sfName));
        }
        // put the animation into cache
        AnimationCache::getInstance()->addAnimation(animation, String::createWithFormat("%s-%s",_name.c_str(), 
                    _animationNames[i1].c_str())->getCString());
    }
}

现在加入一个根据动作编号进行指定动作的函数,查看动画是否载入成功。

void Player::playAnimationForever(int index)
{
    if(index = _animationNum)
    {
        log("illegal animation index!");
        return;
    }
    auto str = String::createWithFormat("%s-%s",_name.c_str(), _animationNames[index].c_str())->getCString();
    auto animation = AnimationCache::getInstance()->getAnimation(str);
    auto animate = Animate::create(animation);
    this->runAction(RepeatForever::create(animate));
}

在HelloWorld::init()的末端加入如下语句,然后运行。

//add player
    Player* player = Player::create(Player::PlayerType::PLAYER);
    player->setPosition(origin.x + player->getContentSize().width/2, origin.y + visibleSize.height/2);
    this->addChild(player);
 
 
    //add enemy1
    Player* enemy1 = Player::create(Player::PlayerType::ENEMY1);
    enemy1->setPosition(origin.x + visibleSize.width - player->getContentSize().width/2, origin.y + visibleSize.height/2);
    this->addChild(enemy1);
 
    //test animation
    player->playAnimationForever(1);
    enemy1->playAnimationForever(1);

结果如下:



用GitHub来管理源代码是个不错的选择。
项目的代码经常会进行删减,随着笔记的进行,最后的代码可能和前期相比面目全非了,但是使用GitHub可以让你访问任意历史版本。
我刚刚进行了一个名为“Note2”的commit,如果有人想下载代码,可以点下面链接,点下载Zip即可。
https://github.com/douxt/Brave_cpp/tree/8752cc055601800eb10ed73d9ea757993dfa352a
即使以后更新了,这个历史版本还是可以访问。

注意:代码中没有包含cocos2d目录,可以将其他项目的cocos2d目录拷贝到项目目录下。

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

作者: douxt

最近看了一下《使用Quick-Cocos2d-x搭建一个横版过关游戏》这个教程。当然这个游戏的名字叫做Brave

我不太懂lua,但感觉代码意思基本能看出来,就想用C++重新写一下这个示例,一方面了解一下lua,另一方面还可以熟悉一下用C++的开发。本人小白,免不了会犯些愚蠢的错误,不过犯了错只要能发现就是进步。

环境:
Cocos2dx 3.2
VS2012
Win32平台

资源及源码:
https://github.com/douxt/Brave_cpp   (不包含cocos2d文件夹,大概4.3MB)
点击 Download Zip 即可下载。
强烈建议自己新建项目,可以避免一些版本相关的错误。

目录

Cocos2dx 3.2  横版过关游戏Brave学习笔记(一)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(二)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(三)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(四)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(五)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(六)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(七)
Cocos2dx 3.2  横版过关游戏Brave学习笔记(八)

1.创建工程

打开命令行,输入:

cocos new -l cpp -d e:/projects/   Brave_cpp

这个命令会帮你创建一个新的HelloWorld工程,而且把整个cocos2d的源码给你拷了一份放在工程下面。

运行一下这个工程,什么,竟然报错了。果断关闭所有VS窗口,重新打开运行,发现竟然正常运行了。
将资源文件复制到Resource文件夹下面备用。
删除掉init中按钮,精灵,标签等代码,让程序变成只显示一个黑屏。

给游戏添加角色

添加背景:

加入代码,加完后init函数内容如下:

bool HelloWorld::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
    
    Size visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();
   
    Sprite* background = Sprite::create("image/background.png");
    background->setPosition(origin + visibleSize/2);
    this->addChild(background);

    return true;
}

这样应该就可以显示出背景了。咦,Vec2类型竟然可以直接与Size类型相加?Size可以支持除法?这是怎么一回事呢?看一下Size源码,Size类定义了除法运算,Vec2难道定义了对Size的加法?不对,Vec2里没有关于Size的加法运算符。那就是Size可以自动转化成Vec2了?看到Size中有个如下的定义:

public:
    operator Vec2() const
    {
        return Vec2(width, height);
    }

精确的含义我没查到,不过应该可以理解为:如果有人把Size当作Vec2,就按这个方法给他一个Vec2.

冥冥之中自有天意,本来应该是一时的手误竟然背后还隐藏着道理……另外,Size还重载了=操作符,可以用Vec2给Size赋值。

导入精灵帧资源
继续在init函数中加入:

 SpriteFrameCache::getInstance()->addSpriteFramesWithFile("image/role.plist","image/role.pvr.ccz");

这个资源里包含了一些玩家、敌人动作的精灵帧,需要提前导入缓存。

显示玩家和怪物

玩家应该是一个类,可以继承自Sprite。原文中为Player建立了一个类,为Enemy1也建立了一个类,然后在后面出现Enemy2的时候又建立了一个类。
我觉得这几个类可以用一个类代替,因为他们属性十分类似,目前只是贴图不同,仅仅因为这个就建立这么多类不太划算。
添加的Player.h如下:

#ifndef __Player__
#define __Player__
#include "cocos2d.h"
USING_NS_CC;

class Player : public Sprite
{
public:
 enum PlayerType
 {
  PLAYER,
  ENEMY1,
  ENEMY2
 };

 bool initWithPlayerType(PlayerType type);

 static Player* create(PlayerType type);


};

#endif

目前先考虑三类角色,player , enemy1, enemy2。创建时,用枚举类型指定是哪一种。

Player.cpp 如下:

#include "Player.h"

bool Player::initWithPlayerType(PlayerType type)
{
 std::string spName = "";
 switch(type)
 {
 case PlayerType::PLAYER:
  spName = "player1-1-1.png";
  break;
 case PlayerType::ENEMY1:
  spName = "enemy1-1-1.png";
  break;
 case PlayerType::ENEMY2:
  spName = "enemy2-1-1.png";
  break;
 }
 this->initWithSpriteFrameName(spName);
 return true;
}


Player* Player::create(PlayerType type)
{
 Player* player = new Player();
 if(player && player->initWithPlayerType(type))
 {
  player->autorelease();
  return player;
 }
 else
 {
  delete player;
  player = NULL;
  return NULL;
 }
}

可以看出,其核心就是根据不同的角色类型,用不同的SpriteFrameName来初始化精灵。

继续在HelloWorld的init类中加入:

 Player* player = Player::create(Player::PlayerType::PLAYER);
 player->setPosition(origin.x + player->getContentSize().width/2, origin.y + visibleSize.height/2);
 this->addChild(player);

 Player* enemy1 = Player::create(Player::PlayerType::ENEMY1);
 enemy1->setPosition(origin.x + visibleSize.width - player->getContentSize().width/2, origin.y + visibleSize.height/2);
 this->addChild(enemy1);

运行程序,结果如下:



刚开始用git,发现项目目录下好多文件是不需要提交的,比如lib文件什么的。虽说可以靠只add需要的文件来实现,但系统总是有提示。
在项目目录下建立一个.gitignore文件,可以使其忽略没必要上传的文件。
例如:


  1. *.[oa]
  2. *.lib
  3. *.sdf
  4. *.opensdf
  5. cocos2d/
  6. .git*
  7. proj.win32/Debug.win32/
  8. proj.android/
  9. proj.ios_mac/
  10. proj.linux/
  11. proj.wp8-xaml/

这样提交,上传就感觉顺多了。

?>