您好,登錄后才能下訂單哦!
寫在前面——有不對的地方,煩請大家批評指正,我也會繼續努力提高自己。如果轉載請注明出處,謝謝大家支持——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進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。