TimothyQiu's Blog

keep it simple stupid

Cocos2d-x 笔记:人类一大步

分类:技术

上一次看 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 常用属性如下:

另外还有 Tag、UserData、UserObject 三人组,类型分别是 intvoid *CCObject,都是用户为节点自定义的数据,CCNode 只是把它存起来以供你日后获取,不作它用。

CCNode 常见的一些子类包括:

出于拯救大众于水火的目的,以上子类的锚点都被默认设成了 (0.5, 0.5)。

p.s. cocos2d-x 中的绝大多数类均以 CC 作为前缀。但这样的做法明显是与它们所在的 cocos2d 这个 namespace 重复的,所幸在将来的 3.X 新版本中这些重复的前缀都会被删掉。

无规矩不成方圆

cocos2d-x 基于 OpenGL,故使用右手坐标系:从左到右、从下到上为坐标轴正方向。

前面说过的 CCPoint 类型,可以用来表示坐标点,也可以用来表示向量;而 CCSize 类型类似,用来表示尺寸;另外还有一个 CCRectCCPointCCSize 的组合,表示矩形区域的左上角和尺寸。

因为 CCPointCCSize 实质都是两个 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。主打这四个函数:

前三个好说,分别是将当前对象的引用计数置一、增一、减一。那么最后一个 autorelease() 是干啥的?

借用 Objective-C 中的解释:release() 是引用计数立即减一,而 autorelease() 则是在不久的将来减一(至少在当前函数之后)。

在 cocos2d-x 的实现中,使用了名叫 CCAutoReleasePool 的「自动释放池」对象。它会在自身被释放(析构)时调用其所持有的所有对象的 release() 方法。CCObject::autorelease() 所做的正是将自己加入自动释放池中。

cocos2d-x 会在每一帧的开始创建一个 CCAutoReleasePool,并在该帧结束时释放它。所以,所有被调用过 autorelease()CCObject 都有机会在当前帧结束后自动释放。(之所以是「有机会」,因为即使调用 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 容器或许并不适合你,于是便有了 CCArrayCCSetCCDirectory。(在 STL 容器中直接存放裸指针,可能由于野指针带来各种麻烦。当然,如果考虑使用智能指针的话,存入对应 std::shared_ptr 即可保证容器生命周期内所含对象有效;而 cocos2d-x 在未来版本中全面引入智能指针代替 Objective-C 风格的内存管理后,单存入 std::weak_ptr 就可以检测到野指针了。)

cocos2d-x 所提供的这些容器所存放的都是 CCObject 对象(其实是指针,但鉴于正确的使用方法只可能是指针,所以方便起见以后就不强调是指针了)。在对象进入容器时,容器自动调用一次 retain() 取得其引用;在对象移出容器或是容器本身被销毁时,容器自动为对象调用一次 release() 放弃引用。

这样就保证了所有在这些容器中的对象都是有效的对象。(还记得 CCAutoReleasePool 吗?它就是用 CCArray 实现的。)

对于容器,除却添加删除,常用的还有遍历。与 STL 提供迭代器不同,CCArrayCCDirectory 都是用 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() 这种更方便的方式。

导演的主打功能是场景调度和游戏流程控制。

细枝末节

在 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 目录下 AppDelegateHelloWorld 这两个类的桥梁:将场景创建出来后交给导演,然后导演就自顾自干活去了。

选择器

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::functionstd::bind 来代替这种「selector」。

以上。

Bad Apple in Windows Task Manager

分类:技术

这几天 Bad Apple 又逆袭了,比如这个这个。虽然周末时候自己也做了一个,但介于身心憔悴和一个遗留问题,暂时先不和上次一样出视频了……

首先,看到那两个演示视频就可以联想到《编程之美》里面的一道面试题,但显然完全不是一回事,因为图像的 x → yy 并不唯一。既然如此,那么就可以推定这是某种程度的「造假」,因为图像已经不是 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() 的调用只有:

这几处,熟悉汇编的都可以很快定位整个语句的范围。至此,我们得到了一个地址及一个长度,把这个范围内的指令全部变成 Nop 指令即可。

当然 OllyDbg 中反汇编的地址是不能直接用的,还需要减去当前模块的基地址,得到偏移量以备使用……

那么来到 C++,FindWindowGetWindowThreadProcessIdOpenProcess 即可获得任务管理器的进程句柄。

得到进程句柄后,我们首先要获得 taskmgr.exe 模块当前的基地址:EnumProcessModulesGetModuleInformation。而后,就可以用上之前得到的偏移量和长度,使用 WriteProcessMemory 把绘制 CPU 曲线的代码覆写为一串 Nop 指令。

世界清静了。

绘制动画

这里就没有什么技术含量了。无非就是自己 SetTimer 开一个定时器,按照一定的帧率往窗口上 TransparentBlt 以前处理好的图片。

需要注意的是,背景网格,这货最麻烦了。我目前没有用 Hook,所以暂时的做法是:自己维护一份干净的背景网格。这样做的缺点是,网格更新时有一定几率察觉到曲线的缺失(用 Hook 后应该会好:Hook 后替换掉 Window Proc,然后正确的 WM_DRAWITEM/WM_PAINT 之后立即把当前的图片绘制上去)。

以上。

将父类成员函数提升到子类

分类:技术

今天在看 C++11 的 Inherited Constructors 特性时发现了一个以前不知道的传统 C++ 奇技淫巧。

class Base {
public:
    void foo(float a);
};

class Derived: public Base {
public:
    void foo(int a);
};

这样的代码,显然 Derived::foo() 会把 Base::foo() 覆盖。

今天得知,using 居然还可以把父类的成员「提升」到子类中:

class Derived: public Base {
public:
    using Base::foo;    // 看这里看这里看这里
    void foo(int a);
};

如此,就相当于在 Derived 中添加了一个和 Base::foo() 一模一样的成员。(当然,从字面上也是很顾名思义的嘛。)

Derived d;
d.foo(3.14f);  // 这样调用的就是 Derived::foo(float a) 了

之前我曾想,两个完全符合「is-a」关系的类,做成聚合显然是不甘心的。但如果父类接口有 N 个,子类只是想添加 1 个接口,然后将父类中的 M 个(M < N)接口暴露出来该怎么办?想想 private 继承,然后自己在子类中写 M 个 wrapper 似乎是个可行的方法,但如果 M 很大似乎依旧不甘心。现在知道了这个技巧似乎好解决了很多呢。(不过,整个类的声明也会随之变得脑残起来。)

顺带一提,传统 C++ 中的这个方法是无法提升构造函数的,而 C++11 中则加入了允许继承(提升)构造函数的特性(当然,目前貌似还没有编译器支持 = =)。

以上。

Vim 中使用 clang complete 为 C/C++ 自动补全

分类:技术

前文再续,书接上回。上回咱们讲到,如何在 Vim 中使用 OmniCppComplete 为 C/C++ 自动补全;今天偶然间发现了个新玩意儿:clang complete。

前情提要

OmniComplete 是 Vim 中的智能补全功能,而 OmniComplete 本身并不知道如何补全,具体的「通过光标前的内容猜测光标后可能出现的内容」的工作是由不同的外部插件实现的。

上回说到的 OmniCppComplete 就是这样一个插件。实际需要预先调用 ctags 对源代码进行词法分析(吧?)生成 tags 文件(token 列表),然后在这个 tags 文件中去进行匹配。所以局限也很快暴露出来:无论是库的头文件还是自己的源代码,都要用户自己事先对它运行一遍 ctags。

clang complete 则是借助 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 支持……

几个 C++ 异常的零碎知识

分类:技术

在派生类构造函数中捕获基类构造函数异常

class Base {
public:
    Base()
    {
        throw std::runtime_error("Error from Base");
    }
};

class Derived : public Base {
public:
    Derived()
    {
        try {
        } catch(std::exception const& e) {
        }
    }
};

类似于这样的代码,是捕获不到异常的,因为执行到构造函数的函数体中时,基类已经构造完毕了。

索性 C艹 有一个神奇的写法:

Derived::Derived()
try : Base() {
} catch(...) {
}

没错,我没有漏写大括号……你完全可以直接把一个 try {} catch {} 当作函数体……同理,对于一般函数这么写也行:

void foo()
try {
} catch(...) {
}

当然,构造函数的 catch 里是无论如何都会帮你确保有一个异常被抛出去的。(你不显式抛,它就帮你把被捕获的异常抛出去。)

后来发现这个东西学名叫做:Function-try block。

C++11 的异常处理

查了一番,似乎大的改变在于:

先看看之前的 Dynamic Exception Specification 是什么:

void foo();              // (1) 允许抛出异常
void foo() throw(X, Y);  // (2) 只允许抛出 X 和 Y 异常
void foo() throw();      // (3) 不允许抛出异常

坑爹之处在于第 2 点:Dynamic Exception Specification 是在运行时进行的,于是,为了确保只能抛出指定类型的异常,就会生成额外代码降低执行效率。

最扭曲的是可自定义的 unexcepted 处理函数。所谓 unexcepted 处理函数,就是一旦你抛出了 throw(...) 中没有出现的类型,系统会去调用的那个函数。默认的 unexcepted 函数直接调用 std::terminate() 结束程序;如果你觉得这不好,你可能会想要去替换掉这个全局的处理函数,你会发现你可以:

  1. 和默认行为一样,调用 std::terminate() 自尽。
  2. 抛出异常,但是:
    1. 这个异常存在于 throw(...) 列表中,那么正常抛出。
    2. 这个异常不存在于列表中,但是 std::bad_exception 在,那么抛 std::bad_exception
    3. 这个异常不存在于列表中,而且 std::bad_exception 也不在,那么系统 std::terminate() 之。

重点在于,这是一个「全局」的处理函数。如果有人自定义这个,相当于如果要写 throw 列表,就要把 std::bad_exception 加上。

于是 C++11 因为这个在实践中基本没人用的东西而废弃了这个名字很长的功能,用 noexcept 取而代之:

void foo();                                 // (1) 允许抛出异常,相当于 noexcept(false)
void foo() noexcept(constant-expression);   // (2) expression 成立时不允许抛出异常
void foo() noexcept;                        // (3) 不允许抛出异常,相当于 noexcept(true)

也就是将原先的 (2) 去掉,然后扩展一下原先的 (3)。一旦不允许抛异常的函数抛出了异常,那么直接 std::terminate() 结束运行。

对应还有一个同名的操作符 noexcept,用来在编译时判断表达式是否允许抛出异常(遇到函数时只通过其声明时的 noexcept 与否判断)。

noexcept(1 + 2)     // false
noexcept(throw 1)   // true
noexcept(foo())     // 返回 foo() 声明时的 noexcept 与否

最好在哪里捕获异常

处处都 catch 的话,就反而比 if 麻烦了。据推荐,最好在这些地方 catch


参考内容:Jon Kalb: Exception-Safe Coding in C++