Cocos2d-x 屏幕適配

爲了適應移動終端的各種分辨率大小,各種屏幕寬高比,在 cocos2d-x(當前穩定版:2.0.4) 中,提供了相應的解決方案,以方便我們在設計遊戲時,能夠更好的適應不同的環境。

而在設計遊戲之初,決定着我們屏幕適配的因素有哪些,簡而言之只有兩點:屏幕大小 和 寬高比。這兩個因素是如何影響遊戲的:

  • 屏幕大小: 從小分辨率 480x320 到 1280x800 分辨率,再到全高清 1080p,從手機到平板,還有蘋果設備的 Retina屏,這麼多不同的分辨率,而且大小差距甚大,不可能做到一套資源走天下,資源往小了設計,在大屏幕會顯示模糊,圖片往大了設計,在小屏幕設備又太浪費,而且小屏幕的手機硬件資源也會相對的緊缺,所以 根據屏幕大小使用不同的資源 是有必要的,而 cocos2d-x 也幫我們解決了這一點。
  • 寬高比: 什麼是寬高比,就是你的屏幕是方的還是長的,靠近方形的分辨率如 480x320,比例爲 3:2,還有 960x540 的16:9 標準寬屏,這也算是兩種總極端情況了,如果能在這兩種比例情況做好適配基本就可以了,如果比 3:2 “更方”如 4:3,比 16:9 “更長”,那麼不論如何佈局,顯示效果差距甚大,最好對固定比例優化吧。當在寬高比在一定範圍內,可以通過靈活編寫程序去適應,而在顯示效果上,cocos2d-x 爲我們提供了三種模式,這些 模式更多的是幫我們解決比例不一的情況而存在 的,如果只是屏幕大小(比例一樣),那通過簡單的放大縮小即可完成。

三種模式

說是三種模式,其實還有一種 無模式,也就是 cocos2d-x 默認的適配方案,現在我們就來認識一下這些模式,並且通過這些模式去認識其中一些概念 FrameSizeWinSizeVisibleSizeVisibleOrigin,以及它們存在的意義,並且最後靈活運行這些概念 創建出一個不屬於這些模式而超越這些模式的新適配解決方案,這是最終目的。

kResolutionUnKnown 認識 FrameSize

這是 cocos2d-x 編寫的默認模式,沒有做任何處理,在這種情況下,遊戲畫面的大小與比例都是不可控的,在程序運行之初,由各個平臺入口函數定義畫面大小:

// proj.linux/main.cpp  linux 平臺手動指定畫面大小
CCEGLView* eglView = CCEGLView::sharedOpenGLView();
eglView->setFrameSize(720, 480);

// proj.android/jni/hellocpp/main.cpp android 平臺由 jni 調用傳入設備分辨率參數
void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(JNIEnv*  env, jobject thiz, jint w, jint h)
{
    if (!CCDirector::sharedDirector()->getOpenGLView())
    {
        CCEGLView *view = CCEGLView::sharedOpenGLView();
        view->setFrameSize(w, h);

        AppDelegate *pAppDelegate = new AppDelegate();
        CCApplication::sharedApplication()->run();
    }
    else
    {
        // other
        ...
    }
}

在此我們首先認識了 FrameSize 參數,在遊戲運行時,我們可以通過 CCEGLView::sharedOpenGLView()->getFrameSize();獲得此值。如果在手機上運行,那麼不同分辨率將會得到不同的值,既然這個值不可控,那麼在寫遊戲中也就沒有參考價值了,比如我們寫一個精靈的位置距離底部 320 高度,在 480x320 分辨率,能看到其在屏幕上方,如果換一臺手機分辨率 960x540 那麼只能顯示在中間靠上的位置,如果設置精靈位置爲距離屏幕上方(高度)320,反之依然,顯示效果不一。

此時可行的方案是使用百分比,如精靈位置在屏幕橫向距離左邊 1/3 寬度,在 1/2 正中間處,而類似這樣的設置也不用依賴 FrameSize 的具體數值。而這樣的做法,使得內部元素像彈簧一樣,隨着 FrameSize 的大小改變而改變,伸縮或者擠壓,對於圖片資源大小也是完全不可控,如果根據屏幕大小放大縮小,那我們可以考慮用下面要說的模式,在此不推薦使用 cocos2d-x 的無模式方案。

kResolutionExactFit and kResolutionShowAll 認識 WinSize

在 AppDelegate.cpp 處可以通過設置:

CCEGLView::sharedOpenGLView()->setDesignResolutionSize(720, 480, kResolutionShowAll);
// 或者
CCEGLView::sharedOpenGLView()->setDesignResolutionSize(720, 480, kResolutionExactFit);

DesignResolutionSize!顧名思義,也就是邏輯上的遊戲屏幕大小,在這裏我們設置了其分辨率爲 720x480 爲例,那麼在遊戲中,我麼設置精靈的位置便可以參照此值,如 左下角 ccp(0,0),右上角 ccp(720, 480),而不論 FrameSize 的大小爲多少,是 720x480 也是,是 480x320 也罷,總能正確顯示其位置,左下角和右上角。能夠實現這一點的原因是,固定了設計分辨率大小,從而確定了其固定的寬高比,它的 優勢 是可以使用具體的數值擺放精靈位置,不會因爲實際屏幕大小寬高比而是內部元素相對位置關係出現混亂

而爲了保持畫面的寬高比,cocos2d-x 做了些犧牲,犧牲了什麼呢?kResolutionExactFit 犧牲了畫質而保持了全屏顯示,對畫面進行了拉伸,這意味着什麼?意味着相對極端情況下,本來精靈是方形的,顯示出來變成長方形,本來圓形的變成了橢圓,固此模式不推薦使用。kResolutionShowAll 爲了保持設計畫面比例對四周進行留黑邊處理,使得不同比例下畫面不能全屏。魚和熊掌不能兼得也 ~

我們可以通過如下方法獲取到 setDesignResolutionSize 所設置的值:

CCSize winSize = CCDirector::sharedDirector()->getWinSize();

我們跟蹤 WinSize 的初始化,獲取過程,在這裏簡單提一下,如下步驟:
// 獲得 winSize
CCSize winSize = CCDirector::sharedDirector()->getWinSize();

// 查看其 getWinSize(); 方法實現
[cocos2dx-path]/cocos2dx/CCDirector.cpp

CCSize CCDirector::getWinSize(void)
{
    return m_obWinSizeInPoints;
}

// 而 m_obWinSizeInPoints 是何時被賦值的
[cocos2dx-path]/cocos2dx/platform/CCEGLViewProtocol.cpp

void CCEGLViewProtocol::setDesignResolutionSize(float width, float height, ResolutionPolicy resolutionPolicy)
{
    ...
    ...
    m_obDesignResolutionSize.setSize(width, height);

    ...
    ...
    CCDirector::sharedDirector()->m_obWinSizeInPoints = getDesignResolutionSize();
}

const CCSize& CCEGLViewProtocol::getDesignResolutionSize() const 
{
    return m_obDesignResolutionSize;
}

具體的優勢:通過設置邏輯分辨率大小,相比無模式,可以幫我們解決了屏幕自動放大縮小問題,並且保持屏幕寬高比,使得遊戲更好設計,可以將設計畫面大小作爲默認背景圖片大小等,唯一點遺憾就是那點前面所提到的一點點犧牲。

kResolutionShowAll 方案可以作爲我們的默認解決方案,使得遊戲的設計更爲簡化,但爲了補填拉伸或留黑邊這點缺憾,進入下一個模式!

kResolutionNoBorder 瞭解 VisibleSize 與 VisibleOrigin

此模式可以解決兩個問題,其一:遊戲畫面全屏;其二:保持設置遊戲時的寬高比例,相比 kResolutionShowAll 有所區別的是,爲了填補留下的黑邊,將畫面稍微放大,以至於能夠正好補齊黑邊,而這樣做的後果可想而知,補齊黑邊的同時,另一個方向上將會有一部分畫面露出屏幕之外,如下示意圖:


黑色邊框標示實際的屏幕分辨率,紫色區域標示遊戲設計大小,而通過放大縮小,保持寬高比固定, 可以看到 Show All 之中的黑色陰影部分爲留邊,而 No Border 的紫色陰影部分則不能顯示,而這紫色區域的大小是遊戲設計之時是不可控的。那麼原設計的畫面大小就失去了 一定的 參考價值了,因爲這可能讓你的畫面顯示殘缺。這時僅僅通過 WinSize 滿足不了我們的設計需求,所以引入了 VisibleSize 與 VisibleOrigin 概念。


如上所示,紫色區域是被屏幕截去的部分,不可顯示的,根據實際情況,可能出現橫向截取和豎向截取,這取決於實際分辨率的寬高比。而 A、B、C、D所標示的是設計分辨率,固定大小。如果我們想讓一個精靈元素顯示在屏幕上方靠邊,那麼如果使用 WinSize 的高度設置其位置,可能出現的情況就是顯示到屏幕之外了。FrameSize 和 WinSize 我們已經知道其概念,而 VisibleSize 和 VisibleOrigin 所代表的是什麼呢,又時如何爲我們解決靠邊的問題!注意上圖下方的定義, VisibleSize = H I J K 是用紫色標註的。 而在上圖是 黑色 標註,標示屏幕實際分辨率,雖然 FrameSize 和 VisibleSize 都是 H I J K,但其意義不同,紫色表明它是與設計分辨率相關的。

FrameSize 是實際的屏幕分辨率,而 VisibleSize 是在 WinSize 之內,保持 FrameSize 的寬高比所能佔用的最大區域,實際屏幕分辨率 H I J K (黑色) 可以大於 WinSize ,但VisibleSize 一定會小於或者等於 WinSize,這兩者相同的是寬高比。VisibleSize 有着 WinSize 大小(隨WinSize 的大小改變而改變),還有着 FrameSize 的寬高比,它標示 在設計分辨率(WinSize)下,在屏幕中的可見區域大小。 而 VisibleOrigin 則標示在設計分辨率下被截取的區域大小,用點 K 標示,有了這些數據,我們想讓遊戲元素始終在屏幕顯示的區域之內不成難事。下面通過幾個數值帶入,加深這些概念的印象。

// 組[1] :
FrameSize:          width = 720, height = 420
WinSize:            width = 720, height = 480
VisibleSize:        width = 720, height = 420
VisibleOrigin:      x = 0, y = 30

// 組[2] :相比 組 [1] FrameSize 不變 VisibleSize 和 VisibleOrigin 隨着 WinSize 的變小而變小
FrameSize:          width = 720, height = 420
WinSize:            width = 480, height = 320
VisibleSize:        width = 480, height = 280
VisibleOrigin:      x = 0, y = 20

// 組[3] : 相比組 [1] WinSize 不變,VisibleSize 隨着 FrameSize 的比例改變而改變
FrameSize:          width = 720, height = 540
WinSize:            width = 720, height = 480
VisibleSize:        width = 640, height = 480
VisibleOrigin:      x = 40, y = 0

// WinSize VisibleSize VisibleOrigin 與都設計的分辨率相關,滿足如下關係
WinSize.width = (VisibleOrigin.x * 2) + VisibleSize.width
WinSize.height = (VisibleOrigin.y * 2) + VisibleSize.height

NoBorder 具體的使用方法可以參考 cocos2d-x 自帶例程 TestCpp ,有詳細的使用方法,並且封裝了 VisibleRect 類,可以獲取設計分辨率,不同比例屏幕之時的主要參考點,屏幕四個拐角,和邊的中點等,讓我們設置元素位置時,使其總能顯示在屏幕之內,這裏就不詳細介紹了。

基於這幾種模式的程序使用方法,cocos2d-x 自帶例程或者網上有很多教程,這裏只詳細解釋了其中各種概念,而知道了這些概念,當然用起來就沒有多大問題了。

kResolutionLeafsoar

!!!這是什麼模式!好吧,Leafsoar 是 一葉 的 ID ,或者是本博客的一級域名而已 :P 在 cocos2d-x 中並沒有這種模式。除卻 UnKnown 與 ExactFit 不說,ShowAll 的優勢是,只需要一個設計分辨率,然後通過 WinSize 設置相對對位即可,而且位置的最大長寬都是確定,方便了開發,但屏幕不能填滿, NoBorder 模式的優勢是在畫面不變形的情況下,實現全屏,顯示效果更好,但 WinSize 一定程度失效,需要通過運行時計算 VisibleSize 和 VisibleOrigin 來設置位置,由於是運行時計算,所以也就會出現,各種屏幕顯示效果不一樣的情況。

ShowAll 和 NoBorder 各有所長,各有所短,而這裏提出的新適配解決方案正是取兩者之長,舍兩者之短的 組合模式。簡單說來就是用 NoBorder 去實現 ShowAll 的思想。NoBorder 可以保證全屏利用,ShowAll 可以更好的使用實際設計座標固定位置,而且相對位置不會隨寬高比的改變而改變,這在編寫遊戲的時候能方便不少。先上一個示意圖,一目瞭然 (兩個圖,兩個方向):


在原來 NoBorder 模式示意圖上添加了新的概念,LsSize = X Y M N (leafsoar 簡寫了,爲了不跟 cocos2d-x 的一些概念混淆,什麼名字不重要,只要瞭解其含義即可),在 NoBorder 模式下的 LsSize 相對於 FrameSize 而言,正如 在 ShowAll 模式下的 WinSize 相對於 FrameSize,所以說這是 ShowAll NoBorder 的組合概念,而這裏的 LsSize 與 WinSize 的寬高比是一致的。

猛地一看,似乎把問題複雜化了,仔細一看,還不如猛地一看 ~~

在 ShowAll 中,WinSize 作爲最高的寬高,以此參照設置位置,因爲在此範圍內都能在屏幕上顯示,用了 NoBorder 使得四周可能被截去一塊區域,而這個區域大小不可控制,所以不能再使用 WinSize 作爲參考點來設置位置,而這裏的 LsSize 同樣,因爲 LsSzie 不論在什麼情況下,總能顯示在屏幕之內,我們可以方便的使用 LsSize 作爲座標系參考,並且可以全屏顯示,在配合 VisibleSize ,相比純的 NoBorder 加強了不少。它可以怎麼用?

可以把 LsSize 當作 ShowAll 中的 WinSize 來用,而黑邊可以使用稍大的圖片填充,或者使用其它圖片修飾邊框,修飾的邊框圖案可大可小,可長可短,填充屏幕,保持全屏。

開始基於 LsSize 的遊戲設計實現

爲了能夠準確實現基於 LsSize 的設計,初步計劃將 LsSize 設定在 480x320 的分辨率方案,爲此做了些準備,首先不使用任何模式情況下,在場景內調用如下:

CCSize size = CCDirector::sharedDirector()->getWinSize();

CCPoint center = ccp(size.width/2, size.height/2);

// 大小 600x500 爲了 NoBorder 看到效果,使用稍大的背景圖
CCSprite* pb = CCSprite::create("Back.jpg");
pb->setPosition(center);
this->addChild(pb, 0);

// 480x320 此圖爲使用於設計分辨率 LsSize 的圖片
CCSprite* pSprite = CCSprite::create("HelloWorld.png");
pSprite->setPosition(center);
this->addChild(pSprite, 0);

// 37x37 在 480x320 畫面的四個拐角處,添加參照
CCSprite* p1 = CCSprite::create("Peas.png");
p1->setPosition(ccpAdd(center, ccp(-240, -160)));
this->addChild(p1);

CCSprite* p2 = CCSprite::create("Peas.png");
p2->setPosition(ccpAdd(center, ccp(240, 160)));
this->addChild(p2);

CCSprite* p3 = CCSprite::create("Peas.png");
p3->setPosition(ccpAdd(center, ccp(-240, 160)));
this->addChild(p3);

CCSprite* p4 = CCSprite::create("Peas.png");
p4->setPosition(ccpAdd(center, ccp(240, -160)));
this->addChild(p4);

顯示效果:(FrameSize = 640x540) 


顯示效果:(ShowAll; FrameSize = 520x320; WinSize = 480x320) 


顯示效果:(NoBorder; FrameSize = 520x320; WinSize = 480x320) 


通過效果我們可以看到,在相同 FrameSize 下 NoBorder 時,畫面由於填充了黑邊,將畫面放大,以至於上下有部分顯示不全,通過拐角四個精靈可以看出。

好!既然我們知道是由於放大所致,那麼我們將畫面縮小呢?cocos2d-x 提供了一個方法,我們調用如下代碼:

CCDirector *pDirector = CCDirector::sharedDirector();
pDirector->setContentScaleFactor(
                CCEGLView::sharedOpenGLView()->getScaleY() );

爲了彌補畫面因需要不填空白出現的方法,我們將畫面縮小,放大係數可以通過 CCEGLView::sharedOpenGLView()->getScaleY() 取得。其實 setContentScaleFactor 方法是爲了適配不同資源而設計的,可以用此方法對不同資源適配,縮放等。效果如下:



我們看到 480x320 的圖片顯示完全正確了,也正是我們想要的效果,但唯一的缺點是 ~~ 拐角處四個精靈的位置依然不是我們想要的,我們設計的位置是以 480x320 設置位置的,而 WinSize 也是 480x320 ,而此時基於 480x320 的設計必然會顯示到屏幕之外,而要想不修改精靈位置,而讓其顯示正確的位置,那麼爲了保證 LsSize 的固定,我們需要一個方法,那就是動態設置 WinSize

什麼意思?我們知道一般這些模式設計遊戲時,是通過 setDesignResolutionSize 設置 WinSize 的,這個值在遊戲運行其間是定植,動態改變的是 VisibleSize 等,而這裏提出了 LsSize 的概念,可想而知,如果 WinSize 固定,那麼 LsSize 會隨着屏幕寬高比的改變而改變,那麼我們反其道而行,固定 LsSize 值,那麼在運行時可以通過實際的寬高比來算得 WinSize 的值,這樣動態算得的 WinSize 值就能夠保證我們的 LsSize 是一個定值了。

相對論,WinSize 與 LsSize 的值是相對的,與其通過固定 WinSize 在運行時動態獲得 LsSize (這也是 NoBorder 的默認方式,而導致的結果是 WinSize 沒有參考價值),不如我們固定 LsSize 而在運行時算得 WinSize 設置來的要更妙一些。

現在不使用 setContentScaleFactor 方法,而修改 setDesignResolutionSize 這裏的值,我們知道 WinSize 是 480x320 時,LsSize 必然會小於此值,而 NoBorder 的放大係數我們可以通過如下方式算得(可以參考setDesignResolutionSize方法內部實現),並在 AppDelegate 裏執行:

CCSize frameSize = CCEGLView::sharedOpenGLView()->getFrameSize();
// 設置 LsSize 固定值
CCSize lsSize = CCSizeMake(480, 320);

float scaleX = (float) frameSize.width / lsSize.width;
float scaleY = (float) frameSize.height / lsSize.height;

// 定義 scale 變量
float scale = 0.0f; // MAX(scaleX, scaleY);
if (scaleX > scaleY) {
    // 如果是 X 方向偏大,那麼 scaleX 需要除以一個放大係數,放大係數可以由樅方向獲取,
    // 因爲此時 FrameSize 和 LsSize 的上下邊是重疊的
    scale = scaleX / (frameSize.height / (float) lsSize.height);
} else {
    scale = scaleY / (frameSize.width / (float) lsSize.width);
}

CCLog("x: %f; y: %f; scale: %f", scaleX, scaleY, scale);

// 根據 LsSize 和屏幕寬高比動態設定 WinSize
CCEGLView::sharedOpenGLView()->setDesignResolutionSize(lsSize.width * scale,
        lsSize.height * scale, kResolutionNoBorder);

顯示效果:(NoBorder 模式 ;FrameSize = 520x320; LsSize = 480x320; WinSize = 動態獲取) 


我們看到在沒有修改源代碼,並且在設計中使用 480x320 的參考系,也既是基於 LsSize 的設計顯示效果如我們預期,那麼我們換一個 FrameSize 來看看是否能夠自動適應呢?如下:

顯示效果:(NoBorder 模式 ;FrameSize = 600x480; LsSize = 480x320; WinSize = 動態獲取) 


到此,基於 LsSize 參考系的遊戲設計已經完成了,這樣做的好處是很明顯的,集 ShowAll 和 NoBorder 的優點於一處,這裏的圖片元素是爲了好定位,實現的需要而寫的,具體場景可以使用背景地圖,或一張大的圖片顯示,而沒有任何影響,也可以繼續使用 VisibleSize 得到 LsSize 之外的部分區域大小,在 LsSize 之外可以使用背景圖片作爲裝飾,即保證了遊戲的全屏,又保證了遊戲設計時的方便,如果使用完全基於 LsSize 的設計實現,除了顯示背景裝飾之外,我們不想讓 LsSize 的內部元素顯示到 LsSize 之外如何做呢?我們只需要設定 LsSize 層的的顯示區域即可,我們可以修改場景的實現:


// 這裏先簡單實現思路

CCScene* HelloWorld::scene() {

    CCScene *scene = CCScene::create();
    // 創建背景層
    CCLayer* b = CCLayer::create();
    scene->addChild(b);

    // 添加背景圖片和設置位置,可以使用其它裝飾,或者小圖片屏幕都行
    CCSize size = CCDirector::sharedDirector()->getWinSize();
    CCPoint center = ccp(size.width/2, size.height/2);
    CCSprite* pb = CCSprite::create("Back.jpg");
    pb->setPosition(center);
    b->addChild(pb, 0);

    // 創建 LsLayer 層
    HelloWorld *lsLayer = HelloWorld::create();
    scene->addChild(lsLayer);

    return scene;
}

// 在 HelloWorld 中重寫 visit() 函數 設定顯示區域
void HelloWorld::visit() {
    glEnable(GL_SCISSOR_TEST);              // 開啓顯示指定區域
    // 在這裏只寫上固定值,在特性環境下,以便快速看效果,實際的值,需要根據實際情況算得
    glScissor(20, 0, 480, 320);     // 只顯示當前窗口的區域
    CCLayer::visit();                       // 調用下面的方法
    glDisable(GL_SCISSOR_TEST);             // 禁用
}

顯示效果:(NoBorder 模式 ;FrameSize = 520x320; LsSize = 480x320; WinSize = 動態獲取) 


屏幕適配新解

看完這篇文章想必對 cocos2d-x 的屏幕適配方案及其原理有了相當的認識,從內部提供的三種模式,再到我們自定義基於 LsSize 的 Leafsoar 模式 (好把,因該叫做 ShowAllNoBorder)。這裏已經給出了完全的實現原理以及實現方法,並配有效果圖,當然這其中還有些細節需要注意,比如我們基於 LsSize 的大小設計,那麼實際的圖片肯定需要比 LsSize 的要大,大多少,太小了不夠適應,太大了又浪費,如何取捨等問題,這一點取決的因素是什麼,留給讀者思考 ~~

一葉將在 GitHub 處建立一個ScreenSolutions 項目,讀者可以從這裏參考實現的方案。(也許此時在 GitHub 所看到的實現並不完全,但已經有了簡單的實現方法,並且能夠運行,如有必要,將會新寫一篇博客,去實現 ScreenSolutions 並且解說)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章