Cocos2d-x 3.3塔防游戏《保卫萝卜》教程05:对游戏原型进行屏幕适配完善
在开启新的篇章之前,我们先简单回顾一下之前学习的章节,包括环境搭建、项目创建、项目解析以及游戏原型的实现。
一、本篇前提
完成前一课的内容,具体参考:Cocos2d-x 3.3塔防游戏《保卫萝卜》教程04:实现简单的游戏原型
二、本篇目标
- 探讨Cocos2d-x手机分辨率适配的相关内容。
- 对前一篇完成的塔防游戏原型进行屏幕适配完善。
三、内容
(一)Cocos2d-x手机分辨率适配
在上一篇的结尾,我们遗留了一个问题:在真机上运行时,女主角、色狼的位置相对于道路都有点偏上,并且背景地图的顶部和底部有一部分没有显示出来,而在Windows下运行却正常。我的手机分辨率是960x540,而我们的地图素材图片分辨率是960x640,两个尺寸的不同导致了这个问题,这其实是不同手机屏幕分辨率适配的问题。
Android手机品种繁多,屏幕尺寸和分辨率也多种多样,为了让游戏适应不同的Android手机,需要做很多工作。相比之下,做IOS游戏要轻松一些,因为IOS设备的尺寸和分辨率种类较少。
影响游戏的两个关键因素是屏幕大小(分辨率)和宽高比。屏幕大小从小屏的480×320手机到大屏甚至平板的2048×1536不等。如果用低分辨率的素材图在高分辨率的设备上,图像会模糊;如果用高分辨率的素材在低分辨率设备上,会增加系统负担。因此,一般我们采取多套不同分辨率素材进行匹配,这个问题相对容易解决。
然而,宽高比带来的问题要麻烦得多。手机的宽高比有3:2、16:9等标准宽屏。在本篇中,我测试用的华为手机为16:9的宽屏。宽高比会造成游戏不按比例的压缩或者拉伸,导致游戏上的元素显示、位置等发生变异,甚至使游戏无法正常使用。所以,宽高比造成的问题比屏幕大小问题要严重得多。比如我们的这个塔防游戏,就因为宽高比的问题造成了游戏人物位置偏移。
那么,宽高比是否也可以像分辨率一样采用多套宽高比的素材解决呢?理论上是可以的,但如果不同的宽高比结合不同的分辨率,需要提供大量的素材。而且新的宽高比的手机不断涌现,为每一款新手机都制作一套素材,成本太大。
在cocos2d-x - 3.3/tests目录下有一个名为cpp - empty - test的示例工程,我们可以用Microsoft Visual Studio 2012打开proj.win32下的工程,参考其中的AppMacros.h、AppDelegate.cpp以及Resources目录下的素材来解决这个问题。
通过这个示例项目,我们可以总结出以下解决方案:
1. 屏幕大小(分辨率)解决方案
按照上述思路,我们一般提供低、中、高、超高四套不同分辨率的素材。低分辨率素材应付一般小屏手机,中分辨率素材应付高分辨率手机,高分辨率素材应付平板,超高分辨率素材应付高清平板或者电视之类的设备。这四套素材分别放在项目Resources文件下的4个文件夹中,例如iphone、iphonehd、ipad、ipadhd。在设备载入游戏时,程序会判断当前设备的分辨率,然后选择不同文件夹下的素材进行载入,以适应不同分辨率的设备。
2. 宽高比解决方案
为了适应设备各种屏幕宽高比,Cocos2dx提供了ResolutionPolicy(分辨率策略),我们可以通过给GLView设置不同的ResolutionPolicy来解决宽高比带来的问题。
ResolutionPolicy有五种类型:
- EXACT_FIT
- NO_BORDER
- SHOW_ALL
- FIXED_HEIGHT
- FIXED_WIDTH
(二)对前一篇完成的塔防游戏原型进行屏幕适配完善
1. 屏幕大小(分辨率)适配
- 第一步:按照上述解决方案,先制作iphone、iphonehd两套素材,然后拷贝到项目的Resources文件下。由于我们这款游戏的目标是手机类的设备,所以只提供了两套分辨率的素材。如果需要对更高分辨率的设备进行支持,那么需要提供更多套的素材。个人建议,如果你的游戏需要支持平板,那么建议单独出个HD版,虽然通过代码和素材的适配能同时支持手机和平板设备,但这样的实现有一定的限制,会在一定程度上降低某类设备的可玩性。
- 第二步:新建AppMacros.h文件,把cpp - empty - test示例工程下同名的文件代码直接拷贝过来进行修改,只需要保留两种不同的分辨率代码即可:
#define DESIGN_RESOLUTION_480X320 0 #define DESIGN_RESOLUTION_960X640 1 /* If you want to switch design resolution, change next line */ #define TARGET_DESIGN_RESOLUTION_SIZE DESIGN_RESOLUTION_960X640
typedef struct tagResource { cocos2d::Size size; char directory[100]; } Resource;
static Resource smallResource = { cocos2d::Size(480, 320), "iphone" }; static Resource mediumResource = { cocos2d::Size(960, 640), "iphonehd" };
if (TARGET_DESIGN_RESOLUTION_SIZE == DESIGN_RESOLUTION_480X320)
static cocos2d::Size designResolutionSize = cocos2d::Size(480, 320);
elif (TARGET_DESIGN_RESOLUTION_SIZE == DESIGN_RESOLUTION_960X640)
static cocos2d::Size designResolutionSize = cocos2d::Size(960, 640);
else
error unknown target design resolution!
endif
// The font size 24 is designed for small resolution, so we should change it to fit for current design resolution
define TITLE_FONT_SIZE (cocos2d::Director::getInstance()->getOpenGLView()->getDesignResolutionSize().width / smallResource.size.width * 24)
- **第三步**:打开AppDelegate.cpp文件,添加对AppMacros.h的引用,然后在applicationDidFinishLaunching方法里添加对当前设备屏幕分辨率进行判断并设置不同的图片素材的代码:
include <cocos2d.h>
include <GLViewImpl.h>
include "AppMacros.h"
// ……
// 获取当前设备屏幕尺寸
Size frameSize = glview->getFrameSize();
vector
通过这段代码,我们解决了低分辨率手机和高分辨率手机的图片素材适配问题。要测试效果,不用分别找低分辨率和高分辨率的手机,只需要在applicationDidFinishLaunching方法中添加一行`glview->setFrameSize(960, 440);`代码,就可以在调试时模拟不同分辨率的手机效果。当游戏开发完成时,删除本行代码即可。
if (!glview) { glview = GLViewImpl::create("DefendTheGirl"); // 设置模拟器分辨率大小 glview->setFrameSize(960, 440); director->setOpenGLView(glview); }
#### 2. 宽高比适配
- **第一步**:按照上面的解决方案,在AppDelegate.cpp的applicationDidFinishLaunching方法中,添加如下代码:
// 设置游戏的设计尺寸以及分辨率策略 glview->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, ResolutionPolicy::FIXED_HEIGHT);
我们先对ResolutionPolicy的5种类型分别进行测试,看看有什么区别,然后决定我们这个游戏应该采用哪种类型效果最佳。为了能明显看出区别,我们通过设置模拟器不同分辨率进行效果测试。
- **EXACT_FIT**:会拉伸素材进行显示,充满整个屏幕,这种方式最简单粗暴,但可能会出现图像变形。
glview->setFrameSize(960, 440);
结论:通过游戏截图可以看到,确实出现了图像变形。无论手机屏幕是何种宽高比,素材都会按照填满手机屏幕的宽高比进行拉伸变形,这种方法不可取。
- **NO_BORDER**:短边占满屏幕,另外一侧超出屏幕,一部分画面在屏幕外,无法显示。
glview->setFrameSize(960, 440); glview->setFrameSize(460, 640);
结论:我们分别用960,440和460,640两种手机尺寸进行测试。通过分析可知,第一种尺寸时,素材宽(960px)占满了整个手机屏幕,而素材高(640px)只显示了中间的440px,素材的上部分和下部分各被手机屏幕遮住了100px;第二种尺寸时,素材高(640px)占满了整个手机屏幕,而素材的宽(960px)只显示了中间的460px,素材的左边部分和右边部分各被遮住了250px。这里说的短边并不是指素材本身宽和高中短的那边,而是素材宽和手机宽进行比较,素材高和手机高进行比较,差值小的就算是短边。比如第一种尺寸时,宽差值 = |素材的宽960px - 手机屏幕宽960px| = 0,高差值 = |素材的高640px - 手机屏幕宽440px| = 200,所以短边是素材的宽。这种方法的宽高适应和设备的宽高比有关,哪个是短边不一定由设备决定。
- **SHOW_ALL**:保持原比例,让一边占满屏幕,另外一侧出现黑边。
glview->setFrameSize(960, 540);
结论:这种方式看起来相对合理,画面能保持原来素材的比例,不会拉伸变形。但遗憾的是,手机的左右两边或者上下两边会出现黑色的空白区,除非素材的宽高比恰好和手机的宽高比相同。这种方式对我们这个游戏来说是一个可选方案,但有黑边不是最佳的用户体验,不过它最简单,可以在任何设备中保持游戏真实的画面。
- **FIXED_HEIGHT**:和NO_BORDER类似,但指定高占满屏幕,宽部分超出屏幕外,无法显示。
glview->setFrameSize(960, 440); glview->setFrameSize(460, 640);
结论:这种方式和NO_BORDER有点像,上面两种尺寸屏幕测试结果均表现为素材高占满手机屏幕的高,而宽要不超出屏幕要不小于屏幕,素材仍旧维持本身的宽高比,同时游戏背景上的人物位置有一定的偏移,对我们这个游戏来说不太合适。
- **FIXED_WIDTH**:和NO_BORDER类似,但指定宽占满屏幕,高部分超出屏幕外,无法显示。
glview->setFrameSize(960, 440); glview->setFrameSize(460, 640);
结论:这种方式和NO_BORDER类似,上面两种尺寸屏幕测试结果均表现为素材宽占满手机屏幕的宽,而高要不超出屏幕要不小于屏幕,素材仍旧维持本身的宽高比,同时游戏背景上的人物位置有一定的偏移。
#### (2)游戏的选择
5种类型的测试结果和分析结果均已完成,现在要为我们的这个塔防游戏选择一个最合适的类型。EXACT_FIT肯定不可取,直接排除。SHOW_ALL是最简单的方案,虽然有小缺陷,但效果能接受,很多知名游戏前期的一些版本也采用这种模式。不过,我们的游戏可以有更高的追求,所以准备在NO_BORDER、FIXED_HEIGHT、FIXED_WIDTH这3个中选择一个。虽然这会增加编码和素材设计的难度,但通过合理的素材设计和代码配合,能完全实现全屏并且游戏比例不扭曲的效果。这3个其实属于一个类型,NO_BORDER其实包括了FIXED_HEIGHT、FIXED_WIDTH两种类型,具体表现为哪种类型由实际设备的宽高比确定,带有一定的不确定性。而FIXED_HEIGHT、FIXED_WIDTH是由开发者直接指定要高适应还是宽适应,这样至少确定了一个不确定因素,可以在一定程度上降低编码和素材设计的难度。
FIXED_HEIGHT、FIXED_WIDTH这2个中应该选哪个,跟要实际开发的游戏有关系。比如横屏游戏比较适合FIXED_WIDTH,而竖屏游戏比较适合FIXED_HEIGHT,同时也可能和游戏素材设计有关。对于我们的这个游戏地图背景,至少要保证地图中道路部分应该完整地呈现在手机屏幕的可视区域,其他部分可以允许被一定的遮盖。由此可见,我们的游戏应该选择FIXED_WIDTH。
#### (3)具体编码实现
- **第一步**:在AppDelegate.cpp的applicationDidFinishLaunching方法中,添加如下代码:
// 设置游戏的设计尺寸以及分辨率策略 glview->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, ResolutionPolicy::FIXED_WIDTH);
- **第二步**:设置模拟器屏幕尺寸`glview->setFrameSize(960, 540);`然后运行游戏。
会发现游戏道路完整显示,背景图上部分和下部分有一定区域被遮盖没有显示出来。这样我们在设计素材的时候,可以刻意把背景图的上下区域加高,尽量让素材高度很高,道路等有效部分尽量居中。比如要适应960x960的正方形屏幕,只需要把地图的上部分和下部分同时加高直至960或者更高,并且加高部分只需要平铺绿色的底纹就可以了,这样游戏就能和谐地满屏幕显示。
- **第三步**:这个游戏还有个问题,由于背景素材底部有部分被挡住了,导致设计时候的坐标原点和实际游戏坐标原点在高度上发生了偏移。
我们在前一篇中,道路的路点坐标是按照相对设计原点进行计算的,现在原点坐标发生了偏移,导致这些路点坐标不准,所以需要在代码上对这个偏移高度进行修正。
设dw为素材宽,dh为素材高,sw为屏幕宽,sh为屏幕高,偏移高度值为x,则:
x = (sh 0.5 - (sw / dw) dh * 0.5) / (sw / dw)
我们在applicationDidFinishLaunching的时候用这个公式把偏移高度计算出来,然后保存到一个静态变量中,方便后续直接使用。
创建GameMediator.h、GameMediator.cpp类,用来保存一些游戏中经常使用的变量,比如我们的偏移高度和素材缩放比例就保存在这个类中,这个类实现一个单实例的模式,功能很简单,以单实例静态变量的方式存放变量。
**GameMediator.h**:
class GameMediator : public cocos2d::CCObject { public: GameMediator(void); ~GameMediator(void); bool init(); // 获取单实例 static GameMediator* sharedMediator(); // 偏移高度 CC_SYNTHESIZE(float, _offsetHeight, OffsetHeight); // 缩放比例 CC_SYNTHESIZE(float, _scaleHeight, ScaleHeight); };
**GameMediator.cpp**:
// 静态实例 static GameMediator _sharedContext;
GameMediator* GameMediator::sharedMediator() { static bool s_bFirstUse = true; if (s_bFirstUse) { _sharedContext.init(); s_bFirstUse = false; } return &_sharedContext; }
GameMediator::GameMediator(void) { }
GameMediator::~GameMediator(void) { }
bool GameMediator::init() { bool bRet = false; do { _offsetHeight = 0; _scaleHeight = 1; bRet = true; } while (0); return bRet; }
- **第四步**:在AppDelegate类中引入GameMediator的头文件,然后在applicationDidFinishLaunching方法中加入如下代码:
// 以宽为标准计算素材缩放比例 float scaleHeight = frameSize.width / designResolutionSize.width; // 高度偏移值计算 // x = (sh 0.5 - (sw / dw) dh 0.5) / (sw / dw) float offsetHeight = (frameSize.height 0.5f - scaleHeight designResolutionSize.height 0.5f) / scaleHeight; // 保存缩放比例 GameMediator::sharedMediator()->setOffsetHeight(offsetHeight); // 保存高度偏移值 GameMediator::sharedMediator()->setScaleHeight(scaleHeight);
- **第五步**:找到MainScene.cpp中init方法声明12个路点坐标的地方做如下修改:
// 获得保存的偏移高度 float offsetHeight = GameMediator::sharedMediator()->getOffsetHeight(); // 获得保存的缩放比例 float scaleHeight = GameMediator::sharedMediator()->getScaleHeight(); // …… // 添加地图1号路径点到集合中 Waypoint waypoint1 = Waypoint::nodeWithTheLocation(Point(920, 435 + offsetHeight)); // …… // 添加地图12号路径点到集合中 Waypoint waypoint12 = Waypoint::nodeWithTheLocation(Point(50, 350 + offsetHeight)); // ……
- **第六步**:路点坐标校正完毕后,我们还需要对色狼和女主角做一下提高半个身位的校正,使得他们的脚底部刚刚在道路的中央。找到MainScene.cpp中init方法中初始化色狼和女主角部分的代码修改如下:
// …… // 获得色狼大叔的高 float dsh = dsSprite->getTextureRect().size.height; // …… // 女主高 float nzh = nhSprite->getTextureRect().size.height; // …… // 获取集合中的最后一个点,12号点 Waypoint *waypoint0 = wayPositions.back(); // 设置运动的开始点 beginningWaypoint = waypoint0; // 设置运动的目标点为12号点的下一个点,11号点 destinationWaypoint = waypoint0->getNextWaypoint(); // 设置色狼当前位置值 myPosition = waypoint0->getMyPosition(); // 提高半个色狼身位 myPosition.add(Vec2(0, dsh / 2.0f)); // 设置色狼在地图的初始位置 dsSprite->setPosition(myPosition); // 设置女主角在地图的初始位置,为集合中的1号点 Point pos = wayPositions.front()->getMyPosition(); // 提高半个女主角身位 pos.add(Vec2(0, nzh / 2.0f)); nhSprite->setPosition(pos); // ……
// 找到MainScene.cpp中update方法中色狼沿着道路移动部分的代码修改如下: // 获得色狼大叔的高 float dsh = dsSprite->getTextureRect().size.height; Point destinationPos = destinationWaypoint->getMyPosition(); // 提升色狼半个身位 destinationPos.add(Vec2(0, dsh / 2.0f)); // 判断色狼大叔是否和目标点碰到 if (this->collisionWithCircle(myPosition, 1, destinationPos, 1)) { // 是否还有下一个目标点 if (destinationWaypoint->getNextWaypoint()) { // 重新设定开始点和目标点 beginningWaypoint = destinationWaypoint; destinationWaypoint = destinationWaypoint->getNextWaypoint(); } } // 获取目标点的坐标 Point targetPoint = destinationWaypoint->getMyPosition(); // 提升色狼半个身位 targetPoint.add(Vec2(0, dsh / 2.0f));
- **第七步**:开始测试修改的效果,通过`glview->setFrameSize(960, 540);`不断修改各种尺寸的屏幕,查看这些尺寸屏幕下的游戏效果。并且编译打包一下so文件,在android真机上看看之前的问题是否解决。打包前记得把所有新加的cpp文件添加到Android.mk的编译列表里。
### 结束语
本篇花了很长的篇幅来解决上一篇遗留的一个问题,但我认为这非常值得,因为屏幕适配问题是一个很重要的问题。尽早合理地选择方案,能减少后续很多的返工量。现在我们解决了这个问题,后续在开发写代码的时候,就可以让编码适应这个方案。