为空间中的每个刚体调用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

7.8 空间模拟

void cpSpaceStep(cpSpace *space, cpFloat dt)

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

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

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

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

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

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

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

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

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

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

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

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

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

7.10 札记

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

8. Chipmunk约束:cpConstraint

约束是用来描述两个刚体如何相互作用的(他们是如何约束彼此的)。约束可以是允许刚体像我们身体的骨头一样轴转动的简单关节,也可以是更抽象的比如齿轮关节或马达关节。

8.1 约束是什么,不是什么

在Chipmunk中,约束都是基于速度的约束。这意味着他们主要通过同步两个刚体的速度进行作用。一个轴关节将两个独立刚体的两个锚点连接起来,公式定义要求两个锚点的速度必须相同并且计算施加在刚体上的冲量以便试图保持这个状态。约束将速度视为主要的输入并且产生一个速度变化作为它的输出。一些约束(尤其是关节)通过改变速度来修正位置的差异。更多详情见下一节。

连接两个刚体的弹簧不是一个约束。它很像约束因为它会创建一个力来影响两个刚体的速度,但是弹簧将距离作为输入,将力作为输出。如果弹簧不是一个约束,你会问为什么还会有两种类型的弹簧约束。原因是他们是阻尼弹簧。弹簧关联的阻尼才是真正的约束,这个约束会根据关联的两个刚体的相对速度来创建速度的变化。因为大部分情况将一个阻尼器和一个弹簧放在一起很方便,我想我还不如将弹簧力作为约束的一部分,而不是用一个阻尼器约束然后让用户单独计算和施加弹簧力。

8.2 属性

  • 得到约束关联的两个刚体
cpBody * cpConstraintGetA(const cpConstraint *constraint)
cpBody * cpConstraintGetB(const cpConstraint *constraint)
  • 约束能够作用于两个刚体的最大力。默认为INFINITY
cpFloat cpConstraintGetMaxForce(const cpConstraint *constraint)
void cpConstraintSetMaxForce(cpConstraint *constraint, cpFloat value)
  • 关节误差百分比一秒钟后仍然没得到修正。这和碰撞偏差机制完全一样,但是这会修正关节的误差而不是重叠碰撞。
cpFloat cpConstraintGetErrorBias(const cpConstraint *constraint)
void cpConstraintSetErrorBias(cpConstraint *constraint, cpFloat value)
  • 约束可以纠错的最大速度。默认为INFINITY
cpFloat cpConstraintGetMaxBias(const cpConstraint *constraint)
void cpConstraintSetMaxBias(cpConstraint *constraint, cpFloat value)
  • 得到约束所添加进去的空间
cpSpace* cpConstraintGetSpace(const cpConstraint *constraint)
  • 使用数据指针。使用指针来从回调中得到拥有该约束的游戏对象的一个引用。
cpDataPointer cpConstraintGetUserData(const cpConstraint *constraint)
void cpConstraintSetUserData(cpConstraint *constraint, cpDataPointer value)
  • 约束被施加的最新的冲量。为了转化成力,除以cpSpaceStep()传进的时间步。你可以使用这点来检查施加的力是否超过了一定的阈值从而实现可断裂的关节。

  • 断裂关节例子

// 创建关节且设置最大力属性
breakableJoint = cpSpaceAddConstraint(space, cpPinJointNew(body1, body2, cpv(15,0), cpv(-15,0)));
cpConstraintSetMaxForce(breakableJoint, 4000);


// 在update函数中,正常步进模拟空间...
cpFloat dt = 1.0/60.0;
cpSpaceStep(space, dt);

if(breakableJoint){
  // 将冲量除以时间步得到施加的力
  cpFloat force = cpConstraintGetImpulse(breakableJoint)/dt;
  cpFloat maxForce = cpConstraintGetMaxForce(breakableJoint);

  // 如果该力大于设定的阈值则断裂关节
  if(force > 0.9*maxForce){
    cpSpaceRemoveConstraint(space, breakableJoint);
    breakableJoint = NULL;
  }
}

要访问特定关节类型的属性,使用提供的getter和setter函数(如cpPinJointGetAnchr1())。更多信息请查看属性列表。

8.3 反馈纠错

Chipmunk的关节并不完美。销关节并不能维系两个锚点之间确切的距离,枢轴关节同样也不能保持关联的锚点完全在一起。他们通过自纠错来处理这个问题。在Chipmunk5中,你有很多额外的控制来实现关节对自身的纠错,甚至可以使用这个特性,以独特的方式使用关节来创建一些物理效果。

  • 伺服马达:如 打开/关闭门或者旋转物件,无需用最大的力
  • 起货机:朝着另外一个物体拉一个物体无需用最大的力
  • 鼠标操作:以粗暴、摇晃的鼠标输入方式自如的与物体交互

cpConstraint结构体有3个属性控制着误差纠正,maxForce,maxBias以及biasCoef.maxForce。关节或者约束在不超过该数值大小的力时才能发挥作用。如果它需要更多的力来维系自己,它将会散架。maxBias是误差纠正可以应用的最大速度了。如果你改变了一个关节的属性,这个关节将不得不自行纠正,一般情况下很快会这么做。通过设置最大速度,你可以像伺服一样使得关节工作,在一段较长的时间以恒定的速率校正自身。最后,biasCoef是在钳位最大值速度前每一步误差纠正的百分比。你可以使用它来使得关节平滑的纠正自身而不是以一个恒定的速度,但可能是三个属性中迄今为止最没用的。

// 在一个顶视角的游戏中,采用这种配置的枢轴关节将会计算与地面之间的摩擦
// 因为关节纠正被禁用,所以关节不会重新摆正自身并只会影响速度。
// 当速度改变时,关节施加的力会被最大力钳位
// 这样它就会像摩擦一样工作
cpConstraint *pivot = cpSpaceAddConstraint(space, cpPivotJointNew2(staticBody, body, cpvzero, cpvzero));
pivot->maxBias = 0.0f; // disable joint correction
pivot->maxForce = 1000.0f;

// 枢轴关节并不施加旋转力,使用一个比率为1.0的齿轮关节来替代
cpConstraint *gear = cpSpaceAddConstraint(space, cpGearJointNew(staticBody, body, 0.0f, 1.0f));
gear->maxBias = 0.0f; // disable joint correction
gear->maxForce = 5000.0f;

// 另外,你可以将关节连接到一个无限大质量的游离刚体上来取代连接到一个静态刚体上
// 你可以使用游离刚体作为控制刚体来连接。可以查看`Tank`演示例子。

8.4 约束和碰撞形状

约束和碰撞形状互不了解双方信息。当为刚体连接关节时,锚点不必处于刚体形状的内部,这么做通常是有意义的。同样的,为两个刚体添加约束并不能阻止刚体形状碰撞。事实上,这便是碰撞组属性存在的主要原因。

8.5 现有关节类型视频演示

  • Youtube地址
  • 优酷地址

8.6 共享内存管理函数

DestroyFree函数由所有关节类型共享。Allocationinit函数对于每种关节类型都是特定的。

9. 约束类型

9.1 销关节

cpPinJoint *cpPinJointAlloc(void)
cpPinJoint *cpPinJointInit(cpPinJoint *joint, cpBody *a, cpBody *b, cpVect anchr1, cpVect anchr2)
cpConstraint *cpPinJointNew(cpBody *a, cpBody *b, cpVect anchr1, cpVect anchr2)

ab是被连接的两个刚体,anchr1anchr2是这两个刚体的锚点。当关节被创建的时候距离便被确定,如果你想要设定一个特定的距离,使用setter函数来重新设定该值。

属性

  • cpVect cpPinJointGetAnchr1(const cpConstraint *constraint)
  • void cpPinJointSetAnchr1(cpConstraint *constraint, cpVect value)
  • cpVect cpPinJointGetAnchr2(const cpConstraint *constraint)
  • void cpPinJointSetAnchr2(cpConstraint *constraint, cpVect value)
  • cpFloat cpPinJointGetDist(const cpConstraint *constraint)
  • void cpPinJointSetDist(cpConstraint *constraint, cpFloat value)

9.2 滑动关节

cpSlideJoint *cpSlideJointAlloc(void)

cpSlideJoint *cpSlideJointInit(
    cpSlideJoint *joint, cpBody *a, cpBody *b,
    cpVect anchr1, cpVect anchr2, cpFloat min, cpFloat max
)

cpConstraint *cpSlideJointNew(cpBody *a, cpBody *b, cpVect anchr1, cpVect anchr2, cpFloat min, cpFloat max)

ab是被连接的两个刚体,anchr1anchr2是这两个刚体的锚点, minmax定义了两个锚点间的最小最大距离。

属性

  • cpVect cpSlideJointGetAnchr1(const cpConstraint *constraint)
  • void cpSlideJointSetAnchr1(cpConstraint *constraint, cpVect value)
  • cpVect cpSlideJointGetAnchr2(const cpConstraint *constraint)
  • void cpSlideJointSetAnchr2(cpConstraint *constraint, cpVect value)
  • cpFloat cpSlideJointGetMin(const cpConstraint *constraint)
  • void cpSlideJointSetMin(cpConstraint *constraint, cpFloat value)
  • cpFloat cpSlideJointGetMax(const cpConstraint *constraint)
  • void cpSlideJointSetMax(cpConstraint *constraint, cpFloat value)

9.3 枢轴关节

cpPivotJoint *cpPivotJointAlloc(void)
cpPivotJoint *cpPivotJointInit(cpPivotJoint *joint, cpBody *a, cpBody *b, cpVect pivot)
cpConstraint *cpPivotJointNew(cpBody *a, cpBody *b, cpVect pivot)
cpConstraint *cpPivotJointNew2(cpBody *a, cpBody *b, cpVect anchr1, cpVect anchr2)

ab是关节连接的两个刚体,pivot是世界坐标系下的枢轴点。因为枢轴点位置是在世界坐标系下,所以你必须确保两个刚体已经处于正确的位置上。另外你可以指定基于一对锚点的轴关节,但是要确保刚体处于正确的位置上因为一旦你空间开启了模拟,关节将会修正它自身。

属性

  • cpVect cpPivotJointGetAnchr1(const cpConstraint *constraint)
  • void cpPivotJointSetAnchr1(cpConstraint *constraint, cpVect value)
  • cpVect cpPivotJointGetAnchr2(const cpConstraint *constraint)
  • void cpPivotJointSetAnchr2(cpConstraint *constraint, cpVect value)

9.4 沟槽关节

cpGrooveJoint *cpGrooveJointAlloc(void)

cpGrooveJoint *cpGrooveJointInit(
    cpGrooveJoint *joint, cpBody *a, cpBody *b,
    cpVect groove_a, cpVect groove_b, cpVect anchr2
)

cpConstraint *cpGrooveJointNew(cpBody *a, cpBody *b, cpVect groove_a, cpVect groove_b, cpVect anchr2)

沟槽在刚体a上从groov_agroov_b,枢轴被附加在刚体banchr2锚点上。所有的坐标都是刚体局部坐标。

属性

  • cpVect cpGrooveJointGetGrooveA(const cpConstraint *constraint)
  • void cpGrooveJointSetGrooveA(cpConstraint *constraint, cpVect value)
  • cpVect cpGrooveJointGetGrooveB(const cpConstraint *constraint)
  • void cpGrooveJointSetGrooveB(cpConstraint *constraint, cpVect value)
  • cpVect cpGrooveJointGetAnchr2(const cpConstraint *constraint)
  • void cpGrooveJointSetAnchr2(cpConstraint *constraint, cpVect value)

9.5 阻尼弹簧

cpDampedSpring *cpDampedSpringAlloc(void)

cpDampedSpring *cpDampedSpringInit(
    cpDampedSpring *joint, cpBody *a, cpBody *b, cpVect anchr1, cpVect anchr2,
    cpFloat restLength, cpFloat stiffness, cpFloat damping
)

cpConstraint *cpDampedSpringNew(
    cpBody *a, cpBody *b, cpVect anchr1, cpVect anchr2,
    cpFloat restLength, cpFloat stiffness, cpFloat damping
)

和滑动关节的定义很类似。restLength是弹簧想要的长度,stiffness是弹簧系数(Young's modulus),dampling用来描述弹簧阻尼的柔软度。

属性

  • cpVect cpDampedSpringGetAnchr1(const cpConstraint *constraint)
  • void cpDampedSpringSetAnchr1(cpConstraint *constraint, cpVect value)
  • cpVect cpDampedSpringGetAnchr2(const cpConstraint *constraint)
  • void cpDampedSpringSetAnchr2(cpConstraint *constraint, cpVect value)
  • cpFloat cpDampedSpringGetRestLength(const cpConstraint *constraint)
  • void cpDampedSpringSetRestLength(cpConstraint *constraint, cpFloat value)
  • cpFloat cpDampedSpringGetStiffness(const cpConstraint *constraint)
  • void cpDampedSpringSetStiffness(cpConstraint *constraint, cpFloat value)
  • cpFloat cpDampedSpringGetDamping(const cpConstraint *constraint)
  • void cpDampedSpringSetDamping(cpConstraint *constraint, cpFloat value)

9.6 阻尼旋转弹簧

cpDampedRotarySpring *cpDampedRotarySpringAlloc(void)

cpDampedRotarySpring *cpDampedRotarySpringInit(
    cpDampedRotarySpring *joint, cpBody *a, cpBody *b,
    cpFloat restAngle, cpFloat stiffness, cpFloat damping
)

cpConstraint *cpDampedRotarySpringNew(cpBody *a, cpBody *b, cpFloat restAngle, cpFloat stiffness, cpFloat damping)

犹如阻尼弹簧,但却在角度层面起作用。restAngle是刚体间想要的相对角度,stiffnessdampling和阻尼弹簧的基本一样。

属性

  • cpFloat cpDampedRotarySpringGetRestAngle(const cpConstraint *constraint)
  • void cpDampedRotarySpringSetRestAngle(cpConstraint *constraint, cpFloat value)
  • cpFloat cpDampedRotarySpringGetStiffness(const cpConstraint *constraint)
  • void cpDampedRotarySpringSetStiffness(cpConstraint *constraint, cpFloat value)
  • cpFloat cpDampedRotarySpringGetDamping(const cpConstraint *constraint)
  • void cpDampedRotarySpringSetDamping(cpConstraint *constraint, cpFloat value)

9.7 旋转限位关节

cpRotaryLimitJoint *cpRotaryLimitJointAlloc(void)
cpRotaryLimitJoint *cpRotaryLimitJointInit(cpRotaryLimitJoint *joint, cpBody *a, cpBody *b, cpFloat min, cpFloat max)
cpConstraint *cpRotaryLimitJointNew(cpBody *a, cpBody *b, cpFloat min, cpFloat max)

旋转限位关节约束着两个刚体间的相对角度。minmax就是最小和最大的相对角度,单位为弧度。它被实现以便可能使范围大于一整圈。

属性

  • cpFloat cpRotaryLimitJointGetMin(const cpConstraint *constraint)
  • void cpRotaryLimitJointSetMin(cpConstraint *constraint, cpFloat value)
  • cpFloat cpRotaryLimitJointGetMax(const cpConstraint *constraint)
  • void cpRotaryLimitJointSetMax(cpConstraint *constraint, cpFloat value)

9.8 棘轮关节

cpRatchetJoint *cpRatchetJointAlloc(void);
cpRatchetJoint *cpRatchetJointInit(cpRatchetJoint *joint, cpBody *a, cpBody *b, cpFloat phase, cpFloat ratchet);
cpConstraint *cpRatchetJointNew(cpBody *a, cpBody *b, cpFloat phase, cpFloat ratchet);

工作起来像套筒扳手。ratchet是"clicks"间的距离,phase是当决定棘轮角度的时候的初始位移。

属性

  • cpFloat cpRatchetJointGetAngle(const cpConstraint *constraint)
  • void cpRatchetJointSetAngle(cpConstraint *constraint, cpFloat value)
  • cpFloat cpRatchetJointGetPhase(const cpConstraint *constraint)
  • void cpRatchetJointSetPhase(cpConstraint *constraint, cpFloat value)
  • cpFloat cpRatchetJointGetRatchet(const cpConstraint *constraint)
  • void cpRatchetJointSetRatchet(cpConstraint *constraint, cpFloat value)

9.9 齿轮关节

cpGearJoint *cpGearJointAlloc(void);
cpGearJoint *cpGearJointInit(cpGearJoint *joint, cpBody *a, cpBody *b, cpFloat phase, cpFloat ratio);
cpConstraint *cpGearJointNew(cpBody *a, cpBody *b, cpFloat phase, cpFloat ratio);

齿轮关节保持着一对刚体恒定的角速度比。ratio总是测量绝对值,目前无法设定相对于第三个刚体的角速度。phase是两个刚体的初始角度偏移量。

属性

  • cpFloat cpGearJointGetPhase(const cpConstraint *constraint)
  • void cpGearJointSetPhase(cpConstraint *constraint, cpFloat value)
  • cpFloat cpGearJointGetRatio(const cpConstraint *constraint)
  • void cpGearJointSetRatio(cpConstraint *constraint, cpFloat value)

9.10 简单马达

cpSimpleMotor *cpSimpleMotorAlloc(void);
cpSimpleMotor *cpSimpleMotorInit(cpSimpleMotor *joint, cpBody *a, cpBody *b, cpFloat rate);
cpConstraint *cpSimpleMotorNew(cpBody *a, cpBody *b, cpFloat rate);

简单马达保持着一对刚体恒定的角速度比。rate是所需的相对角速度。通常你会给马达设定一个最大力(扭矩)否则他们会申请一个无限大的扭矩来使得刚体移动。

属性

  • cpFloat cpSimpleMotorGetRate(const cpConstraint *constraint)
  • void cpSimpleMotorSetRate(cpConstraint *constraint, cpFloat value)

9.11 札记

  • 你可以为两个刚体添加多个关节,但要确保他们彼此不会冲突。否则会引起刚体抖动或者剧烈的旋转。

10. Chipmunk碰撞检测概述

Chipmunk为了使得碰撞检测尽可能快,将处理过程分成了若干阶段。虽然我一直试图保持它概念简单,但实现却有点让人生畏。幸运的是作为Chipmunk库的使用者,你并不需要了解一切关于它是如何工作的。但如果你在尝试发挥Chipmunk的极致,理解这一部分会有所帮助。

10.1 空间索引

在场景中用一个for循环来检查每一个对象是否与其他对象发生碰撞会很慢。所以碰撞检测的第一步(或者就像通常称作的阶段),就是使用高层次空间算法来找出哪些对象应该被检查碰撞。目前Chipmunk支持两种空间索引,轴对齐包围盒树和空间散列。这些空间索引能够快速识别哪些形状彼此靠近,并应做碰撞检查。

10.2 快速碰撞过滤

在空间索引找出彼此靠近的形状对后,将它们传给space,然后再执行一些额外的筛选。在进行任何操作前,Chipmunk会执行几个简单的测试来检测形状是否会发生碰撞。

  • 包围盒测试:如果形状的包围盒没有重叠,那么形状便没发生碰撞。对象如对角线线段会引发许多误报,但你不应该担心。
  • 类别掩码测试:每一个形状的的类别和其他形状的类别掩码进行位运算。如果结果是0,形状之间不发生碰撞。
  • 群组测试:在相同的非零群组中的形状不会发生碰撞。

10.3 基于约束的碰撞过滤

在快速碰撞过滤之后, Chipmunk对关节列表中其中一个刚体检查,看它是否通过关节和其他刚体关联着。如果关节的collideBodies属性为false,碰撞将会被忽略掉。因为大部分场景都不会包含很多关节,所以检测过程非常迅速。

10.4 基本形状与形状间的碰撞检测

最昂贵的测试其实就是检测基于几何形状的重叠。圆与圆,圆与线之间的碰撞检测相当快,多边形和多边形的碰撞检测随着顶点数的增加而更加昂贵。形状越简单,碰撞检测就越快(更重要的是求解器检测的碰撞点就越少)。Chipmunk使用了一个分发表来描述应该使用哪个函数来检测形状是否重叠。

10.5 碰撞处理函数过滤

在检测到两个形状间重叠之后,Chipmunk会查看你是否为该碰撞形状的类型定义了一个碰撞处理函数。对于游戏这样去处理碰撞事件是至关重要的,同时也为你提供了一个非常灵活的方式来过滤掉碰撞。begin()preSolve()回调函数的返回值决定了碰撞的形状对是否该舍弃掉。返回true会保留形状对,false则会舍弃。在begin()回调中中止一个碰撞是永久性的,在preSolve()回调中中止只是应用于当前所处的时间步。如果你没有为碰撞类型定义一个处理函数,Chipmunk将会调用space的默认处理函数,默认会简单的接受所有碰撞。

Wildcard collisions can also return a value, but they are handled in a more complicated way. When you create a collision handler between two specific collision types, it’s your responsibility to decide when to call the wildcard handlers and what to do with their return values. Otherwise, the default is to call the wildcard handler for the first type, then the second type, and use a logical AND of their return values as filtering value. See DefaultBegin() in cpSpace.c for more information.

使用回调过滤碰撞是最灵活的方式,记住,到那时候所有最昂贵的碰撞检测通过你的回调都已经完成。对于每帧有大量碰撞对象的模拟,寻找碰撞所消耗的时间和解决碰撞所消耗的时间相比要小很多,所以这不是一个大问题。不过,尽可能的选择快速碰撞过滤。

11. 碰撞回调

没有任何事件或反馈的物理库对游戏而言帮助并不大。你怎么知道当玩家碰到了一个敌人,以便你扣除一些生命点数?你怎么知道汽车撞击一个东西的力度,这样你就不会在石子击中它的时候播放一个巨响轰隆音?如果你需要决定在特定条件下的碰撞是否应该被忽略,比如要实现单向平台?Chipmunk拥有一套强大的回调系统,你可以实现他们来完成这一切。

11.1 碰撞处理

碰撞处理函数是Chipmunk能够识别的不同碰撞事件的一组4个回调函数。事件类型是:

  • begin():该步中两个形状刚开始第一次接触。回调返回true则会处理正常碰撞,返回falseChipmunk会完全忽略碰撞。如果返回false,则preSolve()postSolve()回调将永远不会被执行,但你仍然会在形状停止重叠的时候接收到一个单独的事件。
  • preSolve():该步中两个形状相互接触。回调返回falseChipmunk在这一步会忽略碰撞,返回true来正常处理它。此外,你可以使用cpArbiterSetFriction()cpArbiterSetElasticity()cpArbiterSetSurfaceVelocity()来提供自定义的摩擦,弹性,或表面速度值来覆盖碰撞值。更多信息请查看cpArbiter。
  • postSolve():两种形状相互接触并且它们的碰撞响应已被处理。如果你想使用它来计算音量或者伤害值,这时你可以检索碰撞冲力或动能。更多信息请查看cpArbiter。
  • separate():该步中两个形状刚第一次停止接触。确保begin()/separate()总是被成对调用,当删除接触中的形状时或者析构space时它也会被调用。

碰撞回调都与cpArbiter结构紧密相关。你应该熟悉那些为好。

注:标记为传感器的形状(cpShape.sensor == true)从来不会得到碰撞处理,所以传感器形状和其他形状间永远不会调用postSolve()回调。它们仍然会调用begin()separate()回调,而preSolve()仍然会在每帧调用回调,即使这里不存在真正的碰撞。

注2:preSolve()回调在休眠算法运行之前被调用。如果一个对象进入休眠状态,postSolve()回调将不会被调用,直到它被唤醒。

11.2 碰撞处理API

typedef int (*cpCollisionBeginFunc)(cpArbiter *arb, struct cpSpace *space, void *data)
typedef int (*cpCollisionPreSolveFunc)(cpArbiter *arb, cpSpace *space, void *data)
typedef void (*cpCollisionPostSolveFunc)(cpArbiter *arb, cpSpace *space, void *data)
typedef void (*cpCollisionSeparateFunc)(cpArbiter *arb, cpSpace *space, void *data)

碰撞处理函数类型。所有这些函数都附带一个arbiter,space和用户data指针,只有begin()preSolve()回调会返回值。更多信息请查看上方碰撞处理。

void cpSpaceAddCollisionHandler(
    cpSpace *space,
    cpCollisionType a, cpCollisionType b,
    cpCollisionBeginFunc begin,
    cpCollisionPreSolveFunc preSolve,
    cpCollisionPostSolveFunc postSolve,
    cpCollisionSeparateFunc separate,
    void *data
)

为指定的碰撞类型对添加一个碰撞处理函数。每当碰撞类型(cpShape.collision_type)为a的形状与碰撞类型为b的形状碰撞时,这些回调就会被调用来处理碰撞。data是用户定义的上下文指针,用来传递到每个回调中。你不想实现的话可以使用NULL,然而Chipmunk会调用它自身的默认版本而不是你为space设置的默认值。如果你需要依赖space的默认回调,你必须单独为每个处理函数定义提供实现。

void cpSpaceRemoveCollisionHandler(cpSpace *space, cpCollisionType a, cpCollisionType b)

移除指定碰撞类型对的碰撞处理函数。

void cpSpaceSetDefaultCollisionHandler(
    cpSpace *space,
    cpCollisionBeginFunc begin,
    cpCollisionPreSolveFunc preSolve,
    cpCollisionPostSolveFunc postSolve,
    cpCollisionSeparateFunc separate,
    void *data
)

当没有具体的碰撞处理时Chipmunk会使用一个默认的注册碰撞处理函数。space在创建时被指定了一个默认的处理函数,该函数在begin()preSolve()回调中返回true,在postSolve()separate()回调中不做任何事情。

11.3 后步回调

后步回调允许你打破在一个回调内增加或删除对象的规则。事实上,它们的主要功能就是帮助你安全的从你想要禁用/破坏一个碰撞/查询回调的空间中移除对象。

后步回调被注册为一个函数和用作键的一个指针。你只能为每个键注册一个postStep()回调。这可以防止你不小心多次删除对象。例如,假设你有子弹和对象A之间的碰撞回调。你想摧毁子弹和对象A,因此你注册一个postStep()回调来从游戏中安全地移除它们。然后,你得到子弹和对象B之间的碰撞回调,你注册一个postStep()回调来删除对象B,第二次postStep()回调来移除子弹。因为你只能为每个键注册一次回调, postStep()回调对于子弹而言只会被调用一次,你不可能意外删除两次。

11.4 例子

更多信息请查看碰撞回调范例。

12. Chipmunk碰撞对:cpArbiter

Chipmunk的cpArbiter结构封装了一对碰撞的形状和关于它们的所有碰撞数据。

为什么称之为仲裁者?简短来说,我一直用的是“仲裁”来形容碰撞解决的方式,然后早在2006年当我在看Box2D的求解器的时候看到了Box2D居然叫它们仲裁者。仲裁者就像是一个法官,有权力来解决两个人之间的纠纷。这是有趣的,使用了合适的名字并且输入比我以前用的CollisionPair要短。它最初只是被设定为一个私有的内部结构,但却在回调中很有用。

12.1 内存管理

你永远不需要创建或释放一个仲裁者。更重要的是,因为它们完全由空间管理,所以你永远不应该存储一个仲裁者的引用,因为你不知道它们什么时候会被释放或重新使用。在回调中使用它们,然后忘记它们或复制出你需要的信息。

12.2 属性

cpFloat cpArbiterGetElasticity(const cpArbiter *arb)
void cpArbiterSetElasticity(cpArbiter *arb, cpFloat value)

计算碰撞对的弹性。在preSolve()回调中设定该值将会覆盖由空间计算的值。默认计算会将两个形状的弹性相乘。

cpFloat cpArbiterGetFriction(const cpArbiter *arb)
void cpArbiterSetFriction(cpArbiter *arb, cpFloat value)

计算碰撞对的摩擦力。在preSolve()回调中设定该值将会覆盖由空间计算的值。默认计算会将两个形状的摩擦力相乘。

cpVect cpArbiterGetSurfaceVelocity(const cpArbiter *arb)
void cpArbiterSetSurfaceVelocity(cpArbiter *arb, cpVect value)

计算碰撞对的表面速度。在preSolve()回调中设定该值将会覆盖由空间计算的值。默认计算会将第二个形状的表面速度从第一个形状的表面速度中减去,然后投射到碰撞的切线上。这使得只有摩擦力受到默认计算的影响。使用自定义计算,你可以使得响应就像一个弹球保险杠一样,或使得表面速度依赖于接触点的位置。

注:不幸的是,有一个老的bug会让表面速度计算逆向(负值)。我真的很久没有注意到这点了。这将在Chipmunk7中得到修正,但现在由于向后兼容的原因我已经先不管它了。

cpDataPointer cpArbiterGetUserData(const cpArbiter *arb)
void cpArbiterSetUserData(cpArbiter *arb, cpDataPointer data)

用户自定义指针。该值将维持形状对直到separate()回调被调用。

注:如果你需要清理这个指针,你应该实现separate()回调来这么做。同时在摧毁空间的时候要小心,因为仍然有可能有激活的碰撞存在。为了触发separate()回调,在处置它之前你需要先移除空间中的所有形状。这正是我建议的方式。见ChipmunkDemo.cChipmunkDemoFreeSpaceChildren()演示了如何轻松做到这一点。

int cpArbiterGetCount(const cpArbiter *arb)
cpVect cpArbiterGetNormal(const cpArbiter *arb, int i)
cpVect cpArbiterGetPoint(const cpArbiter *arb, int i)
cpFloat cpArbiterGetDepth(const cpArbiter *arb, int i)

得到由这仲裁者或特定碰撞点,碰撞点的法向量或深度穿透跟踪的触点的数目。

cpBool cpArbiterIsFirstContact(const cpArbiter *arb)

如果这是两个形状开始接触的第一步则返回true。举例来说这对于声音效果很有用。如果这是特定碰撞的第一帧,在postStep()回调中检测碰撞能量,并用它来确定播放的声效音量。

void cpArbiterGetShapes(const cpArbiter *arb, cpShape **a, cpShape **b)
void cpArbiterGetBodies(const cpArbiter *arb, cpBody **a, cpBody **b)

按照形状或者刚体在该仲裁者关联的碰撞对中定义的顺序一样得到它们。如果你像cpSpaceAddCollisionHandler(space, 1, 2, …)定义了个函数,你会发现a->collision_type == 1b->collision_type == 2

碰撞回调例子

static void
postStepRemove(cpSpace *space, cpShape *shape, void *unused)
{
  cpSpaceRemoveShape(space, shape);
  cpSpaceRemoveBody(space, shape->body);

  cpShapeFree(shape);
  cpBodyFree(shape->body);
}

static int
begin(cpArbiter *arb, cpSpace *space, void *unused)
{
  // 得到参与碰撞的形状
  // 顺序和你在函数定义中的顺序一致
  // a->collision_type将是BULLET_TYPE, b->collision_type将是MONSTER_TYPE 
  CP_ARBITER_GET_SHAPES(arb, a, b);

  // 宏展开后和下面输入一样
  // cpShape *a, *b; cpArbiterGetShapes(arb, &a, &b);

  // 添加一个后步回调来安全从空间中移除和刚体
  // 直接从碰撞处理函数回调中调用 cpSpaceRemove() 会引起崩溃
  cpSpaceAddPostStepCallback(space, (cpPostStepFunc)postStepRemove, b, NULL);

  // 物体死亡,不再处理碰撞
  return 0;
}

#define BULLET_TYPE 1
#define MONSTER_TYPE 2

// 为子弹和怪物定义一个碰撞处理函数
// 一旦怪物被子弹击中则通过移除它的形状和刚体来立马杀死怪物
cpSpaceAddCollisionHandler(space, BULLET_TYPE, MONSTER_TYPE, begin, NULL, NULL, NULL, NULL);

12.3 触点集

通过触点集我们得到接触信息变得更为容易。

cpContactPointSet cpArbiterGetContactPointSet(const cpArbiter *arb)

从仲裁者中得到的触点集结构域。

你可能通过下面的方式来得到并且处理一个触点集:

cpContactPointSet set = cpArbiterGetContactPointSet(arbiter);
for(int i=0; i<set.count; i++){
    // 得到并使用触点的法向量和穿透距离
    set.points[i].point
    set.points[i].normal
    set.points[i].dist
}
void cpArbiterSetContactPointSet(cpArbiter *arb, cpContactPointSet *set)

替换仲裁者的触点集。你不能改变触点的数目,但是可以改变它们的位置,法向量或穿透距离。Sticky演示使用它来使得物体能够获得额外量的重叠。你也可以在乒乓式风格游戏中使用它来修改基于碰撞x轴的碰撞法向量,即使板子是扁平形状。

12.4 帮助函数

void cpArbiterGetShapes(cpArbiter *arb, cpShape **a, cpShape **b)
void cpArbiterGetBodies(const cpArbiter *arb, cpBody **a, cpBody **b)

得到在仲裁者关联的碰撞处理中所定义的形状(或者刚体)。如果你定义了一个处理函数如cpSpaceAddCollisionHandler(space, 1, 2, …),你会发现a->collision_type == 1并且b->collision_type == 2。便捷的宏为你定义并且初始化了两个形状变量。默认的碰撞处理函数不会使用碰撞类型,所以顺序是未定义的。

#define CP_ARBITER_GET_SHAPES(arb, a, b) cpShape *a, *b; cpArbiterGetShapes(arb, &a, &b)
#define CP_ARBITER_GET_BODIES(arb, a, b) cpBody *a, *b; cpArbiterGetBodies(arb, &a, &b);

定义变量并且从仲裁者中检索形状/刚体所用的缩略宏。

cpVect cpArbiterTotalImpulseWithFriction(cpArbiter *arb);
cpVect cpArbiterTotalImpulse(cpArbiter *arb);

返回为解决碰撞而施加于此步骤的冲量。这些函数应该只在postStep()cpBodyEachArbiter()回调中被调用,否则结果将不可确定。如有疑问不知道该使用哪个函数,就使用cpArbiterTotalImpulseWithFriction()

cpFloat cpArbiterTotalKE(const cpArbiter *arb);

计算在碰撞中的能量损失值,包括静摩擦不包括动摩擦。这个函数应该在postSolve()postStep()或者cpBodyEachArbiter()回调中被调用。

13. 查询

Chipmunk空间支持4种空间查询,包括最近点查询、线段查询、形状查询和快速包围盒查询。任何一种类型都可在空间里有效运行,并且点和线段查询可以针对单个形状来进行。所有类型的查询需要一个碰撞组和层,并使用和过滤形状间碰撞一样的规则来过滤出匹配。查看 cpShape 来获得更多信息。如果你不希望过滤掉任何匹配,使用CP_ALL_LAYERS作为层,CP_NO_GROUP作为组。

13.1 最近点查询

点查询对于像鼠标拾取和简单的感应器来说非常有用。它允许你检查离给定点一定距离内是否存在着形状,找到形状上离给定点最近的点或者找到离给定点最近的形状。

typedef struct cpNearestPointQueryInfo {
    /// 最近的形状。如果在范围内没有形状返回NULL。
    cpShape *shape;
    /// 形状表面上最近点(世界坐标系)
    cpVect p;
    /// 离给定点的距离。如果点在形状内部距离则为负值
    cpFloat d;
    /// 距离函数的梯度
    /// 和info.p/info.d相同,即使当info.d是非常小的值时,仍然精确
    cpVect g;
} cpNearestPointQueryInfo;

13.2 线段查询

线段查询就像射线投射一样,但由于并非所有的空间索引都允许处理无限长的射线查询所以它仅限于线段。在实践中这仍然非常快速,你不用过多的担心过长的线段查询会影响到性能。

typedef struct cpSegmentQueryInfo {
    //碰撞的形状,如果没有碰撞发生则为NULL
    const cpShape *shape;
    /// 碰撞点
    cpVect point;
    // 表面命中点的法向量
    cpVect normal;
    // 线段查询的归一化距离,在[0,1]范围内
    cpFloat alpha;
} cpSegmentQueryInfo;

分类查询返回的信息不只是一个简单的是或否,它们也会返回形状被击中的位置以及被击中位置的表面的法向量。t是该查询的开始点和结束点之间的百分比。如果你需要世界空间中的击中点或者到开始点的绝对距离,可以查看下面的线段查询帮助函数。如果线段查询的开始点在形状内部,则t = 0并且n = cpvzero

cpBool cpShapeSegmentQuery(cpShape *shape, cpVect a, cpVect b, cpSegmentQueryInfo *info)

执行从ab的线段与单一形状shape的线段查询。info必须是一个指向cpSegmentQueryInfo结构体的有效的指针,该结构体会被光线投射信息(raycast info)初始化。

typedef void (*cpSpaceSegmentQueryFunc)(cpShape *shape, cpFloat t, cpVect n, void *data)

void cpSpaceSegmentQuery(
    cpSpace *space, cpVect start, cpVect end, cpFloat radius,
    cpShapeFilter filter,
    cpSpaceSegmentQueryFunc func, void *data
)

沿着线段的startend使用给定的radius来查询space过滤筛选出匹配。func函数被调用,附带着线段和任何被发现的形状表面的法向量之间的归一化距离,还有传递给cpSpacePointQuery()的data参数。传感器类形状也被包括在内。

cpShape *cpSpaceSegmentQueryFirst(
    cpSpace *space, cpVect start, cpVect end, cpFloat radius,
    cpShapeFilter filter,
    cpSegmentQueryInfo *info
)

沿着线段的startend使用给定的radius来查询space过滤筛选出匹配。只有遇到的第一个形状会被返回并结束搜索,如果没有发现形状则返回NULLinfo指向的结构体将会被光线投射信息初始化,除非infoNULL。传感器类形状将被忽略。

线段查询辅助函数:

cpVect cpSegmentQueryHitPoint(cpVect start, cpVect end, cpSegmentQueryInfo info)

返回在世界坐标系内线段与形状相交的第一个相交点。

cpFloat cpSegmentQueryHitDist(cpVect start, cpVect end, cpSegmentQueryInfo info)

返回线段与形状第一个相交点的绝对距离。

13.3 AABB查询

AABB查询提供一个快速的方式来粗略检测一个范围内存在的形状。

typedef void (*cpSpaceBBQueryFunc)(cpShape *shape, void *data)

void cpSpaceBBQuery(
    cpSpace *space, cpBB bb,
    cpShapeFilter filter,
    cpSpaceBBQueryFunc func, void *data
)

查询space找到bb附近所有的形状。过滤被应用到了查询上并且和碰撞检测遵循同样的规则。每个包围盒和bb有重叠的形状,都会调用func, 并将data参数传给cpSpaceBBQuery()。传感器类形状也包括在内。

13.4 形状查询

形状查询允许你检测空间中的形状是否和一个指定的区域发生了重叠。如果你想在该位置添加另外一个形状,又或者在AI中使用它进行感应查询的话。你可以通过形状查询来检测物体是否已经存在于一个位置上。

在查询前,你可以创建一个刚体对或者形状对,或者你创建一个shape值为NULL的刚体,通过调用cpShapeUpdate()函数来设置形状的位置和旋转角度。

typedef void (*cpSpaceShapeQueryFunc)(cpShape *shape, cpContactPointSet *points, void *data);

cpBool cpSpaceShapeQuery(cpSpace *space, cpShape *shape, cpSpaceShapeQueryFunc func, void *data);

查询space来找到和shape重叠的所有形状。使用shape的层和群组来过滤筛选得到匹配。func函数由每个重叠的形状调用,附带一个临时的cpContactPointSet的一个指针和传递给cpSpaceBBQuery()data参数。传感器类形状也包括在内。

13.5 闭包

如果你的编译器支持闭包(如Clang), 还有另外一组函数可以调用,如cpSpaceNearestPointQuery_b()等。详情请参考chipmunk.h