我:前段时间去过动画制作组,你们两个的名字与动画制作组中的SpriteFrame和Animation很相似,难道说你们之间有什么关系?
SpriteFrameCache和AnimationCache:没错,我们都是仓库管理员,管理着存放他们的仓库。
我:果然,Cache,看名字就应该能想到。不过你们存储他们的意义何在?
SpriteFrameCache:想必SpriteFrame和您提起过,他为了节省内存空间的使用,对于指定给他的图片,只有在真正使用这张图片时,即调用他的getTexture()时,他才将图片加载进内存创建纹理,这样避免了可能出现的长时间不使用的图片却一直占用着内存空间的情况。但这样的做法对于一些较大的图片或是一些经常使用的图片却并不妥当,因为加载图片必定会耗用一定的时间,由其对于大图片耗用的时间就更长。游戏运行过程中经常的加载图片或是突然加载了一张大图片,很可能使游戏出现不同程度的卡顿,这样游戏的体验性就大大的降低了。比较合理的做法是在游戏开始前有一个加载资源的过程,将经常使用的图片或是比较大的图片先加载进内存并创建为SpriteFrame存入我这个仓库里,实际使用的时候再从我这里取走,这样牺牲了空间节省了时间。
AnimationCache:我与SpriteFrameCache存在的意义大致相同,不过我的着眼点不在图片,而是在整个动画。一个动画通常由多张图片组成,就算每张图片都很小,但如果数量很庞大,那么整个动画所占用的资源也不可小觑。试想如果在即将要播放动画时才一张一张的加载图片组合成动画,游戏就定在那里,对于玩家来说将是多么痛苦的事情。所以在游戏开始前加载资源的过程中,一般也会将经常用到的动画创建出来放到我这里,实际使用的时候再从我这里取走,同样牺牲了空间节省了时间。同时我与SpriteFrameCache也是唇亡齿寒的关系,经常播放的动画所使用的图片自然也就是经常使用的图片,所以这些图片也必须先被创建为SpriteFrame存入SpriteFrameCache那里。没有SpriteFrameCache的鼎力相助,我无法完成我的工作。
(两人此时正深情的对视着……)
我:额,两位。
(无应答)
我:我说两位。
(……)
我:下班了。
SpriteFrameCache和AnimationCache:走了, 走了。终于下班了,去哪儿喝一杯啊。
我:……
我:还是来说说你们是如何工作的吧。SpriteFrameCache,既然AnimationCache需要你的协助,那么还是从你开始,先来说说如何使用你。
SpriteFrameCache:使用我的方式有很多,但我实际需要的其实与SpriteFrame需要的相同,毕竟我是存储他的仓库,我的工作也包含创建他。还记得创建SpriteFrame需要的那些参数吧(texture,rect,rotated,offset,originalSize),不过您无需将这些参数像创建SpriteFrame那样一一指定给我,您可以将这些参数写在一个plist文件中,然后将这个plist文件指定给我,
// 如何使用图片的参数存放在plist文件中,图片名
void addSpriteFramesWithFile(const std::string& plist,const std::string& textureFileName);
/* 函数内部
* 调用Director::getInstance()->getTextureCache()->addImage()将图片加载为纹理。
* 调用addSpriteFramesWithFile()。
*/
当然,如果您已经创建好了纹理也可以,
void addSpriteFramesWithFile(const std::string&plist,Texture2D *texture);
/* 函数内部
* 调用FileUtils::getInstance()->getValueMapFromFile()读取plist文件内容。
* 调用addSpriteFramesWithDictionary()。
*/
又或者,不指定plist文件,您将plist文件的内容指定给我也没问题,
void addSpriteFramesWithFileContent(const std::string& plist_content,Texture2D *texture);
/* 函数内部
* 调用FileUtils::getInstance()->getValueMapFromData()读取plist文件内容。
* 调用addSpriteFramesWithDictionary()。
*/
void addSpriteFramesWithFile(const std::string& plist);
/* 函数内部
* 使用FileUtils::getInstance()->fullPathForFilename()获取plist文件的全路径。
* 调用FileUtils::getInstance()->getValueMapFromFile()读取plist文件内容。
* 尝试从plist中找Metadata域,如果找到了则从Metadata域的textureFileName域得到图片的文件名,
* 之后将plist所在路径与该图片名使用FileUtils::getInstance()->fullPathFromRelativeFile()拼在一起作为图片的完整路径名。
* 如果没有在plist中找到图片名,则将plist文件的全路径中文件名的后缀由.plist更改为.png,以此更改后的全路径作为图片的完整路径名。
* 调用Director::getInstance()->getTextureCache()->addImage()将图片加载为纹理。
* 调用addSpriteFramesWithDictionary()。
*/
最后,如果您已经创建好了SpriteFrame,那我是再高兴不过的了,
// 每个SpriteFrame在SpriteFrameCache中都有个名字,这里作为参数传递了,通常这个名字放在之前提到的plist文件中。
void addSpriteFrame(SpriteFrame *frame,const std::string& frameName);
/* 函数内部 * 调用_spriteFrames.insert()存储SpriteFrame与其名字的映射。 */
此外还要说明一点,我存储的这些SpriteFrame不仅可以用于动画制作,您还可以使用他们直接来创建Sprite,
// xxx.plist中存储了名为xxx.png的SpriteFrame信息。
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("xxx.plist");
// Sprite::createWithSpriteFrameName()会从SpriteFrameCache中查找指定的SpriteFrame,然后使用找到的SpriteFrame创建Sprite。
auto sprite = Sprite::createWithSpriteFrameName("xxx.png");
我:很多函数都调用了addSpriteFramesWithDictionary()。
SpriteFrameCache:嗯,这个函数乍一看很复杂,其实他只做了三件事,
1、继续解析plist文件(plist中存储SpriteFrame信息的格式可以有多种,通过Metadata域中的format域进行标识,不同的格式提供SpriteFrame信息的方式不同。举个简单的例子,比如创建SpriteFrame所需要的rect这个变量,既可以使用一个key-value表示
<key>frame</key> <string>{{2,2},{250,30}}</string>
也可以使用四个key-value表示
<key>x</key>
<float>2</float>
<key>y</key>
<float>2</float>
<key>width</key>
<float>250</float>
<key>height</key>
<float>30</float>
这些不同的表示形式就对应不同的format。其中当format为3时在plist文件中frames域中aliases域会给出SpriteFrame的别名,这些别名与原SpriteFrameName的映射我就会存在_spriteFramesAliases变量中。
)。
2、使用解析出来的信息创建SpriteFrame.
3、存储SpriteFrame与其名字的映射。
我:存储SpriteFrame的plist文件的格式竟然还有多种。
SpriteFrameCache:不用担心,plist文件一般是通过软件创建的,比如TexturePacker,cocostudio的合图功能等等。这些格式是由这些软件来负责的,而解析plist也是我的内部实现的,对于用户只需要使用软件创建plist,之后调用我们添加SpriteFrame即可。
我:添加SpriteFrame的方式有这么多,删除的呢?
SpriteFrameCache:删除的方式也有几种,
您可以指定加载过的plist或者其内容,这样从这个plist中添加的SpriteFrame就会被删除,
void removeSpriteFramesFromFile(const std::string& plist);
/* 函数内部 * FileUtils::getInstance()->fullPathForFilename()读取plist文件的内容。 * 调用removeSpriteFramesFromDictionary()。 */
void removeSpriteFramesFromFileContent(const std::string& plist_content);
/* 函数内部 * 调用FileUtils::getInstance()->getValueMapFromData()读取plist文件内容。 * 调用removeSpriteFramesFromDictionary()。 */
// 以上两个函数都调用了removeSpriteFramesFromDictionary()。
void removeSpriteFramesFromDictionary(ValueMap& dictionary);
/* 函数内部 * dictionary中存储了plist文件的内容,其中可以找到待删除的SpriteFrame的名字,一一从_spriteFrames中删除。 */
或者通过texture,
void removeSpriteFramesFromTexture(Texture2D* texture);
/* 函数内部 * 遍历_spriteFrames中的每一个映射,通过映射获得每一个SpriteFrame。 * SpriteFrame->getTexture()与给定的texture比对,相同则删除该SpriteFrame。 */
又或者通过SpriteFrame的名字,
void removeSpriteFrameByName(const std::string& name);
/* 函数内部 * 如果给定的是SpriteFrame的别名,则会在_spriteFramesAliases中找到其原本的名字。 * 使用SpriteFrame原本的名字删除_spriteFrames中的此SpriteFrame。 */
或者直接删除所有的,
void removeUnusedSpriteFrames();
/* 函数内部 * 清空_spriteFrames。 */
此外,还有一个比较有用的方法,删除掉未被使用的SpriteFrame,
void removeUnusedSpriteFrames();
/* 函数内部 * 删除_spriteFrames中引用计数为1的SpriteFrame(引用计数为1代表此SpriteFrame只被SpriteFrameCache使用)。 */
我:我看还有个获取的方法。
SpriteFrameCache:获取很简单,只有一种方式,
SpriteFrame* getSpriteFrameByName(const std::string& name);
/* 函数内部
* 用给定的name在_spriteFrames中寻找。
* 如果没找到则用name在_spriteFramesAliases中查找别名,并用别名在_spriteFrames中寻找。
* 最终找到了返回SpriteFrame,未找到返回nullptr。
*/
我:你在介绍使用方式的同时也介绍了不少实现方式,同时提到了许多你使用的变量,简单做个总结。
SpriteFrameCache:嗯,好的。其实我只用到了三个变量,
Map<std::string,SpriteFrame*> _spriteFrames; // 存储SpriteFrame的名字与SpriteFrame之间的映射。
ValueMap _spriteFramesAliases; // 存储SpriteFrame的别名与SpriteFrame的名字之间的映射。
std::set<std::string>* _loadedFileNames; // 存储加载过的plist文件名。加载过的plist不会重复加载。
其中最核心的就是_spriteFrames这个变量。每个SpriteFrame在我这里都有个名字,从我这里取走SpriteFrame或是删除SpriteFrame都需要提供他们的名字。
我:终于,你的工作差不多都聊完了。
SpriteFrameCache:其实还未完全,我的实现中还支持SpriteFrame的PolygonInfo以及.9.png类型的图片。
我:这些等我对他们了解了之后,再回过头来看吧。
我:和SpriteFrameCache聊了很长时间,现在终于轮到你了AnimationCache。
AnimationCache:我的使用方式没有SpriteFrameCache的那么多,添加Animation的方式有两种,可以通过plist文件添加,
void addAnimationsWithFile(const std::string& plist);
/* 函数内部 * 读取plist文件的内容。 * 调用addAnimationsWithDictionary(); */
void addAnimationsWithDictionary(const ValueMap& dictionary,const std::string& plist);
/* 函数内部 * 根据plist的内容,获取自己需要用到的spritesheets,并将他们添加进SpriteFrameCache中。 * 根据plist的format,解析出创建AnimationFrame所需信息,并创建它。 * 根据plist的format,解析出创建Animation所需信息,并创建它。 * 将Animation与其名字作为映射存入_animations变量中。 */
一个实际的例子,
auto cache = AnimationCache::getInstance();
cache->addAnimationsWithFile("xxx.plist");
auto dance_animation = cache->getAnimation("dance");
或者您已经创建好了Animation,我也是再高兴不过的了,
void addAnimation(Animation *animation,const std::string& name);
/* 函数内部 * _animations.insert(name,animation); */
删除Animation的方式只有一个,
void removeAnimation(const std::string& name);
/* 函数内部 * _animations.erase(name); */
获取Animation的方式也只有一个,
Animation* getAnimation(const std::string& name);
/* 函数内部
* _animations.at(name);
*/
此外要说明一点,在plist文件中您可以为每一个动画帧指定userInfo,通过在每一个动画帧中加入如下key-value,
<key>notification</key>
<dict>
...
</dict>
具体点的例子,
<key>notification</key>
<dict>
<key>key1</key>
<integer>1234</integer>
<key>key2</key>
<false/>
</dict>
在解析plist中每一动画帧的信息时,会把这些作为用户信息存入AnimationFrame::_userInfo中。还记得Animate的工作方式吗,在动画播放此帧时他将发送这些信息,您只需要在程序中接收这些信息即可,
/* AnimationFrameDisplayedNotification定义在cocos/base/ccMacros.h, * #define AnimationFrameDisplayedNotification "CCAnimationFrameDisplayedNotification" * 是用户自定义事件的名字。 * 当动画帧播放完成时,Animate会将该帧的userInfo通过名为CCAnimationFrameDisplayedNotification的自定义事件发送。 * userInfo会以AnimationFrame::DisplayedEventInfo结构体的形式发送。 */
auto listener = EventListenerCustom::create(AnimationFrameDisplayedNotification,[](EventCustom *e) {
auto userdata = static_cast<AnimationFrame::DisplayedEventInfo *>(e->getUserData());
log("target[%p],data[%s]",userdata->target,Value(*(userdata->userInfo)).getDescription().c_str());
});
我:你的内容真的少很多,那么同SpriteFrameCache一样,对你使用的变量,做个简单的总结。
AnimationCache:我使用的变量只有一个,也是最核心的,
Map<std::string,Animation*> _animations; // 存储Animation的名字与Animation之间的映射。
每个Animation在我这里都有个名字,从我这里取走或是删除Animation都需要提供他们的名字。
我:今天的对话总算是结束了,不过SpriteFrameCache并没有了解完全,以下这些东西还需要了解。
PolygonInfo
.9.png