引言
在游戏开发中,碰撞检测与碰撞响应是两个既有联系又职责不同的概念。传统做法中,物理引擎往往在检测到碰撞的同时自动产生响应(如反弹、阻挡),这在多数情况下很方便,但在某些复杂逻辑中会导致紧耦合、难以维护的问题。例如:
-
玩家穿过一个“陷阱区域”应该触发扣血,但不应被物理阻挡;
-
子弹击中敌人应销毁子弹并加分,但子弹可穿过多数物体;
-
触发器(Trigger)是一种特殊的碰撞体,只检测重叠而不产生物理响应;碰撞响应则是刚体间的力学交互(如反弹、摩擦)。将二者分离,能让逻辑更清晰、组合更灵活。Cocos2d 通过 PhysicsBody的 isDynamic、categoryBitmask、collisionBitmask、contactTestBitmask等属性,可以轻松实现触发器与碰撞响应的分离。
技术背景
1. Cocos2d 物理系统
Cocos2d 支持 Box2D 与 Chipmunk 两种 2D 物理引擎,核心是:
-
-
PhysicsBody:赋予 Node 物理属性(质量、速度、受力等)。
-
PhysicsShape:定义碰撞几何(矩形、圆形、多边形)。
-
Contact 监听:当两个物体的碰撞掩码匹配时触发回调函数。
2. 碰撞掩码机制
-
categoryBitmask:物体的类别标识(如玩家=1,敌人=2,陷阱=4)。
-
collisionBitmask:决定与哪些类别发生物理碰撞(会阻挡/反弹)。
-
contactTestBitmask:决定与哪些类别触发接触事件(可用于触发器检测)。
-
若 A 的
collisionBitmask与 B 的 categoryBitmask有交集 → 发生物理碰撞。
-
若 A 的
contactTestBitmask与 B 的 categoryBitmask有交集 → 触发 onContactBegin事件(可用于触发器逻辑)。
3. 触发器实现原理
触发器 = 静态或动态的 PhysicsBody + 不参与物理碰撞 (collisionBitmask=0) + 参与接触检测 (contactTestBitmask匹配目标类别)。
这样物体可穿透触发器,但会触发回调,在回调里写业务逻辑(扣血、加分、剧情等)。
应用使用场景
不同场景下详细代码实现
场景 1:陷阱区域(触发器)与墙体(物理碰撞响应)分离
技术要点
-
墙体:
collisionBitmask包含玩家类别 → 阻挡玩家。
-
陷阱:
collisionBitmask=0(不与任何物体物理碰撞),但 contactTestBitmask包含玩家类别 → 触发扣血。
C++ 完整代码
// TriggerDemo.h
#ifndef __TRIGGER_DEMO_H__
#define __TRIGGER_DEMO_H__
#include "cocos2d.h"
#include "physics/CCPhysicsWorld.h"
#include "physics/CCPhysicsBody.h"
USING_NS_CC;
class TriggerDemo : public Layer {
public:
static Scene* createScene();
virtual bool init() override;
CREATE_FUNC(TriggerDemo);
private:
void onContactBegin(PhysicsContact& contact);
};
#endif
// TriggerDemo.cpp
#include "TriggerDemo.h"
Scene* TriggerDemo::createScene() {
auto scene = Scene::createWithPhysics();
auto layer = TriggerDemo::create();
scene->addChild(layer);
// 物理世界重力
auto physicsWorld = scene->getPhysicsWorld();
physicsWorld->setGravity(Vec2(0, 0)); // 2D 平面无重力示例
// 启用接触监听
physicsWorld->setContactListener(layer);
return scene;
}
bool TriggerDemo::init() {
if (!Layer::init()) return false;
auto visibleSize = Director::getInstance()->getVisibleSize();
// 玩家(动态刚体,会与墙体碰撞)
auto player = Sprite::create("player.png");
player->setPosition(visibleSize.width / 2, visibleSize.height - 100);
addChild(player);
auto playerBody = PhysicsBody::createCircle(30, PHYSICSSHAPE_MATERIAL_DEFAULT);
playerBody->setDynamic(true);
playerBody->setCategoryBitmask(0x01); // 玩家类别 1
playerBody->setCollisionBitmask(0x02); // 与墙体(类别2)碰撞
playerBody->setContactTestBitmask(0x04); // 与陷阱(类别4)触发接触事件
player->setPhysicsBody(playerBody);
// 墙体(静态刚体,阻挡玩家)
auto wall = Sprite::create("wall.png");
wall->setPosition(visibleSize.width / 2, 150);
addChild(wall);
auto wallBody = PhysicsBody::createBox(Size(visibleSize.width, 20), PHYSICSSHAPE_MATERIAL_DEFAULT);
wallBody->setDynamic(false);
wallBody->setCategoryBitmask(0x02); // 墙体类别 2
wallBody->setCollisionBitmask(0x01); // 与玩家(类别1)碰撞
wallBody->setContactTestBitmask(0x00); // 不需要接触事件
wall->setPhysicsBody(wallBody);
// 陷阱区域(触发器,无物理碰撞,只触发事件)
auto trap = Node::create(); // 无精灵,仅逻辑区域
trap->setPosition(visibleSize.width / 2, 300);
addChild(trap);
auto trapBody = PhysicsBody::createBox(Size(200, 100), PHYSICSSHAPE_MATERIAL_DEFAULT);
trapBody->setDynamic(false);
trapBody->setCategoryBitmask(0x04); // 陷阱类别 4
trapBody->setCollisionBitmask(0x00); // 不与任何物体物理碰撞
trapBody->setContactTestBitmask(0x01); // 与玩家(类别1)触发接触事件
trap->setPhysicsBody(trapBody);
// 可选:绘制陷阱区域边框(调试用)
auto draw = DrawNode::create();
draw->drawRect(Vec2(-100, -50), Vec2(100, 50), Color4F::RED);
trap->addChild(draw);
return true;
}
// 接触回调
bool TriggerDemo::onContactBegin(PhysicsContact& contact) {
auto bodyA = contact.getShapeA()->getBody();
auto bodyB = contact.getShapeB()->getBody();
// 获取类别
int catA = bodyA->getCategoryBitmask();
int catB = bodyB->getCategoryBitmask();
// 判断是否是玩家(1)与陷阱(4)接触
if ((catA == 0x01 && catB == 0x04) || (catA == 0x04 && catB == 0x01)) {
log("玩家进入陷阱,扣血!");
// 这里可调用玩家扣血函数
return false; // 返回false表示不处理物理碰撞(本来也不会碰撞)
}
return true;
}
场景 2:收集物品(触发器)与子弹(穿透型碰撞响应)
Lua 完整代码(Chipmunk)
-- TriggerCollect.lua
local TriggerCollect = class("TriggerCollect", function()
return display.newScene("TriggerCollect")
end)
function TriggerCollect:ctor()
self.physicsWorld = cc.PhysicsWorld:new()
self.physicsWorld:setGravity(cc.p(0, 0))
self:addChild(self.physicsWorld)
self.physicsWorld:setContactListener(self) -- 设置接触监听
self:createPlayer()
self:createCoin()
self:createBullet()
end
function TriggerCollect:createPlayer()
local player = display.newSprite("player.png")
player:setPosition(display.cx, display.cy + 100)
self:addChild(player)
local body = cc.PhysicsBody:createCircle(30)
body:setDynamic(true)
body:setCategoryBitmask(0x01) -- 玩家
body:setCollisionBitmask(0x02) -- 与墙体碰撞
body:setContactTestBitmask(0x08) -- 与金币触发
player:setPhysicsBody(body)
end
function TriggerCollect:createCoin()
local coin = display.newSprite("coin.png")
coin:setPosition(display.cx + 150, display.cy)
self:addChild(coin)
local body = cc.PhysicsBody:createCircle(20)
body:setDynamic(false)
body:setCategoryBitmask(0x08) -- 金币
body:setCollisionBitmask(0x00) -- 无物理碰撞(触发器)
body:setContactTestBitmask(0x01) -- 与玩家触发
coin:setPhysicsBody(body)
end
function TriggerCollect:createBullet()
local bullet = display.newSprite("bullet.png")
bullet:setPosition(display.cx - 150, display.cy)
self:addChild(bullet)
local body = cc.PhysicsBody:createCircle(10)
body:setDynamic(true)
body:setVelocity(cc.p(200, 0)) -- 向右飞行
body:setCategoryBitmask(0x10) -- 子弹
body:setCollisionBitmask(0x20) -- 只与敌人(类别32)碰撞
body:setContactTestBitmask(0x20) -- 与敌人触发伤害
bullet:setPhysicsBody(body)
end
-- 接触回调
function TriggerCollect:onContactBegin(contact)
local shapeA = contact:getShapeA()
local shapeB = contact:getShapeB()
local bodyA = shapeA:getBody()
local bodyB = shapeB:getBody()
local catA = bodyA:getCategoryBitmask()
local catB = bodyB:getCategoryBitmask()
-- 玩家与金币
if (catA == 0x01 and catB == 0x08) or (catA == 0x08 and catB == 0x01) then
print("收集金币!")
-- 移除金币
if catA == 0x08 then bodyA:getNode():removeFromParent() end
if catB == 0x08 then bodyB:getNode():removeFromParent() end
return false
end
-- 子弹与敌人(假设敌人类似设置 category=0x20)
if (catA == 0x10 and catB == 0x20) or (catA == 0x20 and catB == 0x10) then
print("子弹命中敌人")
-- 销毁子弹与敌人
bodyA:getNode():removeFromParent()
bodyB:getNode():removeFromParent()
return false
end
return true
end
return TriggerCollect
原理解释
-
-
物理碰撞由
collisionBitmask控制,决定是否阻挡/反弹。
-
触发器逻辑由
contactTestBitmask控制,决定是否进入 onContactBegin回调。
-
将
collisionBitmask设为 0 可完全避免物理阻挡,只保留检测。
-
-
物理世界每帧检测所有 Shape 重叠 → 检查
collisionBitmask决定是否产生力学响应 → 检查 contactTestBitmask决定是否触发回调 → 回调中执行业务逻辑。
-
-
逻辑与物理解耦,触发器区域可随意放置,不影响原有物理。
-
可组合多种触发条件(进入、停留、离开)通过回调实现。
核心特性
原理流程图
graph TD
A[物理世界更新] --> B[检测所有Shape重叠]
B --> C{检查collisionBitmask重叠?}
C -->|是| D[产生物理响应(阻挡/反弹)]
C -->|否| E[无物理响应]
B --> F{检查contactTestBitmask重叠?}
F -->|是| G[触发onContactBegin回调]
F -->|否| H[无回调]
G --> I[执行触发器逻辑(扣血/加分/剧情)]
D & E & I --> J[下一帧继续]
环境准备
-
引擎:Cocos2d-x 3.17+(C++)、Cocos2d-Lua 3.6+
-
-
配置:启用物理模块(
cocos2d_physics链接)
-
实际详细应用代码示例实现(Cocos Creator TS)
// TriggerSeparation.ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class TriggerSeparation extends cc.Component {
@property(cc.Prefab)
playerPrefab: cc.Prefab = null;
@property(cc.Prefab)
trapPrefab: cc.Prefab = null;
start() {
cc.director.getPhysicsManager().enabled = true;
cc.director.getPhysicsManager().debugDrawFlags = cc.PhysicsManager.DrawBits.e_shapeBit;
this.spawnPlayer();
this.spawnTrap();
}
spawnPlayer() {
const player = cc.instantiate(this.playerPrefab);
player.setPosition(0, 100);
this.node.addChild(player);
const body = player.getComponent(cc.RigidBody);
body.type = cc.RigidBodyType.Dynamic;
body.linearDamping = 1;
// 碰撞掩码
body.collisionFilter.categoryBitMask = 0x01;
body.collisionFilter.maskBits = 0x02; // 与墙体碰撞
// 接触掩码
body.contactTestBitmask = 0x04; // 与陷阱触发
}
spawnTrap() {
const trap = cc.instantiate(this.trapPrefab);
trap.setPosition(0, 0);
this.node.addChild(trap);
const body = trap.getComponent(cc.RigidBody);
body.type = cc.RigidBodyType.Static;
body.collisionFilter.categoryBitMask = 0x04;
body.collisionFilter.maskBits = 0x00; // 无物理碰撞
body.contactTestBitmask = 0x01; // 与玩家触发
}
// 注册接触回调(需在编辑器中绑定或代码注册)
onBeginContact(contact: cc.PhysicsContact, selfCollider: cc.Collider, otherCollider: cc.Collider) {
const catSelf = selfCollider.body.collisionFilter.categoryBitMask;
const catOther = otherCollider.body.collisionFilter.categoryBitMask;
if ((catSelf === 0x01 && catOther === 0x04) || (catSelf === 0x04 && catOther === 0x01)) {
console.log('Trigger: Player entered trap');
}
}
}
运行结果
-
玩家可自由移动,碰到墙体被阻挡,进入陷阱区域时在控制台打印 “玩家进入陷阱,扣血!” 或 “Trigger: Player entered trap”。
-
金币可被穿过并收集,子弹可穿透普通物体只与敌人碰撞。
测试步骤以及详细代码
-
-
-
-
部署场景
-
PC/移动端游戏:用于复杂关卡逻辑、 RPG 触发区、塔防陷阱。
-
-
疑难解答
|
|
|
|
|
|
|
|
|
|
|
触发器必须设 collisionBitmask=0
|
|
|
|
|
|
|
|
|
未来展望
-
可视化触发器编辑器:在 Cocos Creator 中直接绘制并配置掩码。
-
区域事件扩展:支持“进入”“停留”“离开”多状态触发器。
-
技术趋势与挑战
-
趋势:更细粒度的碰撞事件(如触发优先级、条件过滤)。
-
总结
触发器与碰撞响应分离是 Cocos2d 物理系统中实现逻辑与物理解耦的关键技术,通过合理配置 collisionBitmask与 contactTestBitmask,可灵活实现陷阱、收集品、剧情区等玩法。本文提供了 C++、Lua、TypeScript 完整可运行代码,覆盖常见应用场景,并解释了原理与最佳实践,帮助开发者在项目中高效使用该技术。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
评论(0)