跳到主要内容

物理与战斗接触回调

相关源文件

目的与范围

本文记录 GameScene 中的物理接触系统与战斗交互处理,内容包括:

  • 物理接触监听的初始化与事件路由
  • CombatContactHelper 作为接触事件的中央处理器
  • 不同接触类型(落地检测、战斗伤害、拾取物品)
  • 命中框(Hitbox)生成与伤害投递机制
  • 用于在物理回调期间安全修改状态的“延迟执行”模式

关于更宏观的 GameScene 初始化流程,请参阅 初始化流水线。关于包含刷怪管理在内的运行时更新逻辑,请参阅 更新循环与运行时逻辑。关于伤害计算公式细节,请参阅 伤害系统


物理接触系统架构

物理接触系统使用 Cocos2d-x 内置物理引擎(Chipmunk)检测碰撞并触发玩法事件。整体采用事件驱动:接触事件被集中处理,然后分发到受影响实体/系统。

系统概览示意图

GameScene 包装方法

GameScene 提供非常薄的 wrapper:把事件转发给 CombatContactHelper,并补齐场景上下文(玩家指针、输入控制器):

Classes/Scenes/GameScene.cpp L854-L862

bool GameScene::onContactBegin(PhysicsContact &contact)
{
return CombatContactHelper::handleContactBegin(contact, _player, _inputController.get());
}

void GameScene::onContactSeparate(PhysicsContact &contact)
{
CombatContactHelper::handleContactSeparate(contact, _inputController.get());
}

来源: Classes/Scenes/GameScene.cpp L524-L535

Classes/Scenes/GameScene.cpp L854-L862


CombatContactHelper:中央接触处理器

CombatContactHelper 是一个无状态工具类,使用静态方法实现全部接触处理逻辑。它把“物理事件处理”从“场景管理”中解耦,使逻辑可以在 GameSceneDebugScene 中复用。

类结构

Classes/Scenes/CombatContactHelper.h

class CombatContactHelper
{
public:
static bool handleContactBegin(PhysicsContact& contact,
PlayerCharacter* player,
GameInputController* inputController);

static void handleContactSeparate(PhysicsContact& contact,
GameInputController* inputController);

static bool handleContactPreSolve(PhysicsContact& contact,
PhysicsContactPreSolve& solve);
};

所有方法均为 static,所需上下文通过参数传入;类本身不保存成员变量。

接触处理流程

来源: Classes/Scenes/CombatContactHelper.cpp L27-L215


接触类型与处理器

CombatContactHelper 主要处理四类接触事件,通过物理分类位掩码区分。

1. 落地接触(Player + Platform)

目的: 用于跳跃机制,追踪玩家是否处于“落地”状态。

Classes/Scenes/CombatContactHelper.cpp L48-L68

const bool playerIsA = (categoryA & ToMask(GamePhysicsCategory::PLAYER)) != 0;
const bool playerIsB = (categoryB & ToMask(GamePhysicsCategory::PLAYER)) != 0;
const bool platformContact =
(playerIsA && ((categoryB & ToMask(GamePhysicsCategory::PLATFORM | GamePhysicsCategory::COLLISION)) != 0)) ||
(playerIsB && ((categoryA & ToMask(GamePhysicsCategory::PLATFORM | GamePhysicsCategory::COLLISION)) != 0));

if (platformContact && inputController)
{
if (auto contactData = contact.getContactData())
{
Vec2 normal = contactData->normal;
if (playerIsB) { normal = -normal; }
inputController->onGroundContactBegin(normal.y);
}
}

Contact Separate 处理:

Classes/Scenes/CombatContactHelper.cpp L217-L262

当玩家离开平台时,handleContactSeparate 会调用 inputController->onGroundContactEnd(normal.y) 更新落地状态。

为什么法线向量重要: 接触法线的 Y 分量决定这是“地面接触”(法线朝上,normalY > threshold)还是墙/顶接触。关于 GameInputController 如何使用该值,请参阅 输入优先级与上下文

来源: Classes/Scenes/CombatContactHelper.cpp L48-L68

Classes/Scenes/CombatContactHelper.cpp L217-L262


2. 怪物攻击 → 玩家

目的: 当怪物攻击命中框与玩家接触时应用伤害。

Classes/Scenes/CombatContactHelper.cpp L70-L123

const bool monsterAttackVsPlayer =
((categoryA & ToMask(GamePhysicsCategory::MONSTER_ATTACK)) != 0 &&
(categoryB & ToMask(GamePhysicsCategory::PLAYER)) != 0) ||
((categoryB & ToMask(GamePhysicsCategory::MONSTER_ATTACK)) != 0 &&
(categoryA & ToMask(GamePhysicsCategory::PLAYER)) != 0);

if (monsterAttackVsPlayer)
{
auto attackBody = /* get MONSTER_ATTACK body */;
auto victim = dynamic_cast<CharacterBase*>(playerNode);

float rawDamage = static_cast<float>(attackBody->getTag());
if (rawDamage <= 0.0f) { rawDamage = 1.0f; }

DamageInfo dmg{};
dmg.amount = rawDamage;
dmg.attacker = dynamic_cast<CharacterBase*>(attackNode->getUserObject());
dmg.hitWorldPos = getWorldPos(attackNode);
dmg.hasHitWorldPos = true;

// Deferred execution (next frame)
std::string key = StringUtils::format("defer_monster_dmg_%p_%p", ...);
if (!victim->isScheduled(key))
{
victim->scheduleOnce(
[victim, dmg](float)
{
if (!victim || victim->isDead()) return;
victim->takeDamage(dmg);
},
0.0f,
key);
}
}

伤害编码: PhysicsBody::getTag() 存储原始伤害(整数)。≤0 时作为兜底处理(1 点伤害)。

攻击者引用: 攻击命中框把攻击者存入 Node::getUserObject()(在怪物生成命中框时设置),以便触发反伤、击杀归属等回调。

来源: Classes/Scenes/CombatContactHelper.cpp L70-L123


3. 玩家攻击 → 怪物

目的: 当玩家攻击命中框与怪物接触时应用伤害。

Classes/Scenes/CombatContactHelper.cpp L159-L213

const bool playerAttackVsMonster =
((categoryA & ToMask(GamePhysicsCategory::PLAYER_ATTACK)) != 0 &&
(categoryB & ToMask(GamePhysicsCategory::MONSTER)) != 0) ||
((categoryB & ToMask(GamePhysicsCategory::PLAYER_ATTACK)) != 0 &&
(categoryA & ToMask(GamePhysicsCategory::MONSTER)) != 0);

if (playerAttackVsMonster && player)
{
auto attackBody = /* get PLAYER_ATTACK body */;
auto monster = dynamic_cast<CharacterBase*>(monsterNode);

float rawDamage = static_cast<float>(attackBody->getTag());
if (rawDamage != 0.0f)
{
const bool isCrit = rawDamage < 0.0f; // Negative = critical hit
rawDamage = std::fabs(rawDamage);

DamageInfo dmg{};
dmg.amount = rawDamage;
dmg.attacker = player;
dmg.isCritical = isCrit;
dmg.breakDamage = std::max(0, attackNode->getTag()); // Node tag stores break damage
dmg.hitWorldPos = getWorldPos(attackNode);
dmg.hasHitWorldPos = true;

// Deferred execution
std::string key = StringUtils::format("defer_player_dmg_%p_%p", ...);
if (!monster->isScheduled(key))
{
monster->scheduleOnce(
[monster, dmg](float)
{
if (!monster || monster->isDead()) return;
monster->takeDamage(dmg);
},
0.0f,
key);
}
}
}

玩家攻击的伤害编码:

数值含义位置
正数普通攻击伤害body tag = 伤害值
负数暴击伤害body tag = -(伤害值)
0忽略(不触发伤害逻辑)body tag = 0

来源: Classes/Scenes/CombatContactHelper.cpp L159-L213


4. 玩家 + 物品拾取

目的: 玩家接触 ITEM 类别时拾取掉落物。

Classes/Scenes/CombatContactHelper.cpp L125-L157

std::string key = StringUtils::format("defer_pickup_%p_%p",
static_cast<void*>(dropItem),
static_cast<void*>(player));
if (!dropItem->isScheduled(key))
{
dropItem->scheduleOnce(
[dropItem, player](float)
{
if (!dropItem || !player || player->isDead())
{
return;
}
dropItem->pickUp(player);
},
0.0f,
key);
}

拾取流程: DropItem::pickUp 会应用 HP/MP 恢复并将自身从场景中移除。

来源: Classes/Scenes/CombatContactHelper.cpp L125-L157


命中框创建与伤害投递

玩家与怪物攻击会生成临时命中框节点,并创建 PLAYER_ATTACKMONSTER_ATTACK 的物理体。命中框存在时间很短(典型 0.1 秒),用于触发接触事件,然后自毁。

玩家命中框生成示例

Classes/Character/Player/SkillSets/WarriorSkillSet.cpp L298-L334

bool WarriorSkillSet::tryNormalAttack(PlayerCharacter& player, ...)
{
const bool isCrit = rollCritical(player);

bool ok = player.runActionLocked(
[]() { return true; },
[&player, isCrit](const std::function<void()>& done)
{
player.scheduleOnce(
[&player, isCrit](float)
{
if (player.isDead())
{
return;
}

const float damage = player.getAttackPower();
const Rect box = player.getBoundingBox();
const float w = std::max(10.0f, box.size.width * GameConfig::Warrior::HITBOX_WIDTH_RATIO);
const float h = std::max(10.0f, box.size.height * GameConfig::Warrior::HITBOX_HEIGHT_RATIO);

const float dirX = player.isFlippedX() ? -1.0f : 1.0f;
const float cx = box.getMidX() + dirX * (box.size.width * GameConfig::Warrior::HITBOX_OFFSET_X_RATIO);
const float cy = box.getMidY() + GameConfig::Warrior::HITBOX_OFFSET_Y;

player.spawnPlayerAttackHitbox(Vec2(cx, cy),
Size(w, h),
damage,
isCrit,
GameConfig::Warrior::HITBOX_LIFE_SECONDS,
GameConfig::Combat::BREAK_DAMAGE_NORMAL);
},
GameConfig::Warrior::HITBOX_DELAY_SECONDS,
"warrior_melee_hitbox");

player.attackAnimated(done);
},
...
);
}

命中框生命周期示意

关键参数:

参数用途编码方式
damage原始伤害值存入 PhysicsBody::getTag()(int,暴击为负)
isCrit是否暴击为 true 时把伤害以负数写入 body tag
lifetime命中框存在时长(秒)典型 0.08-0.1s,用于避免多段重复命中
breakDamageBoss 破防条伤害存入 Node::getTag()

来源: Classes/Character/Player/SkillSets/WarriorSkillSet.cpp L298-L334

Classes/Character/Player/SkillSets/AssassinSkillSet.cpp L87-L139


多段命中防护(Multi-Hit Prevention)

较短的命中框生命周期(0.08-0.1 秒)可防止同一个命中框在同一目标身上触发多次。由于碰撞体可能在多帧重叠,物理引擎可能对同一次重叠触发多次 onContactBegin

额外兜底: 延迟执行逻辑还包含“唯一性检查”:

Classes/Scenes/CombatContactHelper.cpp L196-L209

std::string key = StringUtils::format("defer_player_dmg_%p_%p",
static_cast<void*>(attackBody),
static_cast<void*>(monster));
if (!monster->isScheduled(key))
{
monster->scheduleOnce(
[monster, dmg](float)
{
if (!monster || monster->isDead())
{
return;
}
monster->takeDamage(dmg);
},
0.0f,
key);
}

如果同一对 (attackBody, monster) 在同一帧内触发多次接触,只有第一次会成功安排回调;后续会因为 isScheduled(key) 为 true 而跳过。

来源: Classes/Scenes/CombatContactHelper.cpp L196-L209

Classes/Scenes/CombatContactHelper.cpp L108-L120


延迟执行模式(Deferred Execution Pattern)

所有伤害应用与物品拾取逻辑都通过 scheduleOnce(..., 0.0f, key) 延迟执行。这是一个关键安全模式,用于避免在物理回调期间修改场景图(scene graph)。

为什么必须延迟执行

不延迟会出现的问题:

  1. 物理引擎遍历接触对(contact pairs)
  2. 回调中修改场景图(例如移除角色节点)
  3. 遍历期间注销 PhysicsBody
  4. 迭代器失效 → 崩溃或未定义行为

延迟执行的解决方式:

  1. 物理回调只安排下一帧任务
  2. 物理遍历在无修改的情况下完成
  3. 下一帧安全执行任务

实现:

Classes/Scenes/CombatContactHelper.cpp L110-L121

std::string key = StringUtils::format("defer_monster_dmg_%p_%p",
static_cast<void*>(attackBody),
static_cast<void*>(victim));
if (!victim->isScheduled(key))
{
victim->scheduleOnce(
[victim, dmg](float)
{
if (!victim || victim->isDead())
{
return;
}
victim->takeDamage(dmg);
},
0.0f, // 下一帧执行
key);
}

唯一性 key: format string 中嵌入指针地址,确保每一对 (hitbox, victim) 都获得唯一 key,从而避免同一帧内重复接触导致重复伤害。

来源: Classes/Scenes/CombatContactHelper.cpp L100-L121

Classes/Scenes/CombatContactHelper.cpp L139-L155


PreSolve:调整物理响应

handleContactPreSolve 在物理引擎进行碰撞求解之前修改碰撞响应参数,用于调整玩家移动手感。

Classes/Scenes/CombatContactHelper.cpp L264-L295

bool CombatContactHelper::handleContactPreSolve(PhysicsContact& contact, 
PhysicsContactPreSolve& solve)
{
auto bodyA = contact.getShapeA()->getBody();
auto bodyB = contact.getShapeB()->getBody();
// ... null checks ...

int categoryA = bodyA->getCategoryBitmask();
int categoryB = bodyB->getCategoryBitmask();

const bool playerInvolved = (categoryA & ToMask(GamePhysicsCategory::PLAYER)) ||
(categoryB & ToMask(GamePhysicsCategory::PLAYER));
const bool platformInvolved =
(categoryA & ToMask(GamePhysicsCategory::PLATFORM | GamePhysicsCategory::COLLISION)) ||
(categoryB & ToMask(GamePhysicsCategory::PLATFORM | GamePhysicsCategory::COLLISION));

if (playerInvolved && platformInvolved)
{
solve.setRestitution(0.0f); // No bounce
solve.setFriction(0.0f); // No sliding friction
}

return true;
}

物理参数

参数默认值覆盖值原因
restitution材质定义0.0f防止玩家落地弹跳
friction材质定义0.0f防止摩擦影响水平移动(水平速度由输入系统控制)

为什么要覆盖? 玩家移动系统(见 输入优先级与上下文)会根据输入手动控制速度;物理摩擦会干扰这一过程,导致移动迟滞或不可预测。将摩擦设为 0 可以把控制权完全交给移动代码。

来源: Classes/Scenes/CombatContactHelper.cpp L264-L295


物理分类与碰撞过滤

物理分类通过位掩码定义哪些实体可碰撞。每个 PhysicsBody 有三种掩码:

掩码类型用途
CategoryBitmask该物体所属类别(通常只设置一个 bit)
CollisionBitmask与哪些类别发生实体碰撞
ContactTestBitmask与哪些类别触发接触回调(即使不发生实体碰撞)

分类定义

Classes/Configs/GamePhysicsCategory.h

enum class GamePhysicsCategory : int
{
PLAYER = 0,
MONSTER = 1,
PLATFORM = 2,
COLLISION = 3,
PLAYER_ATTACK = 4,
MONSTER_ATTACK = 5,
BOMB = 6,
ITEM = 7,
};

inline int ToMask(GamePhysicsCategory category)
{
return 1 << static_cast<int>(category);
}

碰撞矩阵

关键观察:

  • 攻击命中框(PLAYER_ATTACKMONSTER_ATTACK)只做 contact test,不做实体碰撞
  • PLAYERMONSTER 不会彼此实体碰撞(避免互相卡位)
  • ITEM 类别只做 contact test(拾取不推人)

示例:玩家 PhysicsBody 配置

Classes/Scenes/GameScene.cpp L480-L495

auto physicsBody = PhysicsBody::createBox(Size(boxWidth, boxHeight), PLAYER_PHYSICS_MATERIAL);
physicsBody->setDynamic(true);
physicsBody->setRotationEnable(false);
physicsBody->setMass(1.0f);
physicsBody->setLinearDamping(0.0f);

physicsBody->setCategoryBitmask(ToMask(GamePhysicsCategory::PLAYER));
physicsBody->setCollisionBitmask(ToMask(GamePhysicsCategory::PLATFORM |
GamePhysicsCategory::COLLISION |
GamePhysicsCategory::MONSTER_ATTACK |
GamePhysicsCategory::ITEM));
physicsBody->setContactTestBitmask(ToMask(GamePhysicsCategory::PLATFORM |
GamePhysicsCategory::COLLISION |
GamePhysicsCategory::MONSTER_ATTACK |
GamePhysicsCategory::ITEM));

注: MONSTER_ATTACKITEM 同时出现在两类掩码中,因为希望它们触发 contact 事件(即使 ITEM 不产生实体碰撞)。物理引擎通常需要存在“重叠检测”才能触发接触事件;把它们包含进 CollisionBitmask 可以确保产生重叠检测。

来源: Classes/Configs/GamePhysicsCategory.h

Classes/Scenes/GameScene.cpp L480-L495


与其他系统的集成

落地状态追踪

落地接触事件会进入 GameInputController,用于启用/禁用跳跃:

完整落地检测算法请参阅 输入优先级与上下文

来源: Classes/Scenes/GameInputController.cpp

Classes/Scenes/CombatContactHelper.cpp L48-L68


伤害处理链路

延迟执行后,伤害会进入 CharacterBase::takeDamage

完整伤害计算公式(防御、暴击、状态效果等)请参阅 伤害系统

来源: Classes/Character/Base/CharacterBase.cpp


DebugScene 实现

DebugScene 使用与 GameScene 相同的接触处理模式,这体现了 CombatContactHelper 的可复用性:

Classes/Scenes/DebugScene.cpp L758-L768

void DebugScene::initPhysicsContactListener()
{
auto contactListener = EventListenerPhysicsContact::create();
contactListener->onContactBegin = CC_CALLBACK_1(DebugScene::onContactBegin, this);
contactListener->onContactPreSolve = CombatContactHelper::handleContactPreSolve;
contactListener->onContactSeparate = CC_CALLBACK_1(DebugScene::onContactSeparate, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(contactListener, this);
}

bool DebugScene::onContactBegin(PhysicsContact& contact)
{
return CombatContactHelper::handleContactBegin(contact, _player, _inputController.get());
}

GameScene 的差异仅在于回调所绑定的“场景实例”不同。

来源: Classes/Scenes/DebugScene.cpp L758-L768

Classes/Scenes/DebugScene.cpp L770-L773


总结

组件职责关键方法
CombatContactHelper中央接触处理器handleContactBeginhandleContactPreSolvehandleContactSeparate
GameScene注册监听并提供上下文initPhysicsContactListeneronContactBeginonContactSeparate
PhysicsBody::getTag()存储伤害值在接触处理中读取(负数=暴击)
Node::getTag()存储破防伤害在接触处理中读取(用于 Boss 机制)
scheduleOnce(0.0f)延迟状态修改避免物理遍历期间修改场景图
PhysicsContactPreSolve调整碰撞响应玩家-平台接触把 restitution/friction 设为 0
物理分类碰撞过滤PLAYERMONSTERPLAYER_ATTACK

物理接触系统为战斗交互、落地检测与拾取物品提供了稳健的事件驱动基础。通过 CombatContactHelper 的集中处理与延迟执行模式,它能在保持物理与玩法逻辑解耦的同时,安全地完成状态变更。

来源: Classes/Scenes/GameScene.cpp L524-L535

Classes/Scenes/CombatContactHelper.cpp L1-L296

Classes/Scenes/DebugScene.cpp L758-L773