Skip to content

物理系统(Physics System) ​

物理系统是 WasiCore 游戏框架中处理碰撞检测、刚体模拟和物理约束的核心系统。框架提供 3D 物理和 2D 物理两套 API。

> 运行端状态:3D 物理和 2D 物理均已双端可用。服务端可创建 PhysicsWorld / RigidBody / CollisionShape 或 PhysicsWorld2D / RigidBody2D / CollisionShape2D 作为权威物理端;脚本创建的 replicated SceneGraph 发布给玩家后,客户端接收服务端同步的节点 transform。客户端也可继续使用物理系统做本地表现、预览或 Canvas/GameUI 映射。

> SCE 场景静态碰撞查询:客户端如果要对当前 SCE Volumetric3D 场景中的地形、墙壁或平台自行执行物理查询,应在首次查询前调用 Scene.EnsureStaticMeshColliders()。该 API 会显式加载场景静态碰撞信息并挂载到客户端 PhysicsWorld;如果没有先加载,Raycast / SphereCast 可能因静态碰撞尚未挂载而穿透墙壁。重复调用安全,成功后返回 Loaded 或 AlreadyLoaded。

目录 ​


坐标系统 ​

WasiCore 使用左手坐标系。物理系统的所有坐标运算均遵循此约定。

轴方向说明
X水平(左右)地面平面分量
Y水平(前后)地面平面分量
Z垂直(上下)高度轴,正方向向上

3D 物理系统 ​

命名空间:GameGraph.NodeSystem.Component.Physics

3D 物理 API 在客户端和服务端均可使用。服务端创建的物理对象可以用于权威碰撞/运动模拟;如果节点位于 replicated SceneGraph 或 SCE 默认同步场景中,节点 transform 可同步到客户端。客户端也可以继续使用 3D 物理做本地预览、表现或纯客户端原型。

PhysicsWorld ​

PhysicsWorld 是场景级别的物理世界组件,提供射线检测和区域查询能力。

射线与区域查询 API ​

方法签名说明
RaycastRaycast(Vector3 pos, Vector3 dir, float maxDist, uint mask)射线检测,返回最近命中结果
RaycastAllRaycastAll(Vector3 pos, Vector3 dir, float maxDist, uint mask)射线检测,返回所有命中结果
SphereCastSphereCast(Vector3 pos, Vector3 dir, float radius, float maxDist, uint mask)球形扫掠检测
OverlapSphereOverlapSphere(Vector3 center, float radius, uint mask)球形区域重叠查询
OverlapBoxOverlapBox(Vector3 center, Vector3 halfExtents, Quaternion rotation, uint mask)矩形区域重叠查询

> 注意:CollisionMask.All、CollisionMask.Default 若在文档或示例中出现,可能是项目内自定义的常量,并非框架内置 API。请查阅项目定义或使用 uint 掩码值(如 0xFFFFFFFF 表示全部)。

csharp
#if CLIENT
// 从角色位置向前方发射射线,检测 500 单位距离内的物体
var origin = unit.Position;
var direction = unit.Forward;
// mask: 项目定义的 uint 碰撞掩码常量
var hit = physicsWorld.Raycast(origin, direction, 500f, collisionMask);

if (hit.HasValue)
{
    Game.Logger.LogInformation("命中物体距离: {Dist}", hit.Value.Distance);
}

// 检测某点周围 200 单位半径内的所有刚体(mask 为项目定义的 uint 常量)
var overlaps = physicsWorld.OverlapSphere(center, 200f, collisionMask);
#endif

RigidBody ​

RigidBody 是挂载在节点上的刚体组件,控制物体的物理行为。

属性 ​

属性类型说明
LinearVelocityVector3线速度
AngularVelocityVector3角速度
Massfloat质量
Frictionfloat摩擦系数
Restitutionfloat弹性系数(0 = 无弹性,1 = 完全弹性)
UseGravitybool是否受重力影响
Kinematicbool是否为运动学刚体(不受物理力影响,仅手动控制位置)
IsTriggerbool是否为触发器(不产生物理碰撞,仅触发事件)
CollisionLayeruint碰撞层
CollisionMaskuint碰撞掩码(决定与哪些层发生碰撞)

方法 ​

方法签名说明
ApplyForceApplyForce(Vector3 force)施加持续力
AddForceAtPositionAddForceAtPosition(Vector3 force, Vector3 position)在指定世界坐标位置施加力
ApplyTorqueApplyTorque(Vector3 torque)施加扭矩
ApplyImpulseApplyImpulse(Vector3 impulse)施加瞬时冲量
ApplyImpulseAtPositionApplyImpulseAtPosition(Vector3 impulse, Vector3 position)在指定位置施加瞬时冲量
ApplyTorqueImpulseApplyTorqueImpulse(Vector3 torque)施加瞬时扭矩冲量
WakeUpWakeUp()唤醒休眠中的刚体
SleepSleep()使刚体进入休眠
IsSleepIsSleep()查询刚体是否处于休眠状态
SetCollisionFilterSetCollisionFilter(RigidBody.CollisionFilter? filter)设置接触点过滤器;回调返回 Ignore 会过滤本次接触点
csharp
#if CLIENT
// 配置一个动态刚体
var body = node.GetComponent<RigidBody>();
body.Mass = 10f;
body.Friction = 0.5f;
body.Restitution = 0.3f;
body.UseGravity = true;
body.Kinematic = false;

// 向上施加跳跃冲量(Z 轴正方向)
body.ApplyImpulse(new Vector3(0, 0, jumpForce));

// 在物体顶部施加偏移力,产生旋转效果
body.AddForceAtPosition(
    new Vector3(100, 0, 0),
    body.Node.WorldPosition + new Vector3(0, 0, 1)
);
#endif

碰撞形状(ShapeType) ​

ShapeType 枚举定义了 3D 物理支持的碰撞体形状。

枚举值说明典型用途
Box长方体箱子、墙壁、平台
Sphere球体弹丸、拾取物
StaticPlane无限静态平面地面
Cylinder圆柱体柱子、树干
Capsule胶囊体角色碰撞体
Cone圆锥体特殊造型
TriangleMesh三角网格复杂静态地形
ConvexHull凸包不规则动态物体
Terrain地形高度图地形

碰撞事件模式(CollisionEventMode) ​

控制刚体是否产生碰撞事件回调。

枚举值说明
Never不产生碰撞事件
Active仅当刚体处于活动(非休眠)状态时产生事件
Always始终产生碰撞事件

约束类型(ConstraintType) ​

物理约束用于连接两个刚体,限制它们之间的相对运动。

枚举值说明典型用途
Point点约束(球窝关节)绳索连接点、悬挂物
Hinge铰链约束门、翻盖
Slider滑动约束抽屉、活塞
ConeTwist锥形扭转约束布娃娃关节

碰撞回调 ​

3D 物理碰撞事件有两种接收方式:

方式一:Node 事件(推荐) ​

通过 Node 上的事件订阅碰撞,可获取完整的碰撞接触信息(接触点位置、法线、穿透深度、冲量等)。订阅事件会自动启用底层监听,取消所有订阅会自动禁用,无需手动管理。

事件回调参数触发时机
OnCollisionBeginCollisionContact碰撞开始(首帧)
OnCollisionStayCollisionContact碰撞持续(每物理帧)
OnCollisionEndCollisionContact碰撞结束

CollisionContact 主要成员:

成员类型说明
SelfNode发生碰撞的自身节点
OtherNode?发生碰撞的另一个节点
BodySelfRigidBody?自身节点的刚体组件
BodyOtherRigidBody?另一个节点的刚体组件
IsTriggerbool是否为触发器碰撞(至少一方为 Trigger)
ContactPointsContactPoint3D[]?碰撞接触点数组(最多 4 个),碰撞结束时为 null

ContactPoint3D 结构:

字段类型说明
WorldPositionVector3接触点世界坐标
WorldNormalVector3接触点法线方向
Distancefloat穿透深度(负值)
Impulsefloat碰撞冲量
csharp
#if CLIENT
var node = unit.GetNode();

node.OnCollisionBegin += (Node.CollisionContact contact) =>
{
    Game.Logger.LogInformation("碰撞开始,对方: {Other}", contact.Other?.Name);

    if (contact.ContactPoints is { } points)
    {
        foreach (var pt in points)
        {
            Game.Logger.LogInformation("接触点: {Pos}, 法线: {N}, 冲量: {I}",
                pt.WorldPosition, pt.WorldNormal, pt.Impulse);
        }
    }
};

node.OnCollisionEnd += (Node.CollisionContact contact) =>
{
    Game.Logger.LogInformation("碰撞结束,对方: {Other}", contact.Other?.Name);
};
#endif

AllowDuplicateContactEvents 属性:

当碰撞双方都订阅了碰撞事件时,默认只触发一次(性能优化)。设为 true 可让双方都收到事件:

csharp
node.AllowDuplicateContactEvents = true;

方式二:ScriptComponent 回调 ​

通过 ScriptComponent 的生命周期回调接收碰撞,信息较少(仅传递对方 Node)。需要将 IsTrigger 设为 true 以启用触发器模式,或通过 CollisionEventMode 配置碰撞事件。

回调方法参数触发时机
OnTriggerEnterNode node另一个碰撞体首次进入触发区域
OnTriggerStayNode node另一个碰撞体持续处于触发区域内
OnTriggerExitNode node另一个碰撞体离开触发区域
csharp
#if CLIENT
public class DamageZone : ScriptComponent
{
    public override void OnTriggerEnter(Node node)
    {
        Game.Logger.LogInformation("碰撞体进入伤害区域");
    }

    public override void OnTriggerStay(Node node)
    {
        // 持续造成伤害
    }

    public override void OnTriggerExit(Node node)
    {
        Game.Logger.LogInformation("碰撞体离开伤害区域");
    }
}
#endif

> 推荐:需要接触点细节(位置、法线、冲量等)时使用 Node 事件;仅需知道"谁碰了谁"时使用 ScriptComponent 回调。


2D 物理系统(双端) ​

命名空间:GameGraph.NodeSystem.Component.Physics.Physics2D

2D 物理系统在客户端和服务端均可用。服务端可用于 Box2D 权威模拟、碰撞判定、射线和 AABB 查询;客户端可用于本地表现、预览、Canvas/GameUI 映射或轻量预测。需要让客户端看到服务端 2D 物理结果时,仍要通过 replicated Node/SceneGraph 或业务消息同步,并在客户端绘制表现。

下面的触摸、Canvas 或本地预览示例仍放在 #if CLIENT 中;服务端权威模拟代码应放在 #if SERVER 或双端共享逻辑中,按项目同步方案决定。

PhysicsWorld2D ​

2D 物理世界组件,管理 2D 场景的物理模拟。

属性与方法 ​

成员类型/签名说明
GravityVector22D 世界重力(默认方向向下)
RaycastSingleRaycastSingle(Vector2 start, Vector2 end, ...)2D 射线检测,返回最近命中
RaycastAllRaycastAll(Vector2 start, Vector2 end, ...)2D 射线检测,返回所有命中
GetRigidBodyGetRigidBody(Vector2 point)获取指定世界坐标点处的 2D 刚体
GetRigidBodyGetRigidBody(float screenX, float screenY)获取屏幕坐标位置处的 2D 刚体
QueryAABBQueryAABB(Vector2 lowerBound, Vector2 upperBound)AABB 矩形区域查询
csharp
#if CLIENT
// 设置 2D 重力
physicsWorld2D.Gravity = new Vector2(0, -9.8f);

// 从触摸点获取 2D 刚体
var body = physicsWorld2D.GetRigidBody(touchScreenX, touchScreenY);
if (body != null)
{
    Game.Logger.LogInformation("点击到了 2D 物体");
}

// 2D 射线检测
var hit = physicsWorld2D.RaycastSingle(startPoint, endPoint);
#endif

RigidBody2D ​

2D 刚体组件,控制 2D 物体的物理行为。

属性 ​

属性类型说明
BodyTypeBodyType2D刚体类型
Massfloat质量
LinearVelocityVector2线速度
AngularVelocityfloat角速度(弧度/秒)
LinearDampingfloat线性阻尼
AngularDampingfloat角阻尼
GravityScalefloat重力缩放因子
FixedRotationbool是否锁定旋转
Bulletbool是否启用 CCD(连续碰撞检测,防止高速穿透)

BodyType2D 枚举 ​

枚举值说明
Static静态刚体,不参与物理运动(地面、墙壁)
Kinematic运动学刚体,仅通过代码控制位置(移动平台)
Dynamic动态刚体,完全参与物理模拟

2D 碰撞形状 ​

以下碰撞形状组件可挂载到拥有 RigidBody2D 的节点上。

组件说明关键属性
CollisionBox2D矩形碰撞体Size(Vector2)、Center(Vector2)
CollisionCircle2D圆形碰撞体Radius(float)、Center(Vector2)
CollisionEdge2D线段碰撞体Vertex1、Vertex2(Vector2)
CollisionChain2D链条碰撞体(开放或闭合)Vertices(Vector2[])、Loop(bool)
CollisionPolygon2D多边形碰撞体Vertices(Vector2[])

2D 约束类型 ​

2D 物理支持丰富的约束类型,用于连接两个 2D 刚体。

约束说明典型用途
Distance距离约束,保持两点间固定距离绳索、弹簧
Friction摩擦约束表面摩擦模拟
Gear齿轮约束,联动两个旋转约束齿轮组
Motor马达约束,驱动旋转或平移电机、风扇
Mouse鼠标约束,将刚体拖向目标点拖拽交互
Prismatic棱柱约束(滑轨)滑块、电梯
Pulley滑轮约束滑轮系统
Revolute旋转约束铰链、轮轴
Rope绳索约束,限制最大距离绳索、链条
Weld焊接约束,固定连接刚性连接
Wheel车轮约束(旋转 + 弹簧)车辆悬挂
csharp
#if CLIENT
// 创建一个可拖拽的 2D 动态物体
var body2D = node.GetComponent<RigidBody2D>();
body2D.BodyType = BodyType2D.Dynamic;
body2D.Mass = 1f;
body2D.GravityScale = 1f;
body2D.FixedRotation = false;

// 添加圆形碰撞体
var circle = node.GetComponent<CollisionCircle2D>();
circle.Radius = 32f;

// 用鼠标约束实现拖拽
var mouseJoint = node.CreateComponent<ConstraintMouse2D>();
mouseJoint.Target = touchWorldPosition;
mouseJoint.MaxForce = 1000f * body2D.Mass;
#endif

2D 碰撞事件 ​

2D 碰撞事件通过 Node 上的事件订阅,提供完整的接触信息。

事件回调参数触发时机
OnCollisionBegin2DCollisionContact2D2D 碰撞开始
OnCollisionEnd2DCollisionContact2D2D 碰撞结束
OnCollisionPreSolve2DCollisionContact2D, ref bool isActive碰撞求解前,可设置 isActive = false 禁用本次碰撞响应

CollisionContact2D 主要成员:

成员类型说明
SelfNode自身节点
OtherNode?另一个节点
BodySelfRigidBody2D?自身 2D 刚体
BodyOtherRigidBody2D?对方 2D 刚体
ShapeSelfCollisionShape2D?自身碰撞形状
ShapeOtherCollisionShape2D?对方碰撞形状
ContactPointsContactPoint2D[]?接触点数组(最多 2 个)
csharp
#if CLIENT
node.OnCollisionBegin2D += (Node.CollisionContact2D contact) =>
{
    Game.Logger.LogInformation("2D碰撞开始: {Other}", contact.Other?.Name);
};

// PreSolve: 可在碰撞响应前动态禁用碰撞
node.OnCollisionPreSolve2D += (Node.CollisionContact2D contact, ref bool isActive) =>
{
    // 例如:单向平台——只从下方穿过
    if (contact.BodyOther?.LinearVelocity.Y > 0)
    {
        isActive = false;
    }
};
#endif

2D 碰撞事件常见陷阱 ​

以下是在实际 2D 平台跳跃游戏(如马里奥类游戏)开发中总结的常见陷阱和推荐做法。

陷阱 1:用 WorldNormal 判断碰撞方向 ​

ContactPoint2D.WorldNormal 在 2D 碰撞中不可靠用于判断"从哪个方向碰撞"(如:玩家是否从上方踩敌人、是否从下方顶砖块)。法线方向受物理引擎求解顺序、碰撞体形状边缘、穿透深度等因素影响,实际值可能与直觉不符。

csharp
#if CLIENT
// 错误:用 WorldNormal 判断踩踏
if (contact.ContactPoints?[0].WorldNormal.Y < -0.5f)
{
    // 以为是从上方踩到,但实际可能误判
}

// 正确:用双方位置判断
float playerCenterY = playerNode.LocalPosition.Y;
float enemyCenterY = otherNode.LocalPosition.Y;
if (playerCenterY > enemyCenterY)  // 物理坐标 Y 朝上
{
    // 玩家在敌人上方 → 踩踏
}
#endif

> 推荐:需要判断碰撞方向时,始终使用 Node.LocalPosition 的相对位置比较,不依赖法线。

陷阱 2:用 LinearVelocity 判断运动方向 ​

在 OnCollisionBegin2D 回调中读取 RigidBody2D.LinearVelocity,其值可能已被物理引擎在碰撞求解阶段修改,不一定反映碰撞发生前一瞬间的真实速度。

csharp
#if CLIENT
// 不可靠:碰撞回调中的速度可能已被求解修改
if (playerBody.LinearVelocity.Y < 0)
{
    // 以为玩家在下落,但值可能已被碰撞响应修改
}

// 更可靠:结合位置判断 + 速度作为辅助条件
float playerY = playerNode.LocalPosition.Y;
float enemyY = otherNode.LocalPosition.Y;
if (playerY > enemyY && playerBody.LinearVelocity.Y < 0f)
{
    // 玩家在上方且下落中 → 高置信度的踩踏判定
}
#endif

陷阱 3:IsTrigger vs OnCollisionPreSolve2D ​

两种方式都可以实现"检测碰撞但不产生物理阻挡",但适用场景不同:

方式行为适用场景
IsTrigger = true碰撞形状不参与物理求解,仅触发事件金币、旗帜、拾取物、伤害区域
OnCollisionPreSolve2D + isActive = false参与物理检测但禁用碰撞响应单向平台(从下方穿过、从上方站立)

不要对需要事件但不需要阻挡的对象使用 OnCollisionPreSolve2D + isActive = false。这会干扰 OnCollisionBegin2D 的触发行为,且每物理帧都会调用 PreSolve 回调,有性能开销。直接使用 IsTrigger = true 更简洁可靠。

csharp
#if CLIENT
// 推荐:金币/拾取物用 IsTrigger
var coinShape = coinNode.CreateComponent<CollisionCircle2D>();
coinShape.IsTrigger = true;
coinShape.CategoryBits = Cat.Coin;
coinShape.MaskBits = Cat.Player;

// 推荐:敌人用 Kinematic + IsTrigger(不阻挡玩家,仅触发伤害/踩踏判定)
var enemyBody = enemyNode.CreateComponent<RigidBody2D>();
enemyBody.BodyType = BodyType2D.Kinematic;
var enemyShape = enemyNode.CreateComponent<CollisionBox2D>();
enemyShape.IsTrigger = true;
enemyShape.CategoryBits = Cat.Enemy;
enemyShape.MaskBits = Cat.Player;
#endif

陷阱 4:着地检测使用 WorldNormal ​

判断玩家是否站在地面上时,WorldNormal 对于平坦地面通常可用(法线指向 Y 正方向),但对于地面边缘、斜面和多碰撞体交汇处可能不稳定。推荐同时使用接触计数:

csharp
#if CLIENT
private int groundContactCount = 0;
private bool isOnGround = false;

void OnCollisionBegin2D(Node.CollisionContact2D contact)
{
    if (IsGroundNode(contact.Other))
    {
        var pts = contact.ContactPoints;
        // WorldNormal.Y < -0.5f 对于平坦地面可靠(法线向下表示地面向上推玩家)
        if (pts != null && pts.Length > 0 && pts[0].WorldNormal.Y < -0.5f)
        {
            groundContactCount++;
            isOnGround = true;
        }
    }
}

void OnCollisionEnd2D(Node.CollisionContact2D contact)
{
    if (IsGroundNode(contact.Other))
    {
        groundContactCount = Math.Max(0, groundContactCount - 1);
        if (groundContactCount == 0) isOnGround = false;
    }
}
#endif

> 关键:使用计数而非布尔值追踪着地状态,因为玩家可能同时接触多个地面碰撞体。

陷阱 5:AllowDuplicateContactEvents 默认行为吞掉碰撞事件 ​

当碰撞双方的 Node 都订阅了 OnCollisionBegin2D(或 3D 的 OnCollisionBegin),AllowDuplicateContactEvents 默认为 false,引擎只会向其中一个 Node 发送碰撞事件。哪个 Node 收到事件取决于 Box2D 内部的代理创建顺序(通常先创建的 Node 优先),这个顺序对用户不可控且不可预测。

csharp
#if CLIENT
// ❌ 危险模式:两个 Node 都订阅了碰撞事件
// 但 AllowDuplicateContactEvents 默认 false,只有一方收到事件
enemyNode.OnCollisionBegin2D += OnEnemyHit;      // 可能收到
bulletNode.OnCollisionBegin2D += OnBulletContact; // 可能被吞掉!

// 更糟:如果"被选中"的那一侧 handler 是空的或只做日志,
// 另一侧的关键逻辑永远不会执行
enemyNode.OnCollisionBegin2D += (contact) => { }; // 空 handler 抢走了事件
bulletNode.OnCollisionBegin2D += OnBulletDamage;   // 永远不会触发!
#endif

最佳实践:碰撞处理逻辑只在一方订阅 OnCollisionBegin2D,另一方不要订阅。这样不需要 AllowDuplicateContactEvents,也不存在事件被吞的风险。

csharp
#if CLIENT
// ✅ 推荐:只在需要处理碰撞的一方订阅
// 子弹/攻击触发器负责处理命中逻辑
bulletNode.OnCollisionBegin2D += OnBulletContact;
// 敌人不订阅 OnCollisionBegin2D(不需要 AllowDuplicateContactEvents)
#endif

如果确实需要双方都收到事件,必须显式设置 AllowDuplicateContactEvents = true:

csharp
#if CLIENT
// ✅ 如果双方都需要处理碰撞事件
enemyNode.AllowDuplicateContactEvents = true;
enemyNode.OnCollisionBegin2D += OnEnemyHit;
bulletNode.AllowDuplicateContactEvents = true;
bulletNode.OnCollisionBegin2D += OnBulletContact;
// 注意:此时双方都会收到事件,要避免重复处理(如双重伤害)
#endif

陷阱 6:晚订阅碰撞事件导致漏掉初始接触 ​

OnCollisionBegin 和 OnCollisionBegin2D 是“接触开始”事件,不是当前重叠状态查询。事件订阅会打开底层监听,但不会回放订阅前已经发生的 Begin 事件。如果先创建并激活 Node、RigidBody、CollisionShape,再注册碰撞事件,而物理世界已经推进过一帧,已经发生的初始接触就会被漏掉。

csharp
#if CLIENT
var node = graph.CreateChild("Trigger");

// ✅ 先订阅,再创建/激活物理组件
node.OnCollisionBegin += OnCollisionBegin;

var body = node.CreateComponent<RigidBody>();
body.IsTrigger = true;

var shape = node.CreateComponent<CollisionShape>();
shape.SetSphere(100);
#endif

这条规则同样适用于 OnCollisionBegin2D、服务端权威物理和客户端本地物理。推荐让事件注册早于对象进入活动物理模拟,至少早于第一次可能的物理步进。若对象在注册前就可能与其他碰撞体重叠,应在完成订阅后额外使用 OverlapSphere / OverlapBox 或项目自己的重叠查询,显式处理初始状态;不要依赖 OnCollisionBegin 补偿历史接触。

碰撞类别分组推荐(2D 平台游戏) ​

csharp
#if CLIENT
static class Cat
{
    public const int Ground  = 1 << 0;  // 地面/平台/砖块
    public const int Player  = 1 << 1;  // 玩家
    public const int Enemy   = 1 << 2;  // 敌人
    public const int Coin    = 1 << 3;  // 金币/拾取物
    public const int Flag    = 1 << 4;  // 终点/检查点
}

// 玩家:与所有类别碰撞
playerShape.CategoryBits = Cat.Player;
playerShape.MaskBits = Cat.Ground | Cat.Enemy | Cat.Coin | Cat.Flag;

// 地面:仅与玩家碰撞(敌人用代码控制移动边界,不需要物理碰撞)
groundShape.CategoryBits = Cat.Ground;
groundShape.MaskBits = Cat.Player;

// 金币/旗帜:IsTrigger + 仅与玩家碰撞
coinShape.IsTrigger = true;
coinShape.CategoryBits = Cat.Coin;
coinShape.MaskBits = Cat.Player;
#endif

使用示例 ​

3D 射线检测实现瞄准 ​

csharp
#if CLIENT
public class AimingHelper
{
    public Node? FindTarget(PhysicsWorld physicsWorld, Vector3 origin, Vector3 direction, float range, uint mask)
    {
        var hit = physicsWorld.Raycast(origin, direction, range, mask);
        if (!hit.HasValue) return null;

        var rigidBody = GameGraph.NodeSystem.ExtenstionMethod.GetRigidBody(hit.Value);
        return rigidBody?.Node;
    }

    public RigidBody[] FindBodiesInArea(PhysicsWorld world, Vector3 center, float radius, uint mask)
    {
        return world.OverlapSphere(center, radius, mask);
    }
}
#endif

如果预瞄准或落点检测发现客户端射线穿透墙壁,先检查当前场景是否已经成功调用 EnsureStaticMeshColliders(),以及场景的 PhysicsCollision.dat 是否随客户端资源发布;静态碰撞确认加载后,再排查射线方向、距离和碰撞掩码。

3D 触发区域(进入/离开检测) ​

使用 Node 事件(推荐,可获取接触点信息):

csharp
#if CLIENT
var zoneNode = unit.GetNode();
var body = zoneNode.GetComponent<RigidBody>();
body.IsTrigger = true;
body.Kinematic = true;

zoneNode.OnCollisionBegin += (Node.CollisionContact contact) =>
{
    Game.Logger.LogInformation("节点进入治疗区域: {Other}", contact.Other?.Name);
};

zoneNode.OnCollisionEnd += (Node.CollisionContact contact) =>
{
    Game.Logger.LogInformation("节点离开治疗区域: {Other}", contact.Other?.Name);
};
#endif

或使用 ScriptComponent 回调(仅传递对方 Node):

csharp
#if CLIENT
public class HealingZone : ScriptComponent
{
    public override void OnStart()
    {
        var body = Node.GetComponent<RigidBody>();
        body.IsTrigger = true;
        body.Kinematic = true;
    }

    public override void OnTriggerEnter(Node node)
    {
        Game.Logger.LogInformation("节点进入治疗区域");
    }

    public override void OnTriggerExit(Node node)
    {
        Game.Logger.LogInformation("节点离开治疗区域");
    }
}
#endif

2D 平台跳跃角色控制(客户端) ​

csharp
#if CLIENT
public class PlatformerController2D : ScriptComponent
{
    private RigidBody2D _body;
    private bool _isGrounded;

    public override void OnStart()
    {
        _body = Node.GetComponent<RigidBody2D>();
        _body.BodyType = BodyType2D.Dynamic;
        _body.FixedRotation = true;
        _body.GravityScale = 3f;
    }

    public void Move(float horizontalInput)
    {
        var velocity = _body.LinearVelocity;
        velocity.X = horizontalInput * 300f;
        _body.LinearVelocity = velocity;
    }

    public void Jump()
    {
        if (!_isGrounded) return;
        _body.LinearVelocity = new Vector2(_body.LinearVelocity.X, 500f);
        _isGrounded = false;
    }
}
#endif

API 速查表 ​

3D 物理(双端) ​

类关键成员运行端
PhysicsWorldRaycast, RaycastAll, SphereCast, OverlapSphere, OverlapBoxSERVER / CLIENT
RigidBodyMass, LinearVelocity, ApplyForce, ApplyImpulse, IsTrigger, KinematicSERVER / CLIENT
CollisionShape / ShapeTypeBox, Sphere, Capsule, Cylinder, Cone, TriangleMesh, ConvexHull, Terrain, StaticPlaneSERVER / CLIENT
CollisionEventModeNever, Active, AlwaysSERVER / CLIENT
ConstraintTypePoint, Hinge, Slider, ConeTwistSERVER / CLIENT

2D 物理(双端) ​

类关键成员运行端
PhysicsWorld2DGravity, RaycastSingle, RaycastAll, GetRigidBody, QueryAABBSERVER / CLIENT
RigidBody2DBodyType, Mass, LinearVelocity, AngularVelocity, GravityScale, BulletSERVER / CLIENT
CollisionBox2DSize, CenterSERVER / CLIENT
CollisionCircle2DRadius, CenterSERVER / CLIENT
CollisionEdge2DVertex1, Vertex2SERVER / CLIENT
CollisionChain2DVertices, LoopSERVER / CLIENT
CollisionPolygon2DVerticesSERVER / CLIENT

Node 碰撞事件(双端) ​

事件回调参数说明
OnCollisionBeginCollisionContact3D 碰撞开始
OnCollisionStayCollisionContact3D 碰撞持续
OnCollisionEndCollisionContact3D 碰撞结束
OnCollisionBegin2DCollisionContact2D2D 碰撞开始
OnCollisionEnd2DCollisionContact2D2D 碰撞结束
OnCollisionPreSolve2DCollisionContact2D, ref bool2D 碰撞预求解
AllowDuplicateContactEventsbool 属性允许碰撞双方都收到事件

ScriptComponent 碰撞回调(双端) ​

回调参数说明
OnTriggerEnterNode node碰撞体进入
OnTriggerStayNode node碰撞体停留
OnTriggerExitNode node碰撞体离开

注意事项与最佳实践 ​

运行端限制 ​

内容服务端客户端
3D 物理(PhysicsWorld、RigidBody 等)可用,可作为权威物理端可用,可作表现/预览/本地物理
2D 物理(PhysicsWorld2D、RigidBody2D 等)可用,可作为 Box2D 权威模拟端可用,可作本地表现/预览/Canvas 映射

> 同步边界:服务端 2D/3D 物理本身不会自动把任意本地图同步给客户端。需要客户端看到服务端物理结果时,应把物理节点放在 SCE 默认同步场景中,或放在 SceneGraphReplication.ReplicatedUnpublished 的脚本创建图中并 Publish(player);客户端仍需负责可视化、音效、UI 或 Canvas 映射。详见 服务端 GameGraph SceneGraph 发布与同步。

> 验证状态:2026-06-01 的 published_scenegraph_sync_smoke 已验证服务端 replicated Node + local RigidBody / local CollisionShape / local PhysicsWorld 的发布图链路。服务端物理推进后的节点 transform 可同步到已订阅客户端;未订阅玩家不会收到该 SceneGraph。完整流程见 服务端 GameGraph SceneGraph 发布与同步 - 已验证的 smoke 流程。

服务端权威 3D 物理 ​

服务端权威 3D 物理的推荐目标是:服务端负责真实碰撞、运动和游戏判定;客户端负责接收同步结果并做可视化、音效、UI 反馈。不要把渲染组件、音频组件和材质状态当作服务端权威物理的一部分。

推荐分层 ​

对象类型服务端客户端推荐复制语义
静态地形/障碍创建本地 CollisionShape / 静态刚体,用于碰撞查询和模拟加载同一 SCE 场景资产或创建本地表现对象通常双端 local;如果需要动态出现/消失再同步状态
动态权威物体创建 replicated Node,服务端本地 RigidBody 驱动物理接收 replicated Node transform,创建或绑定本地表现NodeReplication.Replicated,物理组件通常 ComponentReplication.Local
需要客户端知道的碰撞形状服务端创建并参与模拟客户端需要调试/预测/可视化时接收形状信息CollisionShape 可按需要 Replicated,不需要时保持 local
纯表现模型/粒子/音效不创建客户端根据 replicated 节点或事件创建客户端 local
触发区/检测区服务端创建本地触发刚体或查询逻辑客户端只显示范围提示或 UI逻辑 local,表现 local;必要时用 Node.SetSyncVar(...) 同步轻量状态

推荐做法 ​

  1. 动态对象复制 Node,不复制所有物理细节 -- 多数情况下客户端只需要看到服务端权威结果。将动态物体的 Node 设为 Replicated,让 transform 同步到客户端;RigidBody 保持服务端 Local,避免客户端误以为也要参与权威模拟。
  2. 视觉平滑在客户端调 -- replicated transform 默认会在客户端通过 SmoothedTransform 平滑到最新网络目标。需要更快追随或更少硬切时,调整对应 SceneGraph 的 TransformSmoothingConstant 和 TransformSnapThreshold。这不会改变服务端权威物理结果,也不会从服务端自动同步到客户端。
  3. 静态障碍优先双端 local -- 地形、墙体、不可移动障碍通常来自同一 SCE 场景资产,服务端和客户端可以各自本地创建/加载。除非障碍会运行时变化,否则没有必要把它们作为 replicated 节点同步。
  4. 轻量节点状态用 SyncVar -- 机关阶段、对象类型、阵营、调试标记等少量节点级状态,可以由服务端 SetSyncVar(...) 写入,客户端 TryGetSyncVar(...) 读取。不要用 SyncVar 存复杂对象、大字符串、背包、配置表或高频大 payload。
  5. 权威判定只在服务端发生 -- 伤害、命中、拾取、触发器、机关状态变更应由服务端物理结果或服务端查询决定,再通过 GameCore 事件、属性同步或 replicated GameGraph 节点传给客户端。
  6. 表现组件放客户端 -- StaticMeshComponent、AnimatedModelComponent、材质、粒子、音效、Spine 等表现层组件应在客户端创建或由 Actor/Unit 表现体系管理。服务端不要依赖这些组件做逻辑。
  7. 发布图不必总是进 Viewport -- 客户端收到 replicated SceneGraph 不等于画面可见,也不要求一定渲染。它可以只作为服务端权威物理/状态的数据源,客户端把节点位置绘制到 Canvas、小地图或调试 UI。若该图要直接显示为 3D 画面,再准备 Octree、local 表现组件、local CameraComponent,并用 Renderer.SetupMainViewport(...) 或项目自己的 viewport 策略绑定视口。SceneGraphMainViewportController 只是可选 helper,用于临时接管主视口时复用保存/恢复逻辑;若主画面仍应显示 SCE 地图,应把表现挂到当前 SCE 场景 / Actor / Unit 体系,或保持发布图为数据源,而不是让临时发布图长期占用主视口。
  8. 调试 mesh 提供 normal -- 用 MeshBuilder 或自定义网格做服务端物理调试可视化时,应写入 normal,避免常规光照下发黑或亮度异常。
  9. 选择合适场景归属 -- 随地图生命周期存在的物理对象放在 SCE 默认场景;临时副本、机关组、调试对象、非 Unit 的独立同步物件可以放在服务端发布的 SceneGraph。
  10. 先同步少量高价值对象 -- 对大量碎片、弹壳、装饰物等非关键物理,优先客户端 local 表现或事件驱动生成。只把会影响玩法判定或其他玩家可见结果的对象纳入服务端权威同步。
  11. 客户端预测谨慎使用 -- 目前文档推荐服务端权威 + 客户端接收 transform。需要预测/回滚/插值策略时,应先为具体玩法建立测试闭环,再固化为项目模式。

场景归属选择 ​

需求建议
物体属于当前地图,随 Scene.Load() / Unload() 存亡使用 SCE Scene,通过 scene.GetSyncRoot() / scene.AddSyncNode(...) 挂载
物体是临时同步图、可单独发给部分玩家、或一个客户端需要同时接收多个图服务端创建 SceneGraphReplication.ReplicatedUnpublished,按玩家 Publish(player)
物体只用于客户端预览或单机原型客户端 local SceneGraph,不发布

推荐做法 ​

  1. 碰撞层合理分组 -- 使用 CollisionLayer 和 CollisionMask 将不同类型的物体(角色、弹丸、地形、触发区域)分到不同碰撞层,减少不必要的碰撞检测开销。
  2. 优先使用运动学刚体 -- 对于不需要物理模拟的物体(如移动平台、NPC),将 Kinematic 设为 true,通过代码直接控制位置。
  3. 触发器与刚体分离 -- 将触发区域(IsTrigger = true)和物理碰撞体分开使用,避免混淆逻辑。
  4. 休眠管理 -- 不要频繁唤醒大量刚体。对于临时不参与模拟的物体,让物理引擎自动管理休眠。
  5. 射线检测掩码 -- 始终传入适当的碰撞掩码以缩小查询范围,避免对场景中所有物体进行检测。
  6. 2D/3D 不混用 -- 同一场景中不要同时使用 3D 和 2D 物理组件;2D 物理专用于纯 2D 游戏场景。

常见错误 ​

错误说明
服务端创建了本地 3D 物理对象但客户端看不到节点所在 SceneGraph 没有发布,或节点/组件使用了 Local 复制语义
客户端收到 replicated 节点但画面仍看不到独立 SceneGraph 没有客户端 local 表现组件、没有 Octree / CameraComponent / Viewport,或主视口被恢复/切走
服务端 2D 物理运行了但客户端画面没变化物理模拟只推进服务端节点和碰撞状态;客户端需要接收 replicated Node 或业务消息,并用 Canvas/GameUI/渲染对象绘制表现
将 Y 轴当作高度轴WasiCore 中 Z 轴是高度轴,重力沿 Z 轴负方向
对触发器施加力IsTrigger = true 的刚体不参与力学模拟
忽略碰撞掩码导致射线或区域查询检测到不相关物体

相关文档 ​