1068 lines
43 KiB
Python
1068 lines
43 KiB
Python
import time
|
||
from datetime import datetime
|
||
from panda3d.core import (
|
||
CollisionTraverser, CollisionHandlerQueue, CollisionNode,
|
||
CollisionRay, CollisionSphere, BitMask32, Point3, Vec3
|
||
)
|
||
from direct.task.TaskManagerGlobal import taskMgr
|
||
|
||
class CollisionManager:
|
||
"""统一的碰撞检测管理器"""
|
||
|
||
def __init__(self, world):
|
||
self.world = world
|
||
self.traverser = CollisionTraverser("main_traverser")
|
||
self.queue = CollisionHandlerQueue()
|
||
|
||
# 碰撞掩码定义
|
||
self.MASKS = {
|
||
# === 基础碰撞掩码定义 ===
|
||
# 每个掩码使用不同的位(bit)来标识,可以进行位运算组合
|
||
|
||
# 'TERRAIN': BitMask32.bit(0), # 地形/地面 - 通常用于地面碰撞检测
|
||
# 'UI_ELEMENT': BitMask32.bit(1), # UI元素 - 界面组件的碰撞检测
|
||
# 'CAMERA': BitMask32.bit(2), # 摄像机 - 相机的碰撞检测
|
||
'MODEL_COLLISION': BitMask32.bit(6), # 模型碰撞 - 通用模型间碰撞检测
|
||
}
|
||
|
||
# 碰撞体形状类型
|
||
self.COLLISION_SHAPES = {
|
||
'SPHERE': 'sphere',
|
||
'BOX': 'box',
|
||
'CAPSULE': 'capsule',
|
||
'PLANE': 'plane',
|
||
'POLYGON': 'polygon',
|
||
'MESH': 'mesh'
|
||
}
|
||
|
||
# 掩码组合定义
|
||
self.MASK_COMBINATIONS = {
|
||
}
|
||
|
||
# 碰撞分组管理
|
||
self.collision_groups = {}
|
||
self.group_enabled = {}
|
||
|
||
# 碰撞回调系统
|
||
self.collision_callbacks = {}
|
||
self.collision_filters = {}
|
||
|
||
# 空间分割系统(八叉树)
|
||
self.spatial_partitioning_enabled = False
|
||
self.octree = None
|
||
self.octree_max_depth = 6
|
||
self.octree_max_objects = 10
|
||
|
||
# 连续碰撞检测
|
||
self.ccd_enabled = False
|
||
self.ccd_threshold = 10.0 # 速度阈值
|
||
|
||
# 性能监控
|
||
self.performance_monitor = CollisionPerformanceMonitor()
|
||
|
||
# 模型间碰撞检测配置
|
||
self.model_collision_enabled = False
|
||
self.collision_detection_frequency = 0.1
|
||
self.collision_distance_threshold = 0
|
||
self.last_collision_check = 0
|
||
self.collision_history = []
|
||
self.max_collision_history = 100
|
||
|
||
# 碰撞状态跟踪 - 避免重复打印
|
||
self.active_collisions = set() # 当前活跃的碰撞对
|
||
|
||
# 模型间碰撞检测任务
|
||
self.collision_task = None
|
||
|
||
print("✅ 碰撞检测管理器初始化完成")
|
||
|
||
def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5):
|
||
"""启用/禁用模型间碰撞检测
|
||
|
||
Args:
|
||
enable: 是否启用碰撞检测
|
||
frequency: 检测频率(秒)
|
||
threshold: 碰撞距离阈值
|
||
"""
|
||
self.model_collision_enabled = enable
|
||
self.collision_detection_frequency = frequency
|
||
self.collision_distance_threshold = threshold
|
||
|
||
if enable:
|
||
print(f"✅ 启用模型间碰撞检测 - 频率: {frequency}s, 阈值: {threshold}")
|
||
self._startCollisionDetectionTask()
|
||
else:
|
||
print("❌ 禁用模型间碰撞检测")
|
||
self._stopCollisionDetectionTask()
|
||
|
||
def _startCollisionDetectionTask(self):
|
||
"""启动碰撞检测任务"""
|
||
if self.collision_task:
|
||
taskMgr.remove(self.collision_task)
|
||
|
||
def collisionDetectionTask(task):
|
||
try:
|
||
self.detectModelCollisions()
|
||
return task.again
|
||
except Exception as e:
|
||
print(f"❌ 碰撞检测任务异常: {str(e)}")
|
||
return task.again
|
||
|
||
self.collision_task = taskMgr.doMethodLater(
|
||
self.collision_detection_frequency,
|
||
collisionDetectionTask,
|
||
"modelCollisionDetection"
|
||
)
|
||
|
||
def _stopCollisionDetectionTask(self):
|
||
"""停止碰撞检测任务"""
|
||
if self.collision_task:
|
||
taskMgr.remove(self.collision_task)
|
||
self.collision_task = None
|
||
|
||
def detectModelCollisions(self, specific_models=None, log_results=True):
|
||
"""检测模型间碰撞"""
|
||
if not self.model_collision_enabled and specific_models is None:
|
||
return []
|
||
|
||
start_time = time.perf_counter()
|
||
current_time = datetime.now()
|
||
|
||
models_to_check = specific_models if specific_models else self.world.models
|
||
if len(models_to_check) < 2:
|
||
return []
|
||
|
||
collision_results = []
|
||
checked_pairs = set()
|
||
current_collision_pairs = set()
|
||
|
||
# 遍历所有模型对
|
||
for i, model_a in enumerate(models_to_check):
|
||
for j, model_b in enumerate(models_to_check[i+1:], i+1):
|
||
pair_key = tuple(sorted([id(model_a), id(model_b)]))
|
||
if pair_key in checked_pairs:
|
||
continue
|
||
checked_pairs.add(pair_key)
|
||
|
||
collision_info = self._checkModelPairCollision(model_a, model_b, current_time)
|
||
if collision_info:
|
||
collision_results.append(collision_info)
|
||
current_collision_pairs.add(pair_key)
|
||
|
||
# 只有新碰撞才打印日志
|
||
if log_results and pair_key not in self.active_collisions:
|
||
self._logCollisionInfo(collision_info)
|
||
print(f"🆕 新碰撞检测到!")
|
||
|
||
# 检测碰撞结束的情况
|
||
ended_collisions = self.active_collisions - current_collision_pairs
|
||
for pair_key in ended_collisions:
|
||
print(f"✅ 碰撞结束: 模型对 {pair_key}")
|
||
|
||
# 更新活跃碰撞状态
|
||
self.active_collisions = current_collision_pairs
|
||
|
||
# 记录性能和历史
|
||
detection_time = time.perf_counter() - start_time
|
||
self.performance_monitor.recordModelCollisionDetection(
|
||
detection_time, len(models_to_check), len(collision_results))
|
||
|
||
if collision_results:
|
||
self.collision_history.extend(collision_results)
|
||
if len(self.collision_history) > self.max_collision_history:
|
||
self.collision_history = self.collision_history[-self.max_collision_history:]
|
||
|
||
return collision_results
|
||
|
||
def _checkModelPairCollision(self, model_a, model_b, timestamp):
|
||
"""使用Panda3D内置碰撞系统检查两个模型是否碰撞"""
|
||
try:
|
||
# 检查模型是否有碰撞节点
|
||
collision_node_a = self._findCollisionNode(model_a)
|
||
collision_node_b = self._findCollisionNode(model_b)
|
||
|
||
if not collision_node_a or not collision_node_b:
|
||
return None
|
||
|
||
# 使用Panda3D的碰撞遍历器
|
||
temp_traverser = CollisionTraverser()
|
||
temp_queue = CollisionHandlerQueue()
|
||
|
||
# 设置碰撞检测
|
||
temp_traverser.addCollider(collision_node_a, temp_queue)
|
||
|
||
# 执行碰撞检测
|
||
temp_traverser.traverse(model_b)
|
||
|
||
if temp_queue.getNumEntries() > 0:
|
||
# 有碰撞,获取碰撞信息
|
||
entry = temp_queue.getEntry(0)
|
||
|
||
center_a = model_a.getPos(self.world.render)
|
||
center_b = model_b.getPos(self.world.render)
|
||
distance = (center_b - center_a).length()
|
||
|
||
return {
|
||
'timestamp': timestamp,
|
||
'model_a': {
|
||
'name': model_a.getName(),
|
||
'center': center_a,
|
||
'radius': 0, # 不再需要手动计算半径
|
||
'node': model_a
|
||
},
|
||
'model_b': {
|
||
'name': model_b.getName(),
|
||
'center': center_b,
|
||
'radius': 0,
|
||
'node': model_b
|
||
},
|
||
'collision_point': entry.getSurfacePoint(self.world.render),
|
||
'distance': distance,
|
||
'penetration_depth': 0, # Panda3D会自动处理
|
||
'collision_normal': entry.getSurfaceNormal(self.world.render)
|
||
}
|
||
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"❌ 检测模型对碰撞失败 ({model_a.getName()} vs {model_b.getName()}): {str(e)}")
|
||
return None
|
||
|
||
def _findCollisionNode(self, model):
|
||
"""查找模型的碰撞节点"""
|
||
for child in model.getChildren():
|
||
if isinstance(child.node(), CollisionNode):
|
||
return child
|
||
return None
|
||
|
||
def _logCollisionInfo(self, collision_info):
|
||
"""输出碰撞信息日志"""
|
||
timestamp = collision_info['timestamp'].strftime('%H:%M:%S.%f')[:-3]
|
||
model_a = collision_info['model_a']
|
||
model_b = collision_info['model_b']
|
||
|
||
print(f"🔥 [{timestamp}] 检测到碰撞:")
|
||
print(f" 模型A: {model_a['name']} (中心: {model_a['center']}, 半径: {model_a['radius']:.2f})")
|
||
print(f" 模型B: {model_b['name']} (中心: {model_b['center']}, 半径: {model_b['radius']:.2f})")
|
||
print(f" 碰撞点: {collision_info['collision_point']}")
|
||
print(f" 中心距离: {collision_info['distance']:.3f}")
|
||
print(f" 穿透深度: {collision_info['penetration_depth']:.3f}")
|
||
print(f" 碰撞法向: {collision_info['collision_normal']}")
|
||
|
||
def getCollisionHistory(self, limit=None):
|
||
"""获取碰撞历史记录
|
||
|
||
Args:
|
||
limit: 返回记录数量限制
|
||
|
||
Returns:
|
||
list: 碰撞历史记录
|
||
"""
|
||
if limit:
|
||
return self.collision_history[-limit:]
|
||
return self.collision_history.copy()
|
||
|
||
def clearCollisionHistory(self):
|
||
"""清除碰撞历史记录"""
|
||
self.collision_history.clear()
|
||
print("✅ 碰撞历史记录已清除")
|
||
|
||
def getCollisionStatistics(self):
|
||
"""获取碰撞统计信息"""
|
||
if not self.collision_history:
|
||
return {"total_collisions": 0, "unique_pairs": 0, "most_frequent_pair": None}
|
||
|
||
# 统计碰撞对
|
||
collision_pairs = {}
|
||
for collision in self.collision_history:
|
||
pair_key = tuple(sorted([
|
||
collision['model_a']['name'],
|
||
collision['model_b']['name']
|
||
]))
|
||
collision_pairs[pair_key] = collision_pairs.get(pair_key, 0) + 1
|
||
|
||
most_frequent_pair = max(collision_pairs.items(), key=lambda x: x[1])
|
||
|
||
return {
|
||
"total_collisions": len(self.collision_history),
|
||
"unique_pairs": len(collision_pairs),
|
||
"most_frequent_pair": {
|
||
"models": most_frequent_pair[0],
|
||
"count": most_frequent_pair[1]
|
||
},
|
||
"collision_pairs": collision_pairs
|
||
}
|
||
|
||
def createCollisionShape(self, model, shape_type='auto', **kwargs):
|
||
"""为模型创建指定类型的碰撞体
|
||
|
||
Args:
|
||
model: 模型节点
|
||
shape_type: 碰撞体类型 ('auto', 'sphere', 'box', 'capsule', 'plane', 'polygon')
|
||
**kwargs: 形状特定参数
|
||
|
||
Returns:
|
||
CollisionSolid: 创建的碰撞体
|
||
"""
|
||
from panda3d.core import (
|
||
CollisionSphere, CollisionBox, CollisionCapsule,
|
||
CollisionPlane, CollisionPolygon, Plane, Vec3
|
||
)
|
||
|
||
# 获取考虑变换后的实际尺寸和中心
|
||
transformed_info = self._getTransformedModelInfo(model)
|
||
if not transformed_info:
|
||
# 默认小球体
|
||
return CollisionSphere(Point3(0, 0, 0), 1.0)
|
||
|
||
center = transformed_info['center']
|
||
radius = transformed_info['radius']
|
||
actual_size = transformed_info['size']
|
||
scale_factor = transformed_info['scale_factor']
|
||
|
||
# 自动选择最适合的形状
|
||
if shape_type == 'auto':
|
||
shape_type = self._determineOptimalShape(model, transformed_info)
|
||
|
||
if shape_type == 'sphere':
|
||
# 优化球形碰撞体
|
||
sphere_radius = kwargs.get('radius', radius)
|
||
|
||
# 支持位置偏移
|
||
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
|
||
sphere_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)
|
||
|
||
return CollisionSphere(sphere_center, sphere_radius)
|
||
|
||
elif shape_type == 'box':
|
||
# 优化盒型碰撞体 - 更精确的尺寸和位置控制(考虑缩放)
|
||
# 获取自定义尺寸,如果没有提供则使用变换后的实际尺寸
|
||
width = kwargs.get('width', actual_size.x)
|
||
length = kwargs.get('length', actual_size.y)
|
||
height = kwargs.get('height', actual_size.z)
|
||
|
||
# 支持位置偏移
|
||
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
|
||
box_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)
|
||
|
||
# 计算盒子的最小和最大点(基于偏移后的中心)
|
||
half_width = width / 2
|
||
half_length = length / 2
|
||
half_height = height / 2
|
||
|
||
min_point = Point3(box_center.x - half_width, box_center.y - half_length, box_center.z - half_height)
|
||
max_point = Point3(box_center.x + half_width, box_center.y + half_length, box_center.z + half_height)
|
||
return CollisionBox(min_point, max_point)
|
||
|
||
elif shape_type == 'capsule':
|
||
# 优化胶囊体碰撞 - 更合理的比例和位置控制(考虑缩放)
|
||
# 使用变换后的实际高度,或自定义高度
|
||
custom_height = kwargs.get('height', actual_size.z)
|
||
|
||
# 更合理的半径计算:基于变换后模型宽度的平均值
|
||
default_radius = min(actual_size.x, actual_size.y) / 2.5 # 稍微小于模型的宽度
|
||
custom_radius = kwargs.get('radius', min(default_radius, custom_height * 0.4))
|
||
|
||
# 支持位置偏移
|
||
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
|
||
capsule_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)
|
||
|
||
# 计算胶囊体的两个端点(确保半径不会超出高度)
|
||
effective_height = max(custom_height, custom_radius * 2.1) # 确保高度至少是半径的2倍多一点
|
||
point_a = Point3(capsule_center.x, capsule_center.y, capsule_center.z - effective_height/2 + custom_radius)
|
||
point_b = Point3(capsule_center.x, capsule_center.y, capsule_center.z + effective_height/2 - custom_radius)
|
||
return CollisionCapsule(point_a, point_b, custom_radius)
|
||
|
||
elif shape_type == 'plane':
|
||
# 优化平面碰撞 - 支持位置偏移和更灵活的法向量
|
||
normal = kwargs.get('normal', Vec3(0, 0, 1))
|
||
|
||
# 支持位置偏移
|
||
pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0))
|
||
plane_point = kwargs.get('point', Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z))
|
||
|
||
# 标准化法向量
|
||
normal.normalize()
|
||
plane = Plane(normal, plane_point)
|
||
return CollisionPlane(plane)
|
||
|
||
elif shape_type == 'polygon':
|
||
# === 创建多边形碰撞体 ===
|
||
# CollisionPolygon特点:
|
||
# - 单个平面多边形,所有顶点必须共面
|
||
# - 适用于:地板、墙面、简单平台等平面区域
|
||
# - 性能较好,但只能表示平面形状
|
||
#
|
||
# Mesh碰撞体特点:
|
||
# - 由多个三角形组成的复杂3D形状
|
||
# - 适用于:复杂地形、建筑物、不规则物体
|
||
# - 性能较差,但可以表示任意复杂形状
|
||
|
||
vertices = kwargs.get('vertices', [])
|
||
if len(vertices) >= 3:
|
||
# 将顶点转换为Point3对象
|
||
# 要求:所有顶点必须在同一平面上
|
||
collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices])
|
||
return collision_poly
|
||
else:
|
||
#print("⚠️ 多边形至少需要3个顶点,回退到球体")
|
||
return CollisionSphere(center, radius)
|
||
|
||
def _determineOptimalShape(self, model, transformed_info):
|
||
"""根据模型特征自动确定最适合的碰撞体形状"""
|
||
# 获取变换后的模型尺寸比例
|
||
size = transformed_info['size']
|
||
max_dim = max(size.x, size.y, size.z)
|
||
min_dim = min(size.x, size.y, size.z)
|
||
|
||
# 根据模型名称和比例判断
|
||
model_name = model.getName().lower()
|
||
|
||
# 角色类模型使用胶囊体
|
||
if any(keyword in model_name for keyword in ['character', 'human', 'player', 'npc']):
|
||
return 'capsule'
|
||
|
||
# 建筑、箱子类使用包围盒
|
||
if any(keyword in model_name for keyword in ['building', 'box', 'cube', 'wall', 'door']):
|
||
return 'box'
|
||
|
||
# 地面、平台使用平面
|
||
if any(keyword in model_name for keyword in ['ground', 'floor', 'platform', 'terrain']):
|
||
return 'plane'
|
||
|
||
# 根据尺寸比例判断
|
||
aspect_ratio = max_dim / min_dim if min_dim > 0 else 1
|
||
|
||
if aspect_ratio > 3: # 细长物体用胶囊体
|
||
return 'capsule'
|
||
elif aspect_ratio < 1.5: # 接近球形用球体
|
||
return 'sphere'
|
||
else: # 其他用包围盒
|
||
return 'box'
|
||
|
||
def _getTransformedModelInfo(self, model):
|
||
"""获取考虑变换后的模型信息
|
||
|
||
Args:
|
||
model: 模型节点
|
||
|
||
Returns:
|
||
dict: 包含变换后的尺寸、中心、半径等信息
|
||
"""
|
||
try:
|
||
# 获取原始包围盒
|
||
bounds = model.getBounds()
|
||
if bounds.isEmpty():
|
||
return None
|
||
|
||
# 获取模型的变换矩阵
|
||
transform = model.getTransform()
|
||
scale = model.getScale()
|
||
|
||
# 计算缩放因子(取三轴缩放的平均值)
|
||
scale_factor = (abs(scale.x) + abs(scale.y) + abs(scale.z)) / 3.0
|
||
|
||
# 获取原始尺寸
|
||
original_size = bounds.getMax() - bounds.getMin()
|
||
|
||
# 应用缩放到尺寸
|
||
actual_size = Vec3(
|
||
original_size.x * abs(scale.x),
|
||
original_size.y * abs(scale.y),
|
||
original_size.z * abs(scale.z)
|
||
)
|
||
|
||
# 获取变换后的中心点(在世界坐标系中)
|
||
original_center = bounds.getCenter()
|
||
if hasattr(model, 'getPos'):
|
||
# 模型在世界坐标系中的位置
|
||
world_center = model.getPos(model.getParent() if model.getParent() else model)
|
||
center = Point3(world_center.x, world_center.y, world_center.z)
|
||
else:
|
||
# 如果无法获取世界位置,使用原始中心
|
||
center = original_center
|
||
|
||
# 计算变换后的半径(考虑缩放)
|
||
original_radius = bounds.getRadius()
|
||
transformed_radius = original_radius * scale_factor
|
||
|
||
# 调试信息
|
||
print(f"🔍 模型 {model.getName()} 变换信息:")
|
||
print(f" 原始尺寸: {original_size}")
|
||
print(f" 缩放因子: {scale}")
|
||
print(f" 变换后尺寸: {actual_size}")
|
||
print(f" 变换后半径: {transformed_radius:.2f}")
|
||
|
||
return {
|
||
'center': center,
|
||
'radius': transformed_radius,
|
||
'size': actual_size,
|
||
'scale_factor': scale_factor,
|
||
'original_size': original_size,
|
||
'scale': scale,
|
||
'transform': transform
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 获取模型变换信息失败: {e}")
|
||
# 回退到原始包围盒
|
||
bounds = model.getBounds()
|
||
if bounds.isEmpty():
|
||
return None
|
||
|
||
original_size = bounds.getMax() - bounds.getMin()
|
||
return {
|
||
'center': bounds.getCenter(),
|
||
'radius': bounds.getRadius(),
|
||
'size': original_size,
|
||
'scale_factor': 1.0,
|
||
'original_size': original_size,
|
||
'scale': Vec3(1, 1, 1),
|
||
'transform': None
|
||
}
|
||
|
||
def createMouseRay(self, screen_x, screen_y, mask_types=['SELECTABLE']):
|
||
"""创建鼠标射线"""
|
||
# 组合掩码
|
||
combined_mask = BitMask32.allOff()
|
||
for mask_type in mask_types:
|
||
if mask_type in self.MASKS:
|
||
combined_mask |= self.MASKS[mask_type]
|
||
|
||
# 坐标转换
|
||
near_point, far_point = self.world.event_handler.robustCoordinateConversion(
|
||
screen_x, screen_y)
|
||
|
||
if not near_point:
|
||
return None
|
||
|
||
# 创建射线节点
|
||
ray_node = CollisionNode('mouse_ray')
|
||
ray_node.setFromCollideMask(combined_mask)
|
||
ray_node.setIntoCollideMask(BitMask32.allOff())
|
||
|
||
# 创建射线
|
||
direction = far_point - near_point
|
||
direction.normalize()
|
||
ray = CollisionRay(near_point, direction)
|
||
ray_node.addSolid(ray)
|
||
|
||
return ray_node, near_point, far_point
|
||
|
||
def performRaycast(self, ray_node, scene_root=None):
|
||
"""执行射线检测"""
|
||
if scene_root is None:
|
||
scene_root = self.world.render
|
||
|
||
# 创建临时节点路径
|
||
ray_np = self.world.cam.attachNewNode(ray_node)
|
||
|
||
try:
|
||
# 清除之前的结果
|
||
self.queue.clearEntries()
|
||
|
||
# 添加碰撞器
|
||
self.traverser.addCollider(ray_np, self.queue)
|
||
|
||
# 执行遍历
|
||
start_time = time.perf_counter()
|
||
self.traverser.traverse(scene_root)
|
||
detection_time = time.perf_counter() - start_time
|
||
|
||
# 记录性能
|
||
self.performance_monitor.recordDetection(
|
||
detection_time, self.queue.getNumEntries())
|
||
|
||
# 处理结果
|
||
results = []
|
||
if self.queue.getNumEntries() > 0:
|
||
self.queue.sortEntries()
|
||
|
||
for i in range(self.queue.getNumEntries()):
|
||
entry = self.queue.getEntry(i)
|
||
results.append({
|
||
'hit_pos': entry.getSurfacePoint(scene_root),
|
||
'hit_normal': entry.getSurfaceNormal(scene_root),
|
||
'hit_node': entry.getIntoNodePath(),
|
||
'distance': entry.getT()
|
||
})
|
||
|
||
return results
|
||
|
||
finally:
|
||
# 清理资源
|
||
self.traverser.removeCollider(ray_np)
|
||
ray_np.removeNode()
|
||
|
||
def getCollisionMask(self, mask_type):
|
||
"""获取碰撞掩码"""
|
||
return self.MASKS.get(mask_type, BitMask32.allOff())
|
||
|
||
def createCollisionGroup(self, group_name, mask_types, enabled=True):
|
||
"""创建碰撞分组
|
||
|
||
Args:
|
||
group_name: 分组名称
|
||
mask_types: 掩码类型列表
|
||
enabled: 是否启用
|
||
"""
|
||
combined_mask = BitMask32.allOff()
|
||
for mask_type in mask_types:
|
||
if mask_type in self.MASKS:
|
||
combined_mask |= self.MASKS[mask_type]
|
||
|
||
self.collision_groups[group_name] = {
|
||
'mask': combined_mask,
|
||
'types': mask_types,
|
||
'objects': []
|
||
}
|
||
self.group_enabled[group_name] = enabled
|
||
|
||
print(f"✅ 创建碰撞分组: {group_name}, 掩码: {mask_types}")
|
||
|
||
def addToCollisionGroup(self, group_name, model, collision_node):
|
||
"""将模型添加到碰撞分组"""
|
||
if group_name in self.collision_groups:
|
||
self.collision_groups[group_name]['objects'].append({
|
||
'model': model,
|
||
'collision_node': collision_node
|
||
})
|
||
|
||
def enableCollisionGroup(self, group_name, enabled=True):
|
||
"""启用/禁用碰撞分组"""
|
||
if group_name in self.group_enabled:
|
||
self.group_enabled[group_name] = enabled
|
||
|
||
# 更新分组中所有对象的碰撞状态
|
||
if group_name in self.collision_groups:
|
||
for obj in self.collision_groups[group_name]['objects']:
|
||
collision_node = obj['collision_node']
|
||
if enabled:
|
||
collision_node.show()
|
||
else:
|
||
collision_node.hide()
|
||
|
||
print(f"{'✅ 启用' if enabled else '❌ 禁用'}碰撞分组: {group_name}")
|
||
|
||
def registerCollisionCallback(self, mask_a, mask_b, callback_func, filter_func=None):
|
||
"""注册碰撞回调函数
|
||
|
||
Args:
|
||
mask_a: 第一个掩码类型
|
||
mask_b: 第二个掩码类型
|
||
callback_func: 碰撞回调函数 callback(collision_info)
|
||
filter_func: 过滤函数 filter(model_a, model_b) -> bool
|
||
"""
|
||
key = tuple(sorted([mask_a, mask_b]))
|
||
if key not in self.collision_callbacks:
|
||
self.collision_callbacks[key] = []
|
||
|
||
self.collision_callbacks[key].append(callback_func)
|
||
|
||
if filter_func:
|
||
if key not in self.collision_filters:
|
||
self.collision_filters[key] = []
|
||
self.collision_filters[key].append(filter_func)
|
||
|
||
print(f"✅ 注册碰撞回调: {mask_a} <-> {mask_b}")
|
||
|
||
def _executeCollisionCallbacks(self, collision_info):
|
||
"""执行碰撞回调"""
|
||
model_a = collision_info['model_a']['node']
|
||
model_b = collision_info['model_b']['node']
|
||
|
||
# 获取模型的掩码类型
|
||
mask_a = self._getModelMaskType(model_a)
|
||
mask_b = self._getModelMaskType(model_b)
|
||
|
||
if mask_a and mask_b:
|
||
key = tuple(sorted([mask_a, mask_b]))
|
||
|
||
# 检查过滤条件
|
||
if key in self.collision_filters:
|
||
for filter_func in self.collision_filters[key]:
|
||
if not filter_func(model_a, model_b):
|
||
return
|
||
|
||
# 执行回调
|
||
if key in self.collision_callbacks:
|
||
for callback_func in self.collision_callbacks[key]:
|
||
try:
|
||
callback_func(collision_info)
|
||
except Exception as e:
|
||
print(f"❌ 碰撞回调执行失败: {e}")
|
||
|
||
def _getModelMaskType(self, model):
|
||
"""获取模型的掩码类型"""
|
||
# 从模型标签或属性中获取掩码类型
|
||
if model.hasTag('collision_mask_type'):
|
||
return model.getTag('collision_mask_type')
|
||
return None
|
||
|
||
def enableSpatialPartitioning(self, enabled=True, max_depth=6, max_objects=10):
|
||
"""启用空间分割优化
|
||
|
||
Args:
|
||
enabled: 是否启用
|
||
max_depth: 八叉树最大深度
|
||
max_objects: 每个节点最大对象数
|
||
"""
|
||
self.spatial_partitioning_enabled = enabled
|
||
self.octree_max_depth = max_depth
|
||
self.octree_max_objects = max_objects
|
||
|
||
if enabled:
|
||
self._buildOctree()
|
||
print(f"✅ 启用空间分割优化 - 深度: {max_depth}, 对象数: {max_objects}")
|
||
else:
|
||
self.octree = None
|
||
print("❌ 禁用空间分割优化")
|
||
|
||
def _buildOctree(self):
|
||
"""构建八叉树"""
|
||
if not hasattr(self.world, 'models') or not self.world.models:
|
||
return
|
||
|
||
# 计算场景边界
|
||
min_bound = Point3(float('inf'), float('inf'), float('inf'))
|
||
max_bound = Point3(float('-inf'), float('-inf'), float('-inf'))
|
||
|
||
for model in self.world.models:
|
||
bounds = model.getBounds()
|
||
if not bounds.isEmpty():
|
||
model_min = bounds.getMin()
|
||
model_max = bounds.getMax()
|
||
|
||
min_bound.x = min(min_bound.x, model_min.x)
|
||
min_bound.y = min(min_bound.y, model_min.y)
|
||
min_bound.z = min(min_bound.z, model_min.z)
|
||
|
||
max_bound.x = max(max_bound.x, model_max.x)
|
||
max_bound.y = max(max_bound.y, model_max.y)
|
||
max_bound.z = max(max_bound.z, model_max.z)
|
||
|
||
# 创建八叉树根节点
|
||
self.octree = OctreeNode(min_bound, max_bound, 0, self.octree_max_depth, self.octree_max_objects)
|
||
|
||
# 将模型插入八叉树
|
||
for model in self.world.models:
|
||
self.octree.insert(model)
|
||
|
||
def detectModelCollisionsOptimized(self, specific_models=None, log_results=True):
|
||
"""使用空间分割优化的碰撞检测"""
|
||
if not self.spatial_partitioning_enabled or not self.octree:
|
||
return self.detectModelCollisions(specific_models, log_results)
|
||
|
||
start_time = time.perf_counter()
|
||
current_time = datetime.now()
|
||
|
||
models_to_check = specific_models if specific_models else self.world.models
|
||
if len(models_to_check) < 2:
|
||
return []
|
||
|
||
collision_results = []
|
||
current_collision_pairs = set()
|
||
|
||
# 使用八叉树进行优化检测
|
||
for model in models_to_check:
|
||
# 获取可能碰撞的模型列表
|
||
potential_collisions = self.octree.query(model)
|
||
|
||
for other_model in potential_collisions:
|
||
if model == other_model:
|
||
continue
|
||
|
||
pair_key = tuple(sorted([id(model), id(other_model)]))
|
||
if pair_key in current_collision_pairs:
|
||
continue
|
||
|
||
collision_info = self._checkModelPairCollision(model, other_model, current_time)
|
||
if collision_info:
|
||
collision_results.append(collision_info)
|
||
current_collision_pairs.add(pair_key)
|
||
|
||
# 执行回调
|
||
self._executeCollisionCallbacks(collision_info)
|
||
|
||
# 只有新碰撞才打印日志
|
||
if log_results and pair_key not in self.active_collisions:
|
||
self._logCollisionInfo(collision_info)
|
||
print(f"🆕 新碰撞检测到!")
|
||
|
||
# 更新活跃碰撞状态
|
||
ended_collisions = self.active_collisions - current_collision_pairs
|
||
for pair_key in ended_collisions:
|
||
print(f"✅ 碰撞结束: 模型对 {pair_key}")
|
||
|
||
self.active_collisions = current_collision_pairs
|
||
|
||
# 记录性能
|
||
detection_time = time.perf_counter() - start_time
|
||
self.performance_monitor.recordModelCollisionDetection(
|
||
detection_time, len(models_to_check), len(collision_results))
|
||
|
||
return collision_results
|
||
|
||
def cleanup(self):
|
||
"""清理碰撞管理器资源"""
|
||
self._stopCollisionDetectionTask()
|
||
self.collision_history.clear()
|
||
self.active_collisions.clear()
|
||
print("✅ 碰撞管理器已清理")
|
||
|
||
def setupAdvancedCollision(self, model, shape_type='auto', mask_type='SELECTABLE',
|
||
group_name=None, **shape_kwargs):
|
||
"""高级碰撞设置方法
|
||
|
||
Args:
|
||
model: 模型节点
|
||
shape_type: 碰撞体形状类型
|
||
mask_type: 掩码类型
|
||
group_name: 碰撞分组名称
|
||
**shape_kwargs: 形状特定参数
|
||
|
||
Returns:
|
||
collision_node_path: 碰撞节点路径
|
||
"""
|
||
# 创建碰撞节点
|
||
cNode = CollisionNode(f'collision_{model.getName()}')
|
||
|
||
# 设置掩码
|
||
if mask_type in self.MASKS:
|
||
cNode.setIntoCollideMask(self.MASKS[mask_type])
|
||
# 设置模型标签用于回调识别
|
||
model.setTag('collision_mask_type', mask_type)
|
||
|
||
# 创建碰撞体
|
||
collision_solid = self.createCollisionShape(model, shape_type, **shape_kwargs)
|
||
cNode.addSolid(collision_solid)
|
||
|
||
# 附加到模型
|
||
cNodePath = model.attachNewNode(cNode)
|
||
|
||
# 添加到分组
|
||
if group_name:
|
||
if group_name not in self.collision_groups:
|
||
self.createCollisionGroup(group_name, [mask_type])
|
||
self.addToCollisionGroup(group_name, model, cNodePath)
|
||
|
||
# 根据调试设置决定是否显示
|
||
if hasattr(self.world, 'debug_collision') and self.world.debug_collision:
|
||
cNodePath.show()
|
||
else:
|
||
cNodePath.hide()
|
||
|
||
print(f"✅ 为 {model.getName()} 设置高级碰撞 - 形状: {shape_type}, 掩码: {mask_type}")
|
||
return cNodePath
|
||
|
||
def example_usage(self):
|
||
"""碰撞系统使用示例"""
|
||
print("\n=== 碰撞系统使用示例 ===")
|
||
|
||
# 1. 创建碰撞分组
|
||
self.createCollisionGroup('characters', ['CHARACTER', 'DYNAMIC_OBJECT'])
|
||
self.createCollisionGroup('environment', ['STATIC_GEOMETRY', 'TERRAIN'])
|
||
self.createCollisionGroup('pickups', ['PICKUP_ITEM', 'INTERACTIVE'])
|
||
|
||
# 2. 注册碰撞回调
|
||
def character_pickup_callback(collision_info):
|
||
print(f"角色拾取物品: {collision_info['model_a']['name']} -> {collision_info['model_b']['name']}")
|
||
|
||
def character_environment_callback(collision_info):
|
||
print(f"角色碰撞环境: {collision_info['model_a']['name']} 碰到 {collision_info['model_b']['name']}")
|
||
|
||
self.registerCollisionCallback('CHARACTER', 'PICKUP_ITEM', character_pickup_callback)
|
||
self.registerCollisionCallback('CHARACTER', 'STATIC_GEOMETRY', character_environment_callback)
|
||
|
||
# 3. 启用空间分割优化
|
||
self.enableSpatialPartitioning(enabled=True, max_depth=6, max_objects=8)
|
||
|
||
# 4. 为模型设置不同类型的碰撞体
|
||
# model1 = ... # 假设已有模型
|
||
# self.setupAdvancedCollision(model1, 'capsule', 'CHARACTER', 'characters')
|
||
# self.setupAdvancedCollision(model2, 'box', 'PICKUP_ITEM', 'pickups')
|
||
# self.setupAdvancedCollision(model3, 'plane', 'TERRAIN', 'environment')
|
||
|
||
print("✅ 碰撞系统示例配置完成")
|
||
|
||
class CollisionPerformanceMonitor:
|
||
"""碰撞检测性能监控器"""
|
||
|
||
def __init__(self):
|
||
self.detection_times = []
|
||
self.collision_counts = []
|
||
self.model_collision_times = [] # 新增:模型间碰撞检测时间
|
||
self.model_collision_data = [] # 新增:模型间碰撞数据
|
||
self.max_records = 100
|
||
|
||
def recordDetection(self, detection_time, collision_count):
|
||
"""记录射线检测性能"""
|
||
self.detection_times.append(detection_time)
|
||
self.collision_counts.append(collision_count)
|
||
|
||
# 限制记录数量
|
||
if len(self.detection_times) > self.max_records:
|
||
self.detection_times.pop(0)
|
||
self.collision_counts.pop(0)
|
||
|
||
def recordModelCollisionDetection(self, detection_time, model_count, collision_count):
|
||
"""记录模型间碰撞检测性能"""
|
||
self.model_collision_times.append(detection_time)
|
||
self.model_collision_data.append({
|
||
'model_count': model_count,
|
||
'collision_count': collision_count,
|
||
'timestamp': time.time()
|
||
})
|
||
|
||
# 限制记录数量
|
||
if len(self.model_collision_times) > self.max_records:
|
||
self.model_collision_times.pop(0)
|
||
self.model_collision_data.pop(0)
|
||
|
||
def getAverageTime(self):
|
||
"""获取平均射线检测时间"""
|
||
if not self.detection_times:
|
||
return 0
|
||
return sum(self.detection_times) / len(self.detection_times)
|
||
|
||
def getAverageModelCollisionTime(self):
|
||
"""获取平均模型间碰撞检测时间"""
|
||
if not self.model_collision_times:
|
||
return 0
|
||
return sum(self.model_collision_times) / len(self.model_collision_times)
|
||
|
||
def getPerformanceReport(self):
|
||
"""获取完整性能报告"""
|
||
report = []
|
||
|
||
# 射线检测性能
|
||
if self.detection_times:
|
||
avg_time = self.getAverageTime()
|
||
max_time = max(self.detection_times)
|
||
avg_collisions = sum(self.collision_counts) / len(self.collision_counts)
|
||
|
||
report.append("=== 射线检测性能 ===")
|
||
report.append(f"平均检测时间: {avg_time*1000:.2f}ms")
|
||
report.append(f"最大检测时间: {max_time*1000:.2f}ms")
|
||
report.append(f"平均碰撞数量: {avg_collisions:.1f}")
|
||
report.append(f"检测次数: {len(self.detection_times)}")
|
||
|
||
# 模型间碰撞检测性能
|
||
if self.model_collision_times:
|
||
avg_model_time = self.getAverageModelCollisionTime()
|
||
max_model_time = max(self.model_collision_times)
|
||
avg_model_count = sum(d['model_count'] for d in self.model_collision_data) / len(self.model_collision_data)
|
||
total_collisions = sum(d['collision_count'] for d in self.model_collision_data)
|
||
|
||
report.append("\n=== 模型间碰撞检测性能 ===")
|
||
report.append(f"平均检测时间: {avg_model_time*1000:.2f}ms")
|
||
report.append(f"最大检测时间: {max_model_time*1000:.2f}ms")
|
||
report.append(f"平均模型数量: {avg_model_count:.1f}")
|
||
report.append(f"总碰撞次数: {total_collisions}")
|
||
report.append(f"检测次数: {len(self.model_collision_times)}")
|
||
|
||
return "\n".join(report) if report else "没有性能数据"
|
||
|
||
class OctreeNode:
|
||
"""八叉树节点"""
|
||
|
||
def __init__(self, min_bound, max_bound, depth, max_depth, max_objects):
|
||
self.min_bound = min_bound
|
||
self.max_bound = max_bound
|
||
self.depth = depth
|
||
self.max_depth = max_depth
|
||
self.max_objects = max_objects
|
||
|
||
self.objects = []
|
||
self.children = []
|
||
self.is_leaf = True
|
||
|
||
def insert(self, model):
|
||
"""插入模型到八叉树"""
|
||
if not self._contains(model):
|
||
return False
|
||
|
||
if self.is_leaf:
|
||
self.objects.append(model)
|
||
|
||
# 如果超过最大对象数且未达到最大深度,则分割
|
||
if len(self.objects) > self.max_objects and self.depth < self.max_depth:
|
||
self._subdivide()
|
||
|
||
# 重新分配对象到子节点
|
||
remaining_objects = []
|
||
for obj in self.objects:
|
||
inserted = False
|
||
for child in self.children:
|
||
if child.insert(obj):
|
||
inserted = True
|
||
break
|
||
if not inserted:
|
||
remaining_objects.append(obj)
|
||
|
||
self.objects = remaining_objects
|
||
else:
|
||
# 尝试插入到子节点
|
||
inserted = False
|
||
for child in self.children:
|
||
if child.insert(model):
|
||
inserted = True
|
||
break
|
||
|
||
if not inserted:
|
||
self.objects.append(model)
|
||
|
||
return True
|
||
|
||
def query(self, model):
|
||
"""查询可能与指定模型碰撞的模型列表"""
|
||
if not self._contains(model):
|
||
return []
|
||
|
||
result = list(self.objects)
|
||
|
||
if not self.is_leaf:
|
||
for child in self.children:
|
||
result.extend(child.query(model))
|
||
|
||
return result
|
||
|
||
def _contains(self, model):
|
||
"""检查模型是否在此节点范围内"""
|
||
bounds = model.getBounds()
|
||
if bounds.isEmpty():
|
||
return False
|
||
|
||
model_min = bounds.getMin()
|
||
model_max = bounds.getMax()
|
||
|
||
return (model_max.x >= self.min_bound.x and model_min.x <= self.max_bound.x and
|
||
model_max.y >= self.min_bound.y and model_min.y <= self.max_bound.y and
|
||
model_max.z >= self.min_bound.z and model_min.z <= self.max_bound.z)
|
||
|
||
def _subdivide(self):
|
||
"""分割节点为8个子节点"""
|
||
center = Point3(
|
||
(self.min_bound.x + self.max_bound.x) * 0.5,
|
||
(self.min_bound.y + self.max_bound.y) * 0.5,
|
||
(self.min_bound.z + self.max_bound.z) * 0.5
|
||
)
|
||
|
||
# 创建8个子节点
|
||
subdivisions = [
|
||
(self.min_bound, center),
|
||
(Point3(center.x, self.min_bound.y, self.min_bound.z), Point3(self.max_bound.x, center.y, center.z)),
|
||
(Point3(self.min_bound.x, center.y, self.min_bound.z), Point3(center.x, self.max_bound.y, center.z)),
|
||
(Point3(center.x, center.y, self.min_bound.z), Point3(self.max_bound.x, self.max_bound.y, center.z)),
|
||
(Point3(self.min_bound.x, self.min_bound.y, center.z), Point3(center.x, center.y, self.max_bound.z)),
|
||
(Point3(center.x, self.min_bound.y, center.z), Point3(self.max_bound.x, center.y, self.max_bound.z)),
|
||
(Point3(self.min_bound.x, center.y, center.z), Point3(center.x, self.max_bound.y, self.max_bound.z)),
|
||
(center, self.max_bound)
|
||
]
|
||
|
||
for min_b, max_b in subdivisions:
|
||
child = OctreeNode(min_b, max_b, self.depth + 1, self.max_depth, self.max_objects)
|
||
self.children.append(child)
|
||
|
||
self.is_leaf = False
|