这篇文章可以说是网上一些资料的整合,也是本人尝试写博客的初次尝试。
文章目录
前言
Cocos2d-x貌似已经逐渐被淘汰,但是对于用它来开发游戏对于新手来说是一次极佳的锻炼编程能力的机会,再加上我兴趣使然,于是决定学一学它,并用CSDN来记录一下。
一、Cocos2d-x的跨平台
众所周知,每个操作系统都有各自的图形库为应用层程序提供API接口,但是Cocos2d-x是基于OpenGL实现的,OpenGL作为一个不基于任何操作系统API的底层图形库有很好的跨平台的能力,使得Cocos2d-x能够处理多种平台的图形显示。在移动端的OpenGL版本为OpenGL ES,因为移动端对浮点运算相对于PC端较差,而OpenGL ES就将部分浮点运算转换成整数运算。
二、Cocos2d-x的VS项目模板
完整的Cocos2d-x模板的结构
安装好Cocos2d-x引擎并配置好环境变量后,可以使用cocos new
命令创建Cocos2d-x的项目模板,命令模板为cocos new [项目名称] -p [项目包名] -l [开发项目的编程语言,目前支持C++,Lua,javascript,c++的系数为cpp] -d [项目保存路径]
。具体各项如下图:
创建项目我写了一个批处理文件,代码如下:
@echo off
echo =============Cocos2D-x新建项目==============
set /p name=输入项目的名称:
set /p savepath=输入项目路径:
echo 项目名为: %name%
set /p packname=输入项目包的名称:
echo 项目包名为: %packname%
pause
echo 按任意键继续...
echo 正在创建新项目...
cocos new %name% -p %packname% -l cpp -d %savepath%
pause
echo 创建总项目完成. 按任意键继续..
创建Visual Studio项目
- 在项目文件夹中创建build文件夹
- 进入build文件夹
- 执行
cmake .. -G"Visual Studio 17 2022" -Tv143 -A win32 ..
。其中143,17随着Windows SDK以及VS的版本。 - 创建VS项目的脚本如下:
@echo off
echo =============Cocos2D-x新建项目==============
set /p savepath=输入项目路径(包含项目文件夹名):
set build=\build
md %savepath%%build%
cmake %savepath% -G"Visual Studio 17 2022" -Tv143 -A win32 -S %savepath% -B %savepath%%build%
echo vs项目创建成功
pause
用VS打开.sln
文件,项目结构如下:
将最下面的[项目名]的解决方案改为启动项即可调试运行。
三、看代码前的预热
从官方文档我们可以知道导演CCDirector,场景CCScene,精灵CCSprite,动作CCAction等概念,其中导演管理多个场景,场景管理多个节点(精灵就是继承自节点),节点可以添加多个子节点,通过场景跟节点可以将游戏划一步步划分成具有层级关系的部分(一个树的结构,其中场景的Order就是节点排列即显示的优先级)从而便于开发,具体查看官方文档。
另外还要说的是坐标系,cocos2d-x采用OpenGL坐标系,左下角为起点,初始为(0,0)横坐标向右递增,纵坐标向上递增。
四、VS项目模板代码分析
将VS项目编译运行后,会显示如下窗口(test为我修改后内容,原为helloworld):
main.cpp
main中只有一个_tWinMain函数,该函数是所有Windows窗口程序的入口函数。里面主要是用以下代码创建应用的实例。
AppDelegate app;
return Application::getInstance()->run();
我们可以从AppDelegate.h文件中看出,AppDelegate类派生自Application类。跟踪getInstance函数
可以发现创建Application类的对象会将Application::sm_pSharedApplication
对象赋值为创建的对象,而getInstance函数
返回的是Application::sm_pSharedApplication
。
AppDelegate类
再看AppDelegate类
之前我们得先知道Application
派生自 ApplicationProtocol
该类为应用程序必须遵守的协议。里面定义了三个函数。分别是:
virtual bool applicationDidFinishLaunching()=0
该函数处理程序启动完成之后,即将运行之前的事情。多运用于加载程序资源以及Cocos2d-x的架构。virtual void applicationDidEnterBackground()=0
该函数处理应用程序即将进入后台的事件。比如停止播放音乐、停止更新与渲染等。virtual void applicationWillEnterForeground() = 0;
该函数处理应用程序即将进入前台的事件。比如启动音乐播放,进行更新与渲染等。
我们再来看AppDelegate类
的几个成员函数,如下:
applicationDidEnterBackground函数
只执行了一个函数Director::getInstance()->stopAnimation();
使用导演类停止更新与渲染。applicationWillEnterForeground函数
类似,它只重新开始更新与渲染。
applicationDidFinishLaunching函数
applicationDidFinishLaunching函数
是跨平台引擎执行的真正入口(main函数
中的run函数
正是执行AppDelegate类
的该函数)。
首先来看最开始两行:
// initialize director
//GLView 可以通过一些函数对EGL视图的帧信息进行操作。
//EGL 是 OpenGL ES 渲染 API 和本地窗口系统(native platform window system)之间的一个中间接口层,它也主要由厂商来实现。
auto director = Director::getInstance();
auto glview = director->getOpenGLView();
这两行是获取导演与视口。导演的对象可以简要理解成游戏中唯一一个控制场景切换,音乐暂停与播放等的实例。
接下来是根据不同的平台修改视口并将它与导演关联。
if(!glview) {
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) || (CC_TARGET_PLATFORM == CC_PLATFORM_MAC) || (CC_TARGET_PLATFORM == CC_PLATFORM_LINUX)
glview = GLViewImpl::createWithRect("test",//窗口标题
cocos2d::Rect(0, 0,//起始位置
designResolutionSize.width, designResolutionSize.height));//矩形宽高
#else
glview = GLViewImpl::create("helloworld");
#endif
director->setOpenGLView(glview);
}
然后是显示FPS以及设置每秒帧数
// turn on display FPS,是否显示每秒帧数
director->setDisplayStats(true);
// set FPS. the default value is 1.0/60 if you don't call this
设置每帧的间隔
director->setAnimationInterval(1.0f / 60);
接着根据设备大小调整显示屏幕大小。setDesignResolutionSize函数
为设置屏幕分辨率(即这里的窗口分别率),setContentScaleFactor函数
为调整设定设备屏幕分别率设计分辨率缩放比。
// Set the design resolution
//设置屏幕大小,ResolutionPolicy::NO_BORDER为适配屏幕大小,不留边,会伸出到屏幕外面,EXACT_FIT是保持横纵比,SHOW_ALL为占满屏幕,不保持横纵比
glview->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, ResolutionPolicy::NO_BORDER);
//获取物理设备的宽高比,返回一个Size结构体,成员是宽和高
auto frameSize = glview->getFrameSize();
// if the frame's height is larger than the height of medium size.
if (frameSize.height > mediumResolutionSize.height)
{
//个映射其实是有2步,一步是把资源映射到设计分辨率,一步是把设计分辨率映射到屏幕。setContentScaleFactor解决的是把资源映射到设计分辨率这一步。
//这个的算法是(资源)/(设计分辨率)
director->setContentScaleFactor(MIN(largeResolutionSize.height/designResolutionSize.height, largeResolutionSize.width/designResolutionSize.width));
}
// if the frame's height is larger than the height of small size.
else if (frameSize.height > smallResolutionSize.height)
{
director->setContentScaleFactor(MIN(mediumResolutionSize.height/designResolutionSize.height, mediumResolutionSize.width/designResolutionSize.width));
}
// if the frame's height is smaller than the height of medium size.
else
{
director->setContentScaleFactor(MIN(smallResolutionSize.height/designResolutionSize.height, smallResolutionSize.width/designResolutionSize.width));
}
查看register_all_packages();函数
发现该函数只是返回一个0,不做任何其他处理。如果需要注册用到的包可在该函数内添加。
//注册要用到的包,不改的话没做任何处理
register_all_packages();
最后面的两行极为重要。
// create a scene. it's an autorelease object
auto scene = HelloWorld::createScene();
//建立director与scene的关系
director->runWithScene(scene);
发现调用的是helloworld类
中的createScene()
成员函数来创建一个场景并使其与导演实例关联。
接着来看框架的最后一个类HelloWorldScene
。
HelloWorldScene类
从该类的定义中可以发现这个类继承自Scene
类。老规矩,先来看看它的几个成员函数。
然后继续挨个来看这些函数。
createScene函数
该函数只调用了HelloWorld
类的create
函数。但是从cpp跟头文件来看并没有声明甚至定义该函数,那么这个函数在哪里呢?我们发现在头文件中有这个一个宏的应用。
CREATE_FUNC(HelloWorld);
转到该宏的定义。
#define CREATE_FUNC(__TYPE__) \
static __TYPE__* create() \
{ \
__TYPE__ *pRet = new(std::nothrow) __TYPE__(); \
if (pRet && pRet->init()) \
{ \
pRet->autorelease(); \
return pRet; \
} \
else \
{ \
delete pRet; \
pRet = nullptr; \
return nullptr; \
} \
}
由此可以看出这个宏是create
函数的声明,返回一个传入参数类型的指针。
problemLoading函数
这个函数没什么好讲的,就是一个打印错误的函数,传入一个错误发生的所在的文件的名称。
init函数
这个函数又是个重头戏。在HelloWorldScene::Create()中
调用。首先调用Scene
的静态成员函数init()
来判断程序是否已经初始化
// 1. super init first
if ( !Scene::init() )
{
return false;
}
然后是通过导演获取屏幕分辨率(即这里的窗口分辨率)以及屏幕起点(窗口游戏世界左下角的位置)。造成屏幕起点不同的原因是对不同设备的适配方式。
//要求导演告诉该类对象游戏视口分辨率
auto visibleSize = Director::getInstance()->getVisibleSize();
//要求告诉屏幕起点,不同适配方式起点不同
Vec2 origin = Director::getInstance()->getVisibleOrigin();
再接着它开始创建一个菜单选项,这个菜单选项是 MenuItemImage
类型。
auto closeItem = MenuItemImage::create(
"CloseNormal.png",//未选中图像
"CloseSelected.png",//选中图像
CC_CALLBACK_1(HelloWorld::menuCloseCallback,//触发后响应函数
this));//父对象
让我们来看一会儿CC_CALLBACK_1
宏,该宏的定义如下:
#define CC_CALLBACK_1(__selector__,__target__, ...) std::bind(&__selector__,__target__, std::placeholders::_1, ##__VA_ARGS__)
推测该宏的作用就是绑定两个指针。
然后开始设置按钮在屏幕位置,其中的计算方式会将按钮放在显示窗口的右下角。这里注意一下,原点的坐标设置方式与OpenGL相同,为游戏世界(即这里的窗口)的左下角。
if (closeItem == nullptr ||
closeItem->getContentSize().width <= 0 ||
closeItem->getContentSize().height <= 0)
{
problemLoading("'CloseNormal.png' and 'CloseSelected.png'");
}
else
{
//设置按钮位置
float x = origin.x + visibleSize.width - closeItem->getContentSize().width/2;
//设计分辨率的宽度+原点的横坐标即最右端点的坐标,
//再减去控件的宽度的一半是为了将控件最右端对齐屏幕(程序窗口)的最右端,纵坐标同理。
float y = origin.y + closeItem->getContentSize().height/2;
closeItem->setPosition(Vec2(x,y));
}
再然后创建一个菜单并将菜单项放入菜单。将菜单的锚点设置为屏幕(即窗口)的原点,Cocos2d-x的锚点默认为(0,5,0.5),即对象的中心。addChild中的zOrder参数(即下面addChild中的参数1)表示绘制层的顺序,默认参数值为0,表示最高优先层导入,该值越大表示该层在最后加载(在最高一层)。
// 创建菜单,它是一个自动释放对象,菜单继承自layer类
//将菜单项放入菜单
auto menu = Menu::create(closeItem, NULL);//可以是多个选项,但最后一个参数必须是NULL
//菜单放到0,0位置,否则菜单锚点不在原点
menu->setPosition(Vec2::ZERO);
//1为层加载时第二大的优先级
this->addChild(menu, 1);
接着创建文本标签,即图像中的test文本:
//创建文本标签
auto label = Label::createWithTTF("test",//文本内容
"fonts/Marker Felt.ttf", //文本字体,可以通过此参数更改不同的字体
24);//字体大小
if (label == nullptr)
{
problemLoading("'fonts/Marker Felt.ttf'");
}
else
{
// position the label on the center of the screen
label->setPosition(Vec2(origin.x + visibleSize.width/2,
origin.y + visibleSize.height - label->getContentSize().height));
// add the label as a child to this layer
this->addChild(label, 1);
}
最重要的莫过于最后一步设置精灵并将其与场景关联。
精灵是可以在屏幕上移动的对象,它能被玩家控制,如果无法控制,那就只是一个节点(Node)。
这一步用一张图片(HelloWorld.png)创建了精灵。
这里插一句关于资源的问题。HelloWorld.png位于Resources文件夹。如下图所示:
所以它的路径直接为它的名字,但是字体位于fonts文件夹中,必须加上文件夹名称,即相对路径。
auto sprite = Sprite::create("HelloWorld.png");//使用图片创建一个精灵
if (sprite == nullptr)
{
problemLoading("'HelloWorld.png'");
}
else
{
// position the sprite on the center of the screen
sprite->setPosition(Vec2(visibleSize.width/2 + origin.x,
visibleSize.height/2 + origin.y));//正中间位置
// add the sprite as a child to this layer
this->addChild(sprite, 0);
}
五、总结
这篇博客是我第一次写博客,并且也是第一次学习Cocos2d-x,所以如果有错误,希望各位大佬能够不吝赐教。