学游戏开发最重要的是刚开始就能做出个小游戏,这才能激发兴趣。
那么,今天咱们就来做一个小游戏–忍者来袭。网上有不少相关的文章(我参考的是这篇文章),但是很多已经不适用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,它有两种触摸事件的监听器:
- EventListenerTouchOneByOne:此类型对每个触摸事件调用一次回调方法。
- EventListenerTouchAllAtOnce:此类型对所有的触摸事件调用一次回调方法。
每个事件监听器支持4个回调,但你只需要为自己关心的事件绑定方法。
- onTouchBegan:手指第一次碰到屏幕时被调用。如果你使用的是EventListenerTouchOneByOne,你必须返回true才能获取另外3个触摸事件。
- onTouchMoved:手指接触屏幕并移动(保持接触)时被调用。
- onTouchEnded:手指离开屏幕时被调用。
- 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个回调函数:
- onContactBegin:It will called at two shapes start to contact, and only call it once.
- onContactPreSolve:Two shapes are touching during this step.
- onContactPostSolve:Two shapes are touching and their collision response has been processed.
- 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)