课程教材《计算机游戏程序设计》(基础篇)(第3版) 提供示例代码,而课程实验在示例代码的基础上提出更高的实验要求。除此之外,本人也会额外加入些个人创意,希望同学们在参考之余也能加入自己的想法。
1.熟悉交互界面设计原理。
2.了解Cocos2d-x中的用户交互、触摸事件、碰撞检测机制。
1.完成游戏编译(70分)
按照“计算机游戏程序设计-第二次试验.pdf”文件“源码实验结果”,成功编译并运行本次实验游戏。
2.完成方案一 (15分)
修改游戏代码,实现方案一,即贪食豆在屏幕范围内能上下左右移动。
3.完成方案二 (15分)
修改游戏代码,实现方案二,即增加计分板功能。
1、 按实验一的步骤创建工程及拖入相关文件,编译运行。初始图如下图所示:
图 1
此时游戏能实现鼠标点击拖动摇杆令移动精灵左右移动(且不越界),以及移动机灵“吃掉”小球的功能。
2、 修改游戏代码,使贪食豆在屏幕范围能360度全方位移动(不仅仅是上下左右)。
观察Joystick.h源代码,发现其声明了一个枚举类型enum Joystick_dir{_LEFT,_RIGHT,_STOP}; 再结合其余文件的代码可知,该枚举类型的作用是用来判断移动精灵的左右移动的。
然而,我打算设计一种“直接根据摇杆的移动角度 来确定 移动精灵的移动方向”的新的角色移动方案,替代掉上面这种方向枚举的方法。
设计思路:
Joystick.h摇杆文件加入一个新的私有变量angle,以及获取该变量值的共有函数getAngle() ;变量angle用来记录摇杆中下图所示的角度(准确说是记录弧度),取值范围是:-π ~ π
(其实不计算弧度直接传原始值也可以进行后续的计算,这里只是为了方便说明)
图 2
然后在HelloWorldScene.cpp文件中获取angle值,根据角度(弧度)值,我们就可以确定角色(移动精灵)的移动方向了,设其每次的移动距离为6,则可确定其在x轴和y轴上的移动距离了。
同时,移动时也不要忘了检测移动精灵与边界的碰撞。确保角色在将要越界时,其对应坐标要在边界上。
核心代码:
Joystick.cpp:
// 获取摇杆力度
float Joystick::getVelocity()
{
return m_centerPoint.getDistance(m_currentPoint);
}
//获取角度
float Joystick::getAngle()
{
return angle;
}
// 更新摇杆按钮位置以及移动角度
void Joystick::update(float dt)
{
m_jsSprite->setPosition(m_jsSprite->getPosition() + (m_currentPoint - m_jsSprite->getPosition()) * 0.5);
//反余弦结果只能是正值,所以要判断上下象限确定角度(弧度)的正负。(-pi~pi)
if(m_currentPoint.y >= m_centerPoint.y)
angle = acos((m_currentPoint.x - m_centerPoint.x) / Joystick::getVelocity());
else
angle = -acos((m_currentPoint.x - m_centerPoint.x) / Joystick::getVelocity());
}
HelloWorldScene.cpp:
void HelloWorld::update(float dt){
for (auto ball:ballVector)
{
//进行碰撞检测
if (bean->getBoundingBox().intersectsRect(ball->getBoundingBox()))
{
HelloWorld::updateScore(ball);
auto actionDown =
CallFunc::create(CC_CALLBACK_0(HelloWorld::removeBall, this, ball));
ball->runAction(actionDown);
}
}
// 控制角色移动
float angle = m_joystick->getAngle(); //角色移动角度
float xx = 6 * cos(angle); //x轴移动距离
float yy = 6 * sin(angle); //y轴移动距离
//判断是否到边界,是 则沿边界移动
if (xx > 0)
{
if (bean->getPositionX() + bean->getContentSize().width / 2 + xx <= 680)
bean->setPositionX(bean->getPositionX() + xx);
else
bean->setPositionX(680 - bean->getContentSize().width / 2);
}
else if (xx < 0)
{
if (bean->getPositionX() - bean->getContentSize().width / 2 + xx >= 200)
bean->setPositionX(bean->getPositionX() + xx);
else
bean->setPositionX(200 + bean->getContentSize().width / 2);
}
else {}
if (yy > 0)
{
if (bean->getPositionY() + bean->getContentSize().height / 2 + yy <= 768)
bean->setPositionY(bean->getPositionY() + yy);
else
bean->setPositionY(768 - bean->getContentSize().height / 2);
}
else if (yy < 0)
{
if (bean->getPositionY() - bean->getContentSize().height / 2 + yy >= 0)
bean->setPositionY(bean->getPositionY() + yy);
else
bean->setPositionY(0 + bean->getContentSize().height / 2);
}
else {}
}
3. 增加计分板。
设计思路:
在HelloWorldScene.cpp文件的addBall函数中创建小球的精灵后,通过ball->setTag(int)给小球精灵设置识别标签。
当在update函数中通过遍历所有的球,检验出角色与小球相碰后,跳到updateScore(ball)函数,通过getTag() 获取tag,然后识别角色碰到是哪一个球,再增加适当的分数(白球增加10分,红球50分,绿球100分)。
关于得分的显示则可用Label类型的Label::createWithSystemFont(...)来实现 ,并且每次更新得分后,通过windows提供的itoa函数把得分变量score转换为字符串类型,再用Label->setString(str) 设置新的得分内容即可。
计分板如下图所示:
图 3
核心代码:
HelloWorldScene.cpp:
bool HelloWorld::init()
{
...
//添加得分层
auto labelScore =
Label::createWithSystemFont("Score:", "Consolas", 30, Size(100, 100));
labelScore->setPosition(Vec2(60, 200));
this->addChild(labelScore, 1);
score = 0;
label = Label::createWithSystemFont("0", "Consolas", 30, Size
(100, 100), TextHAlignment::RIGHT);
label->setPosition(Vec2(130,200));
this->addChild(label, 1, 100);
...
}
//球1
void HelloWorld::addBall1(float dt){
auto ball1 = Sprite::create("Chapter10/ball.png");//使用图片创建小球
ball1->setPosition(Point(220 + rand() % 440, visibleSize.height));//设置小球的初始位置,这里在x方向使用了随机函数rand使得小球在随机位置出现
ball1->setTag(1);
this->addChild(ball1, 5);
this->ballVector.pushBack(ball1);//将小球放进Vector数组
auto moveTo = MoveTo::create(rand() % 5 + 1, Point(ball1->getPositionX(), -10));
auto actionDone = CallFunc::create(CC_CALLBACK_0(HelloWorld::removeBall,
this, ball1));//当小球移动到屏幕下方时回调removeBall函数,移除小球
auto sequence = Sequence::create(moveTo, actionDone, nullptr);
ball1->runAction(sequence);//执行动作
}
//........球2、球3......省略
//更新得分
void HelloWorld::updateScore(Sprite* ball)
{
char str[20];
if (ball->getTag() == 1)
score += 10;
else if (ball->getTag() == 2)
score += 50;
else if (ball->getTag() == 3)
score += 100;
else if (ball->getTag() == 4)
HelloWorld::gameover();
itoa(score, str, 10);
label->setString(str); //分数转成字符串类型再更新分数显示
}
void HelloWorld::update(float dt){
for (auto ball:ballVector)
{
//进行碰撞检测
if (bean->getBoundingBox().intersectsRect(ball->getBoundingBox()))
{
HelloWorld::updateScore(ball);
auto actionDown =
CallFunc::create(CC_CALLBACK_0(HelloWorld::removeBall, this, ball));
ball->runAction(actionDown);
}
}
.......
}
以下为个人附加内容:
4. 不固定摇杆位置。
设计思路:
通过init函数初始化摇杆Joystick时,随意为摇杆设置一个位置,但是通过精灵的setVisible(false)函数给摇杆设置隐藏属性。
下图显示初始状态摇杆被隐藏看不见:
图 4
当我们用鼠标点击游戏屏幕时,监听器监听到该事件,调用函数onTouchBegan(Touch *pTouch, Event *pEvent) 。在该函数中,如果鼠标点击位置在设定的范围之内,我们则把该点设定为摇杆新的中心点m_centerPoint ,并通过setVisible(true)重新显示摇杆精灵。
此时我们就看到在指定的游戏屏幕范围内,以鼠标点击位置为中心,隐藏的摇杆出现了。
下面两图显示鼠标点击不同位置,摇杆出现的位置不同:
图 5
图 6
而监听鼠标移动的调用函数onTouchMoved则与源代码相同。
当鼠标松开后,onTouchEnded函数将被调用,此时再通过setVisible(false)把摇杆隐藏掉即可。
另外,为了扩大摇杆的可使用范围,摇杆可能会出现在小球和移动精灵所在的游戏屏幕上。因此,为避免摇杆遮挡实际的游戏画面,通过setOpacity(90)函数把摇杆图片设置为半透明。
核心代码:
// 初始化 aPoint是摇杆中心 aRadius是摇杆半径 aJsSprite是摇杆控制点 aJsBg是摇杆背景
bool Joystick::init(Vec2 aPoint , float aRadius , char* aJsSprite, char* aJsBg)
{
.......
m_jsSprite = Sprite::create(aJsSprite);//摇杆控制点
m_jsSprite->setPosition(m_centerPoint);
m_jsSprite->setOpacity(90); //避免图片遮挡,摇杆图片加点透明化
m_jsSprite->setVisible(false); //默认情况下摇杆要隐藏
_aJsBg = Sprite::create(aJsBg);//摇杆背景
_aJsBg->setPosition(m_centerPoint);
_aJsBg->setTag(88);
_aJsBg->setOpacity(90);
_aJsBg->setVisible(false);
this->addChild(_aJsBg);
this->addChild(m_jsSprite);
.......
}
bool Joystick::onTouchBegan(Touch *pTouch, Event *pEvent)
{
auto touchPoint = pTouch->getLocation();
//限定摇杆的范围
if (touchPoint.x < m_radius || touchPoint.x > (400 - m_radius) || touchPoint.y < m_radius || touchPoint.y >(768 - m_radius))
{
return false;
}
m_centerPoint = touchPoint;
m_currentPoint = touchPoint;
m_jsSprite->setPosition(m_centerPoint); //鼠标点击位置即为摇杆位置
_aJsBg->setPosition(m_centerPoint);
//摇杆设为可视
m_jsSprite->setVisible(true);
_aJsBg->setVisible(true);
return true;
}
void Joystick::onTouchMoved(Touch *pTouch, Event *pEvent)
{
auto touchPoint = pTouch->getLocation();
if (touchPoint.getDistance(m_centerPoint) > m_radius)
{
m_currentPoint = m_centerPoint + (touchPoint - m_centerPoint).getNormalized() * m_radius;
}else {
m_currentPoint = touchPoint;
}
}
void Joystick::onTouchEnded(Touch *pTouch, Event *pEvent)
{
m_currentPoint = m_centerPoint;
//摇杆操作结束,隐藏摇杆
m_jsSprite->setVisible(false);
_aJsBg->setVisible(false);
}
5. 游戏结束画面。
设计思路:
由于原本游戏没有结束标志,而是一直运行,因此给原游戏设计一个令游戏结束的机制。
在原来白红绿三球的基础上,加上第四个球——炸弹。“炸弹”如下图红圈中所示:
图 7
当移动角色碰到炸弹时,角色通过setVisible(false)函数隐藏,并创建新的精灵——“爆炸”图案,在当前角色的位置显示,以表示“角色碰到炸弹后爆炸”。
此时,通过this->unscheduleAllSelectors()停止所有的定时器调用,停止后就新的球就不会再生成了。
并且,通过Label::createWithSystemFont(...)创建新的文字显示,内容为“Game Over!”。
当前状态即为游戏结束状态,如下图所示:
图 8
核心代码:
//游戏结束
void HelloWorld::gameover() {
this->unscheduleAllSelectors(); //停止所有的定时器
auto boom2 = Sprite::create("Chapter10/boom2.png"); //创建角色碰到炸弹后爆炸的精灵
boom2->setPosition(bean->getPosition()); //根据移动精灵的位置设定爆炸精灵的位置
this->addChild(boom2, 5);
bean->setVisible(false); //隐藏移动精灵
//创建“Game Over!”的label
auto labelGameover =
Label::createWithSystemFont("Game Over!", "Consolas", 60, Size(500, 100));
labelGameover->setPosition(Vec2(visibleSize.width/2 + 15, visibleSize.height/2));
this->addChild(labelGameover, 6);
}
6. 其余。
源代码中各个地方都有一些小修改。如小球和移动角色的初始位置的相关参数,以及一些小bug。
这里举一个HelloWorldScene.cpp文件中修改的一个小bug,其余内容不再一一细述。
在创建小球的函数addBall中,有这么一条创建小球移动动作的代码:
auto moveTo = MoveTo::create(rand() % 5, Point(ball1->getPositionX(), -10));
其中,create的第一个参数描述了小球持续动作的时间。很明显,当rand()%5的随机值取到0时,游戏逻辑是错误的,运行程序后,会发现动作持续时间为0的小球出现一下就消失了,并没有掉落动作。
所以,把该代码中的“rand()%5”修改为“rand()%5 + 1”。