写在前面——有不对的地方,烦请大家批评指正,我也会继续努力提高自己。如果转载请注明出处,谢谢大家支持——Forward。
我的微博——秦京一梦
在上一篇博客中,Forward对场景以及各种场景切换特效进行了一个入门级的学习分享,但是只有一个场景是不能完成游戏开发的。用画家画画来做比喻,有了场景就好像画家拿到了一页画纸,而要完成一幅壮丽的图画,还需要在这页画纸上通过合理的分配,填充上不同的元素。这里所说的元素,类比到游戏开发中,就是场景中的精灵。
今天我们就来学习一下Cocos2dx中的精灵这一概念。按照习惯,我们首先来看看CCSprite这个类相关的类图。
图 1
如图1中所示,CCSprite继承自CCTextureProtocol和CCNodeRGBA类,而CCNodeRGBA有继承自CCNode和CCRGBAProtocol类。将上面的关系翻译成自然语言,我们就很好理解CCSprite这个类了——所谓CCSprite精灵,就是用来描述纹理和颜色信息的节点。为了我们能够更深入的了解CCSprite精灵类,还需要进一步深入的学习。
查看CCSprite类源码,可以看到,里面提供了很多API,常用的比如创建、设置纹理信息、设缩放、旋转、坐标位置、锚点信息、隐藏与显示的设置、子节点的添加与移除等等,具体的Forward在这里不再赘述,相信大家都能看懂。
接下来我们通过程序来进行进一步的学习。这里使用Cocos2dx例子中的资源“grossini”。
图 2
好的,我们先在已经创建好的Cocos2dxDemo工程中添加一个精灵上去。
图 3
那么这个精灵在加载的过程中,我们都做了哪些工作呢?
首先,我们加载动画的代码清单如下:
CCSprite* pSprite = CCSprite::create("grossini.png"); pSprite->setPosition(ccp(240,160)); this->addChild(pSprite);
进入精灵类的create接口,代码清单如下:
CCSprite* CCSprite::create(constchar*pszFileName) { CCSprite *pobSprite = new CCSprite(); if(pobSprite && pobSprite->initWithFile(pszFileName)) { pobSprite->autorelease(); returnpobSprite; } CC_SAFE_DELETE(pobSprite); returnNULL; }
可以看出在CCSprite的create接口中,我们首先创建了一个精灵类对象,然后通过initWithFile接口来对这个精灵做初始化,就Forward前面说的一样,所谓精灵,就是用来描述纹理和颜色信息的节点,创建CCSprite对象的过程我们就得到了这个节点,初始化就相当于给这个节点填充上对应的纹理和其它必需的信息的过程。继续跟进~~
boolCCSprite::initWithFile(constchar *pszFilename) { CCAssert(pszFilename != NULL, "Invalid filename for sprite"); CCTexture2D *pTexture =CCTextureCache::sharedTextureCache()->addImage(pszFilename); if(pTexture) { CCRect rect = CCRectZero; rect.size =pTexture->getContentSize(); return initWithTexture(pTexture, rect); } // don't releasehere. // when loadtexture failed, it's better to get a "transparent" sprite then acrashed program //this->release(); returnfalse; }
这里我们可以看出,通过CCTextureCache的addImage,我们会得到一张纹理信息,并且最终用来加载到CCSprite上的就是张纹理。
继续进入到addImage接口中——
CCTexture2D *CCTextureCache::addImage(constchar * path) { CCAssert(path != NULL, "TextureCache: filep_w_picpath MUST not be NULL"); CCTexture2D * texture = NULL; CCImage* pImage = NULL; // Split updirectory and filename // MUTEX: // Needed sinceaddImageAsync calls this method from a different thread //pthread_mutex_lock(m_pDictLock); std::string pathKey = path; pathKey =CCFileUtils::sharedFileUtils()->fullPathForFilename(pathKey.c_str()); if(pathKey.size() == 0) { returnNULL; } texture =(CCTexture2D*)m_pTextures->objectForKey(pathKey.c_str()); std::string fullpath = pathKey; //(CCFileUtils::sharedFileUtils()->fullPathFromRelativePath(path)); if (!texture) { std::string lowerCase(pathKey); for (unsignedint i = 0; i< lowerCase.length(); ++i) { lowerCase[i] =tolower(lowerCase[i]); } // all p_w_picpathsare handled by UIImage except PVR extension that is handled by our own handler do { if(std::string::npos != lowerCase.find(".pvr")) { texture = this->addPVRImage(fullpath.c_str()); } elseif (std::string::npos != lowerCase.find(".pkm")) { // ETC1 file format, only supportted onAndroid texture = this->addETCImage(fullpath.c_str()); } else { CCImage::EImageFormateImageFormat = CCImage::kFmtUnKnown; if(std::string::npos != lowerCase.find(".png")) { eImageFormat =CCImage::kFmtPng; } else if (std::string::npos != lowerCase.find(".jpg") || std::string::npos !=lowerCase.find(".jpeg")) { eImageFormat =CCImage::kFmtJpg; } else if (std::string::npos != lowerCase.find(".tif") || std::string::npos !=lowerCase.find(".tiff")) { eImageFormat =CCImage::kFmtTiff; } else if (std::string::npos != lowerCase.find(".webp")) { eImageFormat =CCImage::kFmtWebp; } pImage = newCCImage(); CC_BREAK_IF(NULL == pImage); boolbRet = pImage->initWithImageFile(fullpath.c_str(), eImageFormat); CC_BREAK_IF(!bRet); texture = new CCTexture2D(); if(texture && texture->initWithImage(pImage) ) { #ifCC_ENABLE_CACHE_TEXTURE_DATA //cache the texture file name VolatileTexture::addImageTexture(texture, fullpath.c_str(),eImageFormat); #endif m_pTextures->setObject(texture, pathKey.c_str()); texture->release(); } else { CCLOG("cocos2d: Couldn't create texture for file:%s inCCTextureCache", path); } } } while(0); } CC_SAFE_RELEASE(pImage); //pthread_mutex_unlock(m_pDictLock); returntexture; }
可能这个接口的实现略微显得有点长,但并不影响我们的代码阅读与理解。这段代码主要实现了在一张Hash表中去查找对应的纹理信息是否已经加载,如果找到就将对应纹理返回
pathKey =CCFileUtils::sharedFileUtils()->fullPathForFilename(pathKey.c_str()); if(pathKey.size() == 0) { returnNULL; } texture = (CCTexture2D*)m_pTextures->objectForKey(pathKey.c_str());
否则的话就要先创建一个Image然后通过这个Image来完成一张新的纹理的创建,并将这张新创建的纹理加入到Hash表中。
pImage = newCCImage(); CC_BREAK_IF(NULL == pImage); boolbRet = pImage->initWithImageFile(fullpath.c_str(), eImageFormat); CC_BREAK_IF(!bRet); texture = new CCTexture2D(); if(texture &&texture->initWithImage(pImage) ) { #ifCC_ENABLE_CACHE_TEXTURE_DATA //cache the texture file name VolatileTexture::addImageTexture(texture, fullpath.c_str(),eImageFormat); #endif m_pTextures->setObject(texture, pathKey.c_str()); texture->release(); }
接着,我们进入到CCImage类的initWithImageFile接口中。
boolCCImage::initWithImageFile(constchar * strPath, EImageFormat eImgFmt/* = eFmtPng*/) { bool bRet =false; #ifdefEMSCRIPTEN // Emscriptenincludes a re-implementation of SDL that uses HTML5 canvas // operationsunderneath. Consequently, loading p_w_picpaths via IMG_Load (an SDL // API) will be alot faster than running libpng et al as compiled with // Emscripten. SDL_Surface *iSurf = IMG_Load(strPath); int size =4 * (iSurf->w * iSurf->h); bRet = _initWithRawData((void*)iSurf->pixels, size, iSurf->w,iSurf->h, 8, true); unsignedint *tmp = (unsignedint *)m_pData; intnrPixels = iSurf->w * iSurf->h; for(int i = 0; i < nrPixels; i++) { unsignedchar *p = m_pData + i * 4; tmp[i] = CC_RGB_PREMULTIPLY_ALPHA(p[0], p[1], p[2], p[3] ); } SDL_FreeSurface(iSurf); #else unsignedlong nSize = 0; std::string fullPath =CCFileUtils::sharedFileUtils()->fullPathForFilename(strPath); unsignedchar* pBuffer =CCFileUtils::sharedFileUtils()->getFileData(fullPath.c_str(), "rb", &nSize); if (pBuffer!= NULL && nSize > 0) { bRet = initWithImageData(pBuffer,nSize, eImgFmt); } CC_SAFE_DELETE_ARRAY(pBuffer); #endif// EMSCRIPTEN returnbRet; }
在#else到#endif一段,我们应该很清楚地看到,在每次调用initWithImageFile接口来初始化Image对象的时候都会去通过“rb”方式读取文件。至此,我们可以说对一个精灵的创建到纹理加载全过程已经有了比较深入的学习了。
Forward对上面的Demo程序作了一些修改——
mIndex = 1; mElapseTime = 0.0f; pSprite = NULL; pSprite = CCSprite::create("grossini.png"); pSprite->setPosition(ccp(240,160)); this->addChild(pSprite); this->schedule(schedule_selector(HelloWorld::Tick));
首先我们在场景上添加了一个精灵对象,然后通过schedule接口注册了一个回调,具体的回调代码清单如下。
voidHelloWorld::Tick(float dt) { mElapseTime += dt; if (mElapseTime <= 0.3f) { return; } mElapseTime = 0.0f; int tIndex = mIndex % 14 + 1; mIndex++; char strImageName[64] = {0}; sprintf(strImageName,"grossini_dance_%02d.png",tIndex); pSprite->initWithFile(strImageName); }
翻译为自然语言就是,每隔0.3秒,我们对之前创建的grossini_dance对象做一次纹理修改,编译运行——grossini_dance果然动起来了。
但当我们断点调试的时候,会发现在每次使用的纹理图片在第一次使用的时候都会执行到这里。
bool bRet =pImage->initWithImageFile(fullpath.c_str(), eImageFormat); CC_BREAK_IF(!bRet);
上面我们提到“调用initWithImageFile接口来初始化Image对象的时候都会去通过“rb”方式读取文件”而文件操作一般都是比较耗时的,在小的Demo程序中可能看不到这一点对效率造成的影响,但是当我们在做的是一个真正的游戏项目时,这里的时间消耗就不能忽略甚至与不能容忍了。
这就引出了我们接下来要说的另外一问题。当我们在第一次使用图2中“grossini_dance_01.png”到“grossini_dance_14.png”这一套序列图片的纹理信息的时候,我们需要对一张图片进行一次文件读取。
解决这个问题的思路是,我们通过将一张张碎图拼到一张大图上去,所有纹理在第一次加载图片的时候就已经读进内存,以后直接使用,通过降低文件读取次数来提高效率。这里Forward使用TexturePacker工具(不了TP打包工具的同学可以去网上查找一下相关资料,网上资源很丰富哦~^_^),将十四张碎图拼接成一张大图并导出。
图 4
图 5
如图5所示,我们在导出后,得到了一张拼接好的大图和一个plist文件。打开这个plist文件我们会发现,其实它就是用来描述每一张碎图在这张大图的上的位置,像素宽高等等信息的。
下面我们就用这套资源来重新完成上面的那个简易动画。代码清单如下。
mIndex = 1; mElapseTime = 0.0f; pSprite = NULL; CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("grossini.plist"); pSprite = CCSprite::create("grossini.png"); pSprite->setPosition(ccp(240,160)); this->addChild(pSprite); this->schedule(schedule_selector(HelloWorld::Tick));
与之前的不同之处在于,这里使用CCSpriteFrameCache加载了我们导出的plist文件,其实这里就是将plist文件描述的“grossiniDemo.png”大图加载到内存并通过plist文件描述对其划分为一个个小的CCSpriteFrame。
voidHelloWorld::Tick(float dt) { mElapseTime += dt; if (mElapseTime <= 0.3f) { return; } mElapseTime = 0.0f; int tIndex = mIndex % 14 + 1; mIndex++; char strImageName[64] = {0}; sprintf(strImageName,"grossini_dance_%02d.png",tIndex); pSprite->initWithSpriteFrame(CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(strImageName)); }
然后在使用的时候,我们直接通过每个碎图名来获取对应纹理信息即可。
OK!继续断点调试——我们会发现,Image的initWithImageFile接口只调用到了一次,而这一次其实就是“grossini.png”大图加载的时候调用的。
好的,今天的学习就先到这里吧@_@
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。