上一次看 cocos2d-x 是去年的事情了,下了个 SDK 而后按照官方的那个 pew-pew-pew 教程接二连三捣鼓了几下,之后几乎就没怎么去碰了。最近再看感觉连当时自己写的代码都快不认识了,果然还是需要记下笔记 = =
获取 SDK
前往 cocos2d-x 官网下载稳定版打包,或者去官方 GitHub 仓库 git clone
一份(注意看清分支哟)。
个人这回是下载了最新稳定版本,目前是 2.1.4。
创建工程
Cocos2d-x 由若干部分组成,有些采用静态链接、有些则是动态链接、更有些是直接源代码分发的;加上工程本身的跨平台属性,需要针对不同平台写初始化代码;总而言之,从零开始手写一个空工程是相当繁琐的。
所以老版本的策略是:为不同平台下的开发工具提供模板、脚本之类的东西以便获得每次的初始空工程。比如它会为 Visual Studio 安装一个 cocos2d-x 程序模板,这样就可以直接在新建对话框里选择新建一个 Visual C++ 的空 cocos2d-x 工程了。
当然,从 2.1.4 版本开始,官方就不再为 VS 提供模板了,逐步在各平台采用统一的 Python 脚本创建跨平台工程。(吐槽:虽然不提供模板了,但是原先用来安装模板的 install-templates-msvc.bat 文件居然还在,虽然一运行就报错的说……)
这个创建脚本使用 Python 2.X,同时它还有一个 Bug:使用当前目录而不是脚本文件所在目录查找模板文件(GitHub 上的 develop 版已经修复)。所以,要创建工程,我们需要先从命令行进入 tools/project-creator 目录,然后再使用
create_project.py -project <工程名称> -package <Java 包名> -language <语言>
就会在 projects 目录下创建相应的工程。这一点在直接运行 create_project.py
时会给出提示。(3.0 版本将在 cocos2d-x 根目录提供 create-multi-platform-projects.py 文件为你间接调用 tools/project-creator/create_project.py)那么作为一个示例,我们可以输入:
create_project.py -project Whatever -package com.timothyqiu.Whatever -language cpp
咳咳,如果你对为什么语言是 C++ 也要输入「Java 包名」感到困惑,那么看一眼生成后的工程目录结构就明白了:
Whatever -+- Classes
|- Resources
|- proj.android
|- proj.blackberry
|- proj.ios
|- proj.linux
|- proj.mac
|- proj.marmalade
`- proj.win32
结构非常精巧:平台无关的(自己写的逻辑之类的)代码都在 Classes 目录下;程序用到的资源都在 Resources 目录下;其余的平台相关(预置的初始化之类的)代码及工程文件都在各自的目录下。
所以,既然有 Android 工程在,那么创建时显然是需要包名的。
渲染树
如果你熟悉 3D 也许你也熟悉「渲染树」的概念:加入渲染树的节点才能够被渲染,而节点的父子关系也对其位移、缩放、旋转有相应影响。渲染一帧图像的过程就是遍历整棵渲染树、依次绘制各个节点的过程。
cocos2d-x 中的节点,以 CCNode
表示,它除了作为一棵渲染树的节点外,还提供了定时回调以及执行 CCAction
动作的功能。当然, CCNode
还封装了一些类似位置、缩放、旋转的基本属性,其中锚点(Anchor Point)是非常重要的属性:节点的位置和节点左上角、右下角的位置是什么关系?缩放以哪个点为定点?旋转以哪个点为中心?这些问题的答案就是锚点。
cocos2d-x 中对象的属性使用 Getter/Setter 封装,均为 setPropertyName(value)
和 getPropertyName()
、isPropertyName()
的形式。一些 CCNode
常用属性如下:
- 位置(Position)默认为 (0, 0),是一个
CCPoint
类型的值。
- 旋转(Rotation)默认为
0
,顺时针以角度计。
- 缩放(Scale)默认为
1.0f
。
- 尺寸(ContentSize)默认为 (0, 0),是一个
CCSize
类型的值。
- 可见(Visible)默认为
true
。不可见和不存在是两码事。
- 锚点(AnchorPoint)默认为 (0, 0)。每个分量都是一个浮点数,
0.0f
表示左上,1.0f
表示右下(所以 0.5f
表示中心点)。取值也可以不在 0 和 1 的范围内,表示锚点位置在节点范围外。
另外还有 Tag、UserData、UserObject 三人组,类型分别是 int
、void *
和 CCObject
,都是用户为节点自定义的数据,CCNode
只是把它存起来以供你日后获取,不作它用。
CCNode
常见的一些子类包括:
CCScene
场景,一般作为渲染树的根节点存在
CCLayer
层,一般作为精灵的容器存在,能够接受外部输入(触摸事件、加速计等)
CCSprite
精灵,图像节点
CCMenu
抽象菜单,可以添加 CCMenuItem
的 UI 元素。本身是从 CCLayer
派生的
出于拯救大众于水火的目的,以上子类的锚点都被默认设成了 (0.5, 0.5)。
p.s. cocos2d-x 中的绝大多数类均以 CC
作为前缀。但这样的做法明显是与它们所在的 cocos2d
这个 namespace 重复的,所幸在将来的 3.X 新版本中这些重复的前缀都会被删掉。
无规矩不成方圆
cocos2d-x 基于 OpenGL,故使用右手坐标系:从左到右、从下到上为坐标轴正方向。
前面说过的 CCPoint
类型,可以用来表示坐标点,也可以用来表示向量;而 CCSize
类型类似,用来表示尺寸;另外还有一个 CCRect
是 CCPoint
和 CCSize
的组合,表示矩形区域的左上角和尺寸。
因为 CCPoint
和 CCSize
实质都是两个 float
的组合,所以其构造函数也都需要两个 float
。介于每次传入不同类型变量的都显式去 static_cast<float>
过于手残,cocos2d-x 给出了三个对应的宏来帮你写转型:CCPointMake(x, y)
、CCSizeMake(w, h)
和 CCRectMake(x, y, w, h)
。当然,CCPoint
太常用了,以至于它还有一个更省击键数的宏 ccp(x, y)
。
前面还说过旋转属性,它是一个角度值。cocos2d-x 提供了 CC_DEGREES_TO_RADIANS(deg)
和 CC_RADIANS_TO_DEGREES(rad)
宏进行弧度、角度的换算。
内存管理
cocos2d-x 使用了来自 cocos2d-iphone 的 Objective-C 风格,这不仅体现在代码风格上,还体现在内存管理风格上。
Objective-C 的内存管理使用的是(手动)引用计数技术。简而言之,就是对象默认存在 1 个引用、用 retain()
可以增加一个引用、用 release()
可以减少一个引用。一旦引用减少到 0,对象自动释放。
聪明的你一定想到了不少实现方法。不过由于目前版本中所有对象都只是裸指针(例如 CCDirecter *director
的本质是一个指针而非对象),所以,似乎在这一前提下实现引用计数的唯一方法就是:所有对象都继承自同一根类,由这个根类负责进行引用计数工作。当然如此一来,所有对象都应该创建在堆上。
顺带一提,cocos2d-x 当前版本并没有使用智能指针,一是因为 cocos2d-iphone 就是如此,二是因为 cocos2d-x 项目启动时 C++11 仍未定稿,三是因为当时各手机平台上支持情况不一样。不过时过境迁,在 2013 都过去了一半的现在,官方已经决定在 3.0 新版本里引入 C++11 的智能指针。
根对象
cocos2d-x 用来提供引用计数功能的根类就是 CCObject
。主打这四个函数:
CCObject::CCObject()
CCObject::retain()
CCObject::release()
CCObject::autorelease()
前三个好说,分别是将当前对象的引用计数置一、增一、减一。那么最后一个 autorelease()
是干啥的?
借用 Objective-C 中的解释:release()
是引用计数立即减一,而 autorelease()
则是在不久的将来减一(至少在当前函数之后)。
在 cocos2d-x 的实现中,使用了名叫 CCAutoReleasePool
的「自动释放池」对象。它会在自身被释放(析构)时调用其所持有的所有对象的 release()
方法。CCObject::autorelease()
所做的正是将自己加入自动释放池中。
cocos2d-x 会在每一帧的开始创建一个 CCAutoReleasePool
,并在该帧结束时释放它。所以,所有被调用过 autorelease()
的 CCObject
都有机会在当前帧结束后自动释放。(之所以是「有机会」,因为即使调用 release()
同样也只是有机会释放,是否真正释放由引用计数决定。)
所以,大致小结一下就是:
- 每一次
new
或者 retain()
都应该对应存在一次 autorelease()
或者 release()
autorelease()
是有代价的,所以应该尽量使用 release()
创建和销毁
cocos2d-x 中没有使用异常,所以对象的创建使用了两步构造法(Two-phase Construction)。所谓两步构造法,就是构造函数只用来为变量赋初值而不执行逻辑相关代码,所有初始化用的功能代码另写一个初始化函数。(构造函数没有返回值,所以只能用异常与外界沟通。)
CCSprite *sprite = new CCSprite(); // 创建对象
sprite->initWithFile("background.png"); // 初始化对象
上面这两行就是两步构造法的一个实例。介于这么写相当的繁琐,cocos2d-x 为每一个 initXXX()
都提供一个相应的工厂方法 createXXX()
:
CCSprite *sprite = CCSprite::create("background.png");
其实现很简单:
CCSprite *pobSprite = new CCSprite();
if (pobSprite && pobSprite->initWithFile(pszFileName)) {
pobSprite->autorelease();
return pobSprite;
}
CC_SAFE_DELETE(pobSprite);
return NULL;
注意到其中的 autorelease()
了吗?这意味着,如果只是 create()
而没有做其它任何操作,那么这个对象会在本帧结束时释放;但如果在 create()
后调用了 sprite->retain()
,那么这个对象就不会自动释放(如果是 this->addChild(sprite)
则同理,因为 addChild()
会间接调用 retain()
)。
需要注意的是,这种创建方式也导致父类无法自动初始化,必须在子类的 initXXX()
方法里手动去调用直接父类的 initXXX()
方法。于是(如果手写),一般的初始化都类似于:
bool HelloWorldLayer::init()
{
bool success = false;
do {
CC_BREAK_IF(!CCLayer::init());
// actual init code here, break if failed
success = true;
} while (0);
// clean up if nescesary
return success;
}
容器
因为 cocos2d-x 的这种「特殊」内存管理方式,对 retain()
、release()
一无所知的 STL 容器或许并不适合你,于是便有了 CCArray
、CCSet
、CCDirectory
。(在 STL 容器中直接存放裸指针,可能由于野指针带来各种麻烦。当然,如果考虑使用智能指针的话,存入对应 std::shared_ptr
即可保证容器生命周期内所含对象有效;而 cocos2d-x 在未来版本中全面引入智能指针代替 Objective-C 风格的内存管理后,单存入 std::weak_ptr
就可以检测到野指针了。)
cocos2d-x 所提供的这些容器所存放的都是 CCObject
对象(其实是指针,但鉴于正确的使用方法只可能是指针,所以方便起见以后就不强调是指针了)。在对象进入容器时,容器自动调用一次 retain()
取得其引用;在对象移出容器或是容器本身被销毁时,容器自动为对象调用一次 release()
放弃引用。
这样就保证了所有在这些容器中的对象都是有效的对象。(还记得 CCAutoReleasePool
吗?它就是用 CCArray
实现的。)
对于容器,除却添加删除,常用的还有遍历。与 STL 提供迭代器不同,CCArray
和 CCDirectory
都是用 For Each 宏来遍历的。(CCSet
和 STL 相同。)
CCObject *element = nullptr;
CCARRAY_FOREACH(arrayOfSprites, element) {
auto sprite = dynamic_cast<CCSprite *>(element);
// ...
}
导演
CCDirector
类就是导演类,(总)导演每部戏只有一个,所以它是个单件类。cocos2d-x 中所有单件类都通过类似 CCName::sharedName()
的方式获得 CCName *
单件对象。这种(命名)习惯是从 cocos2d-iphone 里带来的,在 3.X 中,将会变成 CCName::getInstance()
这种更方便的方式。
导演的主打功能是场景调度和游戏流程控制。
runWithScene()
开始运行,设置初始场景
replaceScene()
切换场景,不保留旧场景
pushScene()
、popScene()
常见的场景栈式管理
pause()
暂停当前场景(仍有会绘制场景,但是不会执行逻辑)
resume()
恢复当前场景
end()
结束运行
细枝末节
在 SDK 为我们提供的空工程中,除了上面提到的这些「大头」,还有一些零碎的细枝末节。
入口
事实上,每一个 proj.*
目录下都有自己平台的初始化代码,也就是各自类似于 main()
的东东。
而在 Classes
目录下,平台无关代码中,程序的入口便是 AppDelegate
类。没错,这个类(依旧)源自 cocos2d-iphone,确切的说是 iOS,个人理解,这是一个用来接收程序 UI 状态消息的类。
程序通过其中仅有的三个顾名思义的方法 applicationDidFinishLaunching()
applicationDidEnterBackground()
applicationWillEnterForeground()
来感知自身的状态,以作出反应。比如 Did finish launching 时初始化引擎;Did enter background 时暂停背景音乐、暂存状态;Will enter foreground 时取出状态、恢复背景音乐等等。
现有的「空工程」中,applicationDidFinishLaunching()
里的核心其实就是这两句:
CCScene *pScene = HelloWorld::scene();
pDirector->runWithScene(pScene);
它搭建了 Classes 目录下 AppDelegate
和 HelloWorld
这两个类的桥梁:将场景创建出来后交给导演,然后导演就自顾自干活去了。
选择器
在 HelloWorld.cpp
中,我们可以看到这样的语句:
CCMenuItemImage *pCloseItem = CCMenuItemImage::create(
"CloseNormal.png", "CloseSelected.png",
this, menu_selector(HelloWorld::menuCloseCallback)
);
这个 menu_selector
好神奇,究竟是什么?
好吧,这货(没错,还)是从 cocos2d-iphone 来的,我们来看实际代码(2.1.4 版本的 C 风格转换看着不爽,这里使用 C++ 风格类型转换稍作修改):
typedef void (CCObject::*SEL_MenuHandler)(CCObject*);
#define menu_selector(_SELECTOR) static_cast<cocos2d::SEL_MenuHandler>(&_SELECTOR)
你看,没什么大不了的,各种 selector 都只是「成员函数指针 + 强制类型转换」而已,它和它的前一个参数(CCObject *
)共同指定了一个回调。(回想一下,你还记得 C++ 里该如何定义、初始化、使用成员函数指针吗?)
当然,在不久的将来,3.X 版本会使用更灵活的 std::function
和 std::bind
来代替这种「selector」。
以上。
这几天 Bad Apple 又逆袭了,比如这个和这个。虽然周末时候自己也做了一个,但介于身心憔悴和一个遗留问题,暂时先不和上次一样出视频了……
首先,看到那两个演示视频就可以联想到《编程之美》里面的一道面试题,但显然完全不是一回事,因为图像的 x → y
中 y
并不唯一。既然如此,那么就可以推定这是某种程度的「造假」,因为图像已经不是 CPU 占用率曲线了。
既然造假,我们就要造得有良心!那种类似直接在任务管理器上新建/覆盖一个视频窗口的做法略无节操了一些。作为一个死程,我们还是慢慢用程序解决吧……
视频预处理
这个就不用写程序了,看过我之前视频的童鞋一定知道我要用 ffmpeg 来把视频转成位图序列,没错,这次还是它!
而 ffmpeg 同时还提供了非常好用的滤镜支持,要把视频变成 CPU 占用率曲线的样子,我们需要边缘检测(edgedetect)和颜色通道混合器(colorchannelmixer)。
ffmpeg -i <文件名> -r <FPS> -vcodec bmp -vf edgedetect,colorchannelmixer=0:0:0:0:1:1:1:0:0:0:0:0 <输出文件名>
这样就能直接输出黑底绿线的位图序列,以备使用了。
屏蔽掉 CPU 占用率曲线的绘制
这里只考虑 Win 7 及之前的任务管理器,因为这货很好改。首先,这曲线一看就是 LineTo()
函数画出来的,那么我们用 OllyDbg 直接查看任务管理器中所有对于 LineTo()
的调用就可以找到对应代码。
我是 Win 7 的系统,用 OllyDbg 直接打开后,对 LineTo()
的调用只有:
- 绘制性能标签页中的背景网格
- 绘制 CPU 占用率曲线
- 绘制内存占用曲线
- 绘制联网标签页中的背景网格
- 绘制网络活动曲线
这几处,熟悉汇编的都可以很快定位整个语句的范围。至此,我们得到了一个地址及一个长度,把这个范围内的指令全部变成 Nop 指令即可。
当然 OllyDbg 中反汇编的地址是不能直接用的,还需要减去当前模块的基地址,得到偏移量以备使用……
那么来到 C++,FindWindow
→ GetWindowThreadProcessId
→ OpenProcess
即可获得任务管理器的进程句柄。
得到进程句柄后,我们首先要获得 taskmgr.exe 模块当前的基地址:EnumProcessModules
→ GetModuleInformation
。而后,就可以用上之前得到的偏移量和长度,使用 WriteProcessMemory
把绘制 CPU 曲线的代码覆写为一串 Nop 指令。
世界清静了。
绘制动画
这里就没有什么技术含量了。无非就是自己 SetTimer
开一个定时器,按照一定的帧率往窗口上 TransparentBlt
以前处理好的图片。
需要注意的是,背景网格,这货最麻烦了。我目前没有用 Hook,所以暂时的做法是:自己维护一份干净的背景网格。这样做的缺点是,网格更新时有一定几率察觉到曲线的缺失(用 Hook 后应该会好:Hook 后替换掉 Window Proc,然后正确的 WM_DRAWITEM
/WM_PAINT
之后立即把当前的图片绘制上去)。
以上。
前文再续,书接上回。上回咱们讲到,如何在 Vim 中使用 OmniCppComplete 为 C/C++ 自动补全;今天偶然间发现了个新玩意儿:clang complete。
前情提要
OmniComplete 是 Vim 中的智能补全功能,而 OmniComplete 本身并不知道如何补全,具体的「通过光标前的内容猜测光标后可能出现的内容」的工作是由不同的外部插件实现的。
上回说到的 OmniCppComplete 就是这样一个插件。实际需要预先调用 ctags 对源代码进行词法分析(吧?)生成 tags 文件(token 列表),然后在这个 tags 文件中去进行匹配。所以局限也很快暴露出来:无论是库的头文件还是自己的源代码,都要用户自己事先对它运行一遍 ctags。
clang complete 则是借助 clang 来分析源代码,毕竟没有比编译器更了解代码的东西了。
如何安装
需要注意两个前提:
- clang complete 和 OmniCppComplete 冲突
- 如果想成功起效,请确保你有安装 clang……(废话 = =)
那么,你可以选择直接去 Vim 官方插件列表页获取,或者去作者的 GitHub clone
,抑或使用 Vundle 安装即可。
如果获取的是 vmb 文件,那么命令行执行
vim clang_complete.vmb -c 'so %' -c 'q'
Windows 下则直接用 Vim 打开,然后 :source %
……(呃,这好像也是废话 = =)
p.s. 强烈推荐试一试用 Vundle 来管理 Vim 的插件。
clang complete 是即拆即食的(OmniCppComplete 在安装完成后还需要到处生成 tag 文件),编辑状态输入 .
和 ->
和 ::
后都会自动进行补全,一般状态下 Ctrl-X Ctrl-O 或者 Ctrl-X Ctrl-U 都能手动调用补全。
配置文件
如果在补全时发现提示「Pattern not found」提示,可以试试 :copen
查看错误信息。
错误信息很可能是语法错误,或者头文件未找到。
没错,因为 clang 是编译器,每次补全实际上都对整个源代码进行分析。要正确编译,可能需要额外传一些 -D 宏定义
和 -I 头文件路径
给 clang。为了解决这一问题,clang complete 默认会在加载文件时读取该目录下名为 .clang_complete
的文件,该文件中就可以添加这样的信息,例如:
-DNDEBUG
-I../include
`pkg-config gtk+-2.0 --cflags`
当然,链接是不会执行的,所以不必把链接选项写进去。
其它
如果你觉得补全得慢,可以尝试让它使用 libclang 而不是 clang:
let g:clang_use_library=1
let g:clang_library_path="path to libclang"
这个功能需要你的 Vim 有 Python 支持……