cocos2d-x 4.0 学习之路(五)第一个小游戏--忍者来袭

学游戏开发最重要的是刚开始就能做出个小游戏,这才能激发兴趣。
那么,今天咱们就来做一个小游戏–忍者来袭。网上有不少相关的文章(我参考的是这篇文章),但是很多已经不适用V4.0了。所以我在V4.0下重新写了一下。

话不多说,进入正题。
先看一下最终效果:
在这里插入图片描述
这就是我们目标,开始吧!

我们就在HelloWorldScene.cpp和HelloWorldScene.h里面改写代码。
首先把HelloWorldScene.cpp里的init函数,就留下面这些,其他都删除。

bool HelloWorld::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Scene::init() )
    {
        return false;
    }
    // My code here
    
    return true;
}

1.我们设置一下舞台背景,为灰色,看起来清晰一些。

    auto winSize = Director::getInstance()->getVisibleSize();
    auto origin = Director::getInstance()->getVisibleOrigin();

    // set Background with grey colour
    auto background = DrawNode::create();
    background->drawSolidRect(origin, winSize, cocos2d::Color4F(0.6, 0.6, 0.6, 1.0));
    this->addChild(background);

2.请出我们的主角–忍者精灵。
在这里插入图片描述
大家可以下载这个图片,保存到Resource里。
创造精灵分3步,还记得吧。(定义,设定位置,加到场景)
因为我们的player不光是用在init()函数中的,其他的函数也会用到,所以我们在HelloWorldScene.h中定义_player为一个私有变量。

//HelloWorldScene.h
private:
    cocos2d::Sprite* _player;

//HelloWorldScene.cpp
    // Add player
    _player = Sprite::create("player.png");
    _player->setPosition(Vec2(winSize.width * 0.1, winSize.height * 0.5));
    this->addChild(_player);

这样,我们的_player就站好位置了。
在这里插入图片描述
3.增加敌人
在这里插入图片描述
我们做一个方法,专门来生成。不要忘了在头文件中声明一下哦。

void HelloWorld::addMonster(float dt) {
    auto monster = Sprite::create("monster.png");

    // Add monster
    auto monsterContentSize = monster->getContentSize();
    auto selfContentSize = this->getContentSize();
    int minY = monsterContentSize.height / 2;
    int maxY = selfContentSize.height - minY;
    int rangeY = maxY - minY;
    int randomY = (rand() % rangeY) + minY;
    monster->setPosition(Vec2(selfContentSize.width + monsterContentSize.width/2, randomY));
    this->addChild(monster);

    // Let monster run
    int minDuration = 2.0;
    int maxDuration = 4.0;
    int rangeDuration = maxDuration - minDuration;
    int randomDuration = (rand() % rangeDuration) + minDuration;

    // 定义移动的object
    // 在randomDuration这个时间内(2-4秒内),让怪物从屏幕右边移动到左边。(怪物有快有慢)
    auto actionMove = MoveTo::create(randomDuration, Vec2(-monsterContentSize.width / 2, randomY));
    // 定义消除的Object。怪物移出屏幕后被消除,释放资源。
    auto actionRemove = RemoveSelf::create();
    monster->runAction(Sequence::create(actionMove, actionRemove, nullptr));
}

这里面的知识点是物体移动,有两个移动类:
MoveTo:是在多长时间内,移动到指定的点。
MoveBy:是在当前位置移动多少步。
MoveSelf:是为了释放这个精灵的资源,虽然精灵走出了画面,你要是一直不删除,那么精灵越来越多,系统就会崩溃的。
runAction:就是让物体移动起来。

之后,我们需要再生成Player后面调用这个方法:

    // 初始化了随机数生成器。如果不执行这一步,每次运行程序都会产生一样的随机数。
    srand((unsigned int)time(nullptr));
    // 每隔1.5秒生成一个怪物
    this->schedule(CC_SCHEDULE_SELECTOR(HelloWorld::addMonster), 1.5);

schedule为我们提供一个连续动作的功能,用起来非常方便。
这样我们的敌人就出场了:
在这里插入图片描述
4.发射飞镖
在这里插入图片描述
我们发射飞镖是由点击屏幕触发的。这涉及到事件的概念。cocos使用EventDispatcher来处理各种各样的事件,如触摸和其他键盘事件。为了从EventDispatcher中获取事件,你需要注册一个EventListener,它有两种触摸事件的监听器:

  1. EventListenerTouchOneByOne:此类型对每个触摸事件调用一次回调方法。
  2. EventListenerTouchAllAtOnce:此类型对所有的触摸事件调用一次回调方法。

每个事件监听器支持4个回调,但你只需要为自己关心的事件绑定方法。

  1. onTouchBegan:手指第一次碰到屏幕时被调用。如果你使用的是EventListenerTouchOneByOne,你必须返回true才能获取另外3个触摸事件。
  2. onTouchMoved:手指接触屏幕并移动(保持接触)时被调用。
  3. onTouchEnded:手指离开屏幕时被调用。
  4. onTouchCancelled:在特定的结束事件处理的环境中被调用,如你正在触屏的时候,一个电话打了进来打断了这个app进程。在本游戏中,你只用关心触摸发生的时间就好了。

我们先在头文件中声明回调函数:

// HelloWorldScene.h
    bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* unused_event);

然后实现它:

// HelloWorldScene.cpp
bool HelloWorld::onTouchBegan(Touch* touch, Event* unused_event) {
    // 1 - Just an example for how to get the player object
    // 说明一下作为第二个参数传递给addEventListenerWithSceneGraphPriority(eventListener, _player)的_player对象被访问的方式。
    // auto node = unused_event->getcurrentTarget();

    // 2.获取触摸点的坐标,并计算这个点相对于_player的偏移量。
    Vec2 touchLocation = touch->getLocation();
    Vec2 offset = touchLocation - _player->getPosition();
    // 如果offset的x值是负值,这表明玩家正试图朝后射击。在本游戏中这是不允许的。
    if (offset.x < 0){
        return true;
    }

    // 3.在玩家所在的位置创建一个飞镖,将其添加到场景中。
    auto projectile = Sprite::create("Projectile.png");
    projectile->setPosition(_player->getPosition());
    this->addChild(projectile);

    // 4.将偏移量转化为单位向量,即长度为1的向量。
    offset.normalize();
    // 将其乘以1000,你就获得了一个指向用户触屏方向的长度为1000的向量。为什么是1000呢?因为长度应当足以超过当前分辨率下屏幕的边界。
    auto shootAmount = offset * 1000;
    // 将此向量添加到飞镖的位置上去,这样你就有了一个目标位置。
    auto realDest = shootAmount + projectile->getPosition();

    // 5.创建一个动作,将飞镖在2秒内移动到目标位置,然后将它从场景中移除。
    auto actionMove = MoveTo::create(2.0f, realDest);
    auto actionRemove = RemoveSelf::create();
    projectile->runAction(Sequence::create(actionMove, actionRemove, nullptr));

    return true;
}

注释写的很清楚,我就不多解释了。它的生成、移动和资源释放和敌人是一样的。注意一点就是MoveTo的目标点realDest是如何算出的。
好了,Ctrl+F5运行一下,飞镖满天飞,而且非常流畅。
在这里插入图片描述
5.碰撞检测(这个章节在我参考的文章里是没有的)
我们要的当然不是敌人在飞镖满天飞的情况下还能前进,我们必须得让飞镖击中的敌人消失。
碰撞检测是游戏开发中少不了的环节,即两个精灵碰上了,会发什么。
cocos为我们提供了Physics类,里面有一系列的方法来检测物理碰撞。那对于我们的这个程序,要进行下面的功能追加:
a.初始化Physics类。这是硬性要求,既然你想用物理碰撞。把它加载init后面。

    if ( !Scene::init() )
    {
        return false;
    }
    // 初始化Physics
    if (!Scene::initWithPhysics())
    {
        return false;
    }

b.给敌人和飞镖都加上physicsBody。这样我们精灵对象就承载了他们的物理属性。我们就可以用物理碰撞检测了。
分别在敌人和飞镖的addChild前,加上下面的代码:

    // Add monster's physicsBody
    auto physicsBody = PhysicsBody::createBox(monster->getContentSize(), PhysicsMaterial(0.0f, 0.0f, 0.0f));
    physicsBody->setDynamic(false);
    physicsBody->setContactTestBitmask(0xFFFFFFFF);
    monster->setPhysicsBody(physicsBody);
    
    // Add projectile's physicsBody
    auto physicsBody = PhysicsBody::createBox(projectile->getContentSize(), PhysicsMaterial(0.0f, 0.0f, 0.0f));
    physicsBody->setDynamic(false);
    physicsBody->setContactTestBitmask(0xFFFFFFFF);
    projectile->setPhysicsBody(physicsBody);
    projectile->setTag(10);

c.碰撞也是事件,那么我们这回应该注册一个什么样的Listener呢,是EventListenerPhysicsContact(Contact有联系人的意思,这里我们可以理解为接触)。这个监听器里也有4个回调函数:

  1. onContactBegin:It will called at two shapes start to contact, and only call it once.
  2. onContactPreSolve:Two shapes are touching during this step.
  3. onContactPostSolve:Two shapes are touching and their collision response has been processed.
  4. onContactSeparate:It will called at two shapes separated, and only call it once.

在发射飞镖处理后,加入下面代码:

    // 碰撞检测
    auto contactListener = EventListenerPhysicsContact::create();
    contactListener->onContactBegin = CC_CALLBACK_1(HelloWorld::onContactBegin, this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(contactListener, this);

d.我们选用第一个回调函数,并实现它。内容就是当飞镖击中敌人的时候,把敌人删除。

bool HelloWorld::onContactBegin(cocos2d::PhysicsContact& contact)
{
    auto nodeA = contact.getShapeA()->getBody()->getNode();
    auto nodeB = contact.getShapeB()->getBody()->getNode();

    if (nodeA && nodeB)
    {
        if (nodeA->getTag() == 10)
        {
            nodeB->removeFromParentAndCleanup(true);
        }
        else if (nodeB->getTag() == 10)
        {
            nodeA->removeFromParentAndCleanup(true);
        }
    }

    return true;
}

上面的代码稍微解释一下。当两个物体碰撞后,会调用这个方法。首先取得两个Node,因为是回调函数,你确定不了哪个是A哪个是B。所以,我在做成它们PhysicsBody的时候,只有飞镖setTag(10)了,这样哪个getTag是10,哪个就是飞镖,另一个就是敌人,然后删除敌人就OK了。
关于碰撞里面的BitMask(比特掩码)是如何应用的,可以参考官网Collision好好学学。这里就不多介绍了。

其实检测碰撞不只这一种方法,这是物理碰撞检测。还有一种方法就是图形边缘检测–就是之前用的很广的intersectsRect()。现在这个也能用,但是你需要进行更多的计算位置来满足你想要的条件,比较费脑。

好了,游戏初版就这样了。再运行一下,试试你的杀敌效果吧。
在这里插入图片描述
完整代码(Gitee)

发布了104 篇原创文章 · 获赞 8 · 访问量 21万+

猜你喜欢

转载自blog.csdn.net/sunnyboychina/article/details/104837384
今日推荐