Cocos2dx项目实战(1)——简易时钟

结合之前所学的知识,我们来做一个小项目。

本文仅供个人记录和复习,不用于其他用途

前言

这个小程序是我在慕课网上学到的,有兴趣的可以去慕课网搜索“雷过就跑”,这位老师讲得很不错,本章用到的图片资源也可以在那里下载。

设计思路

时钟程序并不复杂,我们只需要三个场景,这三个场景对应三个层:

  • 加载场景
  • 开始场景
  • 时钟场景

对应这三个场景,我们需要相应的转换方法:

  • 创建加载场景
  • 从加载场景转换到开始场景
  • 从开始场景转换到时钟场景

另外,为了方便起见,我们最好创建一个类,用来管理这三个场景。大体上就需要这三个场景,至于具体的逻辑实现,请跟着我往下看:

SceneManager

SceneManager用于管理三个场景。

SceneManager.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef __helloworld__SceneManager__
#define __helloworld__SceneManager__
#include <iostream>
#include "cocos2d.h"
USING_NS_CC;
class SceneManager
{
public:
Scene *loadScene;
Scene *openScene;
Scene *clockScene;
void createLoadScene();
void goOpenScene();
void goClockScene();
};
#endif

代码很简单,我们创建了一个场景管理类,用来统一管理这三个场景。同时,我们还定义了相关的转换场景的方法。

SceneManager.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "SceneManager.h"
#include "LoadLayer.h"
#include "OpenLayer.h"
#include "ClockLayer.h"
void SceneManager::createLoadScene()
{
loadScene = Scene::create();
LoadLayer *layer = LoadLayer::create();
layer->tsm = this;
loadScene->addChild(layer);
}
void SceneManager::goOpenScene()
{
openScene = Scene::create();
OpenLayer *layer = OpenLayer::create();
layer->tsm = this;
openScene->addChild(layer);
Director::getInstance()->replaceScene(openScene);
}
void SceneManager::goClockScene()
{
clockScene = Scene::create();
ClockLayer *layer = ClockLayer::create();
layer->tsm = this;
clockScene->addChild(layer);
Director::getInstance()->replaceScene(clockScene);
}

createLoadScene()不用多说,类似的我们已经写了很多遍。至于这个tsm其实就是SceneManager的指针。goOpenScene()初始化了场景,并且将写好的层加入父节点,同时要记得转换场景。至于goClockScene(),也是一样。

说完了整体的场景架构,接下来就来看看具体的层是怎么实现的。

LoadLayer

LoadLayer对应的是loadScene,用于显示加载画面。

LoadLayer.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef __helloworld__LoadLayer__
#define __helloworld__LoadLayer__
#include <iostream>
#include "cocos2d.h"
#include "SceneManager.h"
USING_NS_CC;
class LoadLayer:public Layer
{
public:
CREATE_FUNC(LoadLayer);
virtual bool init();
void onScheduleOnce(float dt);
public:
SceneManager *tsm;
};
#endif

onScheduleOnce()是一个定时器用到的回调函数,而tsm指针实际上就是为了方便SceneManager转换场景,用于存储当前的SceneManager对象。

LoadLayer.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "LoadLayer.h"
bool LoadLayer::init()
{
Size winsize = Director::getInstance()->getWinSize();
CCLabelTTF *pLabel = CCLabelTTF::create("Loading...", "MONACO.TTF", 30);
pLabel->setPosition(ccp(winsize.width / 2, winsize.height / 2));
this->addChild(pLabel);
scheduleOnce(schedule_selector(LoadLayer::onScheduleOnce), 2.0);
return true;
}
void LoadLayer::onScheduleOnce(float dt)
{
tsm->goOpenScene();
}

这里使用了一个Label标签来显示Loading…字符串。另外呢,我们还使用了一个一次性定时器,在2s之后执行回调函数onScheduleOnce()。这个回调函数使用了SceneManager对象,将场景转换到openScene

简单来说,我们先显示Loading…,2s后就跳到了开始界面。所以这里的加载界面只是做一个样子,并没有和游戏的初始化结合起来(因为游戏太简单了,加载特别快)。

OpenLayer

OpenLayer对应的是openScene,用于展示开始菜单界面。

OpenLayer.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef __helloworld__OpenLayer__
#define __helloworld__OpenLayer__
#include <iostream>
#include "cocos2d.h"
#include "SceneManager.h"
USING_NS_CC;
class OpenLayer:public Layer
{
public:
CREATE_FUNC(OpenLayer);
virtual bool init();
void menuCallBack(Ref *pSender);
public:
SceneManager *tsm;
};
#endif

其他的很简单,关于这个menuCallBakc(),是一个菜单项用到的回调函数,用于实现点击菜单按钮后的效果。至于tsm,已经讲过了。

OpenLayer.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include "OpenLayer.h"
bool OpenLayer::init()
{
Size winsize = Director::getInstance()->getWinSize();
Label *label = Label::createWithTTF("CLOCK PROGRAME", "MONACO.TTF", 48);
label->setPosition(ccp(winsize.width / 2, winsize.height * 3 / 4));
this->addChild(label);
MenuItemLabel *menuItem = MenuItemLabel::create(Label::createWithTTF("BEGIN", "MONACO.TTF", 30), CC_CALLBACK_1(OpenLayer::menuCallBack, this));
menuItem->setTag(101);
menuItem->setPosition(ccp(winsize.width / 2, winsize.height * 0.3));
MenuItemLabel *menuItem_2 = MenuItemLabel::create(Label::createWithSystemFont("EXIT", "MONACO.TTF", 30), CC_CALLBACK_1(OpenLayer::menuCallBack, this));
menuItem_2->setTag(102);
menuItem_2->setPosition(ccp(winsize.width / 2, winsize.height * 0.15));
auto menu = Menu::create(menuItem, menuItem_2, NULL);
menu->setPosition(Point::ZERO);
this->addChild(menu);
return true;
}
void OpenLayer::menuCallBack(Ref *pSender)
{
switch (((MenuItem *)pSender)->getTag())
{
case 101:
{
tsm->goClockScene();
break;
}
case 102:
{
Director::getInstance()->end();
exit(0);
}
default:
break;
}
}

我们先创建一个标签,用于显示标题CLOCK PROGRAME。然后创建了两个菜单项BEGINEXIT,分别用于开始和退出。上一章已经说到,我们需要提供相应的回调函数,使得点击后产生不同的效果。至于为什么要设置tag,自然是为了区分不同的菜单项。至于回调函数menuCallBack(),我们通过提供的MenuItem对象,获取它的tag值。如果为101,那么便跳转到clockScene场景,如果为102就退出程序。

ClockLayer

ClockLayer对应clockScene,是最复杂的层。

ClockLayer.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#ifndef __helloworld__ClockLayer__
#define __helloworld__ClockLayer__
#include <iostream>
#include "cocos2d.h"
#include "SceneManager.h"
USING_NS_CC;
class ClockLayer:public Layer
{
public:
CREATE_FUNC(ClockLayer);
virtual bool init();
void menuCallBack(Ref *pSender);
void timeUpdate(float dt);
public:
SceneManager *tsm;
Sprite *_hour;
Sprite *_minute;
Sprite *_second;
Sprite *_background;
int hRotation = 0;
int mRotation = 0;
int sRotation = 0;
};
#endif

_hour代表时针,_minute代表分针,_second代表秒针,_background代表背景图片。hRotationmRotationsRotation对应时、分、秒针需要旋转的角度。

ClockLayer.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include "ClockLayer.h"
bool ClockLayer::init()
{
Size winsize = Director::getInstance()->getWinSize();
MenuItemLabel *menuItem = MenuItemLabel::create(Label::createWithTTF("BACK", "MONACO.TTF", 30), CC_CALLBACK_1(ClockLayer::menuCallBack, this));
menuItem->setPosition(ccp(winsize.width * 0.9, winsize.height * 0.9));
auto menu = Menu::create(menuItem, NULL);
menu->setPosition(Point::ZERO);
this->addChild(menu);
_hour = Sprite::create("shi.png");
_hour->setPosition(ccp(winsize.width / 2, winsize.height / 2));
_hour->setScale(0.3);
_hour->setAnchorPoint(ccp(0.5, 0));
this->addChild(_hour, 1);
_minute = Sprite::create("fen.png");
_minute->setPosition(ccp(winsize.width / 2, winsize.height / 2));
_minute->setScale(0.3);
_minute->setAnchorPoint(ccp(0.5, 0));
this->addChild(_minute, 2);
_second = Sprite::create("miao.png");
_second->setPosition(ccp(winsize.width / 2, winsize.height / 2));
_second->setScale(0.3);
_second->setAnchorPoint(ccp(0.5, 0));
this->addChild(_second, 3);
_background = Sprite::create("background.jpg");
_background->setPosition(ccp(winsize.width / 2, winsize.height / 2));
_background->setScale(0.5);
this->addChild(_background);
// 获取当前时间,用于windows下
struct tm *tm;
time_t timep;
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)
time(&timep);
#else
struct cc_timeval now;
CCTime::gettimeofdayCocos2d(&now, NULL);
timep = now.tv_sec;
#endif
tm = localtime(&timep);
// 指针偏转
mRotation = tm->tm_min * 6;
sRotation = tm->tm_sec * 6;
if (tm->tm_hour > 12)
{
hRotation = (tm->tm_hour - 12) * 5 * 6 + (mRotation / 72) * 6;
}
else
{
hRotation = tm->tm_hour * 5 * 6 + (mRotation / 72) * 6;
}
_hour->setRotation(hRotation);
_minute->setRotation(mRotation);
_second->setRotation(sRotation);
// 定时器
schedule(CC_SCHEDULE_SELECTOR(ClockLayer::timeUpdate), 1.0);
return true;
}
void ClockLayer::menuCallBack(Ref *pSender)
{
tsm->goOpenScene();
}
void ClockLayer::timeUpdate(float dt)
{
_second->setRotation(_second->getRotation() + 6);
if (_second->getRotation() == 360)
{
_minute->setRotation(_minute->getRotation() + 6);
_second->setRotation(0);
if ((int)_minute->getRotation() % 72 == 0)
{
_hour->setRotation(_hour->getRotation() + 6);
if (_minute->getRotation() == 360)
{
_minute->setRotation(0);
}
}
}
}

首先,创建一个Back菜单项,用于跳转回开始界面。然后就是将时、分、秒针的图片加入对应的精灵,并且设置好位置、大小。接着,导入时钟的背景图片。

获取时间的那一串代码,仅用于windows下、linuxmac下的又是另外一种方法:

1
2
3
4
5
6
7
8
9
10
11
struct cc_timeval now;
CCTime::gettimeofdayCocos2d(&now, NULL);
struct tm *tm;
tm = localtime(&now.tv_sec);
int year = tm->tm_year + 1900;
int month = tm->tm_mon + 1;
int day = tm->tm_mday;
int hour = tm->tm_hour;
int minute = m->tm_min;
int second = tm->tm_sec;
long millSecond = now.tv_sec * 1000 + now.tv_usec / 1000;

win32平台下:

1
2
3
4
5
6
7
8
9
10
struct tm *tm;
time_t timep;
time(&timep);
tm = localtime(&timep);
int year = tm->tm_year + 1900;
int month = tm->tm_mon + 1;
int day = tm->tm_mday;
int hour = tm->tm_hour;
int minute = tm->tm_min;
int second = tm->tm_sec;

至于指针的偏转,我们首先要知道,分针的角度是当前时间的分钟值乘以6,秒针的角度是当前时间的秒钟值乘以6。由于,时针是12小时,所以应该是当前时钟值乘以30并且加上分针角度除以12。

关于时针的角度是怎么算的,我来给大家总结一下。首先,时钟值乘以5乘以6是没有错的。但是呢,时针不可能只停留在整数,所以要根据当前分钟值来算出一个小时中时针走了多少。我们知道,分针转一圈,时针转72度,也就是说时针比分针为1:12。因此,拿分针旋转角度除以12就是时针应该多旋转的角度。注意,这里还要考虑时钟值超过12时的情况。

不过呢,我们现在仅仅只是对时钟的状态进行了初始化,我们还需要一个定时器,1s改变一次时钟,从而达到时钟计时的效果。

秒针很简单,1s变化6度即可。那么一旦秒针转了360度,那么分针转6度,秒针角度归零。至于时针,那么就是当分针角度整除72时,就旋转6度。因为一个大格有五个小格,每一个小格代表6度。当时针转6度时,分针就会旋转72度,所以要除以72。另外,分针也要记得归零。

总结

除了旋转的逻辑复杂一点点,其他的都是很容易理解的,以我们之前所学习的知识是能够做出来的。做出这个小项目也是为了整合之前的知识,弥补代码实战方面的不足。

由于表盘制作的不太精确,指针可能没有指准。效果大致如下: