0%

DragonBones官方已经提供了cocos2d-x的库文件SkeletonAnimationLibraryCPP,
目前支持到了2.2.
但是并未提供3.0版本的库,官方说3.0发布正式版时会支持.

如果现在就要使用3.0版本的话稍作修改也是可以实现的.

另外需要注意的是,在写文章时,这个库的master分支貌似有bug,需要使用dev分支的源码修改.

以下DragonBonesdb表示.

在项目的wiki中已经说明,平台有关的文件只有以下几个:

  • Cocos2dxAtlasNode.cpp db中使用使用的基本现实对象,类似cocos2d-x中的Node
  • Cocos2dxDisplayBridge.cpp dbcocos2d-x现实对象的操作中间件
  • Cocos2dxFactory.cpp db的生成工厂
  • Cocos2dxTextureAtlas.cpp db读取texture的类

其实对于Cocos2dxAtlasNode.cpp可以用Sprite代替,
Cocos2dxTextureAtlas.cpp只用用SpriteFrameCache就好了.

所以这两个类不用改,删掉就好了.

为了实现这一目的,我们需要在db导出素材时选择Zip(XML+PNGS),
这是为了我们可以使用TexturePacker来打包图片素材,用SpriteFrameCache加载.
TexturePacker的打包还是很让人称赞的.

db导出选项

导出以后解压zip,把texture文件夹下的图片用TexturePacker打包到成plist就可以用一下函数加载了:

1
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("xxx.plist");

下面修改Cocos2dxFactory.cpp.

  • CC前缀的修改
  • CCNode.h等头文件路径的修改

主要是这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//这个函数用来根据db的数据生成cocos2d-x的显示对象
Object* Cocos2dxFactory::generateDisplay(ITextureAtlas *textureAtlas, const String &fullName, Number pivotX, Number pivotY)
{
std::stringstream ss;
ss << fullName << ".png";

//用资源名直接用缓存里生成Sprite
auto sp = cocos2d::Sprite::createWithSpriteFrameName(ss.str());
auto size = sp->getContentSize();
sp->setCascadeOpacityEnabled(true);
//设置sp的注册点,注意Y方向,flash和cocos2d-x的坐标系不同
sp->setAnchorPoint(cocos2d::Point(
pivotX / (float)size.width,
1.0f - pivotY / size.height
));
return new CocosNode(sp);
}

Cocos2dxDisplayBridge.cpp基本只用修改头文件和CC前缀就好了,
这个文件只要勇于操作cocos2d-x的显示对象的各种变换和添加移除.

删除其余的Cocos2dx开头的文件,修复各种编译错误.

然后就可以用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("Zombie/t.plist");

dragonBones::Cocos2dxFactory fac;
dragonBones::XMLDataParser parser;
dragonBones::XMLDocument doc;

doc.LoadFile("Zombie/skeleton.xml");
fac.addSkeletonData(parser.parseSkeletonData(doc.RootElement()));

_zombie = fac.buildArmature("Zombie_polevaulter", "", "Zombie");
auto skeletonNode = static_cast<dragonBones::CocosNode*>(_zombie->getDisplay())->node;
skeletonNode->setPosition(50, 300);
addChild(skeletonNode);
_zombie->getAnimation()->gotoAndPlay("anim_walk");

因为db结构很好,和平台相关的类封装的都很好,
所以修改起来很方便.

其实我们只不过修改了generateDisplay函数,改为使用Sprite这样避免了对Cocos2dxAtlasNode的使用.
利用了cocos2d-x自己的素材加载函数,避免了对Cocos2dxTextureAtlas的使用.

额外一些工作就是删除文件,修改头文件路径,修改编译错误.

3.0rc0版本的cocos2d-x创建的lua版本与以往的版本文件夹结构不同:

  • frameworks
    • cocos2d-x cocos2d-x的源文件
    • runtime-src
      • Classes 项目c++源文件
      • proj.android android平台工程文件
      • proj.ios_mac ios_mac平台工程文件
      • proj.win32 win32平台工程文件
      • proj.linux linux平台工程文件
  • res 资源文件
  • runtime
  • src lua源文件

并且编译过程中利用了编译脚本做了一些资源素材的拷贝工作,例如吧src文件夹下的lua脚本和res文件夹下的素材文件拷贝到工程资源目录下.

其中win32平台的编译脚本在vs的工程设置中可以看到:

vs_copy_script

1
2
3
4
5
6
7
8
if not exist "$(OutDir)" mkdir "$(OutDir)"
if exist "$(OutDir)\Resource" rd /s /q "$(OutDir)\Resource"
mkdir "$(OutDir)\Resource"
mkdir "$(OutDir)\Resource\src"
mkdir "$(OutDir)\Resource\res"
xcopy "$(ProjectDir)..\..\cocos2d-x\cocos\scripting\lua-bindings\script" "$(OutDir)\Resource" /e /Y
xcopy "$(ProjectDir)..\..\..\src" "$(OutDir)\Resource\src" /e /Y
xcopy "$(ProjectDir)..\..\..\res" "$(OutDir)\Resource\res" /e /Y

把src拷贝到输出目录/Resource/src下,
把res拷贝到输出目录/Resource/res下,
把cdx框架中的lua源文件拷贝到输出目录/Resource下.

对应的android工程的拷贝配置文件为frameworks/runtime-src/proj.android/build-cfg.json.

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"ndk_module_path" :[
"../../cocos2d-x",
"../../cocos2d-x/cocos/",
"../../cocos2d-x/external",
"../../cocos2d-x/cocos/scripting"
],
"copy_to_assets" :[
"../../../src",
"../../../res",
"../../cocos2d-x/cocos/scripting/lua-bindings/script/"
]
}

可以通过修改目录结尾是否带/符来控制复制的是文件夹还是文件夹下的所有文件.

在ios工程中由于资源路径可以和文件夹路径无关,所以工程中直接引入了,不需要拷贝.

了解了以上复制规则,我们就可以根据自己的需要更改拷贝目录的规则了.

比如我想向以前一样,不需要把lua的源文件和资源文件分为不同的文件夹,放在Resource同级就好了.那么我们可以修改win32的脚本为:

1
2
xcopy "$(ProjectDir)..\..\..\src" "$(OutDir)\Resource" /e /Y
xcopy "$(ProjectDir)..\..\..\res" "$(OutDir)\Resource" /e /Y

android的修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"ndk_module_path" :[
"../../cocos2d-x",
"../../cocos2d-x/cocos/",
"../../cocos2d-x/external",
"../../cocos2d-x/cocos/scripting"
],
"copy_to_assets" :[
"../../../src/", //带分隔符表示文件夹下的文件,而不是文件夹
"../../../res/",
"../../cocos2d-x/cocos/scripting/lua-bindings/script/"
]
}

或者你可以根据自己的需要用luajit编译自己的lua文件,等等操作都可以放在这些脚本里.

  1. 建议给vs安装Visual Assist X插件

    插件提供了文件查找,符号查找,代码模板,高级自动完成等功能

  2. 使用tools/project-creator/create_project.py创建多平台工程

    使用vs打开projects/(appname}\proj.win32下的{appname}.sln解决方案文件.

  3. 通过vs的”解决方案管理器”中邮件解决方案可以导入默认未添加,但是你需要的功能,可以在cdx提供的相应文件夹中找到对应的*.vcxproj文件

    并在你编辑的工程文件属性中添加为依赖项

  4. 如果遇到找不到头文件的问题

    右键编辑的工程文件->属性->c++->常规->附加目录

    查找到需要的头文件的路径加进去.

cdx提供了任意属性的Action回调,
但是runAction的对象需要实现ActionTweenDelegate接口.
鉴于3.0使用了c++11,而c++11提供了std::function,
我们为什么不能利用回调来代替接口实现任意属性的缓动动作呢?
无非是传一个std::function保存在Action里而已,基本代码和ActionTween没有太大区别.

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
#ifndef __ActionTweenWithFunc_H__
#define __ActionTweenWithFunc_H__

#include "CCActionInterval.h"

class ActionTweenWithFunc : public cocos2d::ActionInterval
{
public:
static ActionTweenWithFunc* create(float duration, std::function<void(float cur_num)> func, float from, float to);

bool initWithDuration(float duration, std::function<void(float cur_num)> func, float from, float to);

// Overrides
void startWithTarget(cocos2d::Node *target) override;
void update(float dt) override;
ActionTweenWithFunc* reverse() const override;
ActionTweenWithFunc* clone() const override;
protected:
std::function<void(float cur_num)> _up_func;
float _from, _to;
float _delta;
};

#endif /* __ActionTweenWithFunc_H__ */


#include "ActionTweenWithFunc.h"

ActionTweenWithFunc* ActionTweenWithFunc::create(float aDuration, std::function<void(float cur_num)> func, float from, float to)
{
ActionTweenWithFunc* pRet = new ActionTweenWithFunc();
if (pRet && pRet->initWithDuration(aDuration, func, from, to))
{
pRet->autorelease();
}
else
{
CC_SAFE_DELETE(pRet);
}
return pRet;
}

bool ActionTweenWithFunc::initWithDuration(float aDuration, std::function<void(float cur_num)> func, float from, float to)
{
if (ActionInterval::initWithDuration(aDuration))
{
_up_func = func;
_to = to;
_from = from;
return true;
}
return false;
}

void ActionTweenWithFunc::startWithTarget(cocos2d::Node *target)
{
ActionInterval::startWithTarget(target);
_delta = _to - _from;
}

void ActionTweenWithFunc::update(float dt)
{
float cur_num = _to - _delta * (1 - dt);
_up_func(cur_num);
}

ActionTweenWithFunc* ActionTweenWithFunc::reverse() const
{
return ActionTweenWithFunc::create(_duration, _up_func, _to, _from);
}

ActionTweenWithFunc* ActionTweenWithFunc::clone() const
{
return ActionTweenWithFunc::create(_duration, _up_func, _from, _to);
}

1
2
typedef void (Base::*bfnPrint)();
bfnPrint func = ::Print;

就是说虽然函数指针func取值对象用的是父类.
但是如果Print函数是虚函数,
利用子类实例调用func函数的时候,实际上会调用到子类的Print函数.

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
// virtual_functions.cpp
// compile with: /EHsc
#include <iostream>
using namespace std;

class Base
{
public:
virtual void Print();
};

void Base :: Print()
{
cout << "Print function for class Base\n";
}

class Derived : public Base
{
public:
virtual void Print();
};

void Derived :: Print()
{
cout << "Print function for class Derived\n";
}

int main()
{
Base bObject;
Derived dObject;

typedef void (Base::*bfnPrint)();
bfnPrint func = &Base::Print;


(&bObject->*func)();
(&dObject->*func)();
}

print out

1
2
Print function for class Base
Print function for class Derived

经过不同平台的适配,cdx的touches处理是从
cocos2dx/platform/CCEGLViewProtocol.cpp::handleTouchesBegin(int num, int ids[], float xs[], float ys[])开始的,
传进来的参数是一个touch的点数,touch的id, x, y的数组

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
void CCEGLViewProtocol::handleTouchesBegin(int num, int ids[], float xs[], float ys[])
{
CCSet set;
for (int i = 0; i < num; ++i)
{
//遍历所有传进来的touch点信息
int id = ids[i];
float x = xs[i];
float y = ys[i];
//从touch存储字典中查找当前touch点的信息
CCInteger* pIndex = (CCInteger*)s_TouchesIntergerDict.objectForKey(id);
int nUnusedIndex = 0;

// it is a new touch
if (pIndex == NULL)
{
//touch字典中没有当前的touch点数据
//获取一个touch的标示
nUnusedIndex = getUnUsedIndex();

// The touches is more than MAX_TOUCHES ?
if (nUnusedIndex == -1) {
CCLOG("The touches is more than MAX_TOUCHES, nUnusedIndex = %d", nUnusedIndex);
continue;
}

CCTouch* pTouch = s_pTouches[nUnusedIndex] = new CCTouch();
//初始化本地touch对象的数据
pTouch->setTouchInfo(nUnusedIndex, (x - m_obViewPortRect.origin.x) / m_fScaleX,
(y - m_obViewPortRect.origin.y) / m_fScaleY);

//CCLOG("x = %f y = %f", pTouch->getLocationInView().x, pTouch->getLocationInView().y);

CCInteger* pInterObj = new CCInteger(nUnusedIndex);
//将touch储存在字典中
s_TouchesIntergerDict.setObject(pInterObj, id);
set.addObject(pTouch);
pInterObj->release();
}
}

if (set.count() == 0)
{
CCLOG("touchesBegan: count = 0");
return;
}
//交给touch时间处理器处理这些本地化以后的touch对象
m_pDelegate->touchesBegan(&set, NULL);
}

继续跟踪m_pDelegate->touchesBegan(&set, NULL)的处理在cocos2dx/touch_dispatcher/CCTouchDispatcher.cpp

1
2
3
4
5
6
7
8
9
void CCTouchDispatcher::touchesBegan(CCSet *touches, CCEvent *pEvent)
{
if (m_bDispatchEvents)
{
//交给了本来的touches函数处理,参数pEvent实际是NULL
//第三个参数是事件标示,begin, move, end
this->touches(touches, pEvent, CCTOUCHBEGAN);
}
}

跟下去

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
void CCTouchDispatcher::touches(CCSet *pTouches, CCEvent *pEvent, unsigned int uIndex)
{
CCAssert(uIndex >= 0 && uIndex < 4, "");

CCSet *pMutableTouches;
//一个标示,当true的时候不允许直接添加新的handler对象
m_bLocked = true;

// optimization to prevent a mutable copy when it is not necessary
unsigned int uTargetedHandlersCount = m_pTargetedHandlers->count();
unsigned int uStandardHandlersCount = m_pStandardHandlers->count();
bool bNeedsMutableSet = (uTargetedHandlersCount && uStandardHandlersCount);

pMutableTouches = (bNeedsMutableSet ? pTouches->mutableCopy() : pTouches);

struct ccTouchHandlerHelperData sHelper = m_sHandlerHelperData[uIndex];
//
// process the target handlers 1st
//
if (uTargetedHandlersCount > 0)
{
CCTouch *pTouch;
CCSetIterator setIter;
for (setIter = pTouches->begin(); setIter != pTouches->end(); ++setIter)
{
//遍历所有传进来的本地化touch对象
pTouch = (CCTouch *)(*setIter);

CCTargetedTouchHandler *pHandler = NULL;
CCObject* pObj = NULL;
CCARRAY_FOREACH(m_pTargetedHandlers, pObj)
{
//遍历所有已经注册的toucheHandler
pHandler = (CCTargetedTouchHandler *)(pObj);

if (! pHandler)
{
break;
}

bool bClaimed = false;
if (uIndex == CCTOUCHBEGAN)
{
//调用handler的ccTouchBegan方法.
//并捕获返回值
bClaimed = pHandler->getDelegate()->ccTouchBegan(pTouch, pEvent);

if (bClaimed)
{
//如果handler返回true
//把touch对象标记到handler的处理数组中
pHandler->getClaimedTouches()->addObject(pTouch);
}
}
else if (pHandler->getClaimedTouches()->containsObject(pTouch))
{
//该touch对象在handler的处理数据中
// moved ended canceled
bClaimed = true;
//根据不同状态,调用handler的不同函数
switch (sHelper.m_type)
{
case CCTOUCHMOVED:
pHandler->getDelegate()->ccTouchMoved(pTouch, pEvent);
break;
case CCTOUCHENDED:
pHandler->getDelegate()->ccTouchEnded(pTouch, pEvent);
pHandler->getClaimedTouches()->removeObject(pTouch);
break;
case CCTOUCHCANCELLED:
pHandler->getDelegate()->ccTouchCancelled(pTouch, pEvent);
pHandler->getClaimedTouches()->removeObject(pTouch);
break;
}
}

//这里涉及到是否处理下一个handler
//一定是同时两个条件满足才会停止处理下一个
//bClaimed 被handler处理了
//isSwallowsTouches 是swallows的,也可以理解为handler是阻挡touch继续传递的
//isSwallowsTouches 是怎么设置的呢,稍候会说
if (bClaimed && pHandler->isSwallowsTouches())
{
if (bNeedsMutableSet)
{
pMutableTouches->removeObject(pTouch);
}

break;
}
}
}
}

//
// process standard handlers 2nd
//

//这里是处理多点触控了,原理差不多
if (uStandardHandlersCount > 0 && pMutableTouches->count() > 0)
{
CCStandardTouchHandler *pHandler = NULL;
CCObject* pObj = NULL;
CCARRAY_FOREACH(m_pStandardHandlers, pObj)
{
pHandler = (CCStandardTouchHandler*)(pObj);

if (! pHandler)
{
break;
}

switch (sHelper.m_type)
{
case CCTOUCHBEGAN:
pHandler->getDelegate()->ccTouchesBegan(pMutableTouches, pEvent);
break;
case CCTOUCHMOVED:
pHandler->getDelegate()->ccTouchesMoved(pMutableTouches, pEvent);
break;
case CCTOUCHENDED:
pHandler->getDelegate()->ccTouchesEnded(pMutableTouches, pEvent);
break;
case CCTOUCHCANCELLED:
pHandler->getDelegate()->ccTouchesCancelled(pMutableTouches, pEvent);
break;
}
}
}

if (bNeedsMutableSet)
{
pMutableTouches->release();
}

//
// Optimization. To prevent a [handlers copy] which is expensive
// the add/removes/quit is done after the iterations
//
m_bLocked = false;

//这里是处理touch期间不能添加和删除handler的地方了
//touch期间handler的rm和add都会放在一个数组里,这里touch处理完了
//就开始处理这些数组了
if (m_bToRemove)
{
m_bToRemove = false;
for (unsigned int i = 0; i < m_pHandlersToRemove->num; ++i)
{
forceRemoveDelegate((CCTouchDelegate*)m_pHandlersToRemove->arr[i]);
}
ccCArrayRemoveAllValues(m_pHandlersToRemove);
}

if (m_bToAdd)
{
m_bToAdd = false;
CCTouchHandler* pHandler = NULL;
CCObject* pObj = NULL;
CCARRAY_FOREACH(m_pHandlersToAdd, pObj)
{
pHandler = (CCTouchHandler*)pObj;
if (! pHandler)
{
break;
}

if (dynamic_cast<CCTargetedTouchHandler*>(pHandler) != NULL)
{
forceAddHandler(pHandler, m_pTargetedHandlers);
}
else
{
forceAddHandler(pHandler, m_pStandardHandlers);
}
}

m_pHandlersToAdd->removeAllObjects();
}

if (m_bToQuit)
{
m_bToQuit = false;
forceRemoveAllDelegates();
}
}

我们预留了一些问题:

  • touch的优先级是怎么确定的?
  • 一个handler被处理之后是不是其他的handler都无法处理了?
  • 注册touchhandler的时候基本都是在调用这个函数
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
//pDelegate 不用说了,注册的处理对象
//nPriority 这就是处理的优先级了,数字越小,优先级越高
//看以参看forceAddHandler函数
//bSwallowsTouches 这个参数就代表这个handler是否会吞掉touch
//是的话,就不会处理优先级比他低的handler了
//否的话,就会继续处理优先级低的handler
//这就是为什么scrollview处理之后,scrollview的容器Layer,即使优先级更低
//也能接收到touch事件了,因为scrollview是不吞掉touch的

void CCTouchDispatcher::addTargetedDelegate(CCTouchDelegate *pDelegate, int nPriority, bool bSwallowsTouches)
{
CCTouchHandler *pHandler = CCTargetedTouchHandler::handlerWithDelegate(pDelegate, nPriority, bSwallowsTouches);
if (! m_bLocked)
{
forceAddHandler(pHandler, m_pTargetedHandlers);
}
else
{
/* If pHandler is contained in m_pHandlersToRemove, if so remove it from m_pHandlersToRemove and return.
* Refer issue #752(cocos2d-x)
*/
if (ccCArrayContainsValue(m_pHandlersToRemove, pDelegate))
{
ccCArrayRemoveValue(m_pHandlersToRemove, pDelegate);
return;
}

m_pHandlersToAdd->addObject(pHandler);
m_bToAdd = true;
}
}

flash-console  这是一个用于调试swf的类库,用于添加到舞台上使用
使用方法非常的简单

1
2
import com.junkbyte.console.Cc;
Cc.startOnStage(this, "");

以上代码就能让调试面板在舞台上显示出来,
如果想在面板上输出log信息,可以用一下语句

1
2
3
4
Cc.config.commandLineAllowed = true // Enables full commandLine features
Cc.config.tracing = true; // also send traces to flash's normal trace()
Cc.config.maxLines = 2000; // change maximum log lines to 2000, default is 1000
Cc.config.commandLineAutoScope = true;  //自动切换命令行当前域(比较有用)

还可以根据自己的需要对调试面板进行一下属性设置

1
2
3
4
5
6
7
8
9
10
11
//start之前的配置
Cc.config.commandLineAllowed = true // Enables full commandLine features
Cc.config.tracing = true; // also send traces to flash's normal trace()
Cc.config.maxLines = 2000; // change maximum log lines to 2000, default is 1000

Cc.startOnStage(this); // finally start with these config

//start之后的设置
Cc.commandLine = true;
Cc.width = 460;
Cc.height = 500;

下面是命令行的使用

  • 默认的scope是stage,在命令行输入width,回车可以打印stage的width属性
  • 输入过程是有提示的,按tab可以实现自动补全
  • 另外使用/command命令可以查看内置的一下命令

我们还可以查看和调用自身内部对象的属性和函数,
例如我们有自己的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class InnerData {
public var va:int;
public var vb:String;

public function InnerData() {
va = 10;
vb = "hello word";
}

public function sum(a:int, b:int):int {
return a + b;
}

}

首先要注册带Cc中

1
Cc.store("inner", new InnerData ());

在运行过程中,命令行中输入$inner,回车

这时候会切换到我们的内置数据结构中,我们可以通过代码调用sum函数

1
sum(10,19)

命令行会返回给我们结果.

还有很多强悍的功能,例如watch,官网写的都很详细

演示程序

在libgdx中默认对返回按键的处理是退出程序,
如果我们希望加一个退出确认框,
那如何截获返回按键呢?

首先要知道InputProcessor接口,
这个接口定义了很多输入处理函数,如:

  • 按键按下
  • 点击屏幕
  • 拖动
  • 等等

InputProcessor使用方式如下:

1
Gdx.input.setInputProcessor(inputProcessor);

所以我们需要自己实现一个InputProcessor,
然后设置一下就行了.

那么还有一个问题,
Stage实际上已经实现了InputProcessor,
因为他要处理Actor的点击之类的事情.
那么我们既想保持Stage作出输入处理类,
又想实现自己对输入的一些控制怎么办呢?

  • 继承Stage,复写需要变更的方法
  • 利用多重输出处理类InputMultiplexer
1
2
3
4
InputMultiplexer multiplexer = new InputMultiplexer();
multiplexer.addProcessor(new MyUiInputProcessor());
multiplexer.addProcessor(new MyGameInputProcessor());
Gdx.input.setInputProcessor(multiplexer);

作为我们来讲,一个processor设置成stage,一个设置成自己的实现就可以了.

另外我们需要手动设置input截获返回按键

1
Gdx.input.setCatchBackKey(true);

截获菜单按键也是一样的,如果不手动设置,系统会自己处理掉.

现在复写InputProcessor的keyUp方法,因为keyDown如果按住不放的话会一直调用

1
2
3
4
5
6
7
8
9
10
11
@Override
public boolean keyUp(int keycode) {
//判断按下的是返回按键
if(Input.Keys.BACK == keycode){
//打印一句log
Gdx.app.log("s", "back key typed");
//这里就是推出应用,当然可以定义自己的处理
Gdx.app.exit();
}
return false;
}

下一个,怎么在游戏中保持屏幕唤醒?

在Android项目的主文件中可以看到初始化的时候用到了AndroidApplicationConfiguration,
有一个属性就是是否保持屏幕唤醒useWakelock,设置为true.还有其他一下选项,可以看看api.

但是还有一个重要的东西,
Android的很多功能都是需要在配置文件中申请权限的,
这个屏幕唤醒也是其中一项:需要在AndroidManifest.xml配置:

1
<uses-permissionandroid:name="android.permission.WAKE_LOCK" />

这个选项和application同级.

加上这个权限配置才能真正实现屏幕保持唤醒.

http://code.google.com/p/libgdx/wiki/AssetManager,
wiki里作者给我们列举了使用AssetManager的几种好处:

  • 加载大部分资源使用异步的方式,这样就能在加载的同时不阻塞渲染进程
  • 实现了引用计数,当A和B都依赖C素材的时候,C只有在A,B都销毁了才会销毁.这也意味着即使一个资源加载了很多次,在内存中也之后一份
  • 使用一个单一的实力管理所有素材
  • 可以实现素材的缓存

创建AssetManager的方法是

1
AssetManager manager = new AssetManager();

当加载素材的时候,
AssetManager需要知道用哪个loader来加载这个素材.
这些loader都实现了AssetLoaders里的方法,
其中分为两类:

  • SynchronourAssetLoader
  • AsynchronousAssetLoader

前一种在渲染进程加载资源,
后面一种在另外的进程加载资源.

例如创建Pixmap需要Texture,
并且有些OpenGL素材依赖于渲染进程,
下面是系统已经实现的一些资源和loader的对应关系:

  • Pixmaps via PixmapLoader
  • Textures via TextureLoader
  • BitmapFonts via BitmapFontLoader
  • TextureAtlases via TextureAtlasLoader
  • TiledAtlases via TiledAtlasLoader
  • TileMapRenderers via TileMapRendererLoader
  • Music instances via MusicLoader
  • Sound instances via SoundLoader

使用方法是:

1
2
3
manager.load("data/mytexture.png", Texture.class);
manager.load("data/myfont.fnt", BitmapFont.class);
manager.load("data/mymusic.ogg", Music.class);

这些资源将添加入加载队列,
如果需要在资源加载时使用特殊的初始化参数,
可以参照下面的方法:

1
2
3
4
5
6
7
//加载TextureParameter所使用的参数
//每种资源所传递的加载参数类型是不一样的
TextureParameter param = new TextureParameter();
param.minFilter = TextureFilter.Linear;
param.genMipMaps = true;
//load时传入就可以了
manager.load("data/mytexture.png", Texture.class, param);

目前为止,素材只是添加到了加载丢列,而不会开始加载,我们需要逐帧调用
AssetManager#update()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
public MyAppListener implements ApplicationListener {

public void render() {
if(manager.update()) {
// updata()返回true,证明所有资源加载完成
// 可以执行对应的操作了
}

// 获取加载进度,这个还是比较坑爹的,之后会说明
float progress = manager.getProgress()
... left to the reader ...
}
}

如果需要强制完成所有加载,可以调用如下函数.
意思是将异步加载专为同步加载,
这个函数会阻塞到队列中的素材加载完成.

1
manager.finishLoading();

获取素材的方式是:

1
2
Texture tex = manager.get("data/mytexture.png", Texture.class);
BitmapFont font = manager.get("data/myfont.fnt", BitmapFont.class);

判断某单一素材是否加载完成的方式是:

1
2
3
4
if(manager.isLoaded("data/mytexture.png")) {
// texture is available, let's fetch it and do do something interesting
Texture tex = manager.get("data/mytexture.png", Texture.class);
}

销毁素材:

1
manager.unload("data/myfont.fnt");

销毁所有素材的方式是:

1
2
3
4
5
manager.clear();

//或者
//区别是dispose会把AssetManager也销毁掉
manager.dispose();

另外,我们可以自己定义文件的读取方式.
例如如果素材自己加密了,我们需要解密才能读取
这时候可以实现自己的FileHandleResolver.

里面就一个函数

1
2
//根据文件名,返回一个FileHandle对象
public FileHandle resolve (String fileName);

new的时候传递进去就行了

1
AssetManager manager = new AssetManager(new ExternalFileHandleResolver());

另外一个需要说明的是,当程序切换到后台,很有可能为了节省内存,
素材会被销毁.
那么程序切换到前台是需要重新加载这些资源?
使用AssetManager加载的资源无需担心,
但是如果不是,可以使用:

1
Texture.setAssetManager(manager);

的方式来把这些资源让AssetManager管理.

另外,可能系统提供的loader不满足我们的需要,
那我们可以自己定义Loader来加载我们自己的资源.

从AssetManager的源码中我们可以看到他是怎么添加Loader的:

1
2
3
4
5
6
7
8
9
10
11
12
public AssetManager (FileHandleResolver resolver) {
setLoader(BitmapFont.class, new BitmapFontLoader(resolver));
setLoader(Music.class, new MusicLoader(resolver));
setLoader(Pixmap.class, new PixmapLoader(resolver));
setLoader(Sound.class, new SoundLoader(resolver));
setLoader(TextureAtlas.class, new TextureAtlasLoader(resolver));
setLoader(Texture.class, new TextureLoader(resolver));
setLoader(Skin.class, new SkinLoader(resolver));
setLoader(TileAtlas.class, new TileAtlasLoader(resolver));
setLoader(TileMapRenderer.class, new TileMapRendererLoader(resolver));
//..........不用看的代码
}

如果需要定义自己的Loader可以参照这些Loader实现,
MusicLoader同步加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MusicLoader extends SynchronousAssetLoader<Music, MusicLoader.MusicParameter> {
public MusicLoader (FileHandleResolver resolver) {
super(resolver);
}

//调用load时,返回需要创建的对象
@Override
public Music load (AssetManager assetManager, String fileName, MusicParameter parameter) {
return Gdx.audio.newMusic(resolve(fileName));
}

//这里返回本素材的依赖数组
//例如bitmapfont在配置文件外需要文字图片
//那么可以在这里返回
@Override
public Array<AssetDescriptor> getDependencies (String fileName, MusicParameter parameter) {
return null;
}

static public class MusicParameter extends AssetLoaderParameters<Music> {
}
}

异步加载器PixmapLoader

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
public class PixmapLoader extends AsynchronousAssetLoader<Pixmap, PixmapLoader.PixmapParameter> {
public PixmapLoader (FileHandleResolver resolver) {
super(resolver);
}

Pixmap pixmap;

//非渲染进程调用函数
//没有返回值
@Override
public void loadAsync (AssetManager manager, String fileName, PixmapParameter parameter) {
pixmap = null;
pixmap = new Pixmap(resolve(fileName));
}

//渲染进程调用函数
//返回需要创建的对象,返回空证明还没加载完
@Override
public Pixmap loadSync (AssetManager manager, String fileName, PixmapParameter parameter) {
return pixmap;
}
//同样,返回依赖项
@Override
public Array<AssetDescriptor> getDependencies (String fileName, PixmapParameter parameter) {
return null;
}

static public class PixmapParameter extends AssetLoaderParameters<Pixmap> {
}
}

ok,现在吐槽AssetManager的加载进度.

我如果调用AssetManager#load函数2次,
那么,返回的进度只会出现0,0.5,1.

也就是说AssetManager的进度完全不考虑依赖资源,
load几个,就计算几个.
因此,除非资源特别多,不要指望他返回的进度是平滑的.

在我看来这是很大的问题…..

关于素材,我们需要知道的一个基本的类是Texture,
它是一张图片在程序内存中的映像.

如果需要程序显示一张图片,那么就需要把它转化为一个Texture的实例,
有一个叫SpriteBatch的东西知道怎么把它绘制在屏幕上.

而这个Texture对加载的图片是有要求的长宽都必须是2的整数次幂.
很显然,我们实际中使用的素材不可能原本就是这个大小.
于是我们会把几张实际使用的图片,拼到一张1024*1024的图片上.
当然,有工具替我们完成这些,所以不用着急,先理解一下这些概念.

这样一来,我们可能在程序中使用的不是整个Textrue,而是Textrue中的某一个区域.
于是便有了TextureRegion,可以把它理解为Texture的一个装饰类,
内部持有一个Texture实例,记录了Texture的裁剪信息.
以便我们标记需要使用的区域,
SpriteBatch也提供了绘制TextureRegion的效用,之绘制这个裁剪出来的区域.

那么既然有工具来把很多图片拼凑到一张图片上的工具,
libgdx-texturepacker-gui.

理论上我们应该也不用自己去计算原始图片在这张大图上的位置信息,
实际上这个 工具生成了两个文件,一个裁剪信息文件,一个合并好的png文件.

这样一来,我们应该也不用自己写代码解析这个配置文件吧.

是的,libgdx提供了TextureAtlas这个类用来解析这个配置文件
加载大的png文件,并切割成对应的TextureRegion供我们使用.

当然,你可以把图片都弄成2的整次幂,
但是绝对不提倡,因为我猜测,libgdx的内存和cocos2d是相同的,
因为Texture占用的内存和它存储的图片大小有关.

例如33的Texture实际上占用了88的空间.

我这里已经下载好了texturepacker-gui这个工具,
这个工具会:

  • 把输入目录中的图片文件
  • 拼接到一张或多张png文件中
  • 生成一个配置文件

你可以大致阅读一下这个配置文件,
一般叫pack,
里面记录了生成的png文件们的路径,
拼合的文件的标识符,也就是原始图片的文件名,
以及他们的剪切信息.

在解压以后的目录里有一个test-me,
我这里直接使用它的输出文件测试.

test-me里out文件夹下的文件复制到android工程的assets文件夹下,
于是那里多了两个文件input1.png和pack.

刷新这个工程,否则程序可能无法读取到这两个文件,很神奇.

如果pack文件和png文件在同一目录下,我们创建atlas的时候只需传递pack的路径就可以了.
因为开源嘛,有兴趣可以看一下TextureAtlas的源码.

1
2
3
4
5
6
7
8
//创建TextureAtlas,并读取assets文件夹下的pack文件
TextureAtlas ta = new TextureAtlas("pack");
//获取切割好的素材,"test01"是个标识名,可以在pack文件中看到,实际上就是打包之前的文件名
TextureRegion tr = ta.findRegion("test01");
//用切割好的素材创建一个Image对象
img = new Image(tr);
//添加到舞台上显示出来
stage.addActor(img);

以上简单的几行代码说明了TextureAtlas的使用方法.

现在了解了怎样加载一张图片,你可能会好奇,怎样获得加载进度呢?

libgdx的AssetManager不但为我们提供了异步加载和素材管理功能,
也为我们提供了获取加载进度的方法.
不过它提供的加载进度比较坑爹,
之后我们会介绍.