其他

简明Objective-Chipmunk教程


简明Objective-Chipmunk教程,由泰然翻译组翻译。转载请著名出处。
翻译:ChildhoodAndy,紫夜行者,夜狼。
校对:ZeroYangu0u0

github地址,欢迎斧正
https://github.com/iTyran/ChipmunkTutorial_SimpleObjective

本教程的目标是简单的介绍下在iPhone游戏中使用Objective-Chipmunk。其中有很多注释,理解起来很容易。尽管代码只有100行左右,但我会从下面几个主题展开:

  • 创建一个Chipmunk空间模拟对象
  • 创建一个具有摩擦力的弹性盒子
  • 通过倾斜设备来控制重力
  • 使用碰撞回调来实现基于物体碰撞力度的声音大小
  • 根据碰撞回调来追踪物体处于地面还是空中
  • 使用ChipmunkObject protocol来轻松的将复杂对象加入到空间中
  • 使用CADisplayLink实现平滑动画
  • 整合Chipmunk进CocoaTouch UI

即使你打算只是用vanilla C API, 本教程也可当作一篇Chipmunk在iPhone上使用的不错的介绍。

你可以在GitHub网站上下载本教程以及所有的工程文件。他们都是可以编译的!

我首先该了解什么呢?

这是一篇很棒的基础教程,但是并不打算介绍Objective-C内存管理或者Cocoa Touch APIs。你至少需要对他们有一些了解或者愿意查看苹果官方文档。

Chipmunk是什么?

Chipmunk2D是一个基于MIT协议的2D刚体物理仿真库。设计宗旨:极快、可移植、稳定、易用。出于这个原因,它已经被用于数以百计的游戏,而且几乎横跨了所有系统。这些游戏包括了iPhone AppStore上一些顶级出色的TOP1游戏,如Night Sky等。这几年来,我投入了大量的时间来发展Chipmunk,才使得Chipmunk走到今天。更多信息请查阅Chipmunk官网

Objective-Chipmunk是什么?

Objective-Chipmunk是Objective-C对Chipmunk物理引擎库的封装。虽然Chipmunk的C API非常容易使用,但Objective-C API更棒。原生Objective-C API的主要优点包括集成了Cocoa内存管理模型和ChipmunkObject协议。Chipmunk对象协议统一了Chipmunk的基本类型。另外该封装增加了许多便利的方法来执行常见的安装任务,以及整合了Cocoa Touch API辅助方法。封装会尽量按照Objective-C的方式来实现,并在一些需要的地方添加一些有用的方法变量。

Chipmunk Object协议统一了基础Chipmunk类型,使得它很容易创建自定义的基础类型类型。另外,包装中添加了很多便捷的方法来执行场景的安装任务,。该包装尝试完成object-c的方式,添加更加有意义的方法变量等。

你可以在Objective-Chipmunk网站上查阅更多信息。虽然Objective-Chipmunk并不免费,但是增强的API肯定会为你节约时间和成本。同时你也将会支持Chipmunk的发展!

为什么使用Cocoa Touch实现本教程

Cocoa Touch 实际上能够快速开发一款iPhone 游戏。因为硬件加速的原因会相当快,同时在屏幕上有许多对象也不会慢。Interface Builder也是一款简要的编辑器。我们用这个方式写了多款小游戏,并且让我们避免处理库的依耐性的麻烦。结合游戏的简洁性,使用任何华丽的空想都将是浪费时间。

Chipmunk下载页中,我们有更多可运行的例子包括Cocos2D实例。

让我们开始吧!

许多Chipmunk初学者有个疑问,就是如何利用Chipmunk的优势来构建他们的游戏。很多人尝试使用Chipmunk来建立他们的游戏,但这并不是好的主意。Chipmunk不是一个游戏引擎。它不显示图像,你也不能获得场景里的所有子弹和怪物。

如果要使用Chipmunk,你应该把它作为一个组件。当你在游戏场景里添加游戏对象时,在你创建的场景里添加物理对象到Chipmunk空间里。本教程你可以看到,Chipmunk Object协议很容易实现,但是通过调用单个方法,可以对Chipmunk世界里的组件添加和删除。当你想显示一个精灵,你可以通过Chipmunk Object读取精灵的位置和旋转角度。这就是MVC应用于游戏的缩影,并且使用的非常好。

很多人反其道而行。他们通过遍历所有的碰撞并且更新相关的精灵。这样的工作模式,是不灵活的。它意味着每次碰撞只有一个精灵关联,反之亦然。此外,API迭代Chipmunk世界的形状不是公共明确的API的一部分,所以我不推荐使用它。

主要的目地就是在屏幕内完成一个可以通过倾斜iphone或者触摸移动的刚体。说白了我们通过碰撞回调方法来改变屏幕的颜色并且播放相应的音效。这里有两个基础类:我们喜欢用的一个游戏控制器:ViewContoller,以及游戏对象控制器:FallingButton。

ViewController.m - 简单的游戏控制器

游戏控制器要负责很多事情, 一个游戏控制器最主要的是控制整个游戏的逻辑, 判定玩家什么时候赢什么时候输这类的事情。 我们简单的例子里面并没有任何规则, 所以我们直接跳到其它需要负责的方面:管理游戏循环和处理添加删除游戏对象

初始化设置:

我们用UIViewController来创建游戏的控制器(controller),来一起看一下在viewDidLoad函数中初始化的时候都做了什么,我将一行一行的进行解释

首先我们需要初始化图像和物理属性,我们要把这些卸载一个view controller里面, 我们可以仅用view来进行图像显示,所以我们只需要调用父类的viewDidLoad方法就可以完成初始化了。

[super viewDidLoad];

现在我们就可以把他们添加进来显示在界面上了,相当easy。

初始化物理世界:

space = [[ChipmunkSpace alloc] init];

新创建的space对象里面什么也没有。我们添加的任何物理对象都会飞出屏幕之外。一般使用物理引擎的2D的项目都会在开始的时候设置屏幕的边界,Objective-Chipmunk也有一个很好的简便方法来实现。

[space addBounds:self.view.bounds
       thickness:10.0f
       elasticity:1.0f friction:1.0f
       layers:CP_ALL_LAYERS group:CP_NO_GROUP
       collisionType:borderType
];

thickness控制着边界的厚度,layers和group控制碰撞过滤和碰撞。更多信息请查看碰撞形状文档。
最后再说一下borderType是一个当定义了碰撞回调函数时的标记的对象,比如你想在子弹击中怪物的时候进行碰撞处理,碰撞的对象可以是任何对象,类的对象或者全局NSString都可以,我的borderType就是一个在头文件里面定义的全局NSString变量。

接下来我们设置一下物体碰到界面边界的时候的响应函数。

[space addCollisionHandler:self
        typeA:[FallingButton class] typeB:borderType
        begin:@selector(beginCollision:space:)
        preSolve:nil
        postSolve:@selector(postSolveCollision:space:)
        separate:@selector(separateCollision:space:)
];

总共有4种碰撞事件,你只需要找到一个感兴趣的实现就可以了,如果想知道什么时候两个物体碰撞、什么时候Chipmunk处理碰撞、什么时候结束碰撞 这些都可以通过查阅响应函数文档来了解

最后我们来创建一个掉落按钮的游戏对象,让它在view中显示,把它的物理属性添加到Chipmunk的space中

fallingButton = [[FallingButton alloc] init];
[self.view addSubview:fallingButton.button];
[space add:fallingButton];

因为FallingButton实现了ChipmunkObject协议,我们就可以直接添加或者删除它的所有刚体,形状(shapes)和关节(joints)的碰撞都是在同一个函数里面处理的,所以不需要考虑它的物理属性有多负载

这就是游戏controller的初始化,我们一起来看一下代码。

static NSString *borderType = @"borderType";

- (void)viewDidLoad {
    [super viewDidLoad];

    space = [[ChipmunkSpace alloc] init];
    [space addBounds:self.view.bounds
        thickness:10.0f
        elasticity:1.0f friction:1.0f
        layers:CP_ALL_LAYERS group:CP_NO_GROUP
        collisionType:borderType
    ];

    [space addCollisionHandler:self
        typeA:[FallingButton class] typeB:borderType
        begin:@selector(beginCollision:space:)
        preSolve:nil
        postSolve:@selector(postSolveCollision:space:)
        separate:@selector(separateCollision:space:)
    ];

    fallingButton = [[FallingButton alloc] init];
    [self.view addSubview:fallingButton.button];
    [space add:fallingButton];
}

游戏主循环:更新物理和图像

现在我们来看一下游戏的控制器(controller)都干了些什么,控制整个游戏的更新(update)循环。在这本节教程中用CADisplayLink来接收iPhone OS在屏幕重画是触发的事件,这是我知道的最简单的得到很流畅的动画的方式,让我们来快速的创建一个连接显示和加速(accelerometer)的回调函数:

- (void)viewDidAppear:(BOOL)animated {
    displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(update)];
    displayLink.frameInterval = 1;
    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

    UIAccelerometer *accel = [UIAccelerometer sharedAccelerometer];
    accel.updateInterval = 1.0f/30.0f;
    accel.delegate = self;
}

这是一个很普通的Apple类,所以我就不进行详细的解释了。

接下来我们要创建一个让displayLink调用的update方法

- (void)update {
    cpFloat dt = displayLink.duration*displayLink.frameInterval;
    [space step:dt];

    [fallingButton updatePosition];
}

只是简单的定时得到显示连接来计算距离上一次函数调用的时间,我们可以使用同样的间隔时间更新物理引擎,在物理引擎更新完成之后,更新falling button的位置(显示的图片根据物理属性的位置进行调整)

有一点需要说明一下,Chipmunk不需要随时保持一致,这样可以降低你的CPU的使用次数,而且让你的游戏更为准确

然后我们创建一个accelerometer回调函数用来在iPhone倾斜的时候更新重力方向。

- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)accel {
    space.gravity = cpvmult(cpv(accel.x, -accel.y), 100.0f);
}

相当简单,解释一下cpv这个函数,它的功能是根据x和y坐标创建一个Chipmunk中的一个向量,cpvmult()方法是创建一个单位向量,想要知道更多关于向量的信息可以自己去查阅cpVect C 文档,如果你是在使用Objectiv-C++就可以很容易的重载Chipmunk的运算符了。

碰撞回调:begin

不同的碰撞回调类型对应不同的方法签名,begin回调方法可以写成像下面这样:

- (bool)beginCollision:(cpArbiter*)arbiter space:(ChipmunkSpace*)space

第一个参数是一个指向cpArbiter结构体的指针,arbiter这个词在这里的意思是仲裁者,由于很容易会触发碰撞,所以我决定不创建一个Objective-C的类来封装arbiter,我们很少会用到cpArbiter的结构体,想要知道更多关于cpArbiter可以查阅cpArbiter C 文档

第二个参数是回调注册所在的ChipmunkSpace空间对象。这样你可以注册post-step回调(和post-solve回调不同)来向空间中添加或者删除对象。

可能你首先想在碰撞的回调函数里面做的事是找出来到底是哪两个物体碰撞了,如果你还记得以前说过的东西,当空间中两个特定碰撞类型的物体碰撞时,我们定义来碰撞处理函数。那么我们该如何知道哪个对象是哪个呢?Objective-Chipmunk提供来一个处理宏来定义ChipmunkSpace变量以及初始化他们。

CHIPMUNK_ARBITER_GET_SHAPES(arbiter, buttonShape, border);

这个宏相当于做了这样的事:

ChipmunkShape *buttonShape = GetShapeWithTypeA(arbiter);
ChipmunkShape *border = GetShapeWithTypeB(arbiter);

typeA和typeB和你定义碰撞处理函数的定义的type一样。

游戏中常常要知道物体是否已经接触来地面。使用Chipmunk的碰撞事件,我们可以很容易的通过一个计数器来实现。在begin回调中,增加计数器,并且在seperate回调中,减少计数器。如果计数器为0,则意味着物体不再接触任何东西。我们从这里开始。

我们一直有个问题,就是碰撞的回调函数只会给我们传过来碰撞的shape而不是它对应的游戏里面的对象!幸好。Chipmunk中提供了一个数据指针指向使用它的对象以便于你在任何时候都能通过刚体、shape或者关节来得到游戏中的对象,这个对象指针是在我们创建FallingButton对象的时候赋值的,后面你可以看到详细的解释。

FallingButton *fb = buttonShape.data;

那么现在我们增加以下我们掉落按钮的计数:

fb.touchedShapes++;

我们背景设置为灰色,以便于看清下降的按钮碰撞到的任何东西:

self.view.backgroundColor = [UIColor grayColor];

begin回调函数必须要返回一个boolean的值,如果返回true,那么Chipmunk会以正常的方式处理碰撞,如果返回false,那么Chipmunk会当做这个碰撞没有发生,这个在处理碰撞过滤的时候非常有用比如单向平台或易碎物体。如果我们只需要正常碰撞,将返回值设置为TRUE。做完这些事之后,我们也就完成了我们的begin会调函数:

- (bool)beginCollision:(cpArbiter*)arbiter space:(ChipmunkSpace*)space {
    CHIPMUNK_ARBITER_GET_SHAPES(arbiter, buttonShape, border);

    FallingButton *fb = buttonShape.data;
    fb.touchedShapes++;

    self.view.backgroundColor = [UIColor grayColor];

    return TRUE;
}

碰撞回调:separate

separate的回调方法和begin回调方法有些不一样,因为在调用separate回调函数的时候碰撞已经结束了,所以它不需要返回一个boolean类型来标识是否要忽略碰撞。

- (void)separateCollision:(cpArbiter*)arbiter space:(ChipmunkSpace*)space

首先我们要减少计数,这个跟前面增加的差不多,很简单

CHIPMUNK_ARBITER_GET_SHAPES(arbiter, buttonShape, border);

FallingButton *fb = buttonShape.data;
fb.touchedShapes--;

接下来,我们将背景设置为随机的一种颜色以便于知道物体与边界发生了碰撞,我们只在物体不再与四个边界接触的时候这么做,所以,我们先检测一下计数是不是0

if(fb.touchedShapes == 0){
    self.view.backgroundColor = [UIColor colorWithRed:frand() green:frand() blue:frand() alpha:1.0f];
}

做完这些我们的separate回调函数也就搞定了:

- (void)separateCollision:(cpArbiter*)arbiter space:(ChipmunkSpace*)space {
    CHIPMUNK_ARBITER_GET_SHAPES(arbiter, buttonShape, border);

    FallingButton *fb = buttonShape.data;
    fb.touchedShapes--;

    if(fb.touchedShapes == 0){
        self.view.backgroundColor = [UIColor colorWithRed:frand() green:frand() blue:frand() alpha:1.0f];
    }
}

碰撞回调:post-solve

另一个很常见的回调处理就是播放碰撞声音了。为了让声音听起来自然,我们要基于物体撞击的力度来设置音量的大小。这便是post-solve回调要解决的问题。Chipmunk已经完成了碰撞解决,使得你有机会取得施加的冲力。

post-solve回调方法签名如下:

- (void)postSolveCollision:(cpArbiter*)arbiter space:(ChipmunkSpace*)space

和begin、seperate回调只发生在碰撞的第一帧和最后一帧不同,pre-solve和post-solve回调在两个形状接触过程中被每帧调用。为了播放一个碰撞声音,我们只需要关心第一帧。Chipmunk提供了一个C函数,你可以使用仲裁者来测试这是否是第一帧两个形状发生了碰撞。如果不是第一帧,我们使用它来提前退出。

if(!cpArbiterIsFirstContact(arbiter)) return;

有了这样的方式,现在我们只需要计算出物体间碰撞的力度以及播放一个基于力度大小的声音。

cpFloat impulse = cpvlength(cpArbiterTotalImpulse(arbiter));

float volume = MIN(impulse/500.0f, 1.0f);
if(volume > 0.05f){
    [SimpleSound playSoundWithVolume:volume];
}

cpArbiterTotalImpulse() 返回一个向量。我们只关心力的大小,所以我们使用cpvlength()来得到向量的大小。将我们计算的大小进行截断确保不能大于1.0,并且如果足够响亮就播放声音。简单!

整合上面的代码,我们得到了完整的post-solve回调:

- (void)postSolveCollision:(cpArbiter*)arbiter space:(ChipmunkSpace*)space {
    if(!cpArbiterIsFirstContact(arbiter)) return;

    cpFloat impulse = cpvlength(cpArbiterTotalImpulse(arbiter));

    float volume = MIN(impulse/500.0f, 1.0f);
    if(volume > 0.05f){
        [SimpleSound playSoundWithVolume:volume];
    }
}

上面基本就是游戏控制器了。

Falling Button:一个简单的游戏对象控制器

和游戏控制器类似,游戏对象控制器的主要功能就是管理游戏对象的逻辑更新并将图形和物理两者做绑定。在这篇教程中,falling button的逻辑比较简单。我们所要做的就是点击它的时候让它向随机的方向移动。

初始化设置:

让我们从初始化开始来了解下游戏对象的组成。它由一个初始化的UIButton实例启动。下面是非常标准的Cocoa程序:

button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setTitle:@"Click Me!" forState:UIControlStateNormal];
[button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[button setBackgroundImage:[UIImage imageNamed:@"logo.png"]
    forState:UIControlStateNormal];
button.bounds = CGRectMake(0, 0, SIZE, SIZE);

[button addTarget:self action:@selector(buttonClicked)  forControlEvents:UIControlEventTouchDown];

相当简单。现在我们来为它设置物理特性。我们先定义按钮的质量和转动惯量。等下!你会问这里说的转动惯量是什么意思。这看你怎么想的了。如果你认为物体的质量描述了物体移动起来需要的力度,那么转动惯量就是让物体转动起来需要的力度。

cpFloat mass = 1.0f;
cpFloat moment = cpMomentForBox(mass, SIZE, SIZE);

你可以轻松的为质量赋值并不需要使用任何特定的单位。只要确保他们彼此关联对比的要有意义。如果一个汽车的质量为1,那么一只狗的质量应该为0.01或者类似小的值。因为场景中只有一个按钮,所以我们为它设置的质量大与小无所谓了。反应会如出一辙。另一方面,对转动惯量进行猜测或赋值是个糟糕的想法。假设SIZE是100,那么按照上述方式计算的转动惯量将是850!

为了乐趣所在,你应该尝试着看看不同的转动惯量值的影响,以及为什么使用Chipmunk提供的转动惯量估算函数很重要。你可以在cpBody C 文档里找到更多的这些估算函数。

下面,我们来使用计算好的质量和转动惯量来初始化一个刚体:

body = [[ChipmunkBody alloc] initWithMass:mass andMoment:moment];

刚体拥有物理对象的物理属性。比如质量、位置、速度和角度。一旦你创建了个刚体,你便可以将任意数量的碰撞形状和关节与之关联。刚体有一些读写的属性,比如pos,vel,mass等等。当第一次创建的时候,除了质量和转动惯量外,大部分这些属性都为0。让我们将形状的位置设置在屏幕中间的某处。

body.pos = cpv(200.0f, 200.0f);

现在我们到了有趣的环节。为刚体创建一个碰撞形状使得它可以与屏幕边界形状进行碰撞。Chipmunk目前支持3种碰撞形状:圆形、线段和多边形。它具有便利的构造函数用来创建盒子状多边形。我们将使用它。因为我们不需要将物体存储为一个实例变量,所以我们使用autorelease构造函数。

ChipmunkShape *shape = [ChipmunkPolyShape boxWithBody:body width:SIZE height:SIZE];

这将创建一个长宽为SIZE的方形形状并关联到我们的刚体上。上面那个便捷的构造函数会将box形状居中放置到时刻与刚体位置保持一致的刚体重心位置上。重心也是刚体旋转所围绕的点。

如ChipmunkBody对象一样,ChipmunkShape对象也有一些你可能需要设置的属性:

shape.elasticity = 0.3f;
shape.friction = 0.3f;

elasticity控制着刚体有多大的弹性。当两个形状碰撞时,他们的弹性值会被相乘得到碰撞的弹性值。0意味着没有弹性,1意味着完全反弹。你可以设置大于1.0的值,意味着物体在每次碰撞之后会加速,可能会超出我们的控制。friction工作机制相同,两个形状的摩擦力相乘决定了最终施加的摩擦力。你可以从斜坡面的角度来考虑摩擦力值。如果摩擦力值是1,那么物体将会停止在45度的斜坡上。如果摩擦力值是0.5,那么物体将会停止在Atan(0.5) = 26.6度的斜坡上。

下面我们来设置在碰撞回调中要使用到的属性:

shape.collisionType = [FallingButton class];
shape.data = self;

如果你还记得上面,collisionType在创建碰撞回调被用来当作关键值,data属性可以引用回我们的falling button对象。

最后,我们需要实现ChipmunkObject协议以便我们不必再手动添加碰撞形状和刚体到空间中去。要做到这一点,我们需要做的就是实现一个单一的方法,该方法返回一个NSSet,它包含了这个游戏对象使用的所有基本的Chipmunk对象。最简单的方式就是使用synthesized属性。然后,你需要做的就是创建一个名为chipmunkObjects的实例变量并且使用ChipmunkObjectFlatten()函数来初始化它。你可以传递任何实现ChipmunkObject协议的值给ChipmunkObjectFlatten()函数。别忘记nil终止符和retain它返回的set结合。

chipmunkObjects = [ChipmunkObjectFlatten(body, shape, nil) retain];

将上面的整合一下,我们的初始化代码如下:

- (id)init {
    if(self = [super init]){
        button = [UIButton buttonWithType:UIButtonTypeCustom];
        [button setTitle:@"Click Me!" forState:UIControlStateNormal];
        [button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
        [button setBackgroundImage:[UIImage imageNamed:@"logo.png"] forState:UIControlStateNormal];
        button.bounds = CGRectMake(0, 0, SIZE, SIZE);

        [button addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventTouchDown];

        cpFloat mass = 1.0f;
        cpFloat moment = cpMomentForBox(mass, SIZE, SIZE);

        body = [[ChipmunkBody alloc] initWithMass:mass andMoment:moment];
        body.pos = cpv(200.0f, 200.0f);

        ChipmunkShape *shape = [ChipmunkPolyShape boxWithBody:body width:SIZE height:SIZE];
        shape.elasticity = 0.3f;
        shape.friction = 0.3f;
        shape.collisionType = [FallingButton class];
        shape.data = self;

        chipmunkObjects = [ChipmunkObjectFlatten(body, shape, nil) retain];
    }

    return self;
}

按钮动作:

剩下唯一的事情就是处理点击按钮时所调用的方法了。

frand_unit函数

static cpFloat frand_unit(){return 2.0f*((cpFloat)rand()/(cpFloat)RAND_MAX) - 1.0f;}

- (void)buttonClicked {
    cpVect v = cpvmult(cpv(frand_unit(), frand_unit()), 300.0f);
    body.vel = cpvadd(body.vel, v);

    body.angVel += 5.0f*frand_unit();
}

没什么太复杂的内容。我们只是为刚体的速度和角速度添加了一个随机的改变。

结束语:

既然你瞧见了Objective-Chipmunk是多么容易并且强大,何不将它集成到你的iPhone游戏里面呢?Objective-C API提供的与流行iPhone库如Cocos2D协同良好的高层次API,将会为你省去不少时间(和金钱)。同样你也不必为内存管理担心因为它允许你像你的其他iPhoneApp一样来管理Chipmunk内存。

开心的玩转Chipmunk吧!

C++11 语法记录


文章链接:blog.csdn.net/crayondeng/article/details/18563121

一、Lambda表达式

C++ 11中的Lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。Lambda的语法形式如下:

[函数对象参数] (操作符重载函数参数) mutable或exception声明 ->返回值类型 {函数体}

可以看到,Lambda主要分为五个部分:[函数对象参数]、(操作符重载函数参数)、mutable或exception声明、->返回值类型、{函数体}。下面分别进行介绍:

一、[函数对象参数],标识一个Lambda的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义Lambda为止时Lambda所在作用范围内可见的局部变量(包括Lambda所在类的this)。函数对象参数有以下形式:

1、空。没有使用任何函数对象参数。
2、=。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
3、&。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
4、this。函数体内可以使用Lambda所在类中的成员变量。
5、a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
6、&a。将a按引用进行传递。
7、a, &b。将a按值进行传递,b按引用进行传递。
8、=,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。
9、&, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。

二、(操作符重载函数参数),标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。

三、mutable或exception声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。exception声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)。

四、->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。

五、{函数体},标识函数的实现,这部分不能省略,但函数体可以为空。

下面给出一个例子:

class CTest  
{  
public:  
    CTest() : m_nData(20) { NULL; }  
    void TestLambda()  
    {  
        vector vctTemp;  
        vctTemp.push_back(1);  
        vctTemp.push_back(2);  
          
        // 无函数对象参数,输出:1 2  
        {  
            for_each(vctTemp.begin(), vctTemp.end(), [](int v){ cout << v << endl; });  
        }  
          
        // 以值方式传递作用域内所有可见的局部变量(包括this),输出:11 12  
        {  
            int a = 10;  
            for_each(vctTemp.begin(), vctTemp.end(), [=](int v){ cout << v+a << endl; });  
        }  
          
        // 以引用方式传递作用域内所有可见的局部变量(包括this),输出:11 13 12  
        {  
            int a = 10;  
            for_each(vctTemp.begin(), vctTemp.end(), [&](int v)mutable{ cout << v+a << endl; a++; });  
            cout << a << endl;  
        }  
          
        // 以值方式传递局部变量a,输出:11 13 10  
        {  
            int a = 10;  
            //注意:在lambda表达式中的 a 只是一个拷贝,添加mutable之后,可以修改a的值,但只是修改拷贝的a  
            for_each(vctTemp.begin(), vctTemp.end(), [a](int v)mutable{ cout << v+a << endl; a++; });  
            cout << a << endl;  
        }  
          
        // 以引用方式传递局部变量a,输出:11 13 12  
        {  
            int a = 10;  
            for_each(vctTemp.begin(), vctTemp.end(), [&a](int v){ cout << v+a << endl; a++; });  
            cout << a << endl;  
        }  
          
        // 传递this,输出:21 22  
        {  
            for_each(vctTemp.begin(), vctTemp.end(), [this](int v){ cout << v+m_nData << endl; });  
        }  
          
        // 除b按引用传递外,其他均按值传递,输出:11 12 17  
        {  
            int a = 10;  
            int b = 15;  
            for_each(vctTemp.begin(), vctTemp.end(), [=, &b](int v){ cout << v+a << endl; b++; });  
            cout << b << endl;  
        }  
          
        // 操作符重载函数参数按值传递,输出:1 2  
        {  
            for_each(vctTemp.begin(), vctTemp.end(), [](int v){ cout << v << endl; });  
        }  
          
        // 操作符重载函数参数按引用传递,输出:2 3  
        {  
            for_each(vctTemp.begin(), vctTemp.end(), [](int &v){ v++; cout<

二、auto 关键字

C++ 11中引入的auto主要有两种用途:自动类型推断和返回值占位。
auto自动类型推断,用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推断,可以大大简化我们的编程工作。下面是一些使用auto的例子。

auto a; // 错误,没有初始化表达式,无法推断出a的类型    
auto int a = 10 // 错误,auto临时变量的语义在C++ 11中已不存在    
auto a = 10    
auto c = 'A'   
auto s("hello");    
vector vctTemp;    
auto it = vctTemp.begin();    
auto ptr = [](){ cout << "hello world" << endl; };  

使用auto经常意味着较少的代码量(除非你需要的类型是int这种只有一个单词的)。当你想要遍历STL容器中元素的时候,想一想你会怎么写迭代器代码,老式的方法是用很多typedef来做,而auto则会大大简化这个过程。

std::map> map;  
for(auto it = begin(map); it != end(map); ++it)   
{  
}  

另外,在使用模板技术时,如果某个变量的类型依赖于模板参数,不使用auto将很难确定变量的类型(使用auto后,将由编译器自动进行确定)。

下面是一个具体的例子。

template     
void Multiply(T t, U u)    
{    
      auto v = t*u;    
}  

auto返回值占位,主要与decltype配合使用,用于返回值类型后置时的占位。

template   
auto compose(T1 t1, T2 t2) -> decltype(t1 + t2)  
{  
   return t1+t2;  
}  
auto v = compose(2, 3.14); // v's type is double 

你应该注意到,auto并不能作为函数的返回类型,但是你能用auto去代替函数的返回类型,当然,在这种情况下,函数必须有返回值才可以。auto不会告诉编译器去推断返回值的实际类型,它会通知编译器在函数的末段去寻找返回值类型。在上面的那个例子中,函数返回值的构成是由T1类型和T2类型的值,经过+操作符之后决定的。

自动化推导decltype

关于 decltype 是一个操作符,其可以评估括号内表达式的类型,其规则如下:

1.如果表达式e是一个变量,那么就是这个变量的类型。
2.如果表达式e是一个函数,那么就是这个函数返回值的类型。
3.如果不符合1和2,如果e是左值,类型为T,那么decltype(e)是T&;如果是右值,则是T。

原文给出的示例如下,我们可以看到,这个让的确我们的定义变量省了很多事。

const vector vi;  
typedef decltype (vi.begin()) CIT;  
CIT another_const_iterator;  

还有一个适合的用法是用来typedef函数指针,也会省很多事。比如:

decltype(&myfunc) pfunc = 0;  
typedef decltype(&A::func1) type;

三、std::function

类模版 std::function是一种通用、多态的函数封装。std::function的实例可以对任何可以调用的目标进行存储、复制、和调用操作,这些目标包括函数、lambda表达式、绑定表达式、以及其它函数对象等。
用法示例:
①保存自由函数

void printA(int a)  
{  
    cout< func;  
 func = printA;  
 func(2);  

运行输出: 2

②保存lambda表达式

std::function func_1 = [](){cout<<"hello world"<

运行输出:hello world

③保存成员函数

struct Foo {  
    Foo(int num) : num_(num) {}  
    void print_add(int i) const { cout << num_+i << '\n'; }  
    int num_;  
};  
  
 // 保存成员函数  
    std::function f_add_display = &Foo::print_add;  
    Foo foo(2);  
    f_add_display(foo, 1);  

运行输出: 3

四、bind

bind是一组用于函数绑定的模板。在对某个函数进行绑定时,可以指定部分参数或全部参数,也可以不指定任何参数,还可以调整各个参数间的顺序。对于未指定的参数,可以使用占位符_1、_2、_3来表示。_1表示绑定后的函数的第1个参数,_2表示绑定后的函数的第2个参数,其他依次类推。

下面通过程序例子了解一下用法:

#include   
using namespace std;  
class A  
{  
public:  
    void fun_3(int k,int m)  
    {  
        cout< fc = std::bind(&A::fun_3, a,std::placeholders::_1,std::placeholders::_2);  
    fc(10,20);//print:10 20  
      
    return 0;  
}  

五、nullptr -- 空指针标识

空指针标识(nullptr)(其本质是一个内定的常量)是一个表示空指针的标识,它不是一个整数。(译注:这里应该与我们常用的NULL宏相区别,虽然它们都是用来表示空置针,但NULL只是一个定义为常整数0的宏,而nullptr是C++0x的一个关键字,一个内建的标识符。下面我们还将看到nullptr与NULL之间更多的区别。)

    char* p = nullptr;
    int* q = nullptr;
    char* p2 = 0;           //这里0的赋值还是有效的,并且p=p2

    void f(int);
    void f(char*);

    f(0);         //调用f(int)
    f(nullptr);   //调用f(char*)

    void g(int);
    g(nullptr);       //错误:nullptr并不是一个整型常量
    int i = nullptr;  //错误:nullptr并不是一个整型常量

(译注:实际上,我们这里可以看到nullptr和NULL两者本质的差别,NULL是一个整型数0,而nullptr可以看成是一个空指针。)

六、final 和 override

两者都是用在对于继承体系的控制。

final

用来标明被这个修饰符修饰的class/struct和虚函数已经是最终版本,无法被进一步继承.

class Base final  
{  
public:  
    virtual void test(){}  
};  
  
class D1:public Base  
{  
    void test(){}  
};  

如这个例子,Base类被final修饰,表示其已经无法被继承,编译器会提示如下错误:error C3246: ‘D1′ : cannot inherit from ‘Base’ as it has been declared as ‘final’


再看另外一个例子:

class Base  
{  
public:  
    virtual void test(){}  
};  
  
class D1:public Base  
{  
    void test() final {}  
};  
  
class D2 :public D1  
{  
    void test(){}  
};  

此时虚函数test在D1被final标识,已经表明其是最终版本,无法进一步继承.

override

override关键字用来表示在子类的函数一定重载自基类的同名同性质的虚函数或者纯虚函数,否则无法被编译.

此时虚函数test在D1被final标识,已经表明其是最终版本,无法进一步继承.

override

override关键字用来表示在子类的函数一定重载自基类的同名同性质的虚函数或者纯虚函数,否则无法被编译.

需要注意的一点是,final和override两者很有可能在C++ 98的代码里面被程序员大量的用在其他地方命名,因此C++ 11为了保持和之前代码的兼容性,所以这两个标记只有在修饰class/struct和函数的时候,才会被当成关键字。也就是说,在其他地方依然可以使用这两个字符命名成变量/函数/类/结构体.

比如:

class Base  
{  
public:  
    virtual void test()  
    {  
        int final = 1;  
    }  
    virtual void test2(int i) {}  
    virtual void test3() const {}  
    virtual void override();  
};  

这是正确的.

Chipmunk2D中文手册


Chipmunk2D中文手册,由泰然翻译组翻译。转载请著名出处。
翻译:ChildhoodAndy(完成了大部分的翻译), u0u0gloryming
校对:涵紫

github贡献地址:https://github.com/iTyran/ChipmunkDocsCN
欢迎大家斧正错误,提交PR。

Chipmunk2D 6.2.1

Chipmunk2D是一个基于MIT协议的2D刚体物理仿真库。设计宗旨:极快、可移植、稳定、易用。出于这个原因,它已经被用于数以百计的游戏,而且几乎横跨了所有系统。这些游戏包括了iPhoneAppStore上一些顶级出色的TOP1游戏,如Night Sky等。这几年来,我投入了大量的时间来发展Chipmunk,才使得Chipmunk走到今天。如果您发现Chipmunk2D为您节省了许多时间,不妨考虑捐赠下。这么做会使一个独立游戏制作者非常开心!

首先,我要非常感谢ErinCatto(译者注:Box2D作者), 早在2006年的时候,Chipmunk的冲量求解器便是受到他的范例代码的启发而完成(现在已经发展成一个成熟的物理引擎:Box2D.org)。他持久接触的想法允许对象的稳定堆栈只进行极少的求解器迭代,而我以前的求解器为了让模拟稳定模拟会产生大量的对象或者会消耗大量的CPU资源。

为什么是一个C库

很多人问我为什么用C来写Chipmunk2D,而不是一个我喜欢的其他语言。我通常会对不同的编程语言很兴奋,几个月来,挑选的语言有Scheme, OCaml, Ruby, Objective-C, ooc, Lua, Io等等。它们都有一个共同点,那就是都很容易绑定到C代码。同时我也希望Chipmunk2D高效、易移植、优化简单并且容易调试,而使用C语言就能很简单的达到这些目标。

我从来没有,将来也不太可能去用C来写一个完整的游戏。这里有很多比C有趣的语言,它们有垃圾回收,闭包,面向对象运行时等高级特性。如果你在其它语言中使用Chipmunk2D,可以在Bindings and Ports中找到有用的信息。因为Chipmunk2D基于C99的字集编写,使得它很容易集成到C、C++、Object-C等其它开发语言中。

C API的局限

如果您使用的是C++,Chipmunk提供了操作符*,+和 - (一元和二元)的重载,但如果使用的是C,那么需要退回使用cpvadd() 和 cpvsub()。这有一点点不利于代码阅读,不过当你习惯之后这将不是个问题。大部分的向量操作可能并没任何形式的符号对应(至少不在键盘上)。

C API的另一个问题是访问限制。Chipmunk有许多结构体,字段,函数只能内部使用。要解决这个问题,我把Chipmunk的全部私有API分离到头文件chipmunk_private.h中,同时在共有结构中使用CP_PRIVATE()来改名。你可以通过包含这个头文件或使用这个宏来自由访问私有API,但请注意这些私有API可能在未来版本中改变或消失,并且不会在文档中体现,同时也没有私有API的文档计划。

Chipmunk2D Pro

我们同时在出售Chipmunk2D的扩展版本: Chipmunk2D Pro。主要的特性有:ARM和NEON指令优化,多线程优化,一个为iOS/Mac开发提供的Objective-C封装层,以及自动几何工具。优化主要集中在提高移动性能,同时多线程特性能在支持pthread的平台运行。Objective-C封装层能让你无缝整合到Cocos2D或UIKit等框架,并能获得本地内存管理的优势(包括ARC)。同时Pro版本有大量优秀的API扩展。自动几何工具让你能从图像数据或程序生成并使用几何。

另外,出售Chipmunk2D Pro让我们得以生存,并保持Chipmunk2D的开源。捐献也能棒,但是购买Pro版本你将获得捐献之外的某些东西。

下载与编译

如果你还没有下载,你总可以在这里获取到Chipmunk2D的最新版本。里面包含了CMake的命令行编译脚本, Xcode工程以及Visual Studio ’09 和 ’10工程。

Debug 或 Release?

Debug模式可能略慢,但是包含了大量的错误检测断言,可以帮助你快速定位类似重复移除对象或无法检测的碰撞之类的BUG。我强烈建议你使用Debug模式,直到你的游戏即将Release发售。

XCode (Mac/iPhone)

源码中的Xcode工程可直接build出一个Mac或iOS静态库。另外,你可以运行macosx/iphonestatic.commandmacosx/macstatic.command来生成一个带头文件和debug/release静态库的目录,以便你可以方便的集成到你的项目中。直接在你的项目中引入Chipmunk源码以及正确的编译选项并非易事。iPhone编译脚本能生成一个可用在iOS模拟器和设备的通用库(“fat” library),其中的模拟器版本用的debug模式编译,而设备版本用的release模式编译。

MSVC

我很少使用MSVC,其他开发者帮忙维护了Visual Studio工程文件。MSVC 10工程应该能正常运行,因为我经常在发布稳定版本前测试它。MSVC 9工程可能运行不正常,我很少也没有必要去运行这个工程,如何你遇到问题,请通知我。

命令行

CMake编译脚本能在任何你安装了CMake的系统上运行。它甚至能生成XCode或MSVC工程(查看CMake文档获取更多信息)。

下面的命令编译一个Debug的Chipmunk:

cmake -D CMAKE_BUILD_TYPE=Debug .
make

如何没有-D CMAKE_BUILD_TYPE=Debug参数,将生成一个release版本。

为什么使用CMake?一个非常好心的人完成了这个脚本的最初版本,然后我发现CMake能非常方便的解决跨平台编译问题。我知道有些人非常讨厌安装一些胡乱的non-make编译系统来编译某些东西,但是CMake确实节省了我大量的时间和精力。

Hello Chipmunk(World)

下面的Hello World示例项目中,创建一个模拟世界,模拟一个球掉落到一个静态线段上然后滚动出去,并打印球的坐标。

#include 
#include 

int main(void){
  // cpVect是2D矢量,cpv()为初始化矢量的简写形式
  cpVect gravity = cpv(0, -100);
  
  // 创建一个空白的物理世界
  cpSpace *space = cpSpaceNew();
  cpSpaceSetGravity(space, gravity);
  
  // 为地面创建一个静态线段形状
  // 我们稍微倾斜线段以便球可以滚下去
  // 我们将形状关联到space的默认静态刚体上,告诉Chipmunk该形状是不可移动的
  cpShape *ground = cpSegmentShapeNew(space->staticBody, cpv(-20, 5), cpv(20, -5), 0);
  cpShapeSetFriction(ground, 1);
  cpSpaceAddShape(space, ground);
  
  // 现在让我们来构建一个球体落到线上并滚下去
  // 首先我们需要构建一个 cpBody 来容纳对象的物理属性
  // 包括对象的质量、位置、速度、角度等
  // 然后我们将碰撞形状关联到cpBody上以给它一个尺寸和形状
  
  cpFloat radius = 5;
  cpFloat mass = 1;
  
  // 转动惯量就像质量对于旋转一样
  // 使用 cpMomentFor*() 来近似计算它
  cpFloat moment = cpMomentForCircle(mass, 0, radius, cpvzero);
  
  // cpSpaceAdd*() 函数返回你添加的东西
  // 很便利在一行中创建并添加一个对象
  cpBody *ballBody = cpSpaceAddBody(space, cpBodyNew(mass, moment));
  cpBodySetPos(ballBody, cpv(0, 15));
  
  // 现在我们会球体创建碰撞形状
  // 你可以为同一个刚体创建多个碰撞形状
  // 它们将会附着关联到刚体上并移动更随
  cpShape *ballShape = cpSpaceAddShape(space, cpCircleShapeNew(ballBody, radius, cpvzero));
  cpShapeSetFriction(ballShape, 0.7);
  
  // 现在一切都建立起来了,我们通过称作时间步的小幅度时间增量来步进模拟空间中的所有物体
  // *高度*推荐使用固定长的时间步
  cpFloat timeStep = 1.0/60.0;
  for(cpFloat time = 0; time < 2; time += timeStep){
    cpVect pos = cpBodyGetPos(ballBody);
    cpVect vel = cpBodyGetVel(ballBody);
    printf(
      "Time is %5.2f. ballBody is at (%5.2f, %5.2f). It's velocity is (%5.2f, %5.2f)\n",
      time, pos.x, pos.y, vel.x, vel.y
    );
    
    cpSpaceStep(space, timeStep);
  }
  
  // 清理我们的对象并退出
  cpShapeFree(ballShape);
  cpBodyFree(ballBody);  
  cpShapeFree(ground);
  cpSpaceFree(space);
  
  return 0;

支持

获得支持最好的方式就是访问Chipmunk论坛。上面有许多人使用Chipmunk,应用在我知道的各个平台上。如果你在做一个商业项目,Howling Moon Software(我的公司)可给与支持。我们可以帮助你实现自定义Chipmunk行为,以及bug修复和性能优化。

联系

如果你发现Chipmunk中的任何bug,错误或者该文档中坏掉的链接,又或者对于Chipmunk有任何疑问、评论,都可以通过 slembcke@gmail.com (email或者GTalk)联系我。

开源协议

Chipmunk基于MIT协议。

Copyright (c) 2007-2013 Scott Lembcke and Howling Moon Software

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

这项协议意味着对于商业项目你不必购买许可证或者支付任何费用就能使用Chipmunk。(虽然我们真的很感谢捐赠)

链接

  • Chipmunk论坛 - Chipmunk2D官方论坛
  • Howling Moon Software - 我合办的软件公司(我们提供外包工作)
  • Chipmunk2D Pro - Chipmunk的增强版本,我们为ARM或者多核平台做了一些特定的优化,如从图像或程序数据中进行自动几何操作,以及为Objective-C做了API封装。
  • 游戏 - 使用Chimunk做的游戏清单。至少一小部分我们知道。

Chipmunk2D 基础

概述

在Chimpmunk中有4种基本对象类型,分别是

  • 刚体:一个刚体容纳着一个对象的物理属性(如质量、位置、角度、速度等)。默认情况下,它并不具有任何形状,直到你为它添加一个或者多个碰撞形状进去。如果你以前做过物理粒子,你会发现它们的不同之处是刚体可以旋转。在游戏中,通常刚体都是和一个精灵一一对应关联的。你应该构建你的游戏以便可以使用刚体的位置和角度来绘制你的精灵。
  • 碰撞形状:因为形状与刚体相关联,所以你可以为一个刚体定义形状。为了定义一个复杂的形状,你可以给刚体绑定足够多的形状。形状包含着一个对象的表面属性如摩擦力、弹性等。
  • 约束/关节:约束和关节被用来描述刚体之间是如何关联的
  • 空间:空间是Chipmunk中模拟对象的容器。你将刚体、形状、关节添加进入一个空间,然后将空间作为一个整体进行更新。空间控制着所有的刚体、形状和约束之间的相互作用。

内存管理

对于你将使用的大多数结构体来说,Chipmunk采用了一套或多或少的标准和简单直接的内存管理方式。拿cpSpace结构体来举例:

  • cpSpaceNew() - 分配并初始化一个cpSpace结构体。它先后调用了cpSpaceAlloc()cpSpaceInit(cpSpace *space)
  • cpSpaceFree(cpSpace *space) - 破环并释放cpSpace结构体

你对任何你所分配过空间的结构体都负有释放的责任。 Chipmunk没有采用引用计数和垃圾回收机制。 如果你调用了一个new函数,则必须匹配调用free函数来释放空间,否则会引起内存泄漏。

另外当你需要在栈上分配临时结构体,或者写一个语言绑定又或者在一个低内存容量的环境下编码,这时你需要更多分配和初始化的控制权,便可以使用下面的函数。大部分人永远都不会使用下面几个函数。

  • cpSpaceAlloc() - 为一个cpSpace结构体分配空间,但不进行初始化。所有的分配空间的函数看起来大致就像这样:return (cpSpace *)cpcalloc(1, sizeof(cpSpace));。 如果需要的话你可以自己实现自己的分配空间函数
  • cpSpaceInit(cpSpace *space) - 初始化cpSpace结构体
  • cpSpaceDestroy(cpSpace *space) - 释放由cpSpaceInit()申请的所有内存空间,但并不释放cpSpace结构体本身

就像newfree函数的对应调用一样,任何由alloc函数分配的内存都要由cpfree()或类似的函数来释放,任何由init函数初始化申请空间的对象都要通过destroy函数来释放。

基本类型

chipmunk_types.h定义了Chipmunk使用的一些基本数据类型。这些数据类型可以在编译时改变以便适应你的需求:

  • cpFloat: 浮点型,默认为double
  • cpVect: 2D矢量,cpVect相关文档
  • cpBool: 像每一个优秀的C语言库一样,具有跨语言兼容性,你可以定义自己的布尔类型,默认为int
  • cpDataPointer: 指针类型,可以是回调、用户自定义数据的指针,默认是void*
  • cpCollistionType: 碰撞形状类型的唯一标识符,默认是unsigned int。自定义类型必须支持==运算符
  • cpGroup: 碰撞组唯一标识符,默认是unsigned int。当你不想区分组别的时候,可以定义一个CP_NO_GROUP。自定义类型必须支持==运算符
  • cpLayers: 该类型被用作为层的掩码,默认是unsigned int。CP_ALL_LAYERS被用来定义为所有层位。自定义类型必须支持位操作&运算符

数学运算

首先,Chipmunk默认使用双精度浮点数进行数学计算。在大多数现代台式机处理器下这样很可能更快点,并意味着你可以不用过多担心浮点舍入引起的误差。在编译库的时候你可以修改Chipmunk使用的浮点类型。请查看chipmunk_types.h

Chipmunk为一些常用的数学函数定义了别名以便你可以用Chimpmunk的浮点型来代表float或者double类型计算。在你的代码里,这或许不是一个很充分的理由,但在你使用了错误的float/double版本的数学函数而造成了2%的性能损失,请使用这些别名函数。

有一些函数或许你会发现非常有用:

  • cpFloat cpfclamp(cpFloat f, cpFloat min, cpFloat max) - 截断f在min和max之间
  • cpFloat cpflerp(cpFloat f1, cpFloat f2, cpFloat t) - 对f1和f2进行线性插值
  • cpFloat cpflerpconst(cpFloat f1, cpFloat f2, cpFloat d) - 从f1到f2不超过d的线性插值

浮点数无穷大被定义为INFINITY, 很多数学库中这样定义,但这实际上并不是C标准库的一部分。

Chipmunk矢量:cpVect

结构体定义、常量和构造函数

定义:

typedef struct cpVect{
    cpFloat x, y;
} cpVect

零向量常量:

static const cpVect cpvzero = {0.0f,0.0f};

创建新结构体所用的便捷构造函数:

cpVect cpv(const cpFloat x, const cpFloat y)

操作运算

  • cpBool cpveql(const cpVect v1, const cpVect v2) – 检测两个向量是否相等。在使用C++程序时,Chipmunk提供一个重载操作符==。(比较浮点数时要小心!)
  • cpVect cpvadd(const cpVect v1, const cpVect v2) – 两个向量相加。在使用C++程序时,Chipmunk提供一个重载操作符+
  • cpVect cpvsub(const cpVect v1, const cpVect v2) – 两个向量相减。在使用C++程序时,Chipmunk提供一个重载操作符-
  • cpVect cpvneg(const cpVect v) – 使一个向量反向。在使用C++程序时,Chipmunk提供一个重载一个一元负操作符-
  • cpVect cpvmult(const cpVect v, const cpFloat s) – 标量乘法。在使用C++程序时,Chipmunk提供一个重载操作符*
  • cpFloat cpvdot(const cpVect v1, const cpVect v2) – 向量的点积。
  • cpFloat cpvcross(const cpVect v1, const cpVect v2) – 2D向量交叉相乘的模。2D向量交叉相乘的积作为一个只有z坐标的3D向量的z值。函数返回z坐标的值。
  • cpVect cpvperp(const cpVect v) – 返回一个垂直向量。(旋转90度)
  • cpVect cpvrperp(const cpVect v) – 返回一个垂直向量。(旋转-90度)
  • cpVect cpvproject(const cpVect v1, const cpVect v2) – 返回向量v1在向量v2上的投影。
  • cpVect cpvrotate(const cpVect v1, const cpVect v2) – 使用复杂的乘法运算将向量v1按照向量v2旋转。如果v1不是单位向量,则v1会被缩放。
  • cpVect cpvunrotate(const cpVect v1, const cpVect v2) – 和cpvrotate()相反。
  • cpFloat cpvlength(const cpVect v) – 返回v的长度。
  • cpFloat cpvlengthsq(const cpVect v) – 返回v的长度的平方,如果只是比较长度的话它的速度比cpvlength()快。
  • cpVect cpvlerp(const cpVect v1, const cpVect v2, const cpFloat t) – 在v1和v2之间线性插值。
  • cpVect cpvlerpconst(cpVect v1, cpVect v2, cpFloat d) – 以长度d在v1和v2之间线性插值。
  • cpVect cpvslerp(const cpVect v1, const cpVect v2, const cpFloat t) – 在v1和v2之间球形线性插值。
  • cpVect cpvslerpconst(const cpVect v1, const cpVect v2, const cpFloat a) – 在v1和v2之间以不超过角a的弧度值球形线性插值。
  • cpVect cpvnormalize(const cpVect v) – 返回a的一个归一化副本。作为特殊例子,在调用cpvzero时返回cpvzero。
  • cpVect cpvclamp(const cpVect v, const cpFloat len) – 将v固定到len上。
  • cpFloat cpvdist(const cpVect v1, const cpVect v2) – 返回v1和v2间的距离。
  • cpFloat cpvdistsq(const cpVect v1, const cpVect v2) – 返回v1和v2间的距离的平方。如果只是比较距离的话它比cpvdist()快。
  • cpBool cpvnear(const cpVect v1, const cpVect v2, const cpFloat dist) – 如果v1和v2间的距离小于dist则返回真。
  • cpVect cpvforangle(const cpFloat a) – 返回所给角(以弧度)单位向量。
  • cpFloat cpvtoangle(const cpVect v) – 返回v所指的角度方向的弧度。

Chipmunk轴对齐包围盒:cpBB

结构体定义和构造函数

  • 简单的包围盒结构体,存储着left,bottom,right,top等值。
typedef struct cpBB{
    cpFloat l, b, r ,t;
} cpBB
  • 便捷的构造函数,如cpv()函数一样返回一个副本而不是一个申请的指针。
cpBB cpBBNew(const cpFloat l, const cpFloat b, const cpFloat r, const cpFloat t)
  • 便捷的构造函数,用来构造一个位置为p,半径为r的一个圆的包围盒
cpBB cpBBNewForCircle(const cpVect p, const cpFloat r)

操作运算

  • cpBool cpBBIntersects(const cpBB a, const cpBB b) - 如果边界框相交返回true
  • cpBool cpBBContainsBB(const cpBB bb, const cpBB other) - 如果bb完全包含other返回true
  • cpBool cpBBContainsVect(const cpBB bb, const cpVect v) - 如果bb包含v返回true
  • cpBB cpBBMerge(const cpBB a, const cpBB b) - 返回包含ab的最小的边界框
  • cpBB cpBBExpand(const cpBB bb, const cpVect v) - 返回包含bbv的最小的边界框
  • cpVect cpBBCenter(const cpBB bb) - 返回bb的中心点矢量
  • cpFloat cpBBArea(cpBB bb) - 返回bb矢量表示的边界框的面积
  • cpFloat cpBBMergedArea(cpBB a, cpBB b) - 合并ab然后返回合并后的矢量的边界框的面积
  • cpFloat cpBBSegmentQuery(cpBB bb, cpVect a, cpVect b) - 返回分段查询相交bb的相交点个数,如果没有相交,返回INFINITY
  • cpBool cpBBIntersectsSegment(cpBB bb, cpVect a, cpVect b) - 如果由ab两端点定义的线段和bb相交返回true
  • cpVect cpBBClampVect(const cpBB bb, const cpVect v) - 返回v在边界框中被截断的矢量的副本
  • cpVect cpBBWrapVect(const cpBB bb, const cpVect v) - 返回v包含边界框的矢量的副本

Chipmunk刚体:cpBody

流氓和静态刚体

一般当我们创建一个刚体并将它添加到空间上后,空间就开始对之进行模拟,包括了对刚体位置、速度、受力以及重力影响等的模拟。没被添加到空间(没有被模拟)的刚体我们把它称之为流氓刚体。流氓刚体最重要的用途就是用来当作静态刚体,但是你仍然可以使用它们来实现直接控制物体,如移动平台。

静态刚体是流氓刚体,但被设置了一个特殊的标志以便让Chipmunk知道它们从不移动除非你要求这么做。静态刚体有两个目的。最初,它们被加入用来实现休眠功能。因为静态刚体不移动,Chipmunk知道让与它们接触或者连接的物体安全的进入休眠。接触或连接常规流氓刚体的物体从不允许休眠。静态刚体的第二个目的就是让Chipmunk知道关联到它们的形状从不需要更新它们的碰撞检测数据。Chipmunk也不需要关心静态物体之间的碰撞。一般所有水平几何都会被关联到一个静态刚体上除了移动平台或门等物体。

在Chipmunk5.3版本之前,你要创建一个无限大质量的流氓刚体通过cpSpaceAddStaticShape()来添加静态形状。现在你不必这样做了,并且如果你想使用休眠功能也不应该这样做了。每一个空间都有一个专用的静态刚体,你可以使用它来添加静态形状。Chipmunk也会自动将形状作为静态形状添加到静态刚体上。

内存管理函数

cpBody *cpBodyAlloc(void)
cpBody *cpBodyInit(cpBody *body, cpFloat m, cpFloat i)
cpBody *cpBodyNew(cpFloat m, cpFloat i)

void cpBodyDestroy(cpBody *body)
void cpBodyFree(cpBody *body)

如上是一套标准的Chipmunk内存管理函数。mi是刚体的质量和转动惯量。猜想刚体的质量通常是好的,但是猜想刚体的转动惯量却会导致一个很差的模拟。在任何关联到刚体的形状或者约束从空间移除之前注意不要释放刚体。

创建额外静态刚体

每一个cpSpace都有一个可以直接使用的内置的静态刚体,构建你自己的也非常便利。一个潜在的用途就是用在关卡编辑器中。通过将关卡的物块关联到静态刚体,你仍然可以相互独立的移动和旋转物块。然后你要做的就是在结束后调用cpSpaceRehashStatic()来重建静态碰撞检测的数据。

关于流氓和静态刚体的更多信息,请看Chipmunk空间。

cpBody *cpBodyAlloc(void);
cpBody *cpBodyInitStatic(cpBody *body)
cpBody *cpBodyNewStatic()

创建额外的具有无限的质量和转动惯量的静态物体。

属性

Chipmunk为刚体的多个属性提供了getter/setter函数。如果刚体在休眠状态,设置大多数属性会自动唤醒它们。如果你想,你也可以直接在cpBody结构体内设置字段。它们都在头文件中有记录。

cpFloat cpBodyGetMass(const cpBody *body)
void cpBodySetMass(cpBody *body, cpFloat m)

刚体的质量。

cpFloat cpBodyGetMoment(const cpBody *body)
void cpBodySetMoment(cpBody *body, cpFloat i)

刚体的转动惯量(MoI(译者注:Moment Of Inertia即转动惯量的缩写)或有时只说惯量)。惯量就像刚体的旋转质量。请查阅下面的函数来帮助计算惯量。

cpVect cpBodyGetPos(const cpBody *body)
void cpBodySetPos(cpBody *body, cpVect pos)

刚体重心的位置。当改变位置的时候如果你要计划对空间进行任何查询,你可能还需要调用cpSpaceReindexShapesForBody()来更新关联形状的碰撞检测信息。

cpVect cpBodyGetVel(const cpBody *body)
void cpBodySetVel(cpBody *body, const cpVect value)

刚体重心的线速度。

cpVect cpBodyGetForce(const cpBody *body)
void cpBodySetForce(cpBody *body, const cpVect value)

施加到刚体重心的力。

cpFloat cpBodyGetAngle(const cpBody *body)
void cpBodySetAngle(cpBody *body, cpFloat a)

刚体的角度,弧度制。当改变角度的时候如果你要计划对空间进行任何查询,你可能还需要调用cpSpaceReindexShapesForBody()来更新关联形状的碰撞检测信息。

cpFloat cpBodyGetAngVel(const cpBody *body)
void cpBodySetAngVel(cpBody *body, const cpFloat value)

刚体的角速度,弧度/秒,

cpFloat cpBodyGetTorque(const cpBody *body)
void cpBodySetTorque(cpBody *body, const cpFloat value)

施加到刚体的扭矩。

cpVect cpBodyGetRot(const cpBody *body)

刚体的旋转向量。可通过cpvrotate()或者cpvunrotate()进行快速旋转。

cpFloat cpBodyGetVelLimit(const cpBody *body)
void cpBodySetVelLimit(cpBody *body, const cpFloat value)

刚体的速度极限。、默认为INFINITY(无限大),除非你专门设置它。可以被用来限制下落速度等。

cpFloat cpBodyGetAngVelLimit(const cpBody *body)
void cpBodySetAngVelLimit(cpBody *body, const cpFloat value)

刚体以弧度/秒的角速度限制。默认为INFINITY,除非你专门设置它。

cpSpace* cpBodyGetSpace(const cpBody *body)

获取body所添加进去的cpSpace

cpDataPointer cpBodyGetUserData(const cpBody *body)
void cpBodySetUserData(cpBody *body, const cpDataPointer value)

使用数据指针。使用该指针从回调中获取拥有该刚体的游戏对象的引用。

转动惯量和面积帮助函数

使用以下函数来近似计算出刚体的转动惯量,如果想得到多个,那就将结果相加在一起。

  • cpFloat cpMomentForCircle(cpFloat m, cpFloat r1, cpFloat r2, cpVect offset) – 计算空心圆的转动惯性,r1和r2是在任何特定顺序下的内径和外径。 (实心圆圈的内径为0)
  • cpFloat cpMomentForSegment(cpFloat m, cpVect a, cpVect b) – 计算线段的转动惯量。端点ab相对于刚体。
  • cpFloat cpMomentForPoly(cpFloat m, int numVerts, const cpVect *verts, cpVect offset) – 计算固定多边形的转动惯量,假设它的中心在质心上。offset偏移值被加到每个顶点。
  • cpFloat cpMomentForBox(cpFloat m, cpFloat width, cpFloat height) – 计算居中于刚体的实心矩形的转动惯量。

转动惯量例子

// 质量为2,半径为5的实心圆的转动惯量
cpFloat circle1 = cpMomentForCircle(2, 0, 5, cpvzero);

// 质量为1,内径为1,外径为6的空心圆的转动惯量
cpFloat circle2 = cpMomentForCircle(1, 2, 6, cpvzero);

// 质量为1,半径为3,x轴方向偏离重心量为3的实心圆的转动惯量
cpFloat circle3 = cpMomentForCircle(2, 0, 5, cpv(3, 0));

// 复合对象。居中于重心的1x4的矩形和y轴偏移重心量为3,半径为1的实心圆
// 只需将转动惯量相加到一起
cpFloat composite = cpMomentForBox(boxMass, 1, 4) + cpMomentForCircle(circleMass, 0, 1, cpv(0, 3));

如果你想近似计算诸如质量或密度此类的东西,可以使用下列函数来获取Chipmunk形状区域。

  • cpFloat cpAreaForCircle(cpFloat r1, cpFloat r2) – 空心圆形状面积
  • cpFloat cpAreaForSegment(cpVect a, cpVect b, cpFloat r) – 斜线段面积。(如果半径为0的话永远为0)
  • cpFloat cpAreaForPoly(const int numVerts, const cpVect *verts) – 多边形形状的面积。多边形为凹多边形时返回一个负值。

坐标系转换函数

许多事情被定义在刚体的局部坐标,也就意味着(0,0)是刚体的重心和轴线旋转中心。

  • cpVect cpBodyLocal2World(const cpBody *body, const cpVect v) – 从刚体局部坐标系转换到世界坐标系
  • cpVect cpBodyWorld2Local(const cpBody *body, const cpVect v) – 从世界坐标系转换到刚体的局部坐标系

施加力和力矩

人们有时候容易混淆力和冲力之间的区别。冲力基本上是一个在非常短的时间内施加的一个非常大的力,就像一个球击中一堵墙或者大炮射击一样。Chipmunk的冲力会在一瞬间直接施加在物体的速度上。无论是力还是冲力都受到物体质量的影响。物体质量翻倍,则效果减半。

  • void cpBodyResetForces(cpBody *body) – 对刚体施加0值的力和扭矩
  • void cpBodyApplyForce(cpBody *body, const cpVect f, const cpVect r) – 在离重心相对偏移量为r的位置施加f的力于body
  • void cpBodyApplyImpulse(cpBody *body, const cpVect j, const cpVect r) – 在离重心相对偏移量为r的位置施加j的冲力于body上。

注: cpBodyApplyForce()cpBodyApplyImpulse()两者都是在绝对坐标系中施加力或者冲力,并在绝对坐标系中产生相对的偏移。(偏移量相对于重心位置,但不随刚体旋转)

休眠函数

Chipmunk支持休眠功能,以便其停止使用CPU时间来模拟移动的对象组。更多信息请查阅cpSpace部分。

  • cpBool cpBodyIsSleeping(const cpBody *body) – 如果刚体在休眠则返回true
  • void cpBodyActivate(cpBody *body) – 重设刚体的闲置时间。如果在休眠,则会唤醒它以及和它接触的任何其他刚体。
  • void cpBodySleep(cpBody *body) – 强制一个刚体立即进入休眠,即使它在半空中。不能从回调中被调用。
  • void cpBodyActivateStatic(cpBody body, cpShape filter) – 和cpBodyActivate()功能类似。激活刚体接触的所有刚体。如果filter不为NULL,那么只有通过筛选过滤的刚体才会被唤醒。
void cpBodySleepWithGroup(cpBody *body, cpBody *group)

当对象在Chipmunk中处于休眠时,和它接触或连接在一起的所有刚体都会作为一组进入休眠。当对象被唤醒时,和它一组的所有对象都会被唤醒。

cpBodySleepWithGroup()允许你将群组中的对象一起休眠。如果你通过一个新的组给groups
传递NULL值,则它和cpBodySleep()功能一样。如果你为groups传入一个休眠的刚体,那么当group是唤醒状态时,body也会被唤醒。你可以通过这来初始化关卡并开始堆对象的预休眠状态。

休眠例子

// 构建一堆箱子
// 强制它们进入休眠直到他们第一次被接触
// 将它们放进一组以便接触它们任意一个都会唤醒他们
cpFloat size = 20;
cpFloat mass = 1;
cpFloat moment = cpMomentForBox(mass, size, size);

cpBody *lastBody = NULL;

for(int i=0; i<5; i++){
  cpBody *body = cpSpaceAddBody(space, cpBodyNew(mass, moment));
  cpBodySetPos(body, cpv(0, i*size));

  cpShape *shape = cpSpaceAddShape(space, cpBoxShapeNew(body, size, size));
  cpShapeSetFriction(shape, 0.7);

  // 你可以使用任意休眠刚体作为组别的标识符
  // 这里我们只保存了我们初始化的最后一个刚体的引用
  // 传入NULL值作为组别将启动一个新的休眠组
  // 你必须在完全初始化对象后这么做
  // 添加形状或调用setter函数将会唤醒刚体
  cpBodySleepWithGroup(body, lastBody);
  lastBody = body;
}

迭代器

typedef void (*cpBodyShapeIteratorFunc)(cpBody *body, cpShape *shape, void *data)
void cpBodyEachShape(cpBody *body, cpBodyShapeIteratorFunc func, void *data)

对于关联到body且被加入到空间的每个形状调用func函数。data作为上下文值传递。使用这些回调来删除形状是安全的。

typedef void (*cpBodyConstraintIteratorFunc)(cpBody *body, cpConstraint *constraint, void *data)
void cpBodyEachConstraint(cpBody *body, cpBodyConstraintIteratorFunc func, void *data)

对于关联到body且被加入到空间的每个约束调用func函数。data作为上下文值传递。使用这些回调来删除约束是安全的。

typedef void (*cpBodyArbiterIteratorFunc)(cpBody *body, cpArbiter *arbiter, void *data)
void cpBodyEachArbiter(cpBody *body, cpBodyArbiterIteratorFunc func, void *data)

这个更有趣。对于刚体参与碰撞的每个碰撞对调用func函数。调用cpArbiterGet[Bodies|Shapes]()或者CP_ARBITER_GET_[BODIES|SHAPES]()将会返回刚体或者形状作为第一个参数。你可以用它来检查各种碰撞信息。比如,接触地面,接触另一特定的对象,施加到对象上的碰撞力等。被碰撞处理回调或者cpArbiterIngnore()的传感器形状和仲裁者将不被接触图形跟踪。

注:如果你的编译器支持闭包(如Clang),还有另外一组函数可以调用,如cpBodyEachShape_b()等。更多信息见chipmunk.h

Crushing例子

struct CrushingContext {
  cpFloat magnitudeSum;
  cpVect vectorSum;
};

static void
EstimateCrushingHelper(cpBody *body, cpArbiter *arb, struct CrushingContext *context)
{
  cpVect j = cpArbiterTotalImpulseWithFriction(arb);
  context->magnitudeSum += cpvlength(j);
  context->vectorSum = cpvadd(context->vectorSum, j);
}

cpFloat
EstimateCrushForce(cpBody *body, cpFloat dt)
{
  struct CrushingContext crush = {0.0f, cpvzero};
  cpBodyEachArbiter(body, (cpBodyArbiterIteratorFunc)EstimateCrushingHelper, &crush);

  // 通过比较向量和以及幅度和来查看碰撞的力量彼此相对有多大
  cpFloat crushForce = (crush.magnitudeSum - cpvlength(crush.vectorSum))*dt;
}

嵌入回调

这部分是残留。现在你可以看看星球演示这个例子,看如何使用嵌入回调来实现的行星重力。

杂项函数

  • cpBool cpBodyIsStatic(const cpBody *body) - 如果body是静态刚体的话,返回true。无论是cpSpace.staticBody,还是由cpBodyNewStatic()或者cpBodyInitStatic()创建的刚体。
  • cpBool cpBodyIsRogue(const cpBody *body)- 如果刚体从来没有被加入到空间的话返回true

札记

  • 如果可能的话使用力来修正刚体。这样是最稳定的。
  • 修正刚体的速度是不可避免的,但是在每帧对刚体的速度做巨大的变化会造成一些奇怪的模拟。你可以自由实验,但别说我没警告你哦。
  • 不要在单步中修正刚体的位置除非你确实知道你在干什么。否则你得到的位置、速度则会不同步。
  • 如果在调用cpSpaceRemoveShape()之前你就要释放一个刚体,那么会引起崩溃。

Chipmunk碰撞形状:cpShape

当前有三种类型的碰撞形状:

  1. 圆形:快速简单的碰撞形状
  2. 线段:主要作为静态形状。可以倾斜以便给之一个厚度。
  3. 凸多边形:最慢,但却为最灵活的碰撞形状。

如果你愿意,你可以在一个刚体上添加任意数量的形状。这就是为什么两种类型(形状和刚体)是分离开的。这将会让你足够灵活的来给相同对象的不同区域提供不同的摩擦力、弹性以及回调值。

当创建不同类型的形状的时候,你将永远得到一个cpShape*指针返回。这是因为Chipmunk的形状是不透明的类型。想象具体的碰撞形状类型如cpCircleShape, cpSegmentShapecpPolyShape, 他们都是cpShape的私有子类。你仍然可以使用getter函数来获取他们的属性,但不要将cpShape指针转成他们的特定类型指针。

札记

Chipmunk直到 6.1.2 版本才支持线段、线段碰撞。由于兼容性的原因,你必须明确地全局调用cpEnableSegmentToSegmentCollisions()来启用它们。 (感谢LegoCylon对此的帮助)

属性

Chipmunk为一些碰撞形状属性提供了getter/ setter函数。如果形状关联的刚体在休眠,设置多数属性都会自动唤醒它们。如果你想的话,也可以直接设置cpShape结构的某些字段。他们在头文件中都记录有。

cpBody * cpShapeGetBody(const cpShape *shape)
void cpShapeSetBody(cpShape *shape, cpBody *body)

只有当形状尚未添加进空间的时候才能关联到一个刚体。

cpBB cpShapeGetBB(const cpShape *shape)

上面得到的是形状的碰撞包围盒。只能保证在cpShapeCacheBB()cpSpaceStep()调用后是有效的。移动形状所连接到刚体并不更新它的包围盒。对于没有关联到刚体的用于查询的形状,也可以使用cpShapeUpdate()

cpBool cpShapeGetSensor(const cpShape *shape)
void cpShapeSetSensor(cpShape *shape, cpBool value)

用来标识形状是否是一个感应器的布尔值。感应器只调用碰撞回调,但却不产生真实的碰撞。

cpFloat cpShapeGetElasticity(const cpShape *shape)
void cpShapeSetElasticity(cpShape *shape, cpFloat value)

上面说的是形状的弹性。值0.0表示没有反弹,而值为1.0将提供一个“完美”的反弹。然而由于使用1.0或更高的值会导致模拟不精确,所以不推荐。碰撞的弹性是由单个形状的弹性相乘得到。

cpFloat cpShapeGetFriction(const cpShape *shape)
void cpShapeSetFriction(cpShape *shape, cpFloat value)

上面说的是摩擦系数。Chipmunk使用的是库仑摩擦力模型,0.0值表示无摩擦。碰撞间的摩擦是由单个形状的摩擦相乘找到。摩擦系数表

cpVect cpShapeGetSurfaceVelocity(const cpShape *shape)
void cpShapeSetSurfaceVelocity(cpShape *shape, cpVect value)

上面说的是物体的表面速度。可用于创建传送带或走动的玩家。此值在计算摩擦时才会使用,而不是用于解决碰撞。

cpCollisionType cpShapeGetCollisionType(const cpShape *shape)
void cpShapeSetCollisionType(cpShape *shape, cpCollisionType value)

您可以为Chipmunk的碰撞形状指定类型从而在接触特定类型物体的时候触发回调。更多信息请参见回调部分。

cpGroup cpShapeGetGroup(const cpShape *shape)
void cpShapeSetGroup(cpShape *shape, cpGroup value)

在相同的非零组中,形状间不产生碰撞。在创建了一个许多形状组成的物体,但却不想自身与自身之间发生碰撞,这会很有用。默认值为CP_NO_GROUP

cpLayers cpShapeGetLayers(const cpShape *shape)
void cpShapeSetLayers(cpShape *shape, cpLayers value)

只有在相同的位平面内形状间才发生碰撞。比如(a->layers & b->layers) != 0。默认情况下,一个形状占据所有的位平面。如果你不熟悉如何使用它们,维基百科有篇很好的文章介绍了位掩码的相关知识你可以阅读下。默认值为CP_ALL_LAYERS。

cpSpace* cpShapeGetSpace(const cpShape *shape)

得到形状所添加进去的空间。

cpDataPointer cpShapeGetUserData(const cpShape *shape)
void cpShapeSetUserData(cpShape *shape, cpDataPointer value)

上面说的是用户定义的数据指针。如果你设置将其指向形状关联的游戏对象,那么你可以从Chipmunk回调中访问你的的游戏对象。

碰撞过滤

Chipmunk 有两种主要的途径来忽略碰撞: 群组和层

群组是为了忽略一个复杂对象部分之间的碰撞。玩偶是一个很好的例子。当联合手臂和躯干的时候,他们可以重叠。群组允许这样做。相同群组间的形状不产生碰撞。所以通过将一个布娃娃的所有形状放在同一群组中,就会阻止其碰撞自身的其它部件。

层允许你将碰撞的形状分离在相互排斥的位面。形状不止可以在一个层上,形状与形状发生碰撞,而两者必须至少在一个相同的层上。举一个简单的例子,比如说形状A是在第1层,形状B是在第2层和形状C是在层1和2上。形状A和B不会互相碰撞,但形状C将与这两个A和B发生碰撞

层也可以用于建立基于碰撞的规则。比如说在你的游戏中有四种类型的形状。玩家,敌人,玩家子弹,敌人子弹。玩家应该和敌人发生碰撞,但子弹却不应该和发射者碰撞。图表类似下图:

Filtering_Collisions

图表中‘-’是多余的斑点,数字的地方应该发生碰撞。您可以使用层要定义每个规则。然后将层添加到每个类型:玩家应该在层1和2,敌人应该是在层1和3中,玩家的子弹应该是在层3中,以及敌人的子弹应该是在层2中。这种处理层作为为规则的方式,可以定义多达32个规则。默认cpLayers类型为unsigned int其中在大多数系统是32位的。如果你需要更多的比特来完成工作, 你可以在chipmunk_types.h中重新定义cpLayers类型。

还有最后一个方法通过碰撞处理函数来过滤碰撞。见回调的部分来获取更多信息。碰撞处理程序可以更灵活,但它们也是最慢的方法。所以,你要优先尝试使用群组或层。

内存管理函数

void cpShapeDestroy(cpShape *shape)
void cpShapeFree(cpShape *shape)

DestroyFree函数由所有形状类型共享。分配和初始化函数特定于每一个形状。见下文。

其他函数

  • cpBB cpShapeCacheBB(cpShape *shape) – 同步形状与形状关联的刚体
  • cpBB cpShapeUpdate(cpShape *shape, cpVect pos, cpVect rot) – 设置形状的位置和旋转角度
  • void cpResetShapeIdCounter(void) – Chipmunk使用了一个计数器,以便每一个新的形状是在空间索引中使用唯一的哈希值。因为这会影响空间中碰撞被发现和处理的顺序,你可以在每次空间中添加新的形状时重置形状计数器。如果你不这样做,有可能模拟(非常)略有不同。

圆形形状

cpCircleShape *cpCircleShapeAlloc(void)
cpCircleShape *cpCircleShapeInit(cpCircleShape *circle, cpBody *body, cpFloat radius, cpVect offset)
cpShape *cpCircleShapeNew(cpBody *body, cpFloat radius, cpVect offset)

body 是圆形形状关联的刚体。offset 是在刚体局部坐标系内,与刚体中心的偏移量。

cpVect cpCircleShapeGetOffset(cpShape *circleShape)
cpFloat cpCircleShapeGetRadius(cpShape *circleShape)

圆形形状属性的getter函数。传一个非圆形形状将会抛出一个异常。

线段形状

cpSegmentShape* cpSegmentShapeAlloc(void)
cpSegmentShape* cpSegmentShapeInit(cpSegmentShape *seg, cpBody *body, cpVect a, cpVect b, cpFloat radius)
cpShape* cpSegmentShapeNew(cpBody *body, cpVect a, cpVect b, cpFloat radius)

body是线段形状关联的刚体,ab是端点,radius是线段的厚度。

cpVect cpSegmentShapeGetA(cpShape *shape)
cpVect cpSegmentShapeGetA(cpShape *shape)
cpVect cpSegmentShapeGetNormal(cpShape *shape)
cpFloat cpSegmentShapeGetRadius(cpShape *shape)

线段属性的getter函数。传入一个非线段形状会抛出一个断言。

void cpSegmentShapeSetNeighbors(cpShape *shape, cpVect prev, cpVect next)

当你有一些连接在一起的线段形状时,线段仍然可以与线段间的“裂缝”碰撞。通过设置相邻线段的端点,你告诉Chipmunk来避免裂缝内部碰撞。

多边形形状

cpPolyShape *cpPolyShapeAlloc(void)
cpPolyShape *cpPolyShapeInit(cpPolyShape *poly, cpBody *body, int numVerts, const cpVect *verts, cpVect offset)
cpShape *cpPolyShapeNew(cpBody *body, int numVerts, const cpVect *verts, cpVect offset)

body是多边形关联的刚体,verts是一个cpVect结构体数组,定义了顺时针方向的凸多边形顶点,offset是在刚体局部坐标系中与刚体重心的偏移量。当顶点没形成凸多边形或者不是顺时针顺序的时候会抛出一个断言。

cpPolyShape *cpPolyShapeInit2(cpPolyShape *poly, cpBody *body, int numVerts, const cpVect *verts, cpVect offset, cpFloat radius)
cpShape *cpPolyShapeNew2(cpBody *body, int numVerts, cpVect *verts, cpVect offset, cpFloat radius)

和上面的一样,但允许你创建一个带有半径的多边形形状。(我知道名字有点糊涂,在Chipmunk7中将会清理)

int cpPolyShapeGetNumVerts(cpShape *shape)
cpVect cpPolyShapeGetVert(cpShape *shape, int index)
cpFloat cpPolyShapeGetRadius()

多边形形状属性的getter函数。传递一个非多边形形状或者不存在的index将会抛出一个断言。

修改cpShpaes

简短的回答是,你不能因为这些更改将只拿起一个改变形状的面的位置,而不是它的速度。长的答案是,你可以使用“不安全”的API,只要你认识到这样做不会导致真实的物理行为。这些额外的功能都在单独的头文件chipmunk_unsafe.h中定义。

札记

  • 你可以将多个碰撞形状关联到刚体上。这样你就可以创建几乎任何形状。
  • 关联在同一个刚体上的形状不会产生冲突。你不必担心同个刚体上的形状的重叠问题。
  • 确保刚体和刚体的碰撞形状都被添加进了空间。有个例外,就是当你如果有一个外部刚体或你嵌入自身到刚体。在这种情况下,只需把形状添加进空间。

Chipmunk空间:cpSpace

Chipmunk的空间是模拟的基本单元。你将刚体、形状和约束添加进去然后通过时间来步进更新模拟。

什么是迭代?为什么我要关心?

Chipmunk使用一个迭代求解器来计算出空间刚体之间的力。也就是说它建立了刚体间的所有碰撞、关节和约束的一个列表,并在列表中逐个考虑每一个刚体的若干条件。遍数这些条件便得到迭代次数,且每次迭代会使求解更准确。如果你使用太多的迭代,物理效果看起来应该不错并且坚实稳定,但可能消耗太多的CPU时间。如果你使用过少的迭代,模拟仿真似乎看起来有些糊状或弹性,而物体应该是坚硬的。设置迭代次数可以让你在CPU使用率和物理精度上做出平衡。 Chipmunk中默认的迭代值是10,足以满足大多数简单的游戏。

休眠

休眠是Chipmunk5.3新功能,是指空间停用已停止移动的整个对象群组,以节省CPU时间和电池寿命的能力。为了使用此功能,你必须做两件事情。第一个是,你必须将你的所有静态几何关联到静态刚体。如果对象接触的是非静态流氓体,则它们不能进入休眠,即使它的形状是作为静态形状添加的。第二个是,你必须通过cpSpace.sleepTimeThreshold设置一个时间阈值来显式启用休眠。如果你没有明确设置cpSpace.idleSpeedThreshold,那么Chipmunk会基于当前重力自动产生一个休眠阈值。

属性

int cpSpaceGetIterations(const cpSpace *space)
void cpSpaceSetIterations(cpSpace *space, int value)

迭代次数允许你控制求解器计算的精度。默认值为10。更多信息见上面。

cpVect cpSpaceGetGravity(const cpSpace *space)
void cpSpaceSetGravity(cpSpace *space, cpVect value)

施加到空间的全局重力。默认是cpvzero。可以通过编写自定义积分函数来重写每个刚体。

cpFloat cpSpaceGetDamping(const cpSpace *space)
void cpSpaceSetDamping(cpSpace *space, cpFloat value)

施加到空间的简单的阻尼值。数值0.9意味着每个刚体每秒会损失速度会损失掉10%。默认值为1。像重力一样,阻尼值也可以在每个刚体上重写。

cpFloat cpSpaceGetIdleSpeedThreshold(const cpSpace *space)
void cpSpaceSetIdleSpeedThreshold(cpSpace *space, cpFloat value)

刚体被考虑为静止限制的速度阈值。默认值为0,意味着让空间来估算猜测基于重力的良好的阈值。

cpFloat cpSpaceGetSleepTimeThreshold(const cpSpace *space)
void cpSpaceSetSleepTimeThreshold(cpSpace *space, cpFloat value)

一组刚体休眠需要保持静止闲置的时间阈值。默认值为INFINITY, 禁用了休眠功能。

cpFloat cpSpaceGetCollisionSlop(const cpSpace *space)
void cpSpaceSetCollisionSlop(cpSpace *space, cpFloat value)

支持形状间的重叠量。鼓励将这个值设置高点而不必在意重叠,因为它提高了稳定性。它默认值为0.1。

cpFloat cpSpaceGetCollisionBias(const cpSpace *space)
void cpSpaceSetCollisionBias(cpSpace *space, cpFloat value)

Chipmunk让快速移动的物体重叠,然后修复重叠。即使横扫碰撞被支持,重叠对象也不可避免,并且这是一个高效,稳定的方式来处理重叠的对象。控制重叠百分比的偏置值在1秒后仍然是不固定的,默认~0.2%。有效值是在0到1的范围内,但由于稳定的原因不推荐使用0。默认值的计算公式为cpfpow(1.0F - 0.1F,60.0f),这意味着Chipmunk试图在1/60s内纠正10%的错误。注:非常非常少的游戏需要更改此值。

cpTimestamp cpSpaceGetCollisionPersistence(const cpSpace *space)
void cpSpaceSetCollisionPersistence(cpSpace *space, cpTimestamp value)

空间保持碰撞的帧数量。有助于防止抖动接触恶化。默认值为3,非常非常非常少的游戏需要更改此值。

cpFloat cpSpaceGetCurrentTimeStep(const cpSpace *space)

检索当前(如果你是从cpSpaceStep()回调)或最近(在cpSpaceStep()之外调用)的时间步长。

cpFloat cpSpaceIsLocked(const cpSpace *space)

在回调中返回true时,意味着你不能从空间添加/删除对象。可以选择创建一个post-step回调来替代。

cpDataPointer cpSpaceGetUserData(const cpSpace *space)
void cpSpaceSetUserData(cpSpace *space, cpDataPointer value)

用户定义的数据指针。这点在游戏状态对象或拥有空间的场景管理对象上是很有用的。

cpBody * cpSpaceGetStaticBody(const cpSpace *space)

空间中专用的静态刚体。你不必使用它,而是因为它的内存由空间自动管理,非常方便。如果你想要做回调的话,你可以将它的数据指针指向一些有用的东西。

内存管理函数

cpSpace* cpSpaceAlloc(void)
cpSpace* cpSpaceInit(cpSpace *space)
cpSpace* cpSpaceNew()

void cpSpaceDestroy(cpSpace *space)
void cpSpaceFree(cpSpace *space)

更多标准的Chipmunk内存函数。

void cpSpaceFreeChildren(cpSpace *space)

这个函数将释放所有已添加到空间中的的形状、刚体和关节。不要释放space空间。你仍然需要自己调用cpSpaceFree()。在一个真正的游戏中你可能永远不会使用这个,因为你的游戏状态或者游戏控制器应该会管理从空间移除并释放对象。

操作运算

cpShape *cpSpaceAddShape(cpSpace *space, cpShape *shape)
cpShape *cpSpaceAddStaticShape(cpSpace *space, cpShape *shape)
cpBody *cpSpaceAddBody(cpSpace *space, cpBody *body)
cpConstraint *cpSpaceAddConstraint(cpSpace *space, cpConstraint *constraint)

void cpSpaceRemoveShape(cpSpace *space, cpShape *shape)
void cpSpaceRemoveBody(cpSpace *space, cpBody *body)
void cpSpaceRemoveConstraint(cpSpace *space, cpConstraint *constraint)

cpBool cpSpaceContainsShape(cpSpace *space, cpShape *shape)
cpBool cpSpaceContainsBody(cpSpace *space, cpBody *body)
cpBool cpSpaceContainsConstraint(cpSpace *space, cpConstraint *constraint)

这些函数是从空间中添加和删除形状、刚体和约束。添加/删除函数不能在postStep()回调之外的回调内调用(这和postSolve()回调是不同的!)。当cpSpaceStep()仍然在执行时,试图从空间添加或删除对象会抛出一个断言。更多信息请参见回调部分。添加函数会返回被添加的对象以便你可以在一行中创建和添加一些东西。注意在移除关联到刚体的形状和约束之前不要去释放刚体,否则会造成崩溃。contains函数允许你检查一个对象有没有被添加到空间中。

静态动态转换函数

void cpSpaceConvertBodyToStatic(cpSpace *space, cpBody *body)

将刚体转换为静态刚体。它的质量和力矩将被设置为无穷大,并且速度为0。旧的质量和力矩以及速度都不会被保存。这将有效地将一个刚体和它的形状冻结到一个位置。这不能被一个激活的刚体调用,所以你可能需要先调用cpSpaceRemoveBody()。此外,因为它修改了碰撞检测的数据结构,如果你想从另外一个回调函数或迭代器使用你必须使用后一步的回调。

空间索引

Chipmunk6正式支持2个空间索引。默认是轴对齐包围盒树,该灵感来自于Bullet物理库中使用的包围盒树,但是我将它与我自己的碰撞对缓存一起做了扩展以便为树实现非常好的时间相干性。树无需调整优化,而且在大多数游戏中会发现使用它能获得更好的性能。另外一个可用的索引是空间哈希,当你有着非常多数量且相同尺寸的物体时,它会更快。

有时,你可能需要更新形状的碰撞检测数据。如果你移动静态形状或者刚体,你必须这样做来让Chipmunk知道它需要更新碰撞数据。你可能还希望手动为移动过的普通形状更新碰撞数据,并且仍然想进行查询。

  • void cpSpaceReindexShape(cpSpace space, cpShape shape) – 重新索引一个指定的形状
  • void cpSpaceReindexShapesForBody(cpSpace space, cpBody body) - 重新索引指定刚体上的所有形状
  • void cpSpaceReindexStatic(cpSpace *space) – 重新索引所有静态形状。一般只更新改变的形状会比较快

迭代器

typedef void (*cpSpaceBodyIteratorFunc)(cpBody *body, void *data)
void cpSpaceEachBody(cpSpace *space, cpSpaceBodyIteratorFunc func, void *data)

为空间中的每个刚体调用func函数,同时传递data指针。休眠中的刚体包括在内,但是静态和流氓刚体不包括在内,因为他们没有被添加进空间。

cpSpaceEachBody例子:

// 检测空间中是否所有刚体都在休眠的代码片段

// 这个函数被空间中的每个刚体调用
static void EachBody(cpBody *body, cpBool *allSleeping){
  if(!cpBodyIsSleeping(body)) *allSleeping = cpFalse;
}

// 然后在你的更新函数中这样做
cpBool allSleeping = true;
cpSpaceEachBody(space, (cpSpaceBodyIteratorFunc)EachBody, &allSleeping);
printf("All are sleeping: %s\n", allSleeping ? "true" : "false");
typedef void (*cpSpaceShapeIteratorFunc)(cpShape *shape, void *data)
void cpSpaceEachShape(cpSpace *space, cpSpaceShapeIteratorFunc func, void *data)

为空间中的每个形状调用func函数,同时传递data指针。休眠和静态形状被包括在内。

typedef void (*cpSpaceConstraintIteratorFunc)(cpConstraint *constraint, void *data)
void cpSpaceEachConstraint(cpSpace *space, cpSpaceConstraintIteratorFunc func, void *data)

为空间中的每个约束调用func函数同时传递data指针。

注意:如果你的编译器支持闭包(如Clang), 那么有另外一组函数你可以调用。cpSpaceEachBody_b()等等。更多信息请查看chipmunk.h

空间模拟

void cpSpaceStep(cpSpace *space, cpFloat dt)

通过给定的时间步来更新空间。强烈推荐使用一个固定的时间步长。这样做能大大提高模拟的质量。实现固定的时间步,最简单的方法就是简单的每个帧频步进1/60s(或任何你的目标帧率),而无论花去了多少渲染时间。在许多游戏中这样很有效,但是将物理时间步进和渲染分离是一个更好的方式。这是一篇介绍如何做的好文章

启用和调优空间哈希(散列)

如果你有成千上万个大小大致相同的物体,空间哈希可能会很适合你。

void cpSpaceUseSpatialHash(cpSpace *space, cpFloat dim, int count)

使空间从碰撞包围盒树切换到空间哈希。 空间哈希数据对大小相当敏感。dim是哈希单元的尺寸。设置dim为碰撞形状大小的平均尺寸可能会得到最好的性能。设置dim太小会导致形状填充进去很多哈希单元,太低会造成过多的物体插入同一个哈希槽。

count是在哈希表中建议的最小的单元数量。如果单元太少,空间哈希会产生很多误报。过多的单元将难以做高速缓存并且浪费内存。将count设置成10倍于空间物体的个数可能是一个很好的起点。如果必要的话从那里调优。

关于使用空间哈希有个可视化的演示程序,通过它你可以明白我的意思。灰色正方形达标空间哈希单元。单元颜色越深,就意味着越多的物体被映射到那个单元。一个好的dim尺寸也就是你的物体能够很好的融入格子中。

注意,浅色的灰色意味着每个单元没有太多的物体映射到它。

当你使用太小的尺寸,Chipmunk不得不在每个物体上插入很多哈希单元。这个代价有些昂贵。

注意到灰色的单元和碰撞形状相比是非常小的。

当你使用过大的尺寸,就会有很多形状填充进每个单元。每个形状不得不和单元中的其他形状进行检查,所以这会造成许多不必要的碰撞检测。

注意深灰色的单元意味着很多物体映射到了他们。

Chipmunk6也有一个实验性的单轴排序和范围实现。在移动游戏中如果你的世界是很长且扁就像赛车游戏,它是非常高效。如果你想尝试启用它, 可以查阅cpSpaceUseSpatialHash()的代码。

札记

  • 当从空间中删除对象时,请确保你已经删除了任何引用它的其他对象。例如,当你删除一个刚体时,要先删除掉关联到刚体的关节和形状。
  • 迭代次数和时间步长的大小决定了模拟的质量。越多的迭代次数,或者更小的时间步会提高模拟的质量。请记住,更高质量的同时也意味着更高的CPU使用率。
  • 因为静态形状只有当你需要的时候才重新哈希,所以可能会使用一个更大的count参数来cpHashResizeStaticHash()而不是cpSpaceResizeActiveHash()。如果你有大量静态形状的话,这样做会使用更多的内存但是会提升性能。

Chipmunk约束:cpConstraint

约束是用来描述两个刚体如何相互作用的(他们是如何约束彼此的)。约束可以是允许刚体像我们身体的骨头一样轴转

Lua脚本在C++下的舞步(入门指引)转

作者:freeeyes,编辑:Nacy

转自:http://www.acejoy.com/forum.php?mod=viewthread&tid=1931

现在,越来越多的C++服务器和客户端融入了脚本的支持,尤其在网游领域,脚本语言已经渗透到了方方面面,比如你可以在你的客户端增加一个脚本,这个脚本将会帮你在界面上显示新的数据,亦或帮你完成某些任务,亦或帮你查看别的玩家或者NPC的状态。。。如此等等。

但是我觉得,其实脚本语言与C++的结合,远远比你在游戏中看到的特效要来的迅猛。它可以运用到方方面面的领域,比如你最常见的应用领域。比如,你可以用文本编辑器,写一个脚本语言,然后用你的程序加载一下,就会产生出很绚丽的界面。亦或一两句文本语言,就会让你的程序发送数据给服务器,是不是很酷呢?
本来我想,写一篇关于主流脚本语言Lua和Python的文章,但是感觉这样过于乏味,于是分开来一一介绍,相信对C++了解的你,看过我的文章后会对脚本语言这种东西产生浓厚的兴趣,我想起以前听的一个故事,当年Java的创造者讲课的时候,一开始先拿一个简单的不能简单的小例子,不断的扩展,最后成为一个复杂而完美的程序。今天我也就这样实验一下吧,呵呵。

当然,我本人不敢说对脚本语言了如指掌,只能说略微掌握一些,用过几年,偏颇之处请大家指正。
下面,开始吧,先说LUA!(本文面向初学者)

Lua语言(http://www.lua.org/),想必不少程序员都听过,据我所知,由于《魔兽世界》里面对它的加载,它一下子变成了很多游戏开发者竞相研究的对象,至于这个巴西创造者么,我不过多介绍,大家有兴趣可以谷歌一下。其实网上有很多关于lua的教材和例子,说真的,对于当年的我而言,几乎看不懂,当时很郁闷,感觉Lua复杂的要命,有些惧怕,后来沉下心来一点点研究,觉得其实还是蛮简洁的。只是网上的资料或许偏向于某些功能,导致了逻辑和代码的复杂。后来总结,其实学习一种脚本语言,完全可以抱着放松的心态一点点的研究,反而效果会更好。

在讲代码之前,我要说Lua的一些特点,这些特点有利于你在复杂的代码调用中,清晰的掌握中间的来龙去脉。实际上,你能常常用到的lua的API,不过超过10个,再复杂的逻辑。基本上也是这么多API组成的。至于它们是什么,下面的文章会介绍。另外一个重要之重要的概念,就是栈。Lua与别的语言交互以及交换数据,是通过栈完成的。其实简单的解释一下,你可以把栈想象成一个箱子,你要给他数据,就要按顺序一个个的把数据放进去,当然,Lua执行完毕,可能会有结果返回给你,那么Lua还会利用你的箱子,一个个的继续放下去。而你取出返回数据呢,要从箱子顶上取出,如果你想要获得你的输入参数呢?那也很简单,按照顶上返回数据的个数,再按顺序一个个的取出,就行了。不过这里提醒大家,关于栈的位置,永远是相对的,比如-1代表的是当前栈顶,-2代表的是当前栈顶下一个数据的位置。栈是数据交换的地方,一定要有一些栈的概念。

好了,基础的lua语法不在这里讲,百度一下有很多。
先去http://www.lua.org/ 去下载一个最新的Lua代码(现在稳定版是lua-5.1.4)。它的代码是用C写的,所以很容易兼容很多平台。
在linux下,目录src下就有专门的Makefile。很简单,啥都不用做,指定一下位置编译即可。
在windows下,以VS2005为例,建立一个空的静态库工程(最好不使用预编译头,把预编译头的选项勾去掉),然后把src下的所有文件(除了Makefile)一股脑拷到工程中去。然后将这些文件添加到你的工程中,编译,会生成一个*.llib(*是你起的lua库名),行了,建立一个目录lib,把它拷过去,然后再建立一个include的文件夹,把你工程目录下的lua.h,lualib.h,lauxlib.h,拷贝过去。行了,拿着这两个文件夹,你就可以在你的工程里使用lua了。
行了,材料齐了,我们来看看怎么写一个简单的lua程序吧。

建立一个文件,起名Sample.lua
里面添加这样的代码。

function func_Add(x, y)
   return x+y;
end

这是一个标准的lua语法,一个函数,实现简单的a+b操作,并返回操作结果。
保存退出。
多一句嘴,在Lua里面,是可以支持多数据返回的。
比如你这么写:

function func_Add(x, y)
   return x+y, x-y;
end

意思是返回第一个参数是相加的结果,第二个是相减的结果,也是可以的。在lua里面没有类型的概念。当然,在C++接受这样的返回值的时候,也很简单,请往下看。
好了,材料齐备了,咱们来看看C++程序怎么调用它。
首先,建立一个类,负责加载这个lua文件,并执行函数操作,我们姑且叫做CLuaFn
要加载这个lua文件,按照正常的思路,我们应该先加载,然后再调用不同的函数。恩,对了,咱们就这么做。

extern “C”
{
        #include “lua.h”
        #include “lualib.h”
        #include “lauxlib.h”
};

class CLuaFn
{
public:
        CLuaFn(void);
        ~CLuaFn(void);

        void Init();            //初始化Lua对象指针参数
        void Close();         //关闭Lua对象指针

        bool LoadLuaFile(const char* pFileName);                              //加载指定的Lua文件
        bool CallFileFn(const char* pFunctionName, int nParam1, int nParam2);        //执行指定Lua文件中的函数

private:
        lua_State* m_pState;   //这个是Lua的State对象指针,你可以一个lua文件对应一个。
};

恩,头文件就这么多,看看,一点也不复杂吧,看了cpp我想你会更高兴,因为代码一样很少。我一个个函数给你们介绍。

void CLuaFn::Init()
{
        if(NULL == m_pState)
        {
                m_pState = lua_open();
                luaL_openlibs(m_pState);
        }
}

初始化函数,标准代码,没啥好说的,lua_open()是返回给你一个lua对象指针,luaL_openlibs()是一个好东西,在lua4,初始化要做一大堆的代码,比如加载lua的string库,io库,math库等等等等,代码洋洋洒洒一大堆,其实都是不必要的,因为这些库你基本都需要用到,除了练习你的打字能力别的意义不大,因为代码写法都是固定的。于是在5以后,Lua的创造者修改了很多,这就是其一,一句话帮你加载了所有你可能用到的Lua基本库。

void CLuaFn::Close()
{
        if(NULL != m_pState)
        {
                lua_close(m_pState);
                m_pState = NULL;
        }
}

顾名思义,我用完了,关闭我的Lua对象并释放资源。呵呵,标准写法,没啥好说的。

bool CLuaFn:: LoadLuaFile(const char* pFileName)
{
        int nRet = 0;
        if(NULL == m_pState)
        {
                printf(“[CLuaFn:: LoadLuaFile]m_pState is NULL./n”);
                return false;
        }

        nRet = luaL_dofile(m_pState, pFileName);
        if (nRet != 0)
        {
                printf(“[CLuaFn:: LoadLuaFile]luaL_loadfile(%s) is file(%d)(%s)./n”, pFileName, nRet, lua_tostring(m_pState, -1));
                return false;
        }

        return true;
}

呵呵,这个有点意思,加载一个Lua文件。
这里我要详细的说一下,因为Lua是脚本语言,加载lua文件本身的时候才会编译。
所以,推荐大家在加载文件的时候尽量放在程序的初始化中,因为当你执行luaL_dofile()函数的时候,Lua会启用语法分析器,去分析你的脚本语法是否符合Lua规则,如果你胡乱的传一个文件过去,Lua就会告诉你文件语法错误,无法加载。如果你的Lua脚本很大,函数很多,语法分析器会比较耗时,所以,加载的时候,尽量放在合适的地方,而且,对于一个Lua文件而言,反复加载luaL_dofile()除了会使你的CPU变热没有任何意义。

或许你对printf(“[CLuaFn:: LoadLuaFile]luaL_loadfile(%s) is file(%d)(%s)./n”, pFileName, nRet, lua_tostring(m_pState, -1));这句话很感兴趣,这个在干什么?这里我先说lua_tostring(m_pState, -1)这是在干什么,还记得我说的Lua是基于栈传输数据的么?那么,如果报错,我怎么知道错误是什么?luaL_dofile标准返回一个int,我总不能到lua.h里面遍历这个nRet 是啥意思吧,恩,Lua创造者早就为你想好了,只不过你需要稍微动一下你的脑筋。Lua的创造者在语法分析器分析你的语法的时候,发现错误,会有一段文字告诉你是什么错误,它会把这个字符串放在栈顶。那么,怎么取得栈顶的字符串呢?lua_tostring(m_pState, -1)就可以,-1代表的是当前栈的位置是相对栈顶。当然,你也可以看看栈里面还有一些什么其他古怪的数据,你可以用1,2,3(这些是绝对位置,而-1是相对位置)去尝试,呵呵。不过,相信你得到的也很难看懂,因为一个Lua对象执行的时候,会用很多次栈进行数据交换,而你看到的,有可能是交换中的数据。那么,话说回来,这句话的意思就是”[CLuaFn:: LoadLuaFile]luaL_loadfile(文件名) is file(错误编号)(错误具体描述文字)./n”

bool CLuaFn::CallFileFn(const char* pFunctionName, int nParam1, int nParam2)
{
        int nRet = 0;
        if(NULL == m_pState)
        {
                printf(“[CLuaFn::CallFileFn]m_pState is NULL./n”);
                return false;
        }

        lua_getglobal(m_pState, pFunctionName);

        lua_pushnumber(m_pState, nParam1);
        lua_pushnumber(m_pState, nParam2);

        nRet = lua_pcall(m_pState, 2, 1, 0);
        if (nRet != 0)
        {
                printf(“[CLuaFn::CallFileFn]call function(%s) error(%d)./n”, pFunctionName, nRet);
                return false;
        }

        if (lua_isnumber(m_pState, -1) == 1)
        {
                int nSum = lua_tonumber(m_pState, -1);
                printf(“[CLuaFn::CallFileFn]Sum = %d./n”, nSum);
        }

        return true;
}

这个函数是,传入函数名称和参数,去你的Lua文件中去执行。
lua_getglobal(m_pState, pFunctionName);
这个函数是验证你的Lua函数是否在你当前加载的Lua文件中,并把指针指向这个函数位置。

lua_pushnumber(m_pState, nParam1); //—对应你的x参数
lua_pushnumber(m_pState, nParam2);//—对应你的y参数

这就是著名的压栈操作了,把你的参数压入Lua的数据栈。供Lua语法器去获得你的数据。
lua_pushnumber()是一个压入数字,lua_pushstring()是压入一个字符串。。。

那么你会问,如果我有一个自己的类型,一个类指针或者别的什么,我怎么压入?别着急,方法当然是有的,呵呵,不过你先看看如果简单的如何做,在下几讲中,我会告诉你更强大的Lua压栈艺术。
这里需要注意的是,压栈的顺序,对,简单说,就是从左到右的参数,左边的先进栈,右边的最后进栈。
nRet = lua_pcall(m_pState, 2, 1, 0);
这句话的意思是,执行这个函数,2是输入参数的个数,1是输出参数的个数。当然,如果你把Lua函数改成
return x+y, x-y;
代码需要改成nRet = lua_pcall(m_pState, 2, 2, 0);
明白了吧,呵呵,很简单吧。
当然,如果函数执行失败,会触发nRet,我这里偷了个懒,如果你想得到为什么错了?可以用lua_tostring(m_pState, -1)去栈顶找,明白?是不是有点感觉了?

lua_isnumber(m_pState, -1)
这句话是判定栈顶的元素是不是数字。因为如果执行成功,栈顶就应该是你的数据返回值。

int nSum = lua_tonumber(m_pState, -1);
printf(“[CLuaFn::CallFileFn]Sum = %d./n”, nSum);

这个nSum就是返回的结果。
当然,你会问,如果 return x+y, x-y;我该怎么办?

int nSum = lua_tonumber(m_pState, -1);
int nSub = lua_tonumber(m_pState, -2);

搞定,看见没。按照压栈顺序。呵呵,是不是又有感觉了,对,栈就是数据交互的核心。对Lua的理解程度和运用技巧,其实就是对栈的灵活运用和操作。
好了。你的第一个Lua程序大功告成!竟然不是Hello world,呵呵。
好了,我们看看Main函数怎么写吧,相信大家都会写。

#include “LuaFn.h”

int _tmain(int argc, _TCHAR* argv[])
{
        CLuaFn LuaFn;

        //LuaFn.InitClass();

        LuaFn.LoadLuaFile(“Sample.lua”);
        LuaFn.CallFileFn(“func_Add”, 11, 12);
        getchar();

        return 0;
}

行了,Build一下,看看,是不是你要的结果?如果是,贺喜你,你已经迈出了Lua的第一步。
洋洋洒洒写了一个小时,喝口水吧,呵呵,下一讲,我将强化这个LuaFn类,让它给我做更多的事情。呵呵,最后,我会让你打到,用Lua文件直接画出一个Windows窗体来。并在上面画出各种按钮,列表,以及复选框。是不是感觉很酷?用文本去创造一个程序?很激动吧,恩,确实,Lua能给你做到。只要你有耐心看下去。。。

上一节讲了一些基本的Lua应用,或许你会说,还是很简单么。呵呵,恩,是的,本来Lua就是为了让大家使用的方便快捷而设计的。如果设计的过为复杂,就不会有人使用了。
下面,我要强调一下,Lua的栈的一些概念,因为这个确实很重要,你会经常用到。熟练使用Lua,最重要的就是要时刻知道什么时候栈里面的数据是什么顺序,都是什么。如果你能熟练知道这些,实际你已经是Lua运用的高手了。
说真的,第一次我接触栈的时候,没有把它想的很复杂,倒是看了网上很多的关于Lua的文章让我对栈的理解云里雾里,什么元表,什么User,什么局部变量,什么全局变量位移。说的那叫一个晕。本人脑子笨,理解不了这么多,也不知道为什么很多人喜欢把Lua栈弄的七上八下,代码晦涩难懂。后来实在受不了了,去Lua网站下载了Lua的文档,写的很清晰。Lua的栈实际上几句话足以。
当你初始化一个栈的时候,它的栈底是1,而栈顶相对位置是-1,说形象一些,你可以把栈想象成一个环,有一个指针标记当前位置,如果-1,就是当前栈顶,如果是-2就是当前栈顶前面一个参数的位置。以此类推。当然,你也可以正序去取,这里要注意,对于Lua的很多API,下标是从1开始的。这个和C++有些不同。而且,在栈的下标中,正数表示绝对栈底的下标,负数表示相对栈顶的相对地址,这个一定要有清晰的概念,否则很容易看晕了。
让我们看一些例子,加深理解。

lua_pushnumber(m_pState, 11);
lua_pushnumber(m_pState, 12);

int nIn = lua_gettop(m_pState);
/*–这里加了一行, lua_gettop()这个API是告诉你目前栈里
元素的个数。如果仅仅是Push两个参数,那么nIn的数值是2,
对。没错。那么咱们看看栈里面是怎么放的。我再加两行代码。
*/
lua_pushnumber(m_pState, 11);
lua_pushnumber(m_pState, 12);

int nIn = lua_gettop(m_pState)
//–读取栈底第一个绝对坐标中的元素
int nData1 = lua_tonumber(m_pState, 1);
//–读取栈底第二个绝对坐标中的元素
int nData2 = lua_tonumber(m_pState, 2);
printf(“[Test]nData1 = %d, nData2 = %d./n”);

如果是你,凭直觉,告诉我答案是什么?
现在公布答案,看看是不是和你想的一样。

[Test]nData1 = 11, nData2 = 12

呵呵,那么,如果我把代码换成

lua_pushnumber(m_pState, 11);
lua_pushnumber(m_pState, 12);

int nIn = lua_gettop(m_pState)

//–读取栈顶第一个相对坐标中的元素
int nData1 = lua_tonumber(m_pState, -1); 
//–读取栈顶第二个相对坐标中的元素
int nData2 = lua_tonumber(m_pState, -2);
printf(“[Test]nData1 = %d, nData2 = %d./n”);

请你告诉我输出是什么?
答案是

[Test]nData1 = 12, nData2 = 11

呵呵,挺简单的吧,对了,其实就这么简单。网上其它的高阶运用,其实大部分都是对栈的位置进行调整。只要你抓住主要概念,看懂还是不难的。什么元表,什么变量,其实都一样,抓住核心,时刻知道栈里面的样子,就没有问题。
好了,回到我上一节的那个代码。

bool CLuaFn::CallFileFn(const char* pFunctionName, int nParam1, int nParam2)
{
        int nRet = 0;
        if(NULL == m_pState)
        {
                printf(“[CLuaFn::CallFileFn]m_pState is NULL./n”);
                return false;
        }

        lua_getglobal(m_pState, pFunctionName);

        lua_pushnumber(m_pState, nParam1);
        lua_pushnumber(m_pState, nParam2);

        int nIn = lua_gettop(m_pState); <–在这里加一行。

        nRet = lua_pcall(m_pState, 2, 1, 0);
        if (nRet != 0)
        {
                printf(“[CLuaFn::CallFileFn]call function(%s) error(%d)./n”, pFunctionName, nRet);
                return false;
        }

        if (lua_isnumber(m_pState, -1) == 1)
        {
                int nSum = lua_tonumber(m_pState, -1);
                printf(“[CLuaFn::CallFileFn]Sum = %d./n”, nSum);
        }

        int nOut = lua_gettop(m_pState); <–在这里加一行。

        return true;
}

nIn的答案是多少?或许你会说是2吧,呵呵,实际是3。或许你会问,为什么会多一个?其实我第一次看到这个数字,也很诧异。但是确实是3。因为你调用的函数名称占据了一个堆栈的位置。其实,在获取nIn那一刻,堆栈的样子是这样的(函数接口地址,参数1,参数2),函数名称也是一个变量入栈的。而nOut输出是1,lua_pcall()函数在调用成功之后,会自动的清空栈,然后把结果放入栈中。在获取nOut的一刻,栈内是这幅摸样(输出参数1)。
这里就要再迁出一个更重要的概念了,Lua不是C++,对于C++程序员而言,一个函数会自动创建栈,当函数执行完毕后会自动清理栈,Lua可不会给你这么做,对于Lua而言,它没有函数这个概念,一个栈对应一个lua_State指针,也就是说,你必须手动去清理你不用的栈,否则会造成垃圾数据占据你的内存。
不信?那么咱们来验证一下,就拿昨天的代码吧,你用for循环调用100万次。看看nOut的输出结果。。我相信,程序执行不到100万次就会崩溃,而你的内存也会变的硕大无比。而nOut的输出也会是这样的 1,2,3,4,5,6。。。。。
原因就是,Lua不会清除你以前栈内的数据,每调用一次都会给你生成一个新的栈元素插入其中。
那么怎么解决呢?呵呵,其实,如果不考虑多线程的话,在你的函数最后退出前加一句话,就可以轻松解决这个问题。(Lua栈操作是非线程安全的!)

lua_settop(m_pState, -2);
这句话的意思是什么?lua_settop()是设置栈顶的位置,我这么写,意思就是,栈顶指针目前在当前位置的-2的元素上。这样,我就实现了对栈的清除。仔细想一下,是不是这个道理呢?

bool CLuaFn::CallFileFn(const char* pFunctionName, int nParam1, int nParam2)
{
        int nRet = 0;
        if(NULL == m_pState)
        {
                printf(“[CLuaFn::CallFileFn]m_pState is NULL./n”);
                return false;
        }

        lua_getglobal(m_pState, pFunctionName);

        lua_pushnumber(m_pState, nParam1);
        lua_pushnumber(m_pState, nParam2);

        int nIn = lua_gettop(m_pState); <–在这里加一行。

        nRet = lua_pcall(m_pState, 2, 1, 0);
        if (nRet != 0)
        {
                printf(“[CLuaFn::CallFileFn]call function(%s) error(%d)./n”, pFunctionName, nRet);
                return false;
        }

        if (lua_isnumber(m_pState, -1) == 1)
        {
                int nSum = lua_tonumber(m_pState, -1);
                printf(“[CLuaFn::CallFileFn]Sum = %d./n”, nSum);
        }

        int nOut = lua_gettop(m_pState); <–在这里加一行。
        lua_settop(m_pState, -2);             <–清除不用的栈。

        return true;
}

好了,再让我们运行100万次,看看你的程序内存,看看你的程序还崩溃不?
如果你想打印 nOut的话,输出会变成1,1,1,1,1。。。。
最后说一句,lua_tonumber()或lua_tostring()还有以后我们要用到的lua_touserdata()一定要将数据完全取出后保存到你的别的变量中去,否则会因为清栈操作,导致你的程序异常,切记!

呵呵,说了这么多,主要是让大家如何写一个严谨的Lua程序,不要运行没两下就崩溃了。好了,基础栈的知识先说到这里,以后还有一些技巧的运用,到时候会给大家展示。
下面说一下,Lua的工具。(为什么要说这个呢?呵呵,因为我们下一步要用到其中的一个帮助我们的开发。)
呵呵,其实,Lua里面有很多简化开发的工具,你可以去http://www.sourceforge.net/去找一下。它们能够帮助你简化C++对象与Lua对象互转之间的代码。
这里说几个有名的,当然可能不全。

(lua tinker)如果你的系统在windows下,而且不考虑移植,那么我强烈推荐你去下载一个叫做lua tinker的小工具,整个工具非常简单,一个.h和一个.cpp。直接就可以引用到你的工程中,连独立编译都不用,这是一个韩国人写的Lua与 C++接口转换的类,十分方便,代码简洁(居家旅行,必备良药)。它是基于模板的,所以你可以很轻松的把你的C++对象绑定到Lua中。代码较长,呵呵,有兴趣的朋友可以给我留言索要lua tinker的例子。就不贴在这里了。不过我个人不推荐这个东西,因为它在Linux下是编译不过去的。它使用了一种g++不支持的模板写法,虽然有人在尝试把它修改到Linux下编译,但据我所知,修改后效果较好的似乎还没有。不过如果你只是在 windows下,那就没什么可犹豫的,强烈推荐,你会喜欢它的。

(Luabinder)相信用过Boost库的朋友,或许对这个家伙很熟悉。它是一个很强大的Linux下Lua扩展包,帮你封装了很多Lua的复杂操作,主要解决了绑定C++对象和Lua对象互动的关系,非常强大,不过嘛,对于freeeyes而言,还是不推荐,因为freeeyes很懒,不想为了一个Lua还要去编译一个庞大的boost库,当然,见仁见智,如果你的程序本身就已经加载了boost,那么就应该毫不犹豫的选择它。

(lua++)呵呵,这是我最喜欢,也是我一直用到现在的库,比较前两个而言,lua++的封装性没有那么好,很多东西还是需要一点代码的,不过之所以我喜欢,是因为它是用C写的,可以在windows下和linux下轻松转换。如果鱼与熊掌不能兼得,那么我宁愿选择一个兼顾两者的东西,如果有的话,呵呵。当然,lua++就是这么一个东西,如果你继续看我的文章,或许你也会喜欢它的。

好了,废话少说,就让我选择lua++作为我们继续进行下去的垫脚石吧。
说到Lua++(http://www.codenix.com/~tolua/),这个东西还是挺有渊源的,请你先下载一个。我教你怎么编译。

还记得我昨天说过如何编译Lua么,现在请你再做一遍,不同的是,请把lua++的程序包中的src/lib中的所有h和cpp,还有include下的那个.h拷贝到你上次建立的lua工程中。然后全部添加到你的静态链接库工程中去,重新编译。会生成一个新的lua.lib,这个lua就自动包含了lua++的功能。最后记得把tolua++.h放在你的Include文件夹下。
行了,我们把上次CLuaFn类稍微改一下。

extern “C”
{
        #include “lua.h”
        #include “lualib.h”
        #include “lauxlib.h”
        #include “tolua++”   //这里加一行
};

class CLuaFn
{
public:
        CLuaFn(void);
        ~CLuaFn(void);

        void Init();            //初始化Lua对象指针参数
        void Close();         //关闭Lua对象指针

        bool LoadLuaFile(const char* pFileName);                              //加载指定的Lua文件
        bool CallFileFn(const char* pFunctionName, int nParam1, int nParam2);        //执行指定Lua文件中的函数

private:
        lua_State* m_pState;   //这个是Lua的State对象指针,你可以一个lua文件对应一个。
};

行了,这样我们就能用Lua++下的功能了。
昨天,大家看到了 bool CallFileFn(const char* pFunctionName, int nParam1, int nParam2);这个函数的运用。演示了真么调用Lua函数。
下面,我改一下,这个函数。为什么?还是因为freeeyes很懒,我可不想每有一个函数,我都要写一个C++函数去调用,太累!我要写一个通用的!支持任意函数调用的接口!
于是我创建了两个类。支持任意参数的输入和输出,并打包送给lua去执行,说干就干。

#ifndef _PARAMDATA_H
#define _PARAMDATA_H

#include 

#define MAX_PARAM_200 200

using namespace std;

struct _ParamData
{
public:
        void* m_pParam;
        char  m_szType[MAX_PARAM_200];
        int   m_TypeLen;

public:
        _ParamData()
        {
                m_pParam    = NULL;
                m_szType[0] = ‘/0′;
                m_TypeLen   = 0;
        };

        _ParamData(void* pParam, const char* szType, int nTypeLen)
        {
                SetParam(pParam, szType, nTypeLen);
        }

        ~_ParamData() {};

        void SetParam(void* pParam, const char* szType, int nTypeLen)
        {
                m_pParam = pParam;
                sprintf(m_szType, “%s”, szType);
                m_TypeLen = nTypeLen;
        };

        bool SetData(void* pParam, int nLen)
        {
                if(m_TypeLen < nLen)
                {
                        return false;
                }

                if(nLen > 0)
                {
                        memcpy(m_pParam, pParam, nLen);
                }
                else
                {
                        memcpy(m_pParam, pParam, m_TypeLen);
                }
                return true;
        }

        void* GetParam()
        {
                return m_pParam;
        }

        const char* GetType()
        {
                return m_szType;
        }

        bool CompareType(const char* pType)
        {
                if(0 == strcmp(m_szType, pType))
                {
                        return true;
                }
                else
                {
                        return false;
                }
        }
};

class CParamGroup
{
public:
        CParamGroup() {};
        ~CParamGroup()
        {
                Close();
        };

        void Init()
        {
                m_vecParamData.clear();
        };

        void Close()
        {
                for(int i = 0; i < (int)m_vecParamData.size(); i++)
                {
                        _ParamData* pParamData = m_vecParamData;
                        delete pParamData;
                        pParamData = NULL;
                }
                m_vecParamData.clear();
        };

        void Push(_ParamData* pParam)
        {
                if(pParam != NULL)
                {
                        m_vecParamData.push_back(pParam);
                }
        };

        _ParamData* GetParam(int nIndex)
        {
                if(nIndex < (int)m_vecParamData.size())
                {
                        return m_vecParamData[nIndex];
                }
                else
                {
                        return NULL;
                }
        };

        int GetCount()
        {
                return (int)m_vecParamData.size();
        }

private:
        typedef vector<_ParamData*> vecParamData;
        vecParamData m_vecParamData;
};

#endif

#endif

我创建了两个类,把Lua要用到的类型,数据都封装起来了。这样,我只需要这么改写这个函数。
bool CallFileFn(const char* pFunctionName, CParamGroup& ParamIn, CParamGroup& ParamOut);
它就能按照不同的参数自动给我调用,嘿嘿,懒到家吧!
其实这两个类很简单,_ParamData是参数类,把你要用到的参数放入到这个对象中去,标明类型的大小,类型名称,内存块。而CParamGroup负责将很多很多的_ParamData打包在一起,放在vector里面。

好了,让我们看看CallFileFn函数里面我怎么改的。

bool CLuaFn::CallFileFn(const char* pFunctionName, CParamGroup& ParamIn, CParamGroup& ParamOut)
{
        int nRet = 0;
        int i    = 0;
        if(NULL == m_pState)
        {
                printf(“[CLuaFn::CallFileFn]m_pState is NULL./n”);
                return false;
        }

        lua_getglobal(m_pState, pFunctionName);

        //加载输入参数
        for(i = 0; i < ParamIn.GetCount(); i++)
        {
                PushLuaData(m_pState, ParamIn.GetParam(i));
        }

        nRet = lua_pcall(m_pState, ParamIn.GetCount(), ParamOut.GetCount(), 0);
        if (nRet != 0)
        {
                printf(“[CLuaFn::CallFileFn]call function(%s) error(%s)./n”, pFunctionName, lua_tostring(m_pState, -1));
                return false;
        }

        //获得输出参数
        int nPos = 0;
        for(i = ParamOut.GetCount() – 1; i >= 0; i–)
        {
                nPos–;
                PopLuaData(m_pState, ParamOut.GetParam(i), nPos);
        }

        int nCount = lua_gettop(m_pState);
        lua_settop(m_pState, -1-ParamOut.GetCount());

        return true;
}

呵呵,别的没变,加了两个循环,因为考虑lua是可以支持多结果返回的,所以我也做了一个循环接受参数。
lua_settop(m_pState, -1-ParamOut.GetCount());这句话是不是有些意思,恩,是的,我这里做了一个小技巧,因为我不知道返回参数有几个,所以我会根据返回参数的个数重新设置栈顶。这样做可以返回任意数量的栈而且清除干净。
或许细心的你已经发现,里面多了两个函数。恩,是的。来看看这两个函数在干什么。

bool CLuaFn::PushLuaData(lua_State* pState, _ParamData* pParam)
{
        if(pParam == NULL)
        {
                return false;
        }

        if(pParam->CompareType(“string”))
        {
                lua_pushstring(m_pState, (char* )pParam->GetParam());
                return true;
        }

        if(pParam->CompareType(“int”))
        {
                int* nData = (int* )pParam->GetParam();
                lua_pushnumber(m_pState, *nData);
                return true;
        }
        else
        {
                void* pVoid = pParam->GetParam();
                tolua_pushusertype(m_pState, pVoid, pParam->GetType());
                return true;
        }
}

参数入栈操作,呵呵,或许你会问tolua_pushusertype(m_pState, pVoid, pParam->GetType());这句话,你可能有些看不懂,没关系,我会在下一讲详细的解释Lua++的一些API的用法。现在大概和你说一下,这句话的意思就是,把一个C++对象传输给Lua函数。
再看看,下面一个。

bool CLuaFn:: PopLuaData(lua_State* pState, _ParamData* pParam, int nIndex)
{
        if(pParam == NULL)
        {
                return false;
        }

        if(pParam->CompareType(“string”))
        {
                if (lua_isstring(m_pState, nIndex) == 1)
                {
                        const char* pData = (const char*)lua_tostring(m_pState, nIndex);
                        pParam->SetData((void* )pData, (int)strlen(pData));
                }
                return true;
        }

        if(pParam->CompareType(“int”))
        {
                if (lua_isnumber(m_pState, nIndex) == 1)
                {
                        int nData = (int)lua_tonumber(m_pState, nIndex);
                        pParam->SetData(&nData, sizeof(int));
                }
                return true;
        }
        else
        {
                pParam->SetData(tolua_tousertype(m_pState, nIndex, NULL), -1);
                return true;
        }
}

弹出一个参数并赋值。pParam->SetData(tolua_tousertype(m_pState, nIndex, NULL), -1);这句话同样,我在下一讲中详细介绍。
呵呵,好了,我们又进了一步,我们可以用这个函数绑定任意一个Lua函数格式。而代码不用多写,懒蛋的目的达到了。
呵呵,这一讲主要是介绍了一些基本知识,或许有点多余,但是我觉得是必要的,在下一讲中,我讲开始详细介绍如何绑定一个C++对象给Lua,并让Lua对其修改。然后返回结果。休息一下,休息一下先。

我把Lua基本的栈规则讲了一下,然后完善了一下我的CLuaFn类。让它可以支持任意参数数量和函数名称的传值。当然,这些功能是为了今天这篇文章而铺路的。
看了七猫的回帖,呵呵,确实应该说一下SWIG这个工具,说真的,我对这个工具理解不深,因为没有怎么用过,读过一些关于它的文章,似乎是帮你把C++的功能封装成一个Lua基本库的东西,但是后来研究,他可以很轻松帮你把公用函数封装成一个Lua的基本库(类似C++的dll),但是对于我的需求而言,可能不太一样。因为我大量的是需要在C++里面进行数据传输以及变量的交互,所以为了紧贴C++,我需要很多关联数据的处理。
我是一名C++程序员,所以在很多时候,不想过多的使用Lua的特性,因为个人感觉,Lua的语法要比C++的更加灵活。而我更希望,在函数调用的某些习惯上,遵循一些C++的规则。
好了,废话少说,我们先来看一个类(头文件)。假设我们要把这个对象,传输给Lua进行调用。

#ifndef _TEST_H
#define _TEST_H

class CTest
{
public:
        CTest(void);
        ~CTest(void);

        char* GetData();
        void SetData(const char* pData);

private:
        char m_szData[200];
};
#endif

这个类里面有两个函数,一个是GetData(),一个是SetData(),之所以这么写,我要让Lua不仅能使用我的类,还可以给这个类使用参数。
那么,cpp文件,我们姑且这样写。(当然,你可以进行修改,按照你喜欢的方式写一个方法,呵呵)

char* CTest::GetData()
{
        printf(“[CTest::GetData]%s./n”, m_szData);
        return m_szData;
}

void CTest::SetData(const char* pData)
{
        sprintf(m_szData, “%s”, pData);
}

这是一个标准的类,我需要这个类在Lua里面可以创造出来,并赋予数值,甚至我可以把CTest作为一个Lua函数参数,传给Lua函数让它去给我处理。让我们来看看怎么做。如果使用标准的Lua语法,有点多,所以我就借用一下上次提到的tolua来做到这一切,我一句句的解释。姑且我们把这些代码放在LuaFn.cpp里面。

static int tolua_new_CTest(lua_State* pState)
{
        CTest* pTest = new CTest();
        tolua_pushusertype(pState, pTest, “CTest”);
        return 1;
}

static int tolua_delete_CTest(lua_State* pState)
{
        CTest* pTest = (CTest* )tolua_tousertype(pState, 1, 0);
        if(NULL != pTest)
        {
                delete pTest;
        }
        return 1;
}

static int tolua_SetData_CTest(lua_State* pState)
{
        CTest* pTest = (CTest* )tolua_tousertype(pState, 1, 0);
        const char* pData = tolua_tostring(pState, 2, 0);

        if(pData != NULL && pTest != NULL)
        {
                pTest->SetData(pData);
        }

        return 1;
}

static int tolua_GetData_CTest(lua_State* pState)
{
        CTest* pTest = (CTest* )tolua_tousertype(pState, 1, 0);

        if(pTest != NULL)
        {
                char* pData = pTest->GetData();
                tolua_pushstring(pState, pData);
        }

        return 1;
}

看看这几个静态函数在干什么。
我要在Lua里面使用CTest,必须让Lua里这个CTest对象能够顺利的创造和销毁。tolua_new_CTest()和tolua_delete_CTest()就是干这个的。
tolua_pushusertype(pState, pTest, “CTest”); 这句话的意思是,将一个已经在Lua注册的”CTest”对象指针,压入数据栈。
同理,CTest* pTest = (CTest* )tolua_tousertype(pState, 1, 0);是将数据栈下的对象以(CTest* )的指针形式弹出来。
tolua_SetData_CTest()函数和tolua_GetData_CTest分别对应CTest的SetData方法和GetData()方法。因为我们的SetData方法里面存在变量,那么同样,我们需要使用const char* pData = tolua_tostring(pState, 2, 0);将参数弹出来,然后输入到pTest->SetData(pData);对象中去,当然,你可以有更多若干个参数。随你的喜好。这里只做一个举例。
好了,你一定会问,这么多的静态函数,用在哪里?呵呵,当然是给Lua注册,当你把这些数据注册到Lua里面,你就可以轻松的在Lua中使用它们。
让我们看看,注册是怎么做到的。
还是在CLuaFn类里面,我们增加一个函数。比如叫做bool InitClass();

bool CLuaFn::InitClass()
{
        if(NULL == m_pState)
        {
                printf(“[CLuaFn::InitClass]m_pState is NULL./n”);
                return false;
        }

        tolua_open(m_pState);
        tolua_module(m_pState, NULL, 0);
        tolua_beginmodule(m_pState, NULL);

        tolua_usertype(m_pState, “CTest”);
        tolua_cclass(m_pState, “CTest”, “CTest”, “”, tolua_delete_CTest);

        tolua_beginmodule(m_pState, “CTest”);
        tolua_function(m_pState, “new”, tolua_new_CTest);
        tolua_function(m_pState, “SetData”, tolua_SetData_CTest);
        tolua_function(m_pState, “GetData”, tolua_GetData_CTest);
        tolua_endmodule(m_pState);

        tolua_endmodule(m_pState);

        return true;
}

上面的代码,就是我把上面的几个静态函数,绑定到Lua的基础对象中去。
tolua_beginmodule(m_pState, “CTest”);是只注册一个模块,比如,我们管CTest叫做”CTest”,保持和C++的名称一样。这样在Lua的对象库中就会多了一个CTest的对象描述,等同于string,number等等基本类型,同理,你也可以用同样的方法,注册你的MFC类。是不是有点明白了?这里要注意,tolua_beginmodule()和tolua_endmodule()对象必须成对出现,如果出现不成对的,你注册的C++类型将会失败。
tolua_function(m_pState, “SetData”, tolua_SetData_CTest);指的是将Lua里面CTest对象的”SetData”绑定到你的tolua_SetData_CTest()函数中去。

好的,让我们来点激动人心的东西。还记得我们的Simple.lua的文件么。我们来改一下它。

function func_Add(x, y)
  local test = CTest:new();
  test:SetData(“I’m freeeyes!”);
  test:GetData();
  return x..y;
end

我在这个函数里面,New了一个CTest对象,并进行赋值操作,最后把结果打印在屏幕上。你或许会问,最后一句不是x+y么,怎么变成了x..y,呵呵,在Lua中,..表示联合的意思,就好比在C++里面, string strName += “freeeyes”。原来觉得x+y有点土,索性返回一个两个字符串的联合吧。
好了,我们已经把我们的这个CTest类注册到了Lua里面,让我们来调用一下吧。修改一下Main函数。变成以下的样子。

int _tmain(int argc, _TCHAR* argv[])
{
        CLuaFn LuaFn;

        LuaFn.InitClass();

        LuaFn.LoadLuaFile(“Sample.lua”);

        CParamGroup ParamIn;
        CParamGroup ParamOut;

        char szData1[20] = {‘/0′};
        sprintf(szData1, “[freeeyes]“);
        _ParamData* pParam1 = new _ParamData(szData1, “string”, (int)strlen(szData1));
        ParamIn.Push(pParam1);

        char szData2[20] = {‘/0′};
        sprintf(szData2, “[shiqiang]“);
        _ParamData* pParam2 = new _ParamData(szData2, “string”, (int)strlen(szData2));
        ParamIn.Push(pParam2);
        char szData3[40] = {‘/0′};
        _ParamData* pParam3 = new _ParamData(szData3, “string”, 40);
        ParamOut.Push(pParam3);

        LuaFn.CallFileFn(“func_Add”, ParamIn, ParamOut);

        char* pData = (char* )ParamOut.GetParam(0)->GetParam();
        printf(“[Main]Sum = %s./n”, pData);

        getchar();

        return 0;
}

如果你完全按照我的,你就可以编译你的工程了,运行一下,看看是啥结果?

[CTest::GetData]I’m freeeyes!.
[Main]Sum = [freeeyes][shiqiang]. 

看看,是不是和我输出的一样?

呵呵,有意思吧,你已经可以在Lua里面用C++的函数了,那么咱们再增加一点难度,比如,我有一个CTest对象,要作为一个参数,传输给func_Add()执行,怎么办?
很简单,如果你对上面的代码仔细阅读,你会发现下面的代码一样简洁。为了支持刚才要说的需求,我们需要把Sample.lua再做一点修改。

function func_Add(x, y, f)
  f:SetData(“I’m freeeyes!”);
  f:GetData();
  return x..y;
end

f假设就是我们要传入的CTest对象。我们要在Lua里面使用它。(我们的CLuaFn都不用改,把main函数稍微改一下即可,来看看怎么写。)

// LuaSample.cpp : 定义控制台应用程序的入口点。
//

#include “stdafx.h”
#include “LuaFn.h”

int _tmain(int argc, _TCHAR* argv[])
{
        CLuaFn LuaFn;

        LuaFn.InitClass();

        LuaFn.LoadLuaFile(“Sample.lua”);

        CParamGroup ParamIn;
        CParamGroup ParamOut;

        char szData1[20] = {‘/0′};
        sprintf(szData1, “[freeeyes]“);
        _ParamData* pParam1 = new _ParamData(szData1, “string”, (int)strlen(szData1));
        ParamIn.Push(pParam1);

        char szData2[20] = {‘/0′};
        sprintf(szData2, “[shiqiang]“);
        _ParamData* pParam2 = new _ParamData(szData2, “string”, (int)strlen(szData2));
        ParamIn.Push(pParam2);

        //只追加了这里
        CTest* pTest = new CTest();
        _ParamData* pParam3 = new _ParamData(pTest, “CTest”, sizeof(CTest));
        ParamIn.Push(pParam3);
       //追加结束
        char szData4[40] = {‘/0′};
        _ParamData* pParam4 = new _ParamData(szData4, “string”, 40);
        ParamOut.Push(pParam4);

        LuaFn.CallFileFn(“func_Add”, ParamIn, ParamOut);

        char* pData = (char* )ParamOut.GetParam(0)->GetParam();
        printf(“[Main]Sum = %s./n”, pData);

        getchar();

        return 0;
}

好了,就这么点代码,改好了,我们再Build一下,然后点击运行。看看输出结果,是不是和以前的一样?
恩,是不是有点兴奋了?你成功的让Lua开始调用你的C++对象了!并且按照你要的方式执行!还记得我曾在第一篇文章里面许诺过,我会让你画出一个MFC窗体么?呵呵,如果你到现在依然觉得很清晰的话,说明你的距离已经不远了。

既然已经到了这里,我们索性再加点难度,如果我要把CTest作为一个对象返回回来怎么做?很简单,且看。

int _tmain(int argc, _TCHAR* argv[])
{
        CLuaFn LuaFn;

        LuaFn.InitClass();

        LuaFn.LoadLuaFile(“Sample.lua”);

        CParamGroup ParamIn;
        CParamGroup ParamOut;

        char szData1[20] = {‘/0′};
        sprintf(szData1, “[freeeyes]“);
        _ParamData* pParam1 = new _ParamData(szData1, “string”, (int)strlen(szData1));
        ParamIn.Push(pParam1);

        char szData2[20] = {‘/0′};
        sprintf(szData2, “[shiqiang]“);
        _ParamData* pParam2 = new _ParamData(szData2, “string”, (int)strlen(szData2));
        ParamIn.Push(pParam2);

        CTest* pTest = new CTest();
        _ParamData* pParam3 = new _ParamData(pTest, “CTest”, sizeof(CTest));
        ParamIn.Push(pParam3);
        CTest* pTestRsult = NULL;
        _ParamData* pParam4 = new _ParamData(pTestRsult, “CTest”, sizeof(pTestRsult));
        ParamOut.Push(pParam4);

        LuaFn.CallFileFn(“func_Add”, ParamIn, ParamOut);

        //接受Lua返回参数为CTest类型,并调用其中的方法。
        pTestRsult = (CTest* )ParamOut.GetParam(0)->GetParam();
        pTestRsult->GetData();

        getchar();

        return 0;
}

好,编译,执行。呵呵,看到了吧。

看到这里,如果你能看的明白,说明你已经对Lua如何调用C++接口,以及C++如何调用Lua有了一定的理解。当然,我写的这个类也不是很完善,不过做一半的Lua开发,应该是够用了。以以上的方式,你可以使用Lua驾驭你的C++代码。
好了,咱们既然已经说到这里了,再深一步,如果我的类是继承的,怎么办?呵呵,很好的问题。
比如,我的CTest继承了一个CBase,我的CBase又继承了一个。。。
在Lua里面,一样简单,我拿MFC的例子来举例吧,想必大家更喜欢看。
比如 CCmdTarget继承自CObject。
那么我在注册的时候可以这么写。

tolua_cclass(tolua_S, “CCmdTarget”, ”CCmdTarget”, ”CObject”, NULL);
这个表示CCmdTarget继承自CObject对象。
当然,MFC里面还会有很多类型,比如常数,Lua一样能处理。
举个例子说。

tolua_constant(tolua_S, “ES_AUTOHSCROLL”, ES_AUTOHSCROLL);
这样注册,你就可以在 Lua里面使用ES_AUTOHSCROLL这个常数,它会自动绑定ES_AUTOHSCROLL这个C++常数对象。

呵呵,说了这么多,让我们来点实际的。我给大家一个我以前写的MFC封装类(由于代码太多,我变成附件给大家),你们可以调用,当然,如果你有兴趣,就用我的MFC类,来做一个你喜欢的窗体吧,当然,你必须要用Lua脚本把它画出来,作为最后的考验,呵呵。

附带全部工程(附带Lua及tolua++)

/Files/hmxp8/HelloLua_01_03.rar

Memory Management of JSB

Memory Management of JSB

From http://www.cocos2d-x.org/wiki/Memory_Management_of_JSB write by u0u0

Base on Cocos2d-x 2.1.5, but also applicable to Cocos2d-x 3.0.

The lifecycle of JSB object

You know Javascript has it's own memory management, Garbage collection. And Cocos2d-x simulate a garbage collection system to manager Cocos objects. There is a problem that which one is in charge of memory management when binding Cocos2d-x objects to javascript objects.

Let's look at a typical case.

Object allocation by xxx.create()

The following code allocation a global variable.

gnode = cc.Node.create();

And gnode is not addChild() to other cc.Node.

In a menuItem callback, add following code:

// menuItem callback
onButton:function (sender) {
    sender.addChild(gnode);
}

When click the button, you will see following error message.

Cocos2d: jsb: ERROR: File /Users/u0u0/Documents/project/SK_parkour/scripting/javascript/bindings/generated/jsb_cocos2dx_auto.cpp: Line: 3010, Function: js_cocos2dx_CCNode_addChild
Cocos2d: Invalid Native Object

What happened! What does the "Invalid Native Object" mean?

gnode is a global variable in javascript, mean not be GC.

But the CCNode in side of gnode is GC by Cocos2d-x.

In order to clarify this issue, you need to know spidermonkey and deep into javascript binding codes.

The internal implementation of cc.Node.create()

Detail implementation codes is list as following:

static JSFunctionSpec st_funcs[] = {
    JS_FN("create", js_cocos2dx_CCNode_create, 0, JSPROP_PERMANENT | JSPROP_ENUMERATE),
    JS_FS_END
};

jsb_CCNode_prototype = JS_InitClass(
    cx, global,
    NULL, // parent proto
    jsb_CCNode_class,
    js_cocos2dx_CCNode_constructor, 0, // constructor
    properties,
    funcs,
    NULL, // no static properties
    st_funcs);

cc.Node.create() is maped to C function js_cocos2dx_CCNode_create()

JSBool js_cocos2dx_CCNode_create(JSContext *cx, uint32_t argc, jsval *vp)
{
    if (argc == 0) {
        cocos2d::CCNode* ret = cocos2d::CCNode::create();
        jsval jsret;
        do {
        if (ret) {
            js_proxy_t *proxy = js_get_or_create_proxy(cx, ret);
            jsret = OBJECT_TO_JSVAL(proxy->obj);
        } else {
            jsret = JSVAL_NULL;
        }
    } while (0);
        JS_SET_RVAL(cx, vp, jsret);
        return JS_TRUE;
    }
    JS_ReportError(cx, "wrong number of arguments");
    return JS_FALSE;
}

Object successful allocated by cocos2d::CCNode::create() will be packaged into a new object js_proxy_t which create by js_get_or_create_proxy().

Deeper in js_get_or_create_proxy() only need to focus on following code:

JS_AddObjectRoot(cx, &proxy->obj);

This a spidermonkey api use to add a JSObject to the garbage collector's root set. proxy->obj is a JSObject map to javascript side.

So objects allocated by cc.Node.create() will remain in memory until the balancing call to JS_RemoveObjectRoot().

But cocos2d::CCNode::create() is an autorelease object will be GC in next game frame by Cocos2d-x.

The destructor of CCObject will be call, focus following codes:

// if the object is referenced by Lua engine, remove it
if (m_nLuaID)
{
    CCScriptEngineManager::sharedManager()->getScriptEngine()->removeScriptObjectByCCObject(this);
}
else
{
    CCScriptEngineProtocol* pEngine = CCScriptEngineManager::sharedManager()->getScriptEngine();
    if (pEngine != NULL && pEngine->getScriptType() == kScriptTypeJavascript)
    {
        pEngine->removeScriptObjectByCCObject(this);
    }
}

pEngine->removeScriptObjectByCCObject does the magic thing.

void ScriptingCore::removeScriptObjectByCCObject(CCObject* pObj)
{
    js_proxy_t* nproxy;
    js_proxy_t* jsproxy;
    void *ptr = (void*)pObj;
    nproxy = jsb_get_native_proxy(ptr);
    if (nproxy) {
        JSContext *cx = ScriptingCore::getInstance()->getGlobalContext();
        jsproxy = jsb_get_js_proxy(nproxy->obj);
        JS_RemoveObjectRoot(cx, &jsproxy->obj);
        jsb_remove_proxy(nproxy, jsproxy);
    }
}

Function JS_RemoveObjectRoot remove the JSObject from javascript root set. jsb_remove_proxy will remove proxy from hash table.

Now we can answer the question of the beginning of the article.

Cocos2d-x's garbage collection system is in charge of memory management.

Back to gnode, it's a global variable. The effect of JS_RemoveObjectRoot in the destructor of CCObject just balance the JS_AddObjectRoot in create(). Spidermonkey will not GC this global variable, but the native object of gnode had be released. Access the native object of gnode will trigger an error as you saw earlier.

Object allocation by new

Consider the following codes:

gnode = new cc.Node;

To found the true, you also need deeper into JSB code.

As showing earlier, the constructor of cc.Node is the function js_cocos2dx_CCNode_constructor().

Focus following codes:

if (argc == 0) {
    cocos2d::CCNode* cobj = new cocos2d::CCNode();
    cocos2d::CCObject *_ccobj = dynamic_cast(cobj);
    if (_ccobj) {
        _ccobj->autorelease();
    }

The native object be puted into Cocos2d-x autorelease pool. So new has no different to create().

About retain() and release()

These two functions are used for manual control object's lifecycle. If you want to avoid the error in the previous example.

You got two choices:

  1. add gnode to another CCNode, addChild() does retain for gnode internally.
  2. call gnode.retain() right after create().

In the second case, you need call gnode.release() at a right moment to avoid memory leak. The next section will show it.

ctor() and onExit()

Cocos2d-x JSB use Simple JavaScript Inheritance By John Resig. But the name of constructor is different.

ctor() is the constructor in JSB. Instead, onExit() will actor as the destructor be called before CCNode release.

Following example will show how to manual control the lifecycle of JSB object.

var container = cc.Node.extend({
    ctor:function () {
        this._super();
        this.gnode = cc.Node.create();
        this.gnode.retain();
    },
    onExit:function() {
        this.gnode.release();
        this._super();
    },
});

Parkour Game Starter Kit

sk_parkour_cover

Parkour Game Starter Kit

Introduction

Parkour is a popular game genre. This starter kit equips you with the tools and skills you need to create a well polished and fun Parkour game for the iOS and Android using Cocos2D-x.

Picture speaks louder than words, so here’s a sneak peek of what you will be capable of doing by the end of this book:

overview

In this starter kit, we use gesture to control the runner. Swipe up to jump. Swipe down to crouch. Draw a circle to open the runner's incredible mode, which the runner move faster and can break rocks.

The starter kit use Cocos2d-x javascript binding to implement.

The steps described in this article based on Mac OS X, Xcode development environment.

About the authors

Author_KeNan_Liu

KeNan Liu is a developer and co-founder of ityran.com. He has 7 years experience in mobile software development at multi-platform. Such as Windows Mobile, Brew, iOS and Windows Phone 8. He's current focus is on Cocos2d-x game development. You can follow KeNan on Weibo.

Author_Iven_yang

Iven Yang is a developer currently focusing on Cocos2D-x development, and the co-founder of team Tyran.

About the editor

Author_YiMing_Guo

Yiming Guo is a undergraduate who are interested in mobile networking, cloud computing and data mining. Now is interning at Chengdu, focus on Cocos2d-x game development.

About the artist

Author_Fan_Wang

Fan Wang is a artist with 4 years of experience.

Chapter 1: Getting Started

Note: If you are already familiar with creating multi-platform project of Cocos2d-x, then you might want to skip ahead to the next section.

Get Cocos2d-x

Open your web browser and navigate to the Cocos2D-x download page.

There are several choices of which version of Cocos2D to download. For this Starter Kit, you will be using the latest stable version of Cocos2d-x.

At the time of writing this was cocos2d-x-2.1.5. Download it and extract it to a location of your choice.

Creating a multi-platform project of Cocos2d-x

Open Terminal app. Use the cd command to switch to the directory where you extracted the Cocos2D-x archive, like this:

cd ~/Documents/project/cocos2d-x-2.1.4/tools/project-creator

Creating project use create_project.py

./create_project.py -project Parkour -package org.cocos2d-x.Parkour -language javascript

If you see the following information, then the project successfully created.

proj.ios        : Done!
proj.android        : Done!
proj.win32      : Done!
New project has been created in this path: /Users/u0u0/Documents/project/cocos2d-x-2.1.4/projects/Parkour
Have Fun! 

As described above, create_project.py automatically creates iOS, Android and win32 project. In this document, we use iOS project as an example.

Switch to the project directory and open the project.

cd ~/Documents/project/cocos2d-x-2.1.4/projects/Parkour/proj.ios
open Parkour.xcodeproj 

Now you are done with the project set-up, it’s time to code!

Chapter 2: Setting up Multi-Resolution support

Cocos2d-x provides a set of api to make the game run on devices with different resolutions. The technical details of Multi-Resolution refer to this document.

Detailed explanation of Cocos2d-x Multi-resolution adaptation

This document just show how to use it.

Copy AppMacros.h from HelloCpp to Parkour project.

cp ~/Documents/project/cocos2d-x-2.1.4/samples/Cpp/HelloCpp/Classes/AppMacros.h ~/Documents/project/cocos2d-x-2.1.4/projects/Parkour/Classes

Drag AppMacros.h onto the Classes folder in Xcode project. In the pop-up dialog, make sure that Add to targets is checked, then click Finish.

Open AppDelegate.cpp, include AppMacros.h at the top of the file.

#include "AppMacros.h"

Replace applicationDidFinishLaunching with the following:

// initialize director
CCDirector *pDirector = CCDirector::sharedDirector();
CCEGLView* pEGLView = CCEGLView::sharedOpenGLView();
pDirector->setOpenGLView(pEGLView);

// Set the design resolution
pEGLView->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, kResolutionFixedHeight);

CCSize frameSize = pEGLView->getFrameSize();

vector searchPath;

float mediumGap = (mediumResource.size.height - smallResource.size.height) / 2;

if (frameSize.height > (smallResource.size.height + mediumGap)) {
    searchPath.push_back(mediumResource.directory);
    pDirector->setContentScaleFactor(mediumResource.size.height/designResolutionSize.height);
} else {
    searchPath.push_back(smallResource.directory);
    pDirector->setContentScaleFactor(smallResource.size.height/designResolutionSize.height);
}

// set searching path
CCFileUtils::sharedFileUtils()->setSearchPaths(searchPath);

// turn on display FPS
pDirector->setDisplayStats(true);

// set FPS. the default value is 1.0/60 if you don't call this
pDirector->setAnimationInterval(1.0 / 60);

ScriptingCore* sc = ScriptingCore::getInstance();
sc->addRegisterCallback(register_all_cocos2dx);
sc->addRegisterCallback(register_all_cocos2dx_extension);
sc->addRegisterCallback(register_cocos2dx_js_extensions);
sc->addRegisterCallback(register_all_cocos2dx_extension_manual);
sc->addRegisterCallback(register_CCBuilderReader);
sc->addRegisterCallback(jsb_register_chipmunk);
sc->addRegisterCallback(jsb_register_system);
sc->addRegisterCallback(JSB_register_opengl);
sc->addRegisterCallback(MinXmlHttpRequest::_js_register);

sc->start();

CCScriptEngineProtocol *pEngine = ScriptingCore::getInstance();
CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);
ScriptingCore::getInstance()->runScript("MainScene.js");

Here we use two different resources to adapt different resolutions. Small resources in "iphone" directory, medium resources in "ipad" directory.

Note: we changed the javascript entrance to file MainScene.js, rename main.js to MainScene.js. Directory "res" and "src" under Resources group will not use in Parkour, delete it from Xcode and click "Move to Trash".

Create these two directories, and drag them onto the Resources folder in your Xcode project. In the pop-up dialog, make sure that Create folder references for any added folders is selected, and Add to targets is checked, then click Finish.

addResourceFolder

Your Parkour Resource group should now look like this:

resourceFolder

Parkour design resolution is 480X320, all images reference in this document will place in folder "iphone", and testing in iPhone simulator. After finish all game logic, we add the other resource and test it in different resolutions.

Chapter 3: Adding a Main Menu to Main Scene

Now we got a clean Cocos2d-x JSB project, entrance with "MainScene.js". But need add something in MainScene.js to make it run.

Open "MainScene.js" and replace its contents with the following:

// 1.
require("jsb.js");

// 2.
var MainLayer = cc.Layer.extend({
    // 3.
    ctor:function () {
        this._super();
        this.init();
    },

    // 4.
    init:function () {
        this._super();
        var centerPos = cc.p(winSize.width / 2, winSize.height / 2);

        var spriteBG = cc.Sprite.create("MainBG.png");
        spriteBG.setPosition(centerPos);
        this.addChild(spriteBG);

        cc.MenuItemFont.setFontSize(60);
        var menuItemPlay = cc.MenuItemFont.create("Play", this.onPlay, this);
        var menu = cc.Menu.create(menuItemPlay);
        menu.setPosition(centerPos);
        this.addChild(menu);
    },

    // on play button clicked
    onPlay:function (sender) {
        // 5.
        log("==onPlay clicked");
    }
});

// 6.
MainLayer.scene = function () {
    var scene = cc.Scene.create();
    var layer = new MainLayer();
    scene.addChild(layer);
    return scene;
};

// main entry
try {
    // 7.
    director = cc.Director.getInstance();
    winSize = director.getWinSize();
    // run first scene
    director.runWithScene(MainLayer.scene());
} catch(e) {log(e);}

Some important points to note:

1. require() will load a js module, use file name as parameter. "jsb.js" is the necessary module to be load if you want to use Cocos2d-x jsb to develop game. And a module only need to be loaded once in the runtime, then can use it anywhere.

2. MainLayer = cc.Layer.extend(); It's the Cocos2d-x jsb way of object inheritance, it comes from John Resig's javascript Inheritance. In this case, we are defining a new class inheritance from CCLayer with name MainLayer.

3. ctor() will be called if new a MainLayer. It's the jsb way of constructor. Remember to call this._super() if you override this function.

4. Override init() also need call this._super(). We creates a sprite using the background image you added previously, positions it at the center of the screen and adds it as a child of the MainLayer. Create a menu with a item named "Play", set item callback to function onPlay().

5. Just log a message in function onPlay(), it's ok for now.

6. MainLayer.scene = function (){}; define a static function for MainLayer.

7. Time to load main scene. Use cc.Director.getInstance() get the director, tell director run the first scene.

Note:Here director and winSize are declare as global variables. Both are frequently used.

Build and run the project, and you should see the following displayed on screen:

MainScene

Chapter 4: PlayScene Overview

The main scene consists of three layers.

PlayScene

PlayLayer

The PlayLayer contains the runner, maps, coins and rocks.

The runner moves forward, the camera of the layer moves forward. In this way to keep the runner in the visible range.

The background is composition of two horizontal maps. When then runner move from the first map to the second map, the first map will be reload and place to right of the second map. Map loop repeating in this way.

StatusLayer

StatusLayer is on top of the PlayLayer. Coins count and distance statistics will display in this layer.

Why these two separate out on a new layer?

If coins count and distance statistics add to PlayLayer, they will disappear when the camera moving with the runner. Separate them from PlayLayer will keep things simple.

GameOverLayer

GameOverLayer is a color layer.

GameOverLayer will be displayed when the runner hits the rock, and gives a choice to restart game.

Chapter 5: Setting Up PlayLayer with Physics World

PlayLayer is the most important layer of PlayScene. This layer handling player input, calculating collisions, objects movement and so on.

Create a js file and add to Xcode project

First, create a file with name "PalyScene.js" in Resource directory and drag onto the Resource folder in Xcode project. In the pop-up dialog, make sure that Add to targets is checked, then click Finish.

Second, select targets in Xcode project, switch to label "Build Phases", unfold the item "Copy bundle Resources".

copyBoundleSource

Scroll to the bottom and click "+". In the pop-up dialog, select "PlayScene.js" and then click Add.

addToBoundle

Note: Add js file to "Copy bundle Resources" is a necessary step for iOS project. If not, you will see following error message when require the js.
Cocos2d: Get data from file(PlayScene.jsc) failed!
Cocos2d: JS: /Users/u0u0/Library/Application Support/iPhone Simulator/6.1/Applications/3F9658F6-12CB-422A-89E9-6719D04B4D4B/Parkour.app/MainScene.js:3:Error: can't open PlayScene.js: No such file or directory

PlayLayer with Physics World

Open "PlayScene.js" and replace it's contents with the following:

var PlayLayer = cc.Layer.extend({
    // 1.
    space:null,// chipmunk space

    // constructor
    ctor:function () {
        this._super();
        this.init();
    },

    init:function () {
        this._super();
        this.initPhysics();
        // 2.
        this.scheduleUpdate();
    },

    // 3.
    initPhysics:function() {
        // 4.
        this.space = new cp.Space();
        // 5.
        this.space.gravity = cp.v(0, -350);
        // 6. set up Walls
        var wallBottom = new cp.SegmentShape(this.space.staticBody,
                        cp.v(0, g_groundHight),// start point
                        cp.v(4294967295, g_groundHight),// MAX INT:4294967295
                        0);// thickness of wall
        this.space.addStaticShape(wallBottom);
    },

    update:function (dt) {
        // 7.
        this.space.step(dt);
    }
});

PlayLayer.scene = function () {
    var scene = cc.Scene.create();
    var layer = new PlayLayer();
    scene.addChild(layer);
    return scene;
};

Some important points to note:

1. Define class member variable here. The left side of the semicolon is variable's name, and the right side is its initial value.

2. Schedules the "update" method

3. In this game, we use Chipmunk2D physics engine. Here are two sets of JSB APIs of Chipmunk. One is object-oriented, another is process-oriented. We just use the object-oriented one, which is more friendly.

4. new cp.Space() is the object-oriented API to create a Chipmunk physics world. Corresponding process-oriented API is cp.spaceNew().

5. Set the gravity of physics world. cp.v() is equal to cc.p().

6. In parkour game the runner will run on the ground. The chipmunk way to make a ground is using static shape. New a SegmentShape from the static body of the space, and then add it to the space.

7. update() will be called on every frame. We need to call chipmunk step here to make physics world work.

The global variable g_groundHight is defined in file "Utils.js".

var g_groundHight = 50;

To load the PlayScene, we need to go back to "MainScene.js", add the following in the top of the file.

require("Utils.js");
require("PlayScene.js");

And replace onPlay with the following:

onPlay:function (sender) {
    cc.Director.getInstance().replaceScene(PlayLayer.scene());
}

Build and run, click the "Play" button, you will see a black world on the screen. We will add something in following chapter.

Chapter 6: Running This Way

In this chapter we will add a sprite to the PlayLayer and make it running. We call this sprite the runner.

To make it running, we need an animated sprite. The animation is all thanks to the sprite sheet that was part of the Resource folder.

The sprite sheet consists of parkour.plist and parkour.png. These files were generated by using TexturePacker, a sprite sheet creation tool.

Note: Sprite sheet helps reduce memory consumption, speed up the drawing process and keep the frame rate high.

The animation composed of a series of images.

As following picture:

running

Drag all the images to TexturePacker, then click Publish to output the sprite sheet. The usage of TexturePacker can find from its official site.

Now we got two files "parkour.plist" and "parkour.png", move them to directory Resource/iphone.

Create a js file named "Runner.js" and add it to Xcode project as we done before. Replace it's contents with following:

// 1.
if(typeof RunnerStat == "undefined") {
    var RunnerStat = {};
    RunnerStat.running = 0;
};

// 2.
var Runner = cc.Node.extend({
    sprite:null,
    runningSize:null,
    space:null,
    body:null,// current chipmunk body
    shape:null,// current chipmunk shape
    stat:RunnerStat.running,// init with running status
    runningAction:null,
    spriteSheet:null,
    get offsetPx() {return 100;},

    // 3.
    ctor:function (spriteSheet, space) {
        this._super();

        this.spriteSheet = spriteSheet;
        this.space = space;
        this.init();
    },

    init:function () {
        this._super();

        // 4.
        this.sprite = cc.PhysicsSprite.createWithSpriteFrameName("runner0.png");
        this.runningSize = this.sprite.getContentSize();

        // 5.
        this.initAction();
        // 6.
        this.initBody();
        // 7.
        this.initShape();
        // 8.
        this.sprite.setBody(this.body);
        // 9.
        this.sprite.runAction(this.runningAction);
        // 10.
        this.spriteSheet.addChild(this.sprite, 1);
        // 11.
        this.stat = RunnerStat.running;
    },

    // 12.
    onExit:function() {
        this.runningAction.release();

        this._super();
    },

    // 13.
    getPositionX:function () {
        return this.sprite.getPositionX();
    },

    initAction:function () {
        // init runningAction
        var animFrames = [];
        // num equal to spriteSheet
        for (var i = 0; i < 8; i++) {
            var str = "runner" + i + ".png";
            var frame = cc.SpriteFrameCache.getInstance().getSpriteFrame(str);
            animFrames.push(frame);
        }

        var animation = cc.Animation.create(animFrames, 0.1);
        this.runningAction = cc.RepeatForever.create(cc.Animate.create(animation));
        this.runningAction.retain();
    },

    initBody:function () {
        // create chipmunk body
        this.body = new cp.Body(1, cp.momentForBox(1,
                        this.runningSize.width, this.runningSize.height));
        this.body.p = cc.p(this.offsetPx, g_groundHight + this.runningSize.height / 2);
        this.body.v = cp.v(150, 0);//run speed
        this.space.addBody(this.body);
    },

    initShape:function (type) {
        this.shape = new cp.BoxShape(this.body,
                        this.runningSize.width, this.runningSize.height);
        this.space.addShape(this.shape);
    },
});

Some important points to note:

1. JS way to define a enum for runner state. The runner has many state, but for this chapter we just care the running state.

2. cc.PhysicsSprite have no extend method, so the Runner class is extended from cc.Node.

3. The Runner will be created and placed in physics world of PlayLayer. So reference space in constructor. Reference the sprite sheet pass from PlayLayer too.

4. Before you call cc.PhysicsSprite.createWithSpriteFrameName to create physics sprite, you need to initialize the sprite frame cache from sprite sheet, which created by TexturePacker. This part of work will done in PlayLayer. "runner0.png" is the file name of first frame of animation.

5. In initAction, you create a animation from sprite frame cache, and make this animation repeats forever. Note this line of code this.runningAction.retain(); --- retain() will avoid the CCObject be GC.

6. In initBody, create a chipmunk body for the runner. And set it initial velocity.

7. In initShape, create a chipmunk shape which size equal to sprite size.

8. Associate physics body with the sprite.

9. Tell the sprite run the runningAction.

10. Add the sprite to spriteSheet.

11. Record the state, we will use it in future chapter.

12. Override onExit to release runningAction, remember to call this._super() if you override this function.

13. This helper function will be use to calculate camera moving in PlayLayer.

Switch to PlayScene.js and add the following in the top of the file:

require("Runner.js");

Define new class member variable.

spriteSheet:null,
runner:null,
lastEyeX:0,

Go to init() and make the following changes:

// create sprite sheet of PlayLayer
cc.SpriteFrameCache.getInstance().addSpriteFrames("parkour.plist");
this.spriteSheet = cc.SpriteBatchNode.create("parkour.png");
this.addChild(this.spriteSheet);

this.runner = new Runner(this.spriteSheet, this.space);
// runner is base on Node, addChild to make scheduleOnce and onExit call.
this.addChild(this.runner);

Then go to update() and make the following changes:

// move Camera
this.lastEyeX = this.runner.getPositionX() - this.runner.offsetPx;
var camera = this.getCamera();
var eyeZ = cc.Camera.getZEye();
camera.setEye(this.lastEyeX, 0, eyeZ);
camera.setCenter(this.lastEyeX, 0, 0);

The new position of the runner will be calculated in physics world within every frame. The camera need to follow the runner to keep the runner inside.

Build and run, and then you can see a boy running in the screen.

Chapter 7: Gesture Recognizer

So far the runner can move himself forward. Before add user controls to the runner, you need to handle player's input.

In the game we use gesture control, include swipe up, swipe down and draw circle.

There is an open source project $1 Unistroke Recognizer, which can recognize 16 gesture types include circle. And the $1 Unistroke Recognizer just have a JavaScript version, which can be easily import into Cocos2d-x JSB project.

But $1 Unistroke Recognizer has a drawback: difficult to distinguish swipe up and swipe down. You have to recognize these two gestures by yourself.

Simple Recognizer

Simple Recognizer can recognize simple gesture include swipe up, swipe down, swipe left and swipe right.

Create a js file named "SimpleRecognizer.js". Replace it's contents with following:

// 1.
function Point(x, y)
{
    this.X = x;
    this.Y = y;
}

// class define
function SimpleRecognizer()
{
    this.points = [];
    this.result = "";
}

SimpleRecognizer.prototype.beginPoint = function(x, y) {
    this.points = [];
    this.result = "";
    this.points.push(new Point(x, y));
}

SimpleRecognizer.prototype.movePoint = function(x, y) {
    this.points.push(new Point(x, y));

    if (this.result == "not support") {
        return;
    }

    var newRtn = "";
    var len = this.points.length;
    // 2.
    var dx = this.points[len - 1].X - this.points[len - 2].X;
    var dy = this.points[len - 1].Y - this.points[len - 2].Y;

    if (Math.abs(dx) > Math.abs(dy)) {
        // 3.
        if (dx > 0) {
            newRtn = "right";
        } else {
            newRtn = "left";
        }
    } else {
        // 4.
        if (dy > 0) {
            newRtn = "up";
        } else {
            newRtn = "down";
        }
    }

    // first set result
    if (this.result == "") {
        this.result = newRtn;
        return;
    }

    // if diretcory change, not support Recognizer
    if (this.result != newRtn) {
        this.result = "not support";
    }
}

SimpleRecognizer.prototype.endPoint = function(x, y) {
    if (this.points.length < 3) {
        return "error";
    }
    return this.result;
}

SimpleRecognizer.prototype.getPoints = function() {
    return this.points;
}

Some important points to note:

1. Define the Point as same as dollar library. This allows the project to use both libraries become simple.

2. Each time when touch point moving, calculate difference of the x-axis and y-axis between current touch point and previous touch point.

3. In this case, movement tendency of touch point on the x-axis direction.

4. In this case, movement tendency of touch point on the y-axis direction.

$1 Unistroke Recognizer

Open your web browser and navigate to http://depts.washington.edu/aimgroup/proj/dollar/dollar.js. Save this file to disk and move it to Resource directory. Add dollar.js to Xcode project as chapter 5 does.

Before using this library you need to do some optimization.

These are 16 gesture types in this library. To find the most suitable one, it must traverse all of these types. Comment out useless types can save cpu time. But you can NOT comment out all useless types, otherwise each recognition result will be "circle". You need to keep some distractors.

Let's optimize it.

Open dollar.js and modify the value of NumUnistrokes.

var NumUnistrokes = 4;//16;

Comment out useless types but retain "triangle", "circle", "left square bracket" and "right square bracket". And fix array subscript of these four Unistrokes.

Integrated into the PlayLayer

Switch to PlayScene.js, add following in the top of the file:

require("SimpleRecognizer.js");
require("dollar.js");

Define new class member variable.

recognizer:null,
dollar:null,

Go to init() and add following codes right after this.initPhysics().

// enable touch
this.setTouchEnabled(true);
// set touch mode to kCCTouchesOneByOne
this.setTouchMode(1);

this.dollar = new DollarRecognizer();
this.recognizer = new SimpleRecognizer();

You enable the touch of the layer, and set touch mode to kCCTouchesOneByOne, which receive touch point one at a time in event callbacks.

Add following codes to PlayLayer:

onTouchBegan:function(touch, event) {
    var pos = touch.getLocation();
    this.recognizer.beginPoint(pos.x, pos.y);
    return true;
},

onTouchMoved:function(touch, event) {
    var pos = touch.getLocation();
    this.recognizer.movePoint(pos.x, pos.y);
},

onTouchEnded:function(touch, event) {
    var rtn = this.recognizer.endPoint();

    switch (rtn) {
        case "up":
            log("==jumping");
            break;
        case "down":
            log("==crouching");
            break;
        case "not support":
        case "error":
            // try dollar Recognizer
            // 0:Use Golden Section Search (original) 
            // 1:Use Protractor (faster)
            var result = this.dollar.Recognize(this.recognizer.getPoints(), 1);
            log(result.Name);
            if (result.Name == "circle") {
                log("==incredible");
            }
            break;
    }
},

onTouchCancelled:function(touch, event) {
    log("==onTouchCancelled");
},

Simple Recognizer is faster than $1 Unistroke Recognizer. Use it recognize swipe up and swipe down first, if its fail then use $1 Unistroke Recognizer.

Build and run, try swipe up, swipe down and draw a circle. You will see following log.

Cocos2d: JS: ==jumping
Cocos2d: JS: ==crouching
Cocos2d: JS: circle
Cocos2d: JS: ==incredible

Chapter 8: Jumping and Crouching

In this chapter you’ll add the typical controls for Parkour Game.

Before modifying the runner class, switch to Utils.js and add following code.

if(typeof SpriteTag == "undefined") {
    var SpriteTag = {};
    SpriteTag.runner = 0;
    SpriteTag.coin = 1;
    SpriteTag.rock = 2;
};

Here you define a enum which will use in chipmunk for collision detection.

Go back to Runner.js, finishing the define of RunnerStat by adding following codes:

RunnerStat.jumpUp = 1;
RunnerStat.jumpDown = 2;
RunnerStat.crouch = 3;
RunnerStat.incredible = 4;

Define new class member variables for the runner.

crouchSize:null,
jumpUpAction:null,
jumpDownAction:null,
crouchAction:null,

The shape of the runner will change when crouch down. Record the crouch size which will be used in following code. Go to init() and add following code:

var tmpSprite = cc.PhysicsSprite.createWithSpriteFrameName("runnerCrouch0.png");
this.crouchSize = tmpSprite.getContentSize();

And change

this.initShape();

to

this.initShape("running");

Of course, we also need to modify the implement of initShape(). Replace it's content with following:

initShape:function (type) {
    if (this.shape) {
        this.space.removeShape(this.shape);
    }
    if (type == "running") {
        this.shape = new cp.BoxShape(this.body,
                this.runningSize.width, this.runningSize.height);
    } else {
        // crouch
        this.shape = new cp.BoxShape(this.body,
                this.crouchSize.width, this.crouchSize.height);
    }
    this.shape.setCollisionType(SpriteTag.runner);
    this.space.addShape(this.shape);
},

Initialize three additional animations: jumpUpAction, jumpDownAction and crouchAction in initAction().

// init jumpUpAction
animFrames = [];
for (var i = 0; i < 4; i++) {
    var str = "runnerJumpUp" + i + ".png";
    var frame = cc.SpriteFrameCache.getInstance().getSpriteFrame(str);
    animFrames.push(frame);
}

animation = cc.Animation.create(animFrames, 0.2);
this.jumpUpAction = cc.Animate.create(animation);
this.jumpUpAction.retain();

// init jumpDownAction
animFrames = [];
for (var i = 0; i < 2; i++) {
    var str = "runnerJumpDown" + i + ".png";
    var frame = cc.SpriteFrameCache.getInstance().getSpriteFrame(str);
    animFrames.push(frame);
}

animation = cc.Animation.create(animFrames, 0.3);
this.jumpDownAction = cc.Animate.create(animation);
this.jumpDownAction.retain();

// init crouchAction
animFrames = [];
for (var i = 0; i < 1; i++) {
    var str = "runnerCrouch" + i + ".png";
    var frame = cc.SpriteFrameCache.getInstance().getSpriteFrame(str);
    animFrames.push(frame);
}

animation = cc.Animation.create(animFrames, 0.3);
this.crouchAction = cc.Animate.create(animation);
this.crouchAction.retain();

Ok, now you are done for initialize. Let's look at how to make a jump. Add the following method inside Runner class:

jump:function () {
    if (this.stat == RunnerStat.running) {
        this.body.applyImpulse(cp.v(0, 250), cp.v(0, 0));
        this.stat = RunnerStat.jumpUp;
        this.sprite.stopAllActions();
        this.sprite.runAction(this.jumpUpAction);
    }
},

You just apply a upward impulse to the body of the runner, leave the other things to physics engine. Before switching to jumping up animation, you need to stop the currently running one by calling sprite.stopAllActions().

Jump process is divided into two parts -- Rising up and Falling down. To detect the switching from rising up to falling down, you need watch the linear velocity of the center of gravity of the body.

If the linear velocity in Y-axis is less then 0.1, jump process switch from rising up to falling down. At this time, change the animation of sprite to jumpDownAction.

If the linear velocity in Y-axis is equal to 0, sprite state change from falling down to running. At this time, change the animation of sprite to runningAction.

These work will be done in sprite.step(), and the code would look like the following:

step:function (dt) {
    var vel = this.body.getVel();
    if (this.stat == RunnerStat.jumpUp) {
        if (vel.y < 0.1) {
            this.stat = RunnerStat.jumpDown;
            this.sprite.stopAllActions();
            this.sprite.runAction(this.jumpDownAction);
        }
        return;
    }
    if (this.stat == RunnerStat.jumpDown) {
        if (vel.y == 0) {
            this.stat = RunnerStat.running;
            this.sprite.stopAllActions();
            this.sprite.runAction(this.runningAction);
        }
        return;
    }
},

As for crouch down, just change the shape of the body. Add the following methods inside Runner class:

crouch:function () {
    if (this.stat == RunnerStat.running) {
        this.initShape("crouch");
        this.sprite.stopAllActions();
        this.sprite.runAction(this.crouchAction);
        this.stat = RunnerStat.crouch;
        // after time turn to running stat
        this.scheduleOnce(this.loadNormal, 1.0);
    }
},

Crouch state only last for some time, switch back to running state by calling this.scheduleOnce(this.loadNormal, 1.0).

loadNormal() initialize the shape of the body to running state. Implemented as following:

loadNormal:function (dt) {
    this.initShape("running");
    this.sprite.stopAllActions();
    this.sprite.runAction(this.runningAction);
    this.stat = RunnerStat.running;
},

Now you are done in Runner.js, switch to PlayScene.js and replace onTouchEnded with the following:

onTouchEnded:function(touch, event) {
    var rtn = this.recognizer.endPoint();

    switch (rtn) {
        case "up":
            this.runner.jump();
            break;
        case "down":
            this.runner.crouch();
            break;
        case "not support":
        case "error":
            // try dollar Recognizer
            // 0:Use Golden Section Search (original) 
            // 1:Use Protractor (faster)
            var result = this.dollar.Recognize(this.recognizer.getPoints(), 1);
            log(result.Name);
            if (result.Name == "circle") {
                this.runner.incredibleHulk();
            }
            break;
    }
},

Add following to update:

// runner step, to change animation
this.runner.step(dt);

Build and run, try swipe up or swipe down. You will see the runner jump or crouch.

Chapter 9: Map Loop

Until now, the runner ran alone in a black world. It's time to add a background image.

Background image composed by two maps, which divided into upper and lower part. When the runner run over the gap of two maps, the first map will be reloaded and placed to the back of the second map.

Here's a simple function that format an integer to a specific length in javascript. Switch to Utils.js and add following codes:

function FormatNumberLength(num, length) {
    var r = "" + num;
    while (r.length < length) {
        r = "0" + r;
    }
    return r;
}

Create a js file named "Map.js" and add to Xcode project as we done before. Replace it's contents with following:

require("Utils.js");

var Map = cc.Class.extend({
    layer:null,
    space:null,
    spriteWidth:0,
    // 1.
    mapCount:2,// total map of resource
    map0:null,
    map1:null,
    ground0:null,
    ground1:null,
    curMap:0,// [0, n]

    ctor:function (layer, space) {
        this.layer = layer;
        this.space = space;

        // 2.
        this.map0 = cc.Sprite.create("Map00.png");
        this.map0.setAnchorPoint(cc.p(0, 0));
        this.map0.setPosition(cc.p(0, 0));
        this.layer.addChild(this.map0);

        // 3.
        this.ground0 = cc.Sprite.create("Ground00.png");
        this.ground0.setAnchorPoint(cc.p(0, 0));
        var size = this.ground0.getContentSize();
        this.ground0.setPosition(cc.p(0, g_groundHight - size.height));
        this.layer.addChild(this.ground0);

        this.spriteWidth = this.map0.getContentSize().width;

        this.map1 = cc.Sprite.create("Map01.png");
        this.map1.setAnchorPoint(cc.p(0, 0));
        // 4.
        this.map1.setPosition(cc.p(this.spriteWidth, 0));
        this.layer.addChild(this.map1);

        this.ground1 = cc.Sprite.create("Ground01.png");
        this.ground1.setAnchorPoint(cc.p(0, 0));
        this.ground1.setPosition(cc.p(this.spriteWidth, g_groundHight - size.height));
        this.layer.addChild(this.ground1);
    },

    getMapWidth:function () {
        return this.spriteWidth;
    },

    getCurMap:function () {
        return this.curMap;
    },

    checkAndReload:function (eyeX) {
        // 5.
        var newCur = parseInt(eyeX / this.spriteWidth);
        if (this.curMap == newCur) {
            return false;
        }

        var map;
        var ground;
        if (0 == newCur % 2) {
            // change mapSecond
            map = this.map1;
            ground = this.ground1;
        } else {
            // change mapFirst
            map = this.map0;
            ground = this.ground0;
        }
        log("==load map:" + (newCur + 1));
        this.curMap = newCur;

        // 6.
        var fileName = "Map" + FormatNumberLength((newCur + 1) % this.mapCount, 2) + ".png";
        var texture = cc.TextureCache.getInstance().addImage(fileName);
        map.setTexture(texture);
        map.setPositionX(this.spriteWidth * (newCur + 1));

        // load ground
        var fileName = "Ground" + FormatNumberLength((newCur + 1) % this.mapCount, 2) + ".png";
        var texture = cc.TextureCache.getInstance().addImage(fileName);
        ground.setTexture(texture);
        ground.setPositionX(this.spriteWidth * (newCur + 1));
        return true;
    },
});

Some important points to note:

1. MapCount should equal to the number of files in Resource folder. And should not less than two.

2. Upper part of map is a sprite, which anchor point be changed to (0, 0). Changing the anchor point is designed to simplify the calculation of the coordinate.

3. The difference between upper and lower part is the position coordinate. Set Y-axis coordinate of lower part to g_groundHight - this.ground0.getContentSize().height to keep the runner's foot tread on the ground.

4. X-axis of the second map start with map width.

5. Calculate current map in this way.

6. Set a new texture and position to map which has gone.

Switch to PlayScene.js, add the following in the top of the file:

require("Map.js");

Define new class member variable.

map:null,

Initialize the map by adding following to init():

this.map = new Map(this, this.space);

Every frame check for map reload. Adding following to update():

// check and reload map
if (true == this.map.checkAndReload(this.lastEyeX)) {
    //level up
    this.runner.levelUp();
}

Build and run the project, and you should see the following displayed on screen:

mapLoop

Chapter 10: Adding Coins and Rocks

Until now, you have a runner run in map world, but a complete parkour game requires two more things: coin and rock.

Coin is the game rewords. When the runner hits the coin, the coin will disappear. But rock is an obstacle. When the runner hits the rock, game over.

In addition to the collision handling, they have no difference. Let's start with coin.

Create a js file named "Coin.js" and add it to Xcode project as we done before. Replace its contents with following:

var Coin = cc.Class.extend({
    space:null,
    sprite:null,
    shape:null,
    // 1.
    _map:0,
    get map() {
        return this._map;
    }, 
    set map(newMap) {
        this._map = newMap;
    },

    ctor:function (spriteSheet, space, pos) {
        this.space = space;

        // 2.
        var animFrames = [];
        for (var i = 0; i < 8; i++) {
            var str = "coin" + i + ".png";
            var frame = cc.SpriteFrameCache.getInstance().getSpriteFrame(str);
            animFrames.push(frame);
        }

        var animation = cc.Animation.create(animFrames, 0.1);
        var action = cc.RepeatForever.create(cc.Animate.create(animation));

        this.sprite = cc.PhysicsSprite.createWithSpriteFrameName("coin0.png");

        // 3.
        var radius = 0.95 * this.sprite.getContentSize().width / 2;
        var body = new cp.StaticBody();
        body.setPos(pos);
        this.sprite.setBody(body);

        this.shape = new cp.CircleShape(body, radius, cp.vzero);
        this.shape.setCollisionType(SpriteTag.coin);
        // 4.
        this.shape.setSensor(true);

        this.space.addStaticShape(this.shape);

        // Needed for collision
        body.setUserData(this);

        // add sprite to sprite sheet
        this.sprite.runAction(action);
        spriteSheet.addChild(this.sprite, 1);
    },

    // 5.
    removeFromParent:function () {
        this.space.removeStaticShape(this.shape);
        this.shape = null;
        this.sprite.removeFromParent();
        this.sprite = null;
    },
});

// 6.
var gCoinContentSize = null;
Coin.getContentSize = function () {
    if (null == gCoinContentSize) {
        var sprite = cc.PhysicsSprite.createWithSpriteFrameName("coin0.png");        
        gCoinContentSize = sprite.getContentSize();
    }
    return gCoinContentSize;
};

Some important points to note:

1. Which map the coin belong to. This value will be set in ObjectManager.js, which we will be introduce later.

2. Initialize the coin animation.

3. Use static body for coin to avoid the influence of gravity.

4. Sensors only call collision callbacks, and never generate real collisions

5. removeFromParent will be used in ObjectManager.js.

6. getContentSize is a static method of Class Coin, which will be used in ObjectManager.js for object coordinate calculation.

Next is the rock, create "Rock.js" with following code:

var Rock = cc.Class.extend({
    space:null,
    sprite:null,
    shape:null,
    _map:0,// which map belong to
    get map() {
        return this._map;
    }, 
    set map(newMap) {
        this._map = newMap; 
    },

    ctor:function (spriteSheet, space, pos) {
        this.space = space;

        // 1.
        if (pos.y >= (g_groundHight + Runner.getCrouchContentSize().height)) {
            this.sprite = cc.PhysicsSprite.createWithSpriteFrameName("hathpace.png");
        } else {
            this.sprite = cc.PhysicsSprite.createWithSpriteFrameName("rock.png");
        }

        var body = new cp.StaticBody();
        body.setPos(pos);
        this.sprite.setBody(body);

        // 2.
        this.shape = new cp.BoxShape(body,
            this.sprite.getContentSize().width,
            this.sprite.getContentSize().height);
        this.shape.setCollisionType(SpriteTag.rock);
        this.shape.setSensor(true);

        this.space.addStaticShape(this.shape);
        spriteSheet.addChild(this.sprite);

        // Needed for collision
        body.setUserData(this);
    },

    removeFromParent:function () {
        this.space.removeStaticShape(this.shape);
        this.shape = null;
        this.sprite.removeFromParent();
        this.sprite = null;
    },
});

var gRockContentSize = null;
Rock.getContentSize = function () {
    if (null == gRockContentSize) {
        var sprite = cc.PhysicsSprite.createWithSpriteFrameName("rock.png");
        gRockContentSize = sprite.getContentSize();
    }
    return gRockContentSize;
};

Rock and coin have the following two differences:

1. Rock have two textures, and the choose according to value of Y-coordinate.

2. The shape of rock is box, not circle.

Now you have coin and rock, but how to put them on the map? They can not overlap and maintain proper distance. It's time to introduce ObjectManager.js.

Create a js file named "ObjectManager.js" and add it to Xcode project. Replace its contents with following:

require("Coin.js");
require("Rock.js");

var ObjectManager = cc.Class.extend({
    spriteSheet:null,
    space:null,
    // 1.
    objects:[],

    ctor:function (spriteSheet, space) {
        this.spriteSheet = spriteSheet;
        this.space = space;
        // objects will keep when new ObjectManager();
        // we need clean here
        this.objects = [];
    },

    // 2.
    initObjectOfMap:function (map, mapWidth) {
        var initCoinNum = 7;
        var jumpRockHeight = Runner.getCrouchContentSize().height + g_groundHight;
        var coinHeight = Coin.getContentSize().height + g_groundHight;

        // 2.1
        var randomCoinFactor = Math.round(Math.random()*2+1);
        var randomRockFactor = Math.round(Math.random()*2+1);
        var jumpRockFactor = 0;

        // 2.2
        var coinPoint_x = mapWidth/4 * randomCoinFactor+mapWidth*map;
        var RockPoint_x = mapWidth/4 * randomRockFactor+mapWidth*map;

        var coinWidth = Coin.getContentSize().width;
        var rockWith = Rock.getContentSize().width;
        var rockHeight =  Rock.getContentSize().height;

        var startx = coinPoint_x - coinWidth/2*11;
        var xIncrement = coinWidth/2*3;

        //add a rock
        var rock = new Rock(this.spriteSheet, this.space,
                cc.p(RockPoint_x, g_groundHight+rockHeight/2));
        rock.map = map;
        this.objects.push(rock);
        if(map == 0 && randomCoinFactor==1){
            randomCoinFactor = 2;
        }

        //add 7 coins
        for(i = 0; i < initCoinNum; i++)
        {
            // 2.3
            if((startx + i*xIncrement > RockPoint_x-rockWith/2)
                &&(startx + i*xIncrement < RockPoint_x+rockWith/2))
            {
                var coin1 = new Coin(this.spriteSheet, this.space,
                        cc.p(startx + i*xIncrement, coinHeight+rockHeight));
            } else{
                var coin1 = new Coin(this.spriteSheet, this.space,
                        cc.p(startx + i*xIncrement, coinHeight));
            }

            coin1.map = map;
            this.objects.push(coin1);
        }

        for(i=1;i<4;i++){
            if(i!=randomCoinFactor&&i!=randomRockFactor){
                jumpRockFactor = i;
            }
        }

        // 2.4
        var JumpRockPoint_x = mapWidth/4 * jumpRockFactor+mapWidth*map;
        var jumpRock = new Rock(this.spriteSheet, this.space,
                cc.p(JumpRockPoint_x, jumpRockHeight+rockHeight/2));
        jumpRock.map = map;
        this.objects.push(jumpRock);
    },

    // 3.
    recycleObjectOfMap:function (map) {
        while((function (obj, map) {
            for (var i = 0; i < obj.length; i++) {
                if (obj[i].map == map) {
                    obj[i].removeFromParent();
                    obj.splice(i, 1);
                    return true;
                }
            }
            return false;
        })(this.objects, map));
    },

    // 4.
    remove:function (obj) {
        obj.removeFromParent();
        // find and delete obj
        for (var i = 0; i < this.objects.length; i++) {
            if (this.objects[i] == obj) {
                this.objects.splice(i, 1);
                break;
            }
        }
    },
});

Some important points to note:

1. All coins and rocks are hold a array.

2. Main logic of initialization objects for map.

  • Create 2 random num to Confirm which point we create coin and rock.
  • Calculate the start points of rock and coin into every map though random factor.
  • Add every object into map. Take coins as a example, if the start point of coin is the same as rock's then we must change the height of every coin the point is higher than rock's height or lower than rock's bottom.
  • Add other rocks to map.
  • 3. Every time the map reload, object in this map will be recycled.

    4. When the runner get a coin, remove coin from its parent and objects array.

    Everythins is ready, only integrate to PlayScene.js.

    Switch to PlayScene.js, add the following in the top of the file.

    require("ObjectManager.js");
    

    Define new class member variables.

    objectManager:null,
    shapesToRemove:[],
    

    Go to init() and add following codes right after this.addChild(this.runner).

    this.objectManager = new ObjectManager(this.spriteSheet, this.space);
    this.objectManager.initObjectOfMap(1, this.map.getMapWidth());
    

    Go to initPhysics(), setup chipmunk CollisionHandler.

    this.space.addCollisionHandler(SpriteTag.runner, SpriteTag.coin,
                this.collisionCoinBegin.bind(this), null, null, null);
    this.space.addCollisionHandler(SpriteTag.runner, SpriteTag.rock,
                this.collisionRockBegin.bind(this), null, null, null);
    

    Add two new collision callback functions into PlayLayer.

    collisionCoinBegin:function (arbiter, space) {
        var shapes = arbiter.getShapes();
        this.shapesToRemove.push(shapes[1]);
    },
    
    collisionRockBegin:function (arbiter, space) {
        var rtn = this.runner.meetRock();
        if (rtn == true) {
            log("==gameover");
            director.pause();
        } else {
            // break Rock
            var shapes = arbiter.getShapes();
            this.shapesToRemove.push(shapes[1]);
        }
    },
    

    Then go to update() and make the following changes:

    // Simulation cpSpaceAddPostStepCallback
    for(var i = 0; i < this.shapesToRemove.length; i++) {
        var shape = this.shapesToRemove[i];
        var body = shape.getBody();
        var obj = body.getUserData();
        //TODO add remove animation
        this.objectManager.remove(obj);
    }
    this.shapesToRemove = [];
    
    // check and reload map
    if (true == this.map.checkAndReload(this.lastEyeX)) {
        this.objectManager.recycleObjectOfMap(this.map.getCurMap() - 1);
        this.objectManager.initObjectOfMap(this.map.getCurMap() + 1, this.map.getMapWidth());
        //level up
        this.runner.levelUp();
    }
    

    Aha, You just finish the main logic of the game. Build and run, try controll the runner to gain coins and avoid rocks.

    You can download full source code from here.

    用In-App Purchases赚钱的40个秘密

    翻译:俊华、EY、夜狼、一叶、魏凡缤、

    校对:glory、u0u0

    原文:40 Secrets to Making Money with In-App Purchases

    对于用In-app purchases赚钱可以归纳为一个问题--为什么我们会去购买东西?
    我给你一个提示--几乎都是因为消费满足了客户的情绪和心理需求。如果你能了解人们的想法,那么你的app将会更有优势,而这也是本篇文章想要探讨的內容。

    在本文中,我将介绍40个秘密,教你使用正确的方法在IAP中赚取更多的钱。这里你将要学到:

    1.如何使用诱导效应来调控价格

    2.如何将用户的积极情感与你的商店联系起来

    3.如何优化你的游戏“whales”

    4.以及其它37个的技巧

    亲,你准备好像专业人员那样开始赚钱了吗?那就来深入的了解吧!

    一、增加难度

    让我们从最基础的开始——能使用户通过你的应用程序来放松是最重要的。

    想想,你会把钱花在一个不熟悉的游戏上吗?我想应该不会吧!

    如果你的游戏符合下列要求那么这个策略将是最好的:用户通过很少的约束就能够充分体验你的应用程序。如果这样做了,你会有以下两个好处:

    1.你提高了用户的平均会话时间,也就是用户花费在玩你的应用程序上的时间.

    2.你提高的可能性——也就是说,那些更愿意花大笔钱玩你游戏的人,也会买昂贵的物品来帮助他们在游戏中进步.

    用户如果不想在你的游戏中花费大量金钱,那他们将会花费大量时间去尝试着掌握它的技能。而在用户花时间玩游戏的过程中,你可以利用其它的收入来源,比如广告、Tapjoy集成、SponsorPay或其它被动消费的方法。

    僵尸围城2(Contract Killer Zombies 2 )中就可以看到这种策略:

    p1

    随着游戏进程的前进,增加新地图的难度也在同步递增。如果没有利用IAP的话更困难的地图很可能需要用户花费额外的时间来击败大BOSS,当然也可以花钱来增强武器去打败它!

    二、增加隐藏章节

    玩家去购买付费内容是为了得到能够提供额外“奖金”的材料,而这样恰恰可以鼓励玩家利用你的IAP。火柴人狙击手(Sniper Shooter)使用了这种策略:

    p2

    在这个游戏中,你需要购买一个具有特定特征的狙击步枪,这样才有权访问游戏中的某些章节。当然,你可以购买虚拟货币,或者通过完成某些在棘手子任务之间的章节来获取虚拟货币。

    三、提供IAP中的样品

    如果你不知道这个物品有什么用,你会购买吗?让用户可以体验你IAP中的部分内容,甚至整个程序(无论是消费品和非补充的)。例如,如果用户能在游戏的开始就能使用消费品升级,那他们在体验过游戏之后,就更有可能购买更多的东西!水果忍者(Fruit Ninja)就是用了这个策略:

    p3

    最初,有三个免费的偏转炸弹任你自由支配。当你已经完成了少数进度的时候,你意识到它们是多么有用,所以你会愿意购买一些偏转炸弹来应付之后更困难的游戏关卡!

    四、让玩家赚取IAP物品

    这种策略非常成功的借鉴了糖果传奇(Candy Crush Saga)。用户在玩游戏的过程中解锁连续的IAP物品,而这项技术正是基于这个理念。

    p4

    这个策略提供了以下好处:

    1.增加了你的app中IAP的关注度

    2.增加了IAP的感知价值

    3.减少对付费内容不感兴趣的用户的烦恼

    五、让他们不释手!

    这个提示来自我的朋友——Triolith Entertainment的CEO 马格努斯.索德伯格。三重镇(Triple Town)是一个非常好的吸引用户上瘾的游戏例子,并向用户提供支付选项以提高他们的游戏体验。

    p5

    三重镇是一个在每个游戏会话中移动有限次数的战略拼图游戏。当玩家移动步数用完时,他们需要等待更新周期来继续玩游戏。
    然而,对于那些不能忍受等待的玩家,app提供了一个“无限变成”的非补充IAP内容。基本上,每一个钩都有合理的价格,它们可以让玩家通过删除约束或者提供一个很酷的权利来更好的体验游戏。

    六、增加“救我”选项

    当玩家处于高难度的游戏行动或者面临死亡的威胁,或是接近Game Over,或是面临其他一些不良的结果,可以选择“救我”的IAP来自我拯救。这种类型的IAP可以使濒危的用户保持当前的分数或者游戏进度,而不致于从头开始。
    玩家们总是热衷于争夺游戏中排行榜的名次,而这是推广你的app的绝佳机会,因为app依赖于处在兴奋状态的玩家的快速决策

    p6

    而无尽奔跑的游戏是最适合应用这一策略的。你可以很明显地发现使用这一策略的app,例如,地铁冲浪(Subway Surfers)和神庙逃亡(Temple Run)。

    七. 引起玩家的好奇心

    该技巧来自于营销天才Trey Smith,你可以在Trey Smith – Live at App Empire 2012中学习他一些聪明的方法。

    你需要引起玩家的好奇心来让他们想要解锁IAP內容。

    Extreme Road Trip 2就是一个做到这点的很好的例子。你可以模糊或者隐藏IAP內容,直到它们被解锁--可以这么说,这可以提升玩家想要看看藏在幕后的是什么的欲望。

    p7

    八. 使用(价格)诱导效应

    让我们尝试一个小实验。你和你重要的另一半在一家电影院,计划要看“Django Unchained”且你知道有个习惯--所有的电影约会都必须伴随著爆米花。

    你靠近柜台并且看了一下菜单上的爆米花尺寸:

    p8

    你会选择哪一个,为什么呢?下面显示了大多数的人在这样的情境下会选择什么

    內部方案: 诱导效应A

    如果你选择大的爆米花,你就掉入诱导效应的陷阱里—除非你只是刚好肚子饿了(所以选大的)。

    74%的人都会选大的,通常不会有人选择中的,所以,你可以假设有26%的人会选择小的。

    我打赌你会选择大尺寸的爆米花,因为它看起來比中尺寸的來的有价值—只是多了50分美元,当然选它。

    现在,再假装你遇到了以下这样的菜单:

    p9

    现在,你会选择哪个尺寸的爆米花呢?看看下面大部份的人会选择什么:

    內部方案: 沒有诱导效应

    在这情境下,87%的人会选择小寸的 这是很大的不同

    在一个平均有100人的剧场里,这里显示爆米花在这两种不同的情境下所销售的结果会是怎样:

    方案A: 诱导效应(Decoy Effect) — $7.00 * 74人+ $3.00 * 26人 = $596.00

    方案B: 没有诱导效应(No Decoy Effect) — $7.00 * 13人 + $3.00 * 87人 = $352.00

    方案A中增加了一个6.5美元的诱导价格,而销售额就大幅增加了244美元。

    诱导效应通常是由三个物品所组成,而其中一个就是用来诱导的物品。你用來做诱导的那项物品必需看起来让人觉得是很大的折扣。

    九. 提供一个身临其境的体验

    这里有个虽然是小把戏却可以带来很大的效用的方法,且这只需要一点时间来实现。你曾经在拉斯维加斯的赌场玩过吗?看看你的周围,你将看不到任何的时间,这可以让人们忽略对时间的概念且让他们花更多的时间在赌博上。

    p10

    去掉你游戏中的状态栏可以增加人们游玩的时间,而当人们花更多时间在你的app上时,你就有更多的机会让他们去购买物品。

    十. 提供“移除广告”的选项

    虽然看似每个人都知道运用这个策略,但是不要小看它!它虽然简单,但是很多玩家都希望能够专心的使用你的App而不受广告的骚扰。

    当做一个实验,去看看App Store上那些沒有提供这个功能(移除广告)的产品它们的评语(很多应用的差评都是因为用户受到了广告的骚扰)。你会惊讶原来有这么多人需要这个功能。

    十一. 如果犹豫,那就卖可消费性的商品

    最近来自Flury的研究—一个提供手机分析服务和网络广告的机构—指出所有IAP模式中,消费性产品的收益比其他类型的产品来的大。

    来自Flury的图表可看出IAP模式中不同类型产品所带来的收益的百分比:

    p11

    如果你只选择一种策略来让你的app赚钱,那么就选择消费性物品,它可以为你带来很大的收益与价值。

    十二. 为用户提供他们喜欢的统计资料

    玩家喜欢知道他们在游戏中与其他玩家的竞争详细资料,用IAP来提供給你的使用者一个选择以让他们知道他们与世界上其他玩家相比起来是如何。

    Ruzzle 这个app就是一个透过IAP来提供这选项的很好的例子

    p12

    十三:时间无价

    多数人认为时间是宝贵的礼物--游戏玩家也不例外.现实中通常缺乏耐心的人较多,而且他们想尽快的足他们的需求.手机游戏是对宝贵时间合理使用的特殊例子.手机游戏大部分是用户在等待时候玩的,并且游戏可以随时随地的结束.

    每次游戏玩家想要取得最大金币数量.你可以利用这点,向他们售时间道具.

    如果你浏览App Store,你会发现很多游戏使用这个方法,非常有效. Clash of Clans 是一款非常成功的游戏,他的运营模式就是基于此.

    p13

    Clash of Clans允许你通过虚拟货币加快建设或者加快军队建设速度.这是一个非常成功的策略,但是你可以更进一步,收集一些指标围绕鼓励人们用钱换时间。

    在你的游戏里,使用好的方法来衡量和调整你的IAP策略的有效性是:设置一个服务器,你可以监视和改变定时事件的长度。用这种方式,你可以改变时间来找到事件周期和卖IAP物品之间最好的组合。

    十四. Bundle Your IAP Content捆绑你的IAP内容

    如果你有一个股票的游戏物品,它可以做为一个组集链接和销售一体,完成他!人们喜欢大量的购买----参考the crowds at Sam’s Club

    Draw Something做得相当好,为他们的应用程序提供颜色和给主题团体涂色,展示如下截图:

    p14

    十五.帮助用户更好的体验

    在参观的地方,人们会在情感和经历上形成强烈的关联.你的app也不例外.你可以使用一个简单的策略帮助用户体验你的app,给他们一种温暖,舒适的感觉.

    首次启动你的游戏时,引导人们到你的商店里,并且在访问中赠送给他们礼物.他们会认识到你的善意举动,并关联对你的商店有好感.用户对你的app的愉悦心情就会更加愿意以后花费金钱去购买.

    p15

    十六.提供纪念品

    在游戏里制作一些比其他物品更加昂贵的.使用钻石图覆盖它,为他镀金,做你可以做的想做的,让它从其他物品脱颖而出.它将是你的”纪念品”----在你app里的物品中,在某方面它是超出普通玩家.

    看看Noble Nutlings在商店里提供的,截屏如下:

    p16

    正如你所见到的,金色轮子是最贵的选项----在列表中它比下一个选项贵出12倍!这种非常好的鱼钩纪念品对于和铁杆玩家来说。Whales会立刻花钱获得relic,而铁杆玩家将会花数小时时间解锁relic而不支付-----你可以从这两种风格的玩家赚取收入.

    十七.让你的用户开心!

    加油---谁不想买一个红色flying冰箱?

    p17

    Mega Jump这个游戏案例是非常成功的游戏.该游戏是由一个玩笑开始演变到成千上万的玩家花费他们辛苦赚得钱购买可播放的红色冰箱角色上.

    十八.提供有限时间购买

    在你的app里通过购买有限时间道具制作一些紧迫感.提供50%的关键物品折扣---或者更多---提升用户购买的本能.

    下面是Contract Killer Zombies 的截图:

    p18

    倒计时定时器显示还剩多少时间是非常有效的,因为它制造的一种紧张的压迫感,从而引导玩家快速购买的决定。

    如果你可以远程设置和控制你的IAP物品,调整设置的“剩余时间”和“成本”,看看哪个组合销售的数量最高。

    十九.在正确的时间提供适合的道具

    在合适的时间提供恰当的道具给玩家是一种强有力的销售技术。

    p19

    例如,在你面对一个充满僵尸的竞技场时,Contract Killer Zombies 2 游戏弹出雇佣佣兵选项.Picture玩家在这种情况下知道对抗他们胜利是不高的---适合的时间提供帮助是一个很好地方法来驱动IAP.

    二十.制作美而有用的商店

    Noble Nutlings有一个非常好的商店,如下图:

    p20

    商店使用容易,也真的好看,同时还直入主题.最好的选择是它支持先试用后买 .在Apple 商店里使用这种方式是一大优势;在他们购买前,人们可以试用,这种模式的购买往后是越来越高的。

    二十一.颜色心理学的使用

    想想怎么呈现你的IAP内容,通常人们按下按钮弹出你的商店购买你的商品.那么如何利用颜色影响用户的购买习惯呢?下面的图呈现出颜色匹配相应的情绪:

    p21

    在设计制作你的资产时,你可以使用它作为一个参考帮助.好的idea是使用一种”A/B测试”技术来了解什么样的颜色或者颜色组合可以获得更高的使用率.

    你不需要重新设计你的UI;有时候小的改动如改变购买按钮的颜色就足以获得很好的效果.

    clutch.io 是一个很好地在线使用A/B测试你的app工具.最近被Twitter收购,但提供所需工具可以设置你服务器,从而让你的app支持A/B测试.

    这里有些关于颜色影响买家的行为的例子:

    1.黄色通常用于吸引买家关注

    2.红色造成紧迫感;试图结合有时间限制的分配。

    3.蓝色创造一种信任和安全的感觉。

    4.对于我们的眼睛处理来说,绿色是最简单的颜色,绿色通常用于帮助人们放松。也许这就是为什么我们喜欢在这么多网站上使用!:]

    5.橙色创建一种激情澎湃。

    6.黑色是强大的和它被用于市场奢侈品。如果在你的商店使用一个relic,将其与黑色和金色配合使用将会给人这种产品是无价的感觉。

    二十二. 让你的商店容易被找到

    试著为用户创造机会可以找到游戏內商店的方法,找到你的商店的方法越简单,人们就越有机会进行购买。

    看这个Draw Something的截屏,它不止为用户提供了一种找到商店的途经而是三种。

    p22

    二十三. 提供一次性付费

    Eternity Warriors 2 很好的李永乐 一次性付费功能:

    p23

    一次性付费是很吸引人的功能,但时如果频的繁展示那会真的非常烦人。不要在程序每次启动时展示你的“一次性付费”功能,应该偶尔展示让用户觉得交易很少。但一定要展示“一次性付费”时,应该提供很酷的物品或者给予他们很多,以让他们觉得很值。

    二十四. 用特效吸引起玩家的注意力

    如果想让装备脱颖而出,或者用户点击按钮访问你的商店,应该使用些闪亮的特效或者装饰来引起人们的注意,如下面的截图所示:

    p24

    二十五. 提供多买

    买一定数量的东西后给予免费赠品,这是超市中为鼓励多买时常用的招数。然而,不要太过火。如果人们习惯于你的“廉价交易”,这可能会有蚕食其它购买内容的风险。

    下图所示提供的应用内购买就是非常好的例子:

    p25

    二十六. 创建自己的经济体系

    创建一个由成就和高级虚拟奖金组成的经济体。成就货币可被玩家在游戏中采集并且用来换取应用内购买里面的内容。高级货币只提供给人民币玩家。

    你可以让用户选择,是在游戏的过程中收集奖励货币(可以利用其它方式,如广告),或者是他们购买高级货币来换取奖励。

    在这幅图中,就是使用高级货币来消费的例子:

    p26

    通过远端下载的 plist 文件来修改虚拟系统中的属性 是一个不错的控制的方式。这简化了维护 IAP 内容价格的方法,也能引导处理IAP子内容中更加复杂的脚本。

    二十七. 使用 Applicasa 来管理你的 IAP 策略

    Applicasa.com 是一个手机游戏管理平台,它可以帮助你建立并优化你的 IAP 内容。

    尽管它们的产品还处于测试阶段,但是可以看见它有很大潜力,它的管理方式是一流的。

    你可以通过一下几种方式来帮助管理你的 IAP 内容:

    1.要明白你的价格是否太高或者太低

    2.实时跟踪了解可用的虚拟获取和每天的交易量

    3.审核每以次 IAP 交易

    4.建立一个动态的虚拟商店,你可以在那里更新商品的价格,并添加新的商品。

    5.通过跟踪商品的交易量,以明白哪些商品是受欢迎的。

    6.运行 A/B 测试你任意的内容。

    7.根据用户的行为和活动自动划分用户群,这可以让你根据用户群,来推销不同的内容。以在不同的情况下促进消费。

    需要注意记住一点,每个用户都是不同的,不要给每个人都推荐同样的东西。

    下图展示了如何使用 Applicasa 来划分不同的人群:

    p27

    Applicasa 同样能够帮助你根据用户的行为创建促销、优惠或公告等:

    1.消费习惯-如谁几乎没有消费,谁消费了很多

    2.使用习惯-如哪些人玩的很少,哪些人经常玩

    3.地域来源-根据国家和文化优化服务,如消费模式

    4.人群特征-根据用户的年龄和性别优化应用内提供的内容

    下面两张图便能说明,用户的消费习惯随着如年龄和性别的不同而不同:

    p28

    二十八.为 IAP 提供奖励机制

    当用户执行某些操作时,给他在 IAP 内容中的奖励,他们通常会因此感谢你,同样,它会提高你的品牌信誉并具有增值效应。

    “Subway Surfers” 在用户登陆到 “Game Center” 时给与相应的奖励:

    p29

    当用户每天登录时,分发一些虚拟币作为礼品,这会增加你应用的寿命周期,换句话说,就是随着日子一天天过去,用户有多少天会继续使用你的应用。

    我们发现 “The Simpsons™: Tapped Out:” 就是根据用户返回应用并给与奖励的很好例子:

    p30

    不仅仅是奖励可以帮助用户在每天返回游戏,“神秘礼物” 也同样增加了用户好奇心,以让用户多在游戏中停留几天。

    以下由 “Localytics” 提供的图表说明了其对应用内购买会话数量增加的可能性:

    p31

    最终能够使得你的用户不断回到你的APP的最可靠的保证在于你的产品,一个优秀的产品才能够让用户话费更多用于内购。一个忠实用户基群是一个成功的持续化的游戏所必须的,如果你对如何针对这方面制定商业策略有兴趣,可以看看Beintoo.com 一个移动参与平台,为移动和网页应用提供针对性的忠诚计划。

    二十九.管理关卡的难度

    如果你的游戏是分级的,你可以提供一个IAP,允许用户跳过他们觉得难的关卡.同样你也可以根据你朋友的plist 文件在关卡难度和应用收益找到一个最佳平衡点,从而调整每个关卡的难度.

    在更改游戏难度时,你需要注意.如果你挫败你的用户,他们会过早的退出应用程序,而他们再次回到应用的可能性将会直线下降。你可以提供一个power-ups作为调整游戏的难度,通过使用power-ups你可以评估的关卡难度。

    三十.适应不同的人和品味

    在你的游戏中有不同类型的游戏人物,让用户满意,让他们购买定制服装,或给他们选择,让他们觉得代表了他们真实的自我。

    Temple Run游戏已经做了一个极好的方式,如下显示:

    p32

    三十一.瞬间提供热量支持

    俗话说,在你站在超市门口时,你是不会意识到你的手纸用完了。在玩家开始玩你的游戏之前不要让他们进入游戏的购买页面,而是在打斗中最需要帮助时给予及时的帮助。

    Contract Killer Zombies 2就使用这种技巧获取较大的影响。子弹用完了?需要更强的武器打BOSS?没问题!让玩家暂停游戏并购买他们需要的东西还允许他们回到重要的事情-打僵尸!

    p33

    三十二.制作购买推荐

    当玩家对级数妥协的时候,他们经常想“靠!之前打Boss的时候我应该买武器的!”你可以让玩家玩的更容易,还可以提高你的IAP转换率,通过在升级之前提供一些专属物品。

    Eternity Warriors 2给玩家提供多种他们每级可能需要的优先物品,如下图所示:

    p34

    三十三. 贩卖真实世界中的商品

    Fruit Ninja和Angry Birds是很成功的app所以他们可以通过他们的app贩卖自己的商品。 大多数的开发者无法达到这样水准的成功,可是如果你有不少忠实玩家,你可以试著在你的app中销售一些真实世界的物品,例如T-Shirt,或者马克杯。

    p35

    Temple Run 提供游戏品牌墙纸购买,没理由你不在你的应用中用同样的方法!

    p36

    三十四. 关注最受欢迎的App

    最受欢迎的App是由那些精通如何鼓励用户参与和如何最大化in-app purchases策略的工作室开发。下載和体验他们的游戏,并分析什么原因让玩家如此上瘾,以及他们采用了什么方法激励人们购买。你可以从App Annie、Appdate网站查看销售榜上排名在前面的游戏:

    p37

    三十五.利用应用内内容达到收费以外的目的

    尽管大部分的用户不会为你的游戏付费,但是他们乐于接受游戏中的虚拟货币,用于购买服务。

    可以通过奖励游戏中虚拟货币来激励用户去完成一些对你来说有价值的行为。

    一些值得鼓励你用户做的事:

    1.通过email,SMS,或者Facebook来邀请朋友一起玩。

    2.完成一些可以对你产生价值的简单操作,比如,Tapjoy,Sponsorpay。

    3.在社交网络上分享游戏中得分。

    4.登陆到用户自己的社交网络。

    5.订阅你的邮件列表。

    6.成为游戏注册用户。

    在商业策略中,钱不是唯一的。在社交网络上给予反馈,如同通过邮件注册搭建你的营销网络,都是非常有商业价值的举动。

    三十六.为大客户提供更好的服务

    平均来说,低于2%的用户会为IAP内容付费。根据W3i研究者的数据显示,定价介于$0.99到$1.99的IAP内容产生的收益只占总收益的6%。然而,定价介于$9.99到$19.99的IAP内容产生的收益占总收益的47%。你可以试图优化你的内容来激励用户产生更多的IAP交易,但事实是IAP并非一个“长尾游戏”。数据显示,乐于花钱的用户会花大价钱。尽管低价的内容会卖得更好,但长远来说,你更多的收益来自于那些高价的内容——尽管会卖得更少些。

    三十七. 礼貌地询问

    说到客户满意度,没有什么比友好更强大。如果你希望用户做些什麼,比如用行动交换虚拟货币,或者通过应用內购买进行升级,你都要尽可能的使用最令人愉快的语言。

    三十八.分析并优化你的策略

    即使你的工作已经完成了,仍然有可以改进的空间。

    你应该问自己三个问题:

    1.我该如何增加我的IAP策略的效率额?

    2.我该如何是我的IAP内容更具吸引力?

    3.怎样才能使我的内容更易于购买?

    为了回答这几个问题,比较同一个游戏的两个版本。

    p38

    哇,买一把枪是个大交易。但是不幸的是,用户余额不足。所以用户点击购买按钮,在购买之前用户会被重定向到充值界面,这个两步的过程戏剧性的降低了IAP内容的转换率。

    但是,看看新版本,购买过程有了细微的变化。

    p39

    你注意到了变化吗?这次只需要一步就能完成购买,供应商明确地定价了商品,这是转换率有效的保证。总是试图减少你的用户和你的IAP内容之间的障碍。

    三十九.使用强大的营销语言

    谈到销售,语言是一个无法置信的强有力的工具。

    这里有一些可供选择的措辞,能够给用户行为带来无法置信的影响。

    1.出售——52%的用户更有可能进入一个带有出售标志的商店。

    2.保证——60%的用户更愿意购买与“保证”这个词有关联的物品。

    3.免费——并不是每个人都喜欢免费的东西?

    为了说明“免费”的强大,我们看下面的场景。

    用户可以在两中巧克力之间选择。一种是大牌子的,正在促销,被定价为$0.19——比正常价格便宜50%。另外一种巧克力,在质量和味道上完全相同。但是并非是大牌子,被定价在$0.01。

    p40

    在这种情况下,75%的客户会选择促销的巧克力,只有25%的用户会选择后面的这个。

    p41

    现在,我们把两个物品的价格都降低一美分。然后,情况改变了,71%的用户选择免费的巧克力,而只有29%的用户继续选择促销的大牌巧克力

    这个故事的寓意。“免费”是一个非常强大的工具,用于改变用户的行为,但是小心,不要伤害你的IAP策略的其他领域。

    四十. 永远不要惹恼你的用户

    最后,绝对不要做任何事来试图惹恼你的用户,不快乐的用户不会从你这里购买任何东西,更坏的结果是他们会删除你的APP,切断你另外的营收渠道,比如广告。用户的口口相传可以帮你塑造声誉,但也可以摧毀你积累的声誉,所以把精力放在提供更具吸引力的游戏经验上來吧,別让贪婪毁了你的游戏。要经常把用户在游戏中的乐趣放在第一位。

    去哪呢?

    希望你在这个“40个小技巧”能找到对你的IAP策略有益的东西。最应该记住的东西是,很多很多的开发者和工作室都经历了很多艰难的过程。你应该好好学习别人的经验——在犯别人犯过的错误之前。

    我会提供一些有用的链接,助你商业化你的APP:

    1.如果想在你的应用中尝试IAP,一个好的开始的地方。

    2.一个关于内购的基本问题的初级教程。

    3.选取了Flurry博客上关于IAP统计和其他IAP资源的文章。

    4.第一部分和第二部分是PlayHaven上的一篇关于如何最大化用户价值的文章

    如果你有任何问题,评论,或者更好的方法在管理和使用应用的IAP策略上,来参加下面的讨论吧。

    泰然网填问卷调查送书活动

    [赠书活动]

    1.收听泰然网官方微博

    2.转发本条微博 http://e.weibo.com/2703776721/A6uSZ6DrD

    3. 认真完成问卷调查

    即有机会获赠随机发放的《Cocos2d权威指南》或《Cocos2d应用开发实践指南》一本。本次活动每两天抽取一名幸运奖,奖品于活动结束统一发放。问卷调查入口:http://www.sojump.com/jq/2669549.aspx《Cocos2d应用开发实践指南》:http://item.jd.com/11265566.html

    cocos2d-ray

    cocos2d-zilong

    [泰然视频教程]精灵表单-part1

    时间轴:子龙山人 听译:子龙山人 压制:u0u0

    TexturePacker出品的游戏基础知识视频,非常生动的说明了什么是SpriteSheets,它有什么作用。
    泰然视频组听译制作了这个视频。
    enjoy!

    TortoiseGit github 免输用户名密码 Push

    大家好,我是bilt,新人,请大家多多关照!

            最近开始使用Git GitHub做代码管理,windows上比较流行的方式就是msysgit+ TortoiseGit的方式了,在每次push时都需要输入用户名和密码,真心很烦人,在google找了一堆文章,说的云山雾里的。。。结果还是不行!只好自己研究下了!!!

    1.运行 开始->TortoiseGit->Puttygen.

    QQ截图20130701180024

    阅读全文»

    ?>