如何使用Cocos2d-x来制作一个塔防游戏

2014.10.25 技术干货 by cocos

前言

教程截图:

image

在引子教程中,我们已经花时间讨论了最终要完成一个什么样的作品。现在,让我们开始写代码吧。对任何塔防游戏来说,第一步就是创建“爬行怪(Creeps)”。这些怪物会入侵你的塔防世界,你需要把它们击退。因此,我们将在这个教程里学些什么东西呢?因为,这只是教程的第一部分,所以不可能全部涉及到,下面是我们将要实现的功能列表:

  • 怎么制作waypoint,其实就是敌人沿着固定路径的点.
  • 如何加载一个tile map,并能从中加载对象,而不是硬编码那些对象。
  • 怎么创建爬行怪(Creep)/坏家伙(Bad Guy))/敌人(Enemy)对象。
  • 怎样使敌人沿着我们预先设定好的路径行走。

如果没有上述这些功能特性,这个游戏就不能称之为一个合格的塔防游戏。首先,我将向大家展示,如何创建一个enemy,并让它沿着预先设定好的路径行走,具体方式就是沿着一系列的waypoint前进。到这个系列教程结束的时候,你将拥有制作塔防游戏的全部知识了,尽情发挥,去创造更加好玩的TD吧!

这里有本教程的完整源代码

一个“waypoint”在维基百科里面被定义为“一组坐标集合,它标识了物理空间的一个点”。我喜欢这个定义,所以我就使用它啦。我们可以把一个舞台想像成一个物理空间,而waypoint的位置就是舞台上面的x,y值。

我们会在舞台上创建一系列的waypoint,然后让敌人沿着这些waypoint移动,直到行进至终点!听起来很复杂?其实并不是很复杂。现在,为了让我们的塔防游戏起点更高一些,我们将使用Tiled地图来做舞台,你可以从http://www.mapeditor.org下载tile 地图编辑器。当然,我们也需要下载Cocos2d-x,从http://www.Cocos2d-x.org/可以下载。这两个工具都会帮助我们完成一些让人感到很自豪的事,并且可以充分利用iphone的一些特性。

好,你之前已经下载本教程源代码了,你可能看到我们写了一大堆的类。大部分都比较清楚明了,下面是它们的列表以及功能说明:

  1. AppDelegate – 创建窗口,加载CCDirector,同时加载第一个Scene。
  2. TutorialScene – 我们主要的视图对象,负责加载地图并且设置creep的位置
  3. DataModel –一个简单的数据接口,存储了游戏的主要数据,方便查找用。
  4. Creep – 游戏中的坏蛋,刚开始有两个,随着游戏的进行,会越来越多。
  5. Waypoint –可以和tile map编辑器发生联系的类,跟预定义路径有关。
  6. Wave – 控制某一个时刻Creep出现的顺序的类。

看起来好像有好多类,但是,AppDelegate是cocos2d模板自带的类,而Waypoint和Wave目前为止,它们的功能也非常简单。实际上,Waypoint只是从Node继承而来,它们只需要x,y值就行了,从tile map中读取。

如果你们不相信我的话,可以打开Waypoint类的头文件和实现文件看一看,下面是它们的定义: Waypoint.h:

    #pragma  once
    #include "cocos2d.h"

    USING_NS_CC;

    class WayPoint:public Node
    {
    public:
        virtual bool init();
        CREATE_FUNC(WayPoint);
    };

Waypoint.cpp:

    #include "WayPoint.h"

    USING_NS_CC;

    bool WayPoint::init()
    {
        if (!Node::init())
        {
            return false;
        }

        return true;
    }

对于DataModel类,让我们直接看看代码吧:

    #pragma once
    #include "cocos2d.h"
    #include "CCVector.h"
    #include "WayPoint.h"
    #include "Creep.h"
    #include "Tower.h"
    #include "projectile.h"
    #include "GameHUD .h"
    #include "TutorialSence.h"

    USING_NS_CC;

    class DataModel 
    {
    public:
        TutorialSence* _gameLayer;
        GameHUD*       _gameHUDLayer;
        Vector<WayPoint*> waypoints;
        Vector<Creep*> targets;
        Vector<Wave*> waves;
        Vector<Tower*> towers;
        Vector<Projectile*> projectiles;
        static DataModel* getModel();
    private:
        DataModel(){};
        static DataModel * m_pInstance;
    };

所以,这里大部分代码都是很直白的。DataModel是一个单例的类.我们这样做有两个原因:其一,我们这样做的目的是用来保存之后游戏的状态,其二,我们把它做成单例是因为整个游戏中,我们只想让一个DataModel对象存在。我们可以从任何类中访问DataModel,只需要包含相应的头文件,然后调用下面的方法就行了:

    DataModel *m = DataModel::getModel();

下面是单例的具体实现:

    DataModel* DataModel::getModel()
    {
        if (m_pInstance == NULL)
            m_pInstance = new DataModel();
        return m_pInstance;
    }

我们也保存了游戏里面所有的主要角色–“targets”是我们的缓慢爬行的敌人,“waypoints”是敌人要沿着走的路径点,而“waves“则存储wave类,wave类包含了已经出了多少个敌人了,出现敌人的速度是多少等等。

那么 UIPanGestureRecognizer和CCLayer对象呢?呃,CCLayer是指向game layer的一个引用,所有的游戏逻辑都在这个层里面发生。这里保存一个引用的话,你在其它类中可以非常方便地访问到主GameScene。而 UIPanGestureRecognizer类是用来实现平滑地滚动iphone屏幕用的。因为塔防游戏不能局限于480×320的范围,经常需要滑动地图。有了这个类,我们就可以定义任何大小的地图了。

现在,我们已经消除了对上面给出的这么多类的恐惧了。那么具体代码看起来怎么样呢。首先,让我们来看看”坏人“吧!我们已经知道”Wave“和”DataModel“类是干嘛用的了,这两个类对大家来说应该不会陌生了。先看看Creep的代码:

    #pragma once
    #include "cocos2d.h"
    #include "WayPoint.h"

    USING_NS_CC;

    class Creep: public Sprite 
    {
    public:
        int curHp;
        int moveDuration;
        int curWaypoint;
        int tag;
        Sprite* sprite;
        void creepLogic(float dt);
        virtual bool init();
        Creep* initWithCreep(Creep* copyFrom);
        WayPoint* getNextWaypoint();
        WayPoint* getCurrentWaypoint();
        CREATE_FUNC(Creep);
    };

    class FastRedCreep: public Creep 
    {
    public:
        static Creep* creep();
    };

    class StrongGreenCreep: public Creep
    {
    public:
        static Creep* creep();
    };

我们创建了一个creep类,里面定义了生命值,移动速度和当前处于地图上的哪个点。这里包含了我们目前为止需要了解的全部信息。我们还定义了其他两种类型的creep,因为,哪个塔防游戏没有不同类型的敌人呢?有一个快速移动的红色creep和一个行动缓慢,但是生命值很多的creep—我们还可以添加更多其它类型的creep类型,但是,这里为了简单,我们只实现这3种。

现在,因为我们已经看到头文件了,我想你肯定想知道实现文件是什么样的。但是,也是考虑简单的因素,目前我只向你展示那些对我们来说比较重要的内容。首先,让我们看看,具体creep类是怎么实现的:

    Creep* FastRedCreep::creep()
    {
        auto creep = Creep::create();
        creep->sprite = Sprite::create("Enemy1.png");
        creep->addChild(creep->sprite, 0);
        creep->curHp = 10;
        creep->moveDuration = 4;
        creep->curWaypoint = 0;
        return creep;
    }

这就是我们怎么实现creep的-我们只定义了一个类方法,可以用 “[FastRedCreep creep]”的方式来调用,调有之后会返回一个creep对象,然后我们就可以把它加到scene里面去,并让它工作了。因为,Creep是从CCSprite派生出来的,所以我们可以自动获得CCSprite的所有好处。当然,你也可以从CCNode派生,然后里面包含一个CCSprite的引用。具体是从CCSprite派生还是CCNode,这两者都各有利弊。(我本人喜欢从CCNode继承,因为符合”优先使用组合而不是继承“的面向对象原则,但是,有时候,为了使之能加到CCSpriteBatchNode里面去,而选择继承CCSprite,反正各有好处,大家自己去权衡)。

  接下来,在Creep类中,我们要用到DataModel类和WayPoint类,先看下面代码:

    WayPoint* Creep::getCurrentWaypoint()
    {
        DataModel* m = DataModel::getModel();
        WayPoint* waypoint = (WayPoint *)m->waypoints.at(this->curWaypoint);
        return waypoint;
    }

    WayPoint* Creep::getNextWaypoint()
    {
        DataModel* m = DataModel::getModel();
        int lastWaypoint = (int)m->waypoints.size();
        this->curWaypoint++;
        if (this->curWaypoint > lastWaypoint)
            this->curWaypoint = lastWaypoint - 1;

        WayPoint *waypoint = (WayPoint *)m->waypoints.at(curWaypoint);
        return waypoint;
    }

这里定义了creep的获得当前位置点的方法,还有得到下一个行进点的方法。你可以看到两处同样的 “WayPoint *waypoint = (WayPoint *) [m._waypoints objectAtIndex:self.curWaypoint];” 调用,它调用DataModel类来查找并返回一个”curWayPoint“所指示的WayPoint对象。当我们想走下一步的时候,我们就递增”curWaypoint”的值,然后看它是否超过数组的最大值。如果是,则减1.然后从DataModel类中查找出具体的WayPoint。这样子可以循环获得waypoint。这样的话,在还没有塔的情况下,我们的creep会一波接一波的循环进攻。

creep创建之后并能够前进的代码在TutorialScene类中,如下所示:

    void TutorialSence::FollowPath(Node* sender)
    {
        Creep *creep = (Creep *)sender;

        WayPoint * waypoint = creep->getNextWaypoint();

        int moveDuration = creep->moveDuration;
        auto actionMove = MoveTo::create(moveDuration,waypoint->getPosition());
        auto actionMoveDone = CallFuncN::create(this,callfuncN_selector(TutorialSence::FollowPath));
        creep->stopAllActions();
        creep->runAction(Sequence::create(actionMove,actionMoveDone,NULL));
    }

基于我们前面所讨论过的,这里面的代码应该比较容易懂。但是,这里面执行动画的方法,如果你之前没有看到一些教程的话,可能会觉得有点陌生。在AddTarget方法被调用之后,一个creep对象被创建了。上面这个函数会重复地调用自身。它不断地判断“sender”参数,这个参数在任何情况下都等于creep对象,因为是creep对象run的action。得到creep对象之后,就计算得到下一个waypoint。这时,我们让creep运行两个action,从当前点移动到下一点,并且在移动结束后又递归调自身。“MoveTo”action把精灵从一个(x,y)点变换到目标点的(x,y)处。

我们将要涉及到的大部分内容都在“TutorialScene”类中。它的头文件目前还比较干净,但是,它需要和我们的txm文件地图系统关联起来,因此,定义成下面的样子:

    #pragma  once
    #include "cocos2d.h"
    #include "Creep.h"
    #include "WayPoint.h"
    #include "Wave.h"
    #include "GameHUD .h"

    USING_NS_CC;

    class TutorialSence: public Layer
    {
    public:
        TMXTiledMap* tileMap;
        TMXLayer* background;
        int currentLevel;
        Point position;

        GameHUD* gameHUD;

        static Scene* createScene();
        void addWaypoint();
        void addWaves();
        void addTower(Point pos);
        Point tileCoordForPosition(Point position);
        bool canBuildOnTilePosition(Point pos);
        virtual bool init();
        virtual void update(float dt);
        void FollowPath(Node* sender);
        Wave* getCurrentWave();
        Wave* getNextWave();
        void gameLogic(float dt);
        void addTarget();
        Point boundLayerPos(Point newPos);
        CREATE_FUNC(TutorialSence);
    };

下面是TutorialScene的init方法:

    bool TutorialSence::init()
    {
        if (!Layer::init()) 
        {
            return false;
        }

        this->tileMap = TMXTiledMap::create("TileMap.tmx");
        this->background = tileMap->layerNamed("Background");
        this->background->setAnchorPoint(ccp(0, 0));
        this->addChild(tileMap, 0);

        this->addWaypoint();
        this->addWaves();

        this->scheduleUpdate();
        this->schedule(schedule_selector(TutorialSence::gameLogic), 1.0f);
        this->currentLevel = 0;
        this->position = ccp(-228, -122);

        return true;
    }

我们加载并保存了新创建的“CCTMXTiledMap”对象,然后在第四步的时候加到游戏层里面去了。然后,调用“addWayPoint”方法,下面会有详细说明。同时,还调用了“addWaves”方法,这里我们设定的游戏总共有2波。

然后,我们使用scheduler来做游戏主循环,更新游戏逻辑,下面有阐述。最后,我们把当前的关卡设置为0,并且把layer的位置移动到一个位置好的视角。

现在,我们需要看看真正有意思的代码了。打开下载工程resource文件夹下面的.tmx文件。记住,你可以从 mapeditor.org下载地图编辑器。(你也可以从我的博客翻译的Ray的tiled map教程里面找到下载链接,我上传的是java版的,qt版的貌似下载不到了)

image

好,开始工作—我们不能把教程搞得60页长。所以,上面这个图我特意把它缩小了,这样我们的教程看起来就很短啦:)。这里面定义了我们的creep将要行走的路径。因为,它太小了,我们可能看不出什么东西来,所以直接打开.tmx文件,看看里面有些什么对象。(就是上图中的灰色矩形,很小的,在路径的每个拐弯处)

    <objectgroup name="Objects" width="27" height="20">
    <object name="Waypoint0" x="887" y="292"/>
    <object name="Waypoint1" x="438" y="296"/>
    <object name="Waypoint2" x="429" y="22"/>
    <object name="Waypoint3" x="22" y="23"/>
    <object name="Waypoint4" x="24" y="493"/>
    <object name="Waypoint5" x="433" y="497"/>
    <object name="Waypoint6" x="437" y="337"/>
    <object name="Waypoint7" x="888" y="339"/>
    </objectgroup>

现在,这里坐标点倒底能干什么呢?在解释之前,先让我们看一看“addWaypoint”方法:

    void TutorialSence::addWaypoint()
    {
        DataModel *m = DataModel::getModel();
        auto *objects = this->tileMap->objectGroupNamed("Objects");
        WayPoint *wp = NULL;
        std::string stringWithFormat = "Waypoint";
        int wayPointCounter = 0;
        ValueMap wayPoint;
        wayPoint = objects->objectNamed(stringWithFormat + std::to_string(wayPointCounter));
        while (wayPoint.begin()!= wayPoint.end())
        {
            int x = wayPoint.at("x").asInt();
            int y = wayPoint.at("y").asInt();
            wp = WayPoint::create();
            wp->setPosition(ccp(x, y));
            m->waypoints.pushBack(wp);
            wayPointCounter++;
            wayPoint = objects->objectNamed(stringWithFormat + std::to_string(wayPointCounter));
        }
        wp = NULL;
    }

我们将遍历TMX文件中所有的对象,然后把相应的数据拿出来!每一个对象都被命名为“WayPoint#”,因为这个顺序,所以加载进行非常方便。然后,我们创建一个WayPoint类,并且设置它的位置,然后把它加到DataModel的_waypoints数组中去,方便后来查找。

  好,那你又是怎么加载creep的呢?容易吗?你看看吧:

    void TutorialSence::addTarget()
    {

        DataModel *m = DataModel::getModel();
        Wave* wave = this->getCurrentWave();
        if (wave->totalCreeps < 0) {
             return;
        }
        wave->totalCreeps--;

        Creep *target = NULL;
        int random = CCRANDOM_0_1() * 2;
        if (random == 0)
        {
            target = FastRedCreep::creep();
        }
        else
        {
            target = StrongGreenCreep::creep();
        }

        WayPoint *waypoint = target->getCurrentWaypoint();
        target->setPosition(waypoint->getPosition());
        waypoint = target->getNextWaypoint();
        this->addChild(target, 1);

        int moveDuration = target->moveDuration;
        auto actionMove = CCMoveTo::create(moveDuration, waypoint->getPosition());
        auto actionMoveDone = CallFuncN::create(this, callfuncN_selector(TutorialSence::FollowPath));
        target->runAction(CCSequence::create(actionMove, actionMoveDone, NULL));
        target->tag = 1;
        m->targets.pushBack(target);
    }

当addTarget被调用的时候,我们首先获得当前的波数,然后判断是否结束。然后,我们随机产生一个“Fast Creep”或者是一个“Strong Creep”,然后基于第一个waypoint来设置它的位置。(你应该记得,如果curWayPoint是0的话,那么就会得到tmx文件中的Waypoint0所代表的位置)。最后,我们把对象tag设置为1,然后把它添加到DataModel里去。

但是,谁来调addTarge方法呢?好吧,在下面的scheduler方法中调用:

    void TutorialSence::gameLogic(float dt)
    {
        DataModel *m = DataModel::getModel();
        Wave * wave = this->getCurrentWave();
        static double lastTimeTargetAdded = 0;

        double now = 0;
        if (lastTimeTargetAdded == 0 || now - lastTimeTargetAdded >= wave->spawnRate) 
        {
            this->addTarget();
            lastTimeTargetAdded = now;
        }
    }

    void TutorialSence::update(float dt)
    {
        DataModel *m = DataModel::getModel();
        Vector<Projectile*> projectilesToDelete;

        for each(Projectile *projectile in m->projectiles) 
        {
            Rect projectileRect = Rect(projectile->getPositionX() - (projectile->getContentSize().width / 2),
                projectile->getPositionY() - (projectile->getContentSize().height / 2),
                projectile->getContentSize().width,
                projectile->getContentSize().height);

            Vector<Creep*> targetsToDelete;

            for each(Creep *target in m->targets) 
            {
                Rect targetRect = Rect(target->getPositionX() - (target->sprite->getContentSize().width / 2),
                    target->getPositionY() - (target->sprite->getContentSize().height / 2),
                    target->sprite->getContentSize().width,
                    target->sprite->getContentSize().height);

                if (projectileRect.intersectsRect(targetRect)) 
                {
                    projectilesToDelete.pushBack(projectile);

                    Creep *creep = target;
                    creep->curHp -= 1;

                    if (creep->curHp <= 0) 
                    {
                        targetsToDelete.pushBack(creep);
                    }
                    break;
                }
            }

            for each(Creep *target in targetsToDelete)
            {
                m->targets.eraseObject(target);
                this->removeChild(target, true);
            }       
        }

        for each(Projectile *projectile in projectilesToDelete)
        {
            m->projectiles.eraseObject(projectile);
            this->removeChild(projectile,true);
        }
    }

因此,目前”gameLogic“决定什么时候添加一个新的target,考虑的因素就是”spawnRate“,也就是怪物出现的频率。我们的update方法这里只是列出来,并没有实现,因为暂时还不需要用到。

因此,我们学到了什么呢?

  • 怎么制作waypoint,其实就是敌人沿着固定路径的点.
  • 如何加载一个tile map,并能从中加载对象,而不是硬编码那些对象。
  • 怎么创建爬行怪(Creep)/坏家伙(Bad Guy))/敌人(Enemy)对象。
  • 怎样使敌人沿着我们预先设定好的路径行走。

我们还没有涉及到的有:

我们怎么处理creep的旋转,主要是在它行进过程中改变面朝方向。 当我们到达waypoints的尾部的时候,该怎么做呢?   上面这两个问题,会在下一个教程中予以解答。

教程截图:

image

欢迎来到《如何使用cocos2d制作一个塔防游戏》的第二部分–今天,我们将添加代码来放置炮塔。哈哈,放置炮塔,很hgih吧!如果你还没有读过如何使用Cocos2d-x来制作一个塔防游戏:第一部分,你可能要先看看再继续。

好吧,你从上个教程可能学到了,那个还不是完整真实的游戏–我们需要使之更真实。但是,我们并不需要一个复杂的A*算法,或者DFS/BFS(深度优先和广度优先),或者Best-first算法来查找路径。实际上,我们根本不需要基于tile的地图。但是,为了遵循KISS原则(keep it simple,stupid),我们还是采用tild map的做法。

所以,让我们来看看如何放置炮塔,以及炮塔是怎么工作的—-请原谅我的烂枪法。

这里有本教程的完整源代码

炮塔放置的方式就是,玩家从Game HUD 中点选中一个塔,然后拖动到某个可以放置的位置,然后松手就ok了。塔也有自己的攻击范围,用一个带阴影的圆表示。这个圆会在你点选中Hud中的炮塔的时候显示出来,放置好炮塔之后就消失了,表明不可以再移动了,除非把它卖掉!现在,有意思的部分来了—-如果用户把塔拖到沙漠地带,我们就在那个位置显示一个”机关炮“。如果拖到其他地方(比如creep行走的路上面),我们就让它不可以放置,如果松手,塔就会回到hud里面。

image

你可以看到,好多地方都有沙子,一个塔可以放置在一个给定的tile上面。我们实现的方式就是两个函数—”tileCoordForPosition“和”addTower“方法。 “tileCoordForPosition” 是一种快速定位给定point是哪个tile的坐标点,而“addTower”函数就是实际放置炮塔的方法。具体实现如下:

Point TutorialSence::tileCoordForPosition(Point position)
{
    int x = position.x / this->tileMap->getTileSize().width;
    int y = ((this->tileMap->getMapSize().height * this->tileMap->getTileSize().height) - position.y) / this->tileMap->getTileSize().height;
    return ccp(x, y);
}

void TutorialSence::addTower(Point pos)
{
    DataModel *m = DataModel::getModel();
    Tower *target = NULL ;
    Point towerLoc = this->tileCoordForPosition(pos);

    int tileGid = this->background->tileGIDAt(towerLoc);
    Value props = this->tileMap->propertiesForGID(tileGid);
    ValueMap map = props.asValueMap();

    int type_int = map.at("buildable").asInt();
    if (1 == type_int) 
    {
        target = MachineGunTower::tower();
        target->setPosition(ccp((towerLoc.x * 32) + 16, this->tileMap->getContentSize().height - (towerLoc.y * 32) - 16));
        this->addChild(target,1);
        target->setTag(1);
        m->towers.pushBack(target);
    }
    else 
    {
        log("Tile Not Buildable");
    }
}

你可能看到“addTower”方法在我们用户想让他放置在某个位置的时候被调用了—然后我们使用“tileCoordPosition”方法来得到tile位置,并且使用这个位置来得到tile本身。然后查找tile的属性是不是“buildable”的。如果属性值==1,那么就可以放置。相应的,我们就创建一个tower,然后计算一下放置的位置,然后把塔加到层里去。如果属性“buidable”==null或者0的话,那么就什么也不做,只输出一句话。

由于我们刚刚提到了tower类,所以,它的实现非常重要,让我们来看一看:

#pragma once
#include "cocos2d.h"
#include "Creep.h"
#include "projectile.h"

class Tower: public Sprite 
{
public:
    int range;
    Sprite* sprite;
    Creep* target;
    Sprite * selSpriteRange;
    Creep* getClosesTarget();
    CREATE_FUNC(Tower);
};

class MachineGunTower : public Tower 
{
public:
    Projectile* nextProjectile;
    static Tower* tower();
    bool virtual init();
    void towerLogic(float dt);
    void finishFiring();
    void creepMoveFinished(Node* sender);
};

这是一个非常基础的类—我们定义了炮塔可以攻击的范围,同时还有选中一个炮塔并移动它的时候,会出现的一个虚拟的射程范围精灵。你可能也注意到了,我们还定义了一个MachineGunTower类,它从Tower派生过来的。因为,每一种tower都有一些特殊的属性,所以我们定义不同的类来使得事情变得更加简单,并且更容易管理。基本上,所有的tower都有一些共同的特点,比如射程是多少,杀伤力是多少等。

接下来,看它的实现:

#include "Tower.h"
#include "DataModel.h"

Tower* MachineGunTower::tower()
{
    Tower* tower = Tower::create();
    tower->sprite = Sprite::create("MachineGunTurret.png");
    tower->addChild(tower->sprite, 0);
    tower->range = 200;
    tower->schedule(schedule_selector(towerLogic), 0.2);
    return tower;
}

bool MachineGunTower::init()
{
    if (!Sprite::init()) 
    {
        return false;
    }
    return true;
}

Creep* Tower::getClosesTarget()
{
    Creep *closestCreep = NULL;
    double maxDistant = 99999;

    DataModel *m = DataModel::getModel();

    for each(Sprite *target in m->targets) 
    {
        Creep *creep = (Creep *)target;
        double curDistance = ccpDistance(this->getPosition(), creep->getPosition());

        if (curDistance < maxDistant) {
            closestCreep = creep;
            maxDistant = curDistance;
        }
    }
    if (maxDistant < this->range)
        return closestCreep;
    return NULL;
}

void MachineGunTower::towerLogic(float dt)
{
    this->target = this->getClosesTarget();

    if (this->target != NULL) 
    {
        // Rotate player to face shooting direction
        Point shootVector = this->target->getPosition() - this->getPosition();
        float shootAngle = ccpToAngle(shootVector);
        float cocosAngle = CC_RADIANS_TO_DEGREES(-1 * shootAngle);

        float rotateSpeed = 0.5 / M_PI;
        float rotateDuration = fabs(shootAngle * rotateSpeed);

        this->runAction(Sequence::create(RotateTo::create(rotateDuration,cocosAngle),NULL));

        this->runAction(Sequence::create(RotateTo::create(rotateDuration, cocosAngle), CallFunc::create(this, callfunc_selector(MachineGunTower::finishFiring)), NULL));
    }
}

void MachineGunTower::finishFiring()
{
    DataModel *m = DataModel::getModel();

    if (this->target != NULL && this->target->curHp > 0 && this->target->curHp < 100)
    {
        this->nextProjectile = Projectile::projectile();
        this->nextProjectile->setPosition(this->getPosition());

        this->getParent()->addChild(this->nextProjectile, 1);
        m->projectiles.pushBack(this->nextProjectile);

        float delta = 1.0f;
        Point shootVector = -(this->target->getPosition() - this->getPosition());
        Point normalizedShootVector = ccpNormalize(shootVector);
        Point overshotVector = normalizedShootVector * 320;
        Point offscreenPoint = (this->getPosition() - overshotVector);

        this->nextProjectile->runAction(Sequence::create(MoveTo::create(delta, offscreenPoint), CallFuncN::create(this, callfuncN_selector(MachineGunTower::creepMoveFinished)), NULL));

        this->nextProjectile->setTag(2);

        this->nextProjectile = NULL;

    }

}

void MachineGunTower::creepMoveFinished(Node* sender)
{
    DataModel * m = DataModel::getModel();
    Sprite *sprite = (Sprite *)sender;
    this->getParent()->removeChild(sprite,true);

    m->projectiles.eraseObject((Projectile*)sprite);
}

这里也没太多好说的—只是一个基类,我们甚至不需要实现init方法。因为,我们将在下一个教程中添加一些代码,这里暂时空白,不过并没有关系。我们加载炮塔的图片,同时设置射程,并且创建了一个towerLogic调度器,时间间隔是0.2秒。这样,塔就可以坐在那儿了,但是,这个towerLogic方法还是空的,不过下个教程中,我们将添加一些代码!

现在,你看到了tower的代码,接下来一件比较重要的事,就是向你展示如何实现游戏的hud–它里面包含了可以被选中的tower,而且可以把它们拖到游戏layer中去。这时,我们有两个层被加到场景中去了,下面是gameHud的代码:

#pragma once
#include "cocos2d.h"

USING_NS_CC;

class GameHUD: public Layer 
{
public:
    Sprite* background;
    Sprite* selSpriteRange;
    Sprite* selSprite;
    Vector<Sprite*> movableSprites;
    static GameHUD* _sharHUD;
    virtual bool init();
    static GameHUD* shareHUD();
    CREATE_FUNC(GameHUD);
    virtual void onEnter();
    bool onTouchBegan(Touch *touch, Event *event);
    void onTouchMoved(Touch *touch, Event *event);
    void onTouchEnded(Touch* touch, Event* event);
};

这里,我们定义了层的背景图片,selSprite是tower图片的一个拷贝,可以选中它,并在屏幕上拖动。而selSpriteRange是一个射程图片,当我们选中一个塔在屏幕上移动的时候,在塔的周围会有一个圆形的阴影,代表了塔的射程。最后,我们定义了movableSprites数组,它是gameLayer中可以供玩家选取的所有的炮塔图片的集合。

让我们看看gameHud的具体实现吧:

bool GameHUD::init()
{
    if (!Layer::init()) 
    {
        return false;
    }

    Size winSize = CCDirector::getInstance()->getWinSize();

    // Draw the background of the game HUD
    CCTexture2D::setDefaultAlphaPixelFormat(kCCTexture2DPixelFormat_RGB565);
    background = Sprite::create("hud.png");
    background->setScaleX (2);
    background->setAnchorPoint(ccp(0, 0));
    this->addChild(background);
    CCTexture2D::setDefaultAlphaPixelFormat(kCCTexture2DPixelFormat_Default);

    // Load the images of the towers we'll have and draw them to the game HUD layer
    Vector<String*> images;
    images.pushBack(StringMake("MachineGunTurret.png"));
    images.pushBack(StringMake("MachineGunTurret.png"));
    images.pushBack(StringMake("MachineGunTurret.png"));
    images.pushBack(StringMake("MachineGunTurret.png"));
    for (int i = 0; i < images.size(); ++i)
    {
        String* image = images.at(i);
        auto *sprite = Sprite::create(image->getCString());
        float offsetFraction = ((float)(i + 1)) / (images.size() + 1);
        sprite->setPosition(ccp(winSize.width*offsetFraction, 35));
        this->addChild(sprite);
        movableSprites.pushBack(sprite);
    }

    return true;
}

我希望代码中那两行注释已经解释清楚所有的一切了。首先,我们为gameLayer加载了一张背景图片,然后我们循环遍历一个图片名字数组,并用这些图片名字创建精灵,然后把这些精灵存到movableSprites数组中去。你可以从Ray的教程中找到一种方法来把四个图片放置到同一行上面。

现在,让我们来处理一下gameLayer的touch事件:

bool GameHUD::onTouchBegan(Touch *touch, Event *event)
{
    Point touchLocation = this->convertToWorldSpace(this->convertTouchToNodeSpace(touch));

    Sprite * newSprite = NULL;
    for each(Sprite* sprite in this->movableSprites)
    {
        Rect pos_rect = Rect((sprite->getPositionX()-sprite->getContentSize().width/2), (sprite->getPositionY()-sprite->getContentSize().height/2), sprite->getContentSize().width, sprite->getContentSize().height);
        float xMin = pos_rect.getMinX();
        float xMax = pos_rect.getMaxX();
        float yMIn = pos_rect.getMinY();
        float yMax = pos_rect.getMaxY();
        if (pos_rect.containsPoint(touchLocation))
        {
            DataModel *m = DataModel::getModel();
            //m.gestureRecognizer.enabled = NO;
            selSpriteRange = Sprite::create("Range.png");
            selSpriteRange->setScale(4);
            this->addChild(selSpriteRange, -1);
            selSpriteRange->setPosition(sprite->getPosition());

            newSprite = Sprite::createWithTexture(sprite->getTexture()); //sprite;
            newSprite->setPosition(sprite->getPosition());
            selSprite = newSprite;
            this->addChild(newSprite);
        }
    }

    return true;
}

上面这段代码做了些什么事呢–首先,遍历“movableSprite”数组,然后使用CCRectContainsPoint方法来判断touchLocation是不是在图片的范围之内。如果是的话,那么就通知Model对象,把gestureRecognizer禁用掉。这个变量是指向 “UIPanGestureRecognizer”的。当我们选中一个炮塔的时候,就存储当前选中的炮塔放到selSprite中,并且加载射程范围图片。这样的话,当你放置一个炮塔,你就知道它能够射击多远了。

void GameHUD::onTouchMoved(Touch* touch,Event* event) 
{
    Point touchLocation = this->convertToWorldSpace(this->convertTouchToNodeSpace(touch));

    Point oldTouchLocation = touch->getPreviousLocationInView();
    oldTouchLocation = Director::getInstance()->convertToGL(oldTouchLocation);
    oldTouchLocation = this->convertToNodeSpace(oldTouchLocation);

    Point translation = ccpSub(touchLocation,oldTouchLocation);

    if (selSprite) 
    {
        Point newPos = selSprite->getPosition()+translation;
        selSprite->setPosition(newPos);
        selSpriteRange->setPosition(newPos);

        DataModel *m = DataModel::getModel();
        Point touchLocationInGameLayer = m->_gameLayer->convertTouchToNodeSpace(touch);

        BOOL isBuildable = m->_gameLayer->canBuildOnTilePosition(touchLocationInGameLayer);
        if (isBuildable) 
        {
            selSprite->setOpacity(200);
        }
        else 
        {
            selSprite->setOpacity(50);
        }
    }
}

现在,上面的代码可以移动炮塔在屏幕上来回移动,并且可以知道哪些地方可以放塔,哪些地方不可以。这里更有趣的地方是,我添加了 canBuildOnTilePosition方法,它可以帮助我们判断那个地方是否可以放置一个炮塔。

bool TutorialSence::canBuildOnTilePosition(Point pos)
{
    Point towerLoc = this->tileCoordForPosition(pos);
    int tileGid = this->background->getTileGIDAt(towerLoc);
    Value props = this->tileMap->getPropertiesForGID(tileGid);
    ValueMap map = props.asValueMap();
    int type_int;
    if (map.size() == 0)
    {
        type_int = 0;
    }
    else
    {
        type_int = map.at("buildable").asInt();
    }

    if (1 == type_int)
    {
        return true;
    }
    return false;
}

这里还是采用检查对于位置tile的属性的值,如果是1,则表明可以放置,返回yes。否则就返回no。如果是不可以建筑的,我们就把选中的精灵设置透明度为50,如果是的话,则把透明度设置为200.我们也可以这样,显示红色的圈,则表示不能建筑,如果是绿色的,则表示可以建筑。这个功能留给读者自行添加。

现在回到touchEnd事件处理:

void GameHUD::onTouchEnded(Touch* touch, Event* event)
{
    Point touchLocation = this->convertTouchToNodeSpace(touch);
    DataModel *m = DataModel::getModel();

    if (selSprite) 
    {
        Rect backgroundRect = Rect(background->getPositionX(),
            background->getPositionY(),
            background->getContentSize().width,
            background->getContentSize().height);

        if (!backgroundRect.containsPoint(touchLocation) && m->_gameLayer->canBuildOnTilePosition(touchLocation))
        {
            Point touchLocationInGameLayer = m->_gameLayer->convertTouchToNodeSpace(touch);
            m->_gameLayer->addTower(touchLocationInGameLayer);
        }

        this->removeChild(selSprite,true);
        selSprite = NULL;
        this->removeChild(selSpriteRange,true);
        selSpriteRange = NULL;
    }
}

这里需要检查的就是,我们可能取消我们的放置操作。如果玩家把tower又放置到hud 背景上,那么就应该把这个选中的精灵和射程图片都清除掉。

我们又一次做到了!现在,我们定义了一个hud层,上面放置了一些炮塔,我们可以从hud里面拖一塔出来,放置在地图中。接下来,我们会把第一部分和第二部分教程合起来,然后添加tower射击的功能。

创建游戏HUD。

显示了把炮塔从一个地方拖到另一个地方。 还有一些内容请参考这个链接:tile map editor tool!

仍然有待解决的问题:

1.如何让炮塔开火并杀死creep。 2.如何制作一波一波的creep,如何制作特殊creep。

前言

欢迎来到塔防游戏教程系列的第三部分–今天,我将把你们带到离梦想更进的地方。如果你还没有读到第一部分教程,在继续之前,请先返回。

在这个教程中,我们将完成两件大事–首先,我们要把part1part2的功能合并到一起,然后给炮塔添加旋转瞄准器的功能,让它可以瞄准creep进行射击。

  • Towers 应该可以瞄准离它最近的creep
  • Towers 应该面朝着向它靠近的creep
  • Creeps改变方向的时候,也应该旋转身体。

这里有本教程的完整源代码。 作为一个游戏开发者,我们自己也面临着很多困难,而且制作一个游戏也会影响到玩家。然后,我们并不想被困难吓倒,但是,使游戏更逼近现实生活将需要更多的时间和精力,那样有时候可能并不值。所以,我们做一个真正的游戏程序员做的事–造假(fake it)。下面,我们看看寻找最近creep的方法:

Creep* Tower::getClosesTarget()
{
    Creep *closestCreep = NULL;
    double maxDistant = 99999;

    DataModel *m = DataModel::getModel();

    for each(Sprite *target in m->targets) 
    {
        Creep *creep = (Creep *)target;
        double curDistance = ccpDistance(this->getPosition(), creep->getPosition());

        if (curDistance < maxDistant) {
            closestCreep = creep;
            maxDistant = curDistance;
        }
    }
    if (maxDistant < this->range)
        return closestCreep;
    return NULL;
}

这个代码在Tower.m文件中,它使用DataModel类来搜索所有可能的creep对象,然后比较这些creep同塔的距离,如果小于塔的射程的话,那么就返回该creep对象。

我们用来旋转炮塔的代码需要一些数学知识,它的逻辑看起来如下所示:

void MachineGunTower::towerLogic(float dt)
{
    this->target = this->getClosesTarget();

    if (this->target != NULL) 
    {
        // Rotate player to face shooting direction
        Point shootVector = this->target->getPosition() - this->getPosition();
        float shootAngle = ccpToAngle(shootVector);
        float cocosAngle = CC_RADIANS_TO_DEGREES(-1 * shootAngle);

        float rotateSpeed = 0.5 / M_PI;
        float rotateDuration = fabs(shootAngle * rotateSpeed);

        this->runAction(Sequence::create(RotateTo::create(rotateDuration,cocosAngle),NULL));

        this->runAction(Sequence::create(RotateTo::create(rotateDuration, cocosAngle), CallFunc::create(this, callfunc_selector(MachineGunTower::finishFiring)), NULL));
    }
}

你也注意到了,tower和creep的逻辑看起来差不了很多,这里数学都很相似。因此,我们采用一石二鸟之计,同时解释tower和creep的逻辑:

void Creep::creepLogic(float dt)
{
    WayPoint* waypoint = this->getCurrentWaypoint();

    Point waypointVector = waypoint->getPosition()-this->getPosition();
    float waypointAngle = ccpToAngle(waypointVector);
    float cocosAngle = CC_RADIANS_TO_DEGREES(-1 * waypointAngle);

    float rotateSpeed = 0.5 / M_PI;
    float rotateDuration = fabs(waypointAngle * rotateSpeed);

    this->runAction(Sequence::create(RotateTo::create(rotateDuration,cocosAngle),NULL));
}

我们将计算出tower和creep改变面朝方向时需要旋转的角度。我们首先计算当前的位置和参考点的位置(对于tower就是creep对象,对于creep就是waypoint对象)的向量,然后使用cocos2d的一个函数ccpToAngle得到弧度值。然后使用 CC_RADIANS_TO_DEGREES这个宏转换成角度。

最后,我们考虑0~359度的范围,但是,cocos2d只能处理0~180和0~-180的旋转。

就这么多–当你仔细思考一下,你会发现这一切都很明了。。。这是一个非常简单的教程,但是很关键—-接下来,我们要给炮塔添加子弹,最终它可以杀死一些creeps。

前言

教程截图:

image

这部分教程,我们将实现我们刚开始所讨论的完整的塔防游戏。这是本系列教程的第四部分,也是最后一部分,在继续阅读之前,建议你先查看前3个教程,可以从这里开始。

今天,我们将学习到新的东西–“让炮塔开火!”。这是塔防游戏中最重要的组成部分,也是本程序中最有意思的部分。我当然不可能在这个教程中覆盖塔防游戏中所有好玩的东西,但是,我保证我们会在不将的将来创建各式各样的tower。我想,在学习完这个系列教程之后,你自己也可以制作出非常好玩的塔防游戏。我会尽量让代码的robust性更好的!

这里有本教程的完整源代码

首先,我们需要做的就是创建实际的子弹(projectile)–projectile类目前有一点点简单,它里面什么也没有,但是,不代表它将来也没有内容。

Projectile.h

#pragma once
#include "cocos2d.h"

USING_NS_CC;

class Projectile: public Sprite 
{
public:
    static Projectile* projectile();
};

Projectile.cpp

#include "Projectile.h"

Projectile* Projectile::projectile()
{
    Projectile* projectile = (Projectile*)Sprite::create("Projectile.png");
    if (projectile != NULL)
    {
        return projectile;
    }
    return NULL;
}

我们现在添加新的方法到update函数中,此函数在TowerScene中。那里面处理当前的子弹和creep的碰撞检测。这个基本的逻辑很简单:

1.遍历所有的projectiles

2.对于每一个projectiles,遍历所有的targets

3.看projectile的边界是否和target的边界有交叉

4.如果交叉了,减少creep的生命值,同时把projectile加到一个即将被删除的数组 projectilesToDelete里面去

5.如果creep的生命值==0,那么就把它加到targetToDelete数组里面。

具体代码如下:(其实这段代码和Ray的教程《如何使用cocos2d制作一个简单的iphone游戏》里面的忍者发射飞盘一样的)

void TutorialSence::update(float dt)
{
    DataModel *m = DataModel::getModel();
    Vector<Projectile*> projectilesToDelete;

    for each(Projectile *projectile in m->projectiles) 
    {
        Rect projectileRect = Rect(projectile->getPositionX() - (projectile->getContentSize().width / 2),
            projectile->getPositionY() - (projectile->getContentSize().height / 2),
            projectile->getContentSize().width,
            projectile->getContentSize().height);

        Vector<Creep*> targetsToDelete;

        for each(Creep *target in m->targets) 
        {
            Rect targetRect = Rect(target->getPositionX() - (target->sprite->getContentSize().width / 2),
                target->getPositionY() - (target->sprite->getContentSize().height / 2),
                target->sprite->getContentSize().width,
                target->sprite->getContentSize().height);

            if (projectileRect.intersectsRect(targetRect)) 
            {
                projectilesToDelete.pushBack(projectile);

                Creep *creep = target;
                creep->curHp -= 1;

                if (creep->curHp <= 0) 
                {
                    targetsToDelete.pushBack(creep);
                }
                break;
            }
        }

        for each(Creep *target in targetsToDelete)
        {
            m->targets.eraseObject(target);
            this->removeChild(target, true);
        }       
    }

    for each(Projectile *projectile in projectilesToDelete)
    {
        m->projectiles.eraseObject(projectile);
        this->removeChild(projectile,true);
    }
}

最后,我们回到tower代码,看看我们是怎么处理开火机制的。在towerLogic代码里面,我们在RotateTo action之后再添加了一个action,叫做CCCallFunc。它会触发一个finishFiring方法。

this->runAction(Sequence::create(RotateTo::create(rotateDuration, cocosAngle), CallFunc::create(this, callfunc_selector(MachineGunTower::finishFiring)), NULL));

finishFiring只是在我们知道tower朝着正确的方向的时候才开火。它主要是创建一个新的子弹,然后把它加到DataModel的projectile数组中,同时给它一个位置和目的点。

void MachineGunTower::finishFiring()
{
    DataModel *m = DataModel::getModel();

    if (this->target != NULL && this->target->curHp > 0 && this->target->curHp < 100)
    {
        this->nextProjectile = Projectile::projectile();
        this->nextProjectile->setPosition(this->getPosition());

        this->getParent()->addChild(this->nextProjectile, 1);
        m->projectiles.pushBack(this->nextProjectile);

        float delta = 1.0f;
        Point shootVector = -(this->target->getPosition() - this->getPosition());
        Point normalizedShootVector = ccpNormalize(shootVector);
        Point overshotVector = normalizedShootVector * 320;
        Point offscreenPoint = (this->getPosition() - overshotVector);

        this->nextProjectile->runAction(Sequence::create(MoveTo::create(delta, offscreenPoint), CallFuncN::create(this, callfuncN_selector(MachineGunTower::creepMoveFinished)), NULL));

        this->nextProjectile->setTag(2);

        this->nextProjectile = NULL;

    }
}

你可以看到上面的代码,我们在projectile上面运行另一个CCMoveTo action,使之朝一个特定的方向移动。这样就会使得看起来是tower在发射子弹,但是,现在默认的距离是320.一旦子弹到达目的地后, “creepMoveFinished”方法就会被调用,它会从DataModel projectile数组中把这个projectile移除掉,并且把它从舞台中剔除。

void MachineGunTower::creepMoveFinished(Node* sender)
{
    DataModel * m = DataModel::getModel();
    Sprite *sprite = (Sprite *)sender;
    this->getParent()->removeChild(sprite,true);

    m->projectiles.eraseObject((Projectile*)sprite);
}

恩,就这些了,编译并运行,你现在可以旋转炮塔了,而且它们可以攻击被杀死creep了。很酷吧?其实,我们的工作还没有完成,我们其实还需要添加一些塔的威力,还需要一个买卖系统,同时还要一个波数控制系统(可以控制当前波的速度快慢,总共多少波数等问题)。很多事情要做,不过我们已经上路了。。。

  继续完善吧!

预备知识

教程截图:

image

我们已经学习到了好多简单的游戏机制了(比如精灵、菜单等),现在我们来试着制作一款塔防游戏。 首先我们得搞清楚塔防游戏的定义:

  • 敌人从一个方向出来,然后沿着一条预先设定好的路径行走,去往某个目的地(通常是地图的另一端)
  • 你可以沿着那条路径来建筑一些“塔”,当敌人经过这个塔的时候,塔就会向它开火。为了能够抵挡一波又一波更猛的怪物的进攻,你需要不断升级你的箭塔来加强火力。
  • 你可以通过挡住最后一波进攻来取得游戏的胜利(通常是20~50波),或者你也可以让他们一波接一波的通过,然后你输了。当然,你也可以向别人炫耀,你坚持到了97波啦。

这不是一个“随意的游戏”,我们需要更好地定义下“随意”这个词:玩家在玩游戏的时候,可以简单的放置一些炮塔,当战斗打响后,也可以稍微升级下炮塔。那些没有玩过塔防游戏的人,一般会在48波~50波左右就会挂掉,因为你并没有考虑到同时可能会有多种类型的怪物会进攻。你可以花一些时间来调整你的游戏参数,看看你能不能更快得取得游戏胜利,或者赚更多的钱(因为打死一个怪物一般有钱,建筑炮塔也要花钱),或者造最少的塔来取得游戏的胜利。

你可能会奇怪,难到上面这些就是制作一个塔防游戏的关键?为什么不直接给我源代码呢?耐心点!我们马上就会开始编码了。这里我想说的是,塔防游戏并不是一种新的游戏方式,因此,你想让游戏取得成功的话,那么你必须从头好好设计。所以,如果我教你们制作塔防游戏的话,我希望你们能制作一些让我玩起来觉得很趣的作品来。没有什么比制作一件让人嘘声、骂声一片的烂游戏更让人觉得难为情的了。所以,我认为,塔防游戏的质量是关键。

1.游戏不应该让玩家手忙脚乱,但是,对于那些比较快速取得游戏胜利的玩家,可以给以额外的“分数”奖励。

2.没有任何形式的资源管理–这意味着,你对于每个塔的伤害值了如指掌,而且,你并不需要建筑一些特殊的塔来解琐其它的塔的升级。

3.塔的信息和策略很重要—-地图的形状,总共可以建筑多少个塔,这些都需要认真考虑。如果,你只从头到尾建筑一种类型的塔就可以把整个游戏通关,请问那有什么意思?!

4.结尾部分重要(比如,你会在第50波的时候放出一个大boss,血非常厚,移动速度也比较快等)。但是,具体怎么弄,你可以参考一下别人的做法。这主要看你的地图是怎么设计的,还有,你的塔的升级的怎么设置的。(可以升几级?升级威力提升如何等等。)

5.最重要的部分:千万不要让一种塔能够“疯狂升级”!!!(比如爆击一下500万?。。。)因为你可以建筑不同种类的塔,比如减速塔、多目标塔、激光塔等。我发现最好的玩的td游戏,就是那些游戏里面有许多种不同类型的塔,而且每种类型的功能都非常专一,非常有用,有策略性。当然也有例外情况啦,不过最好要使塔的种类多样化。