由于各种智能手机的屏幕大小都不一致,会出现同一张图片资源在不同的设备分辨率下显示不一样的问题。为避免这样的情况,需要Cocos引擎能提供多分辨率的支持,也就是说要求实现这样的效果 — 开发者不需要考虑程序实际运行在什么分辨率下而只需要制定设置好设计分辨率就行,接着引擎便会自动实现设计分辨率到屏幕分辨率的转化,以及不同资源分辨率到设计分辨率的转化。下面逐一分析理解涉及到的概念:
一 设计分辨率
顾名思义,指由开发者自定义的分辨率,最终引擎会拿实际的屏幕分辨率和这个自定义的设计分辨率得到缩放因子。但实际的项目开发中还有细节要兼顾,如是否需要宽高都等比缩放,故有一知识点—缩放策略。在cocos2dx中通过setDesignResolutionSize来设置,方法使用如下:
setDesignResolutionSize(float width, float height, ResolutionPolicy resolutionPolicy);
参数width和height指定设计分辨率的尺寸,resolutionPolicy指定缩放策略,由ResolutionPolicy枚举定义,如下图所示:
//Cocos2dConstants.lua cc.ResolutionPolicy = { EXACT_FIT = 0, NO_BORDER = 1, SHOW_ALL = 2, FIXED_HEIGHT = 3, FIXED_WIDTH = 4, UNKNOWN = 5, }
EXACT_FIT :充满整个屏幕,唯一一个不按等比缩放的策略,宽高比不相等可能导致拉伸或压缩发生形变,开发中不建议用;
NO_BORDER :充满屏幕,等比缩放,实质上是屏幕宽、高分别和设计分辨率宽、高计算缩放因子,取较(大)者作为宽、高的缩放因子。保证了设计区域总能一个方向上铺满屏幕,而另一个方向一般会超出屏幕区域。 而区域是被居中对齐到屏幕(如上图);
SHOW_ALL :保持全部元素可见,等比缩放。实质是屏幕宽、高分别和设计分辨率宽、高计算缩放因子,取较(小)者作为宽、高的缩放因子。保证了设计区域全部显示到屏幕上,但可能会有黑边(如上图)。
以上三者都不需要开发者对元素的位置进行调整,都是优先字面上的意思,NO_BORDER是“没黑边,但可能有裁剪”,SHOW_ALL是“完全没裁剪,但可能有黑边”,可以说两者刚好是相反的情况。
FIXED_XXXX :也是等比缩放,按开发者指定的某一边来建立缩放比例。与上述不同的是,它使用屏幕左下角作为原点,且充满屏幕。
二 调整元素位置
除了FIXED_XXXX,其他的缩放策略的绘制区域都是设计分辨率表示的有效区域,都是设计分辨率对应区域的原点(如上述的中点)。但是FIXED_XXXX对应的区域改变了,则程序中设定的绝对坐标往往失效,如下图的在一个分辨率中位于中点的点(100,50),在另一分辨率下就不再是中点了:
故针对使用FIXED_XXXX缩放策略的项目,开发过程中尽量不要使用绝对坐标,除此以外别的策略都没有问题。解决这个问题的方法是不固定边方向的坐标不要用绝对坐标,可以通过引入visibleSize辅助调整,如auto p2 = Vec2(visibleSize.width/2 - 10,visibleSize.height/2 + 3),这样便可实现逻辑对齐的自适应了。
三 视口设置
ViewPort的设置在OpenGL的GPU渲染管线中的屏幕映射占重要作用,在Cocos2dx中通过setViewPortInPoints方法来设置视口的大小:
void Director::setViewport() { if (_openGLView) { _openGLView->setViewPortInPoints(0, 0, _winSizeInPoints.width, _winSizeInPoints.height); } } //CCGLView.cpp void GLView::setViewPortInPoints(float x , float y , float w , float h) { glViewport((GLint)(x * _scaleX + _viewPortRect.origin.x), (GLint)(y * _scaleY + _viewPortRect.origin.y), (GLsizei)(w * _scaleX), (GLsizei)(h * _scaleY)); }
setViewPortInPoints方法将基于设计分辨率的坐标信息转换为基于屏幕实际像素大小的坐标信息,然后使用GL指令glViewPort进行设置。
四 资源分辨率
上面已经讲述了设计分辨率到屏幕分辨率的转化流程,下面简述设计分辨率到资源分辨率的转化。虽说场景元素的位置不应和屏幕的实际分辨率有什么关系的,但是在实际的项目应用开发中,我们最好使设计分辨率和资源分辨率保持一致,这样开发者只需要设置好设计分辨率之后对应的资源UI元素就能被放置在正确的位置上了。
在Cocos2dx中使用setContentScaleFactor(float scaleFactor)方法来对资源进行相应缩放,参数scaleFactor表示设计分辨率与资源分辨率的缩放因子。
void Director::setContentScaleFactor(float scaleFactor) { if (scaleFactor != _contentScaleFactor) { _contentScaleFactor = scaleFactor; _isStatusLabelUpdated = true; } }
补充总结一点,由于纹理坐标使用归一化的坐标值,因此对图元的贴图是与分辨率无关的。但是对于2D绘图,渲染系统要依赖于纹理的实际大小来计算顶点坐标,这就需要对不同分辨率的资源进行确定的缩放因子值缩放。
Size Texture2D::getContentSize() const { Size ret; ret.width = _contentSize.width / CC_CONTENT_SCALE_FACTOR(); ret.height = _contentSize.height / CC_CONTENT_SCALE_FACTOR(); return ret; }
到此为止,相关的概念已经比较笼统的理解了一遍,下面举之前我的项目中屏幕适配方案来加深巩固下对这些内容的理解:
在lua入口加载文件开头设置好设计分辨率长和宽,以及缩放策略:
CONFIG_SCREEN_WIDTH = 960 CONFIG_SCREEN_HEIGHT = 540 CONFIG_SCREEN_AUTOSCALE = "SHOW_ALL"
这里设置宽和高分别为960和540,秉着“设计分辨率和资源分辨率保持一致”的做法,项目中用Cocos Studio拼的界面中panel的大小和设计分辨率保持一致,只要在编辑器中调整好UI控件的位置就好,之后程序中再也不用理会。同时,策略使用“SHOW_ALL”保证全部元素都能看到,而出现的黑边会额外用花纹图片挡住,后面会继续讲解。
屏幕分辨率是960x640,在SHOW_ALL策略下960x540的设计分辨率,便以小的缩放因子为主,那明显高height方向上会出现黑边,需要添加花纹图片掩盖黑边
ClsStarttScene.onEnter = function(self) local utils = require("update/utils") utils.makeOutSideEdge("update/screen_edge.jpg") self:showLogo() end utils.makeOutSideEdge = function(file_path) local glview = CCDirector:sharedDirector():getOpenGLView() local framesize = glview:getFrameSize() local scaleX = framesize.width / CONFIG_SCREEN_WIDTH local scaleY = framesize.height / CONFIG_SCREEN_HEIGHT local parent = getNotification() if scaleY > scaleX then --上下出现黑边 local viewportsprite_down = ViewPortSprite:create(file_path, CONFIG_SCREEN_WIDTH, CONFIG_SCREEN_HEIGHT); parent:addChild(viewportsprite_down) --按照游戏主窗口的x轴大小缩放花纹图片的大小 local need_width = framesize.width local contentsize = viewportsprite_down:getContentSize() local sp_width = contentsize.width*scaleX viewportsprite_down:setScaleX(need_width/sp_width) local offsetX = ((sp_width - need_width)/2)/scaleX ... ... ... .... else --Iphonex等机型是左右出现黑边 local viewportsprite_left = ViewPortSprite:create(file_path, CONFIG_SCREEN_WIDTH, CONFIG_SCREEN_HEIGHT); parent:addChild(viewportsprite_left) viewportsprite_left:setRotation(-90) viewportsprite_left:setAnchorPoint(CCPoint(0,0)) local contentsize = viewportsprite_left:getContentSize() --横向资源,旋转90度放直 local sp_ct = {} sp_ct.height = contentsize.width sp_ct.width = contentsize.height
... ... ... ... end
效果如下,完美:
适配效果实现了,但还有两个细节需要特别学习记录一下。一个是只要设置了设计分辨率,游戏程序都会重新更新重设视口,投影变换矩阵等等,后续添加的UI元素都会在这个设计分辨率基础上进行渲染,如下:
void GLView::updateDesignResolutionSize() { if (_screenSize.width > 0 && _screenSize.height > 0 && _designResolutionSize.width > 0 && _designResolutionSize.height > 0) { _scaleX = (float)_screenSize.width / _designResolutionSize.width; _scaleY = (float)_screenSize.height / _designResolutionSize.height; if (_resolutionPolicy == ResolutionPolicy::NO_BORDER) { _scaleX = _scaleY = MAX(_scaleX, _scaleY); } else if (_resolutionPolicy == ResolutionPolicy::SHOW_ALL) { _scaleX = _scaleY = MIN(_scaleX, _scaleY); } else if ( _resolutionPolicy == ResolutionPolicy::FIXED_HEIGHT) { _scaleX = _scaleY; _designResolutionSize.width = ceilf(_screenSize.width/_scaleX); } else if ( _resolutionPolicy == ResolutionPolicy::FIXED_WIDTH) { _scaleY = _scaleX; _designResolutionSize.height = ceilf(_screenSize.height/_scaleY); } // calculate the rect of viewport float viewPortW = _designResolutionSize.width * _scaleX; float viewPortH = _designResolutionSize.height * _scaleY; _viewPortRect.setRect((_screenSize.width - viewPortW) / 2, (_screenSize.height - viewPortH) / 2, viewPortW, viewPortH); // reset director's member variables to fit visible rect auto director = Director::getInstance(); director->_winSizeInPoints = getDesignResolutionSize(); director->_isStatusLabelUpdated = true; director->setGLDefaultValues(); //重设 } } void GLView::setDesignResolutionSize(float width, float height, ResolutionPolicy resolutionPolicy) { CCASSERT(resolutionPolicy != ResolutionPolicy::UNKNOWN, "should set resolutionPolicy"); if (width == 0.0f || height == 0.0f) { return; } _designResolutionSize.setSize(width, height); _resolutionPolicy = resolutionPolicy; updateDesignResolutionSize(); }
另外一个细节就是添加挡住黑边的花纹图片的时候,不应该在游戏逻辑设计分辨率下而是在实际屏幕分辨率下添加,这样计算主窗口大小比较方便调整坐标,但要记住恢复游戏的设计分辨率。如下:
ViewPortSprite* ViewPortSprite::create(const char *pszFileName, int nViewPortW, int nViewPortH) { ViewPortSprite *pobSprite = new ViewPortSprite(); if (pobSprite && pobSprite->initWithFile(pszFileName)) { pobSprite->autorelease(); pobSprite->setViewPort(nViewPortW, nViewPortH); pobSprite->ignoreAnchorPointForPosition(true); return pobSprite; } CC_SAFE_DELETE(pobSprite); return NULL; } void ViewPortSprite::updateViewPort() { CCSize szframsize = CCDirector::sharedDirector()->getOpenGLView()->getFrameSize(); glViewport(0, 0, szframsize.width, szframsize.height); } void ViewPortSprite::draw(void) { updateViewPort(); CCSprite::draw(); //绘制完毕后要记得恢复 CCDirector::sharedDirector()->getOpenGLView()->setDesignResolutionSize(m_nViewPortW, m_nViewPortH, kResolutionShowAll); }
默认情况下,当不调用方法显示去修改,游戏初始化之后设计分辨率和屏幕分辨率是保持一致的。