cocos2dx像素碰撞检测
引言
在开发游戏时,碰撞检测是一个至关重要的功能,它几乎存在于所有类型的游戏中,例如判断子弹是否击中敌人、玩家是否撞到墙壁等。在 Cocos2d-x 中,有多种碰撞检测技术可供选择,而本文将重点介绍像素级完美碰撞检测。我之所以写这篇文章,是因为我在 Stack Overflow(http://stackoverflow.com/questions/18546066/pixel-perfect-collision-detection-in-cocos2dx)上提出的关于像素级完美碰撞检测的问题没有得到满意的答案,并且我认为可能还有其他开发者也在寻找相关的解决方案。
碰撞检测技术概述
边界框碰撞检测
大多数游戏引擎和框架默认使用的碰撞检测机制是“边界框”碰撞检测。简单来说,在这种检测系统中,被检测碰撞的精灵或对象会被视为能够完全包围它们的最小矩形。然后检查这两个矩形是否发生重叠,如果重叠则认为发生了碰撞。然而,这种简单的碰撞检测系统有时并不准确,特别是当使用带有 alpha 值的精灵(通常是 PNG 文件)或者对象被旋转了一定角度时。
像素级完美碰撞检测
像素级完美碰撞检测则会检查相关对象是否真正发生了碰撞,而不仅仅是它们的边界框重叠。需要注意的是,这种检测系统虽然更准确,但显然会消耗更多的性能。因此,在实际开发中,需要根据游戏的具体需求来明智地选择使用哪种碰撞检测系统。
实现思路
我们将开发一个单例类用于碰撞检测,并且可以将其轻松集成到任何项目中。实现过程中会用到以下技术和类:
- 单例类:
CollisionDetection - OpenGL 顶点和片段着色器
- Cocos2d-x 的 CCRenderTexture 类
理论步骤
- 创建辅助绘制缓冲区:创建一个
CCRenderTexture对象,作为辅助绘制缓冲区。 - 初步边界框检测:首先进行简单的边界框碰撞检测,检查两个精灵的边界框是否重叠。
- 绘制对象到缓冲区:如果边界框检测成功,则将两个相关对象绘制到第一步创建的辅助缓冲区中。同时,将其可见性设置为
false,这样即使绘制了内容,最终用户也不会看到。 - 使用着色器设置颜色:使用 OpenGL 片段着色器将其中一个对象完全绘制为红色,另一个对象完全绘制为蓝色。
- 读取像素数据:使用 OpenGL 的
glReadPixels函数读取边界框碰撞区域(交集区域)内所有像素的数据。 - 检查像素碰撞:遍历所有像素值,检查是否有单个像素同时包含红色和蓝色。如果有,则认为对象真正发生了碰撞;否则,没有碰撞。
代码实现
CollisionDetection.h
//
// CollisionDetection.h
// Created by Mudit Jaju on 30/08/13.
//
// SINGLETON class for checking Pixel Based Collision Detection
#ifndef __CollisionDetection__
#define __CollisionDetection__
#include <iostream>
#include "cocos2d.h"
USING_NS_CC;
class CollisionDetection {
public:
// Handle for getting the Singleton Object
static CollisionDetection* GetInstance();
// Function signature for checking for collision detection
// spr1, spr2 are the concerned sprites
// pp is bool, set to true if Pixel Perfection Collision is required. Else set to false
// _rt is the secondary buffer used in our system
bool areTheSpritesColliding(CCSprite* spr1, CCSprite* spr2, bool pp, CCRenderTexture* _rt);
private:
static CollisionDetection* instance;
CollisionDetection();
// Values below are all required for openGL shading
CCGLProgram *glProgram;
ccColor4B *buffer;
int uniformColorRed;
int uniformColorBlue;
};
#endif /* defined(__CollisionDetection__) */
CollisionDetection.cpp
//
// CollisionDetection.cpp
// Created by Mudit Jaju on 30/08/13.
//
// SINGLETON class for checking Pixel Based Collision Detection
#include "CollisionDetection.h"
// Singleton Instance set to NULL initially
CollisionDetection* CollisionDetection::instance = NULL;
// Handle to get Singleton Instance
CollisionDetection* CollisionDetection::GetInstance() {
if (instance == NULL) {
instance = new CollisionDetection();
}
return instance;
}
// Private Constructor being called from within the GetInstance handle
CollisionDetection::CollisionDetection() {
// Code below to setup shaders for use in Cocos2d-x
glProgram = new CCGLProgram();
glProgram->retain();
glProgram->initWithVertexShaderFilename("SolidVertexShader.vsh", "SolidColorShader.fsh");
glProgram->addAttribute(kCCAttributeNamePosition, kCCVertexAttrib_Position);
glProgram->addAttribute(kCCAttributeNameTexCoord, kCCVertexAttrib_TexCoords);
glProgram->link();
glProgram->updateUniforms();
glProgram->use();
uniformColorRed = glGetUniformLocation(glProgram->getProgram(), "u_color_red");
uniformColorBlue = glGetUniformLocation(glProgram->getProgram(), "u_color_blue");
// A large buffer created and re-used again and again to store glReadPixels data
buffer = (ccColor4B *)malloc( sizeof(ccColor4B) * 10000 );
}
bool CollisionDetection::areTheSpritesColliding(cocos2d::CCSprite* spr1, cocos2d::CCSprite* spr2, bool pp, CCRenderTexture* _rt) {
bool isColliding = false;
// Rectangle of the intersecting area if the sprites are colliding according to Bounding Box collision
CCRect intersection;
// Bounding box of the Two concerned sprites being saved
CCRect r1 = spr1->boundingBox();
CCRect r2 = spr2->boundingBox();
// Look for simple bounding box collision
if (r1.intersectsRect(r2)) {
// If we're not checking for pixel perfect collisions, return true
if (!pp) {
return true;
}
float tempX;
float tempY;
float tempWidth;
float tempHeight;
// OPTIMIZE FURTHER
// CONSIDER THE CASE WHEN ONE BOUDNING BOX IS COMPLETELY INSIDE ANOTHER BOUNDING BOX!
if (r1.getMaxX() > r2.getMinX()) {
tempX = r2.getMinX();
tempWidth = r1.getMaxX() - r2.getMinX();
} else {
tempX = r1.getMinX();
tempWidth = r2.getMaxX() - r1.getMinX();
}
if (r2.getMaxY() < r1.getMaxY()) {
tempY = r1.getMinY();
tempHeight = r2.getMaxY() - r1.getMinY();
} else {
tempY = r2.getMinY();
tempHeight = r1.getMaxY() - r2.getMinY();
}
// We make the rectangle for the intersection area
intersection = CCRectMake(tempX * CC_CONTENT_SCALE_FACTOR(), tempY * CC_CONTENT_SCALE_FACTOR(), tempWidth * CC_CONTENT_SCALE_FACTOR(), tempHeight * CC_CONTENT_SCALE_FACTOR());
unsigned int x = intersection.origin.x;
unsigned int y = intersection.origin.y;
unsigned int w = intersection.size.width;
unsigned int h = intersection.size.height;
// Total pixels whose values we will get using glReadPixels depends on the Height and Width of the intersection area
unsigned int numPixels = w * h;
// Setting the custom shader to be used
spr1->setShaderProgram(glProgram);
spr2->setShaderProgram(glProgram);
glProgram->use();
// Clearing the Secondary Draw buffer of all previous values
_rt->beginWithClear( 0, 0, 0, 0);
// The below two values are being used in the custom shaders to set the value of RED and BLUE colors to be used
glUniform1i(uniformColorRed, 255);
glUniform1i(uniformColorBlue, 0);
// The blend function is important we don't want the pixel value of the RED color being over-written by the BLUE color.
// We want both the colors at a single pixel and hence get a PINK color (so that we have both the RED and the BLUE pixels)
spr1->setBlendFunc((ccBlendFunc){GL_SRC_ALPHA, GL_ONE});
// The visit() function draws the sprite in the _rt draw buffer its a Cocos2d-x function
spr1->visit();
// Setting the shader program back to the default shader being used by our game
spr1->setShaderProgram(CCShaderCache::sharedShaderCache()->programForKey(kCCShader_PositionTextureColor));
// Setting the default blender function being used by the game
spr1->setBlendFunc((ccBlendFunc){CC_BLEND_SRC, CC_BLEND_DST});
// Setting new values for the same shader but for our second sprite as this time we want to have an all BLUE sprite
glUniform1i(uniformColorRed, 0);
glUniform1i(uniformColorBlue, 255);
spr2->setBlendFunc((ccBlendFunc){GL_SRC_ALPHA, GL_ONE});
spr2->visit();
spr2->setShaderProgram(CCShaderCache::sharedShaderCache()->programForKey(kCCShader_PositionTextureColor));
spr2->setBlendFunc((ccBlendFunc){CC_BLEND_SRC, CC_BLEND_DST});
// Get color values of intersection area
glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
_rt->end();
// Read buffer
unsigned int step = 1;
for(unsigned int i = 0; i < numPixels; i+=step) {
ccColor4B color = buffer[i];
// Here we check if a single pixel has both RED and BLUE pixels
if (color.r > 0 && color.b > 0) {
isColliding = true;
break;
}
}
}
return isColliding;
}
SolidColorShader.fsh
#ifdef GL_ES
precision lowp float;
#endif
varying vec2 v_texCoord;
uniform sampler2D u_texture;
uniform int u_color_red;
uniform int u_color_blue;
void main()
{
vec4 color = texture2D(u_texture, v_texCoord);
gl_FragColor = vec4(u_color_red, 0, u_color_blue, color.a);
}
SolidVertexShader.vsh
attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec4 a_color;
#ifdef GL_ES
varying lowp vec4 v_fragmentColor;
varying mediump vec2 v_texCoord;
#else
// 这里原文未完整,推测后续可能还有内容
#endif
总结
通过以上步骤和代码,我们实现了 Cocos2d-x 中的像素级完美碰撞检测。如果你在实现过程中有任何问题,请在评论中留言,我会尽力解答。
需要注意的是,虽然本文的代码是为 Cocos2d-x 框架编写的,但基本思路可以很容易地应用到其他语言或框架中。在实际开发中,要根据游戏的性能需求和具体场景,合理选择碰撞检测的方法。