第二十九章:3D物理系统

第二十九章:3D物理系统

物理系统是3D游戏中实现真实感交互的基础。Godot 4提供了功能完整的3D物理引擎,支持刚体模拟、碰撞检测、角色控制、关节约束等功能。本章将全面讲解Godot的3D物理系统,帮助开发者创建物理真实的游戏世界。

29.1 物理系统概述

29.1.1 物理引擎基础

# physics_overview.gd
# 物理系统概述

extends Node3D

## Godot 3D物理系统组件
## 
## 物理体类型:
## - StaticBody3D: 静态物体,不移动但参与碰撞
## - RigidBody3D: 刚体,完全由物理引擎控制
## - CharacterBody3D: 角色物体,代码控制移动
## - AnimatableBody3D: 可动画的物理体
## - Area3D: 区域检测,不产生物理响应
## 断开绳索
func break_rope_at(index: int):
    if index < 0 or index >= joints.size():
        return
    
    # 移除关节
    var joint = joints[index]
    joints.remove_at(index)
    joint.queue_free()
    
    # 解冻后半部分
    for i in range(index + 1, segments.size()):
        segments[i].freeze = false

## 施加力到绳索末端
func apply_force_to_end(force: Vector3):
    if segments.is_empty():
        return
    segments[-1].apply_central_force(force)

29.7.3 布娃娃系统

# ragdoll_system.gd
# 布娃娃系统

extends Node3D

## 布娃娃配置
class RagdollConfig:
    var bone_masses: Dictionary = {
        "head": 5.0,
        "spine": 10.0,
        "upper_arm": 2.0,
        "lower_arm": 1.5,
        "hand": 0.5,
        "upper_leg": 5.0,
        "lower_leg": 3.0,
        "foot": 1.0
    }
    
    var joint_limits: Dictionary = {
        "neck": {"swing": 30, "twist": 45},
        "spine": {"swing": 20, "twist": 30},
        "shoulder": {"swing": 90, "twist": 90},
        "elbow": {"swing": 0, "twist": 150},
        "hip": {"swing": 60, "twist": 45},
        "knee": {"swing": 0, "twist": 150}
    }

## 骨骼到刚体的映射
var bone_bodies: Dictionary = {}
var skeleton: Skeleton3D
var is_ragdoll_active: bool = false

## 激活布娃娃
func activate_ragdoll(skel: Skeleton3D, impact_force: Vector3 = Vector3.ZERO):
    skeleton = skel
    is_ragdoll_active = true
    
    # 为每个骨骼创建刚体
    _create_bone_bodies()
    
    # 创建关节
    _create_joints()
    
    # 禁用骨骼动画
    _disable_animation()
    
    # 应用冲击力
    if impact_force.length() > 0:
        _apply_impact(impact_force)

## 停用布娃娃
func deactivate_ragdoll():
    is_ragdoll_active = false
    
    # 清理刚体和关节
    for body in bone_bodies.values():
        body.queue_free()
    bone_bodies.clear()
    
    # 恢复动画
    _enable_animation()

func _create_bone_bodies():
    var config = RagdollConfig.new()
    
    for i in range(skeleton.get_bone_count()):
        var bone_name = skeleton.get_bone_name(i)
        var bone_transform = skeleton.get_bone_global_pose(i)
        
        # 创建刚体
        var body = RigidBody3D.new()
        body.mass = _get_bone_mass(bone_name, config)
        body.global_transform = skeleton.global_transform * bone_transform
        
        # 创建碰撞形状
        var collision = _create_bone_collision(bone_name, i)
        body.add_child(collision)
        
        add_child(body)
        bone_bodies[bone_name] = body

func _create_bone_collision(bone_name: String, bone_index: int) -> CollisionShape3D:
    var collision = CollisionShape3D.new()
    
    # 根据骨骼类型选择形状
    if bone_name.contains("head"):
        var sphere = SphereShape3D.new()
        sphere.radius = 0.12
        collision.shape = sphere
    elif bone_name.contains("spine") or bone_name.contains("chest"):
        var box = BoxShape3D.new()
        box.size = Vector3(0.3, 0.25, 0.2)
        collision.shape = box
    else:
        var capsule = CapsuleShape3D.new()
        capsule.radius = 0.05
        capsule.height = 0.25
        collision.shape = capsule
    
    return collision

func _create_joints():
    # 遍历骨骼层次结构,创建关节
    for i in range(skeleton.get_bone_count()):
        var parent_index = skeleton.get_bone_parent(i)
        if parent_index < 0:
            continue
        
        var bone_name = skeleton.get_bone_name(i)
        var parent_name = skeleton.get_bone_name(parent_index)
        
        if not bone_bodies.has(bone_name) or not bone_bodies.has(parent_name):
            continue
        
        var body = bone_bodies[bone_name]
        var parent_body = bone_bodies[parent_name]
        
        # 创建锥形扭转关节
        var joint = ConeTwistJoint3D.new()
        joint.global_position = body.global_position
        
        add_child(joint)
        joint.node_a = parent_body.get_path()
        joint.node_b = body.get_path()
        
        # 应用关节限制
        _apply_joint_limits(joint, bone_name)

func _apply_joint_limits(joint: ConeTwistJoint3D, bone_name: String):
    var config = RagdollConfig.new()
    var limits = {"swing": 45.0, "twist": 30.0}
    
    # 根据骨骼名称查找限制
    for key in config.joint_limits.keys():
        if bone_name.to_lower().contains(key):
            limits = config.joint_limits[key]
            break
    
    joint.set_param(ConeTwistJoint3D.PARAM_SWING_SPAN, deg_to_rad(limits.swing))
    joint.set_param(ConeTwistJoint3D.PARAM_TWIST_SPAN, deg_to_rad(limits.twist))

func _get_bone_mass(bone_name: String, config: RagdollConfig) -> float:
    for key in config.bone_masses.keys():
        if bone_name.to_lower().contains(key):
            return config.bone_masses[key]
    return 1.0

func _apply_impact(force: Vector3):
    # 将力应用到身体中心
    if bone_bodies.has("spine"):
        bone_bodies["spine"].apply_central_impulse(force)

func _disable_animation():
    var anim_player = skeleton.get_parent().get_node_or_null("AnimationPlayer")
    if anim_player:
        anim_player.stop()

func _enable_animation():
    var anim_player = skeleton.get_parent().get_node_or_null("AnimationPlayer")
    if anim_player:
        anim_player.play()

## 物理帧更新骨骼位置
func _physics_process(_delta):
    if not is_ragdoll_active:
        return
    
    for bone_name in bone_bodies.keys():
        var body = bone_bodies[bone_name]
        var bone_index = skeleton.find_bone(bone_name)
        if bone_index < 0:
            continue
        
        # 将刚体变换应用到骨骼
        var global_pose = skeleton.global_transform.inverse() * body.global_transform
        skeleton.set_bone_global_pose_override(bone_index, global_pose, 1.0, true)

29.8 载具物理

29.8.1 VehicleBody3D

# vehicle_controller.gd
# 载具控制器

extends VehicleBody3D

## 引擎参数
@export_group("Engine")
@export var max_engine_force: float = 200.0
@export var max_brake_force: float = 50.0
@export var max_steer_angle: float = 25.0

## 换挡参数
@export_group("Transmission")
@export var gear_ratios: Array[float] = [-3.0, 0.0, 3.5, 2.5, 1.8, 1.3, 1.0]
@export var final_drive_ratio: float = 3.5
@export var shift_rpm: float = 6000.0
@export var idle_rpm: float = 1000.0

## 当前状态
var current_gear: int = 1  # 0=倒挡, 1=空挡, 2-6=前进挡
var current_rpm: float = 0.0
var speed_kmh: float = 0.0

## 轮子引用
@onready var wheel_fl: VehicleWheel3D = $WheelFL
@onready var wheel_fr: VehicleWheel3D = $WheelFR
@onready var wheel_rl: VehicleWheel3D = $WheelRL
@onready var wheel_rr: VehicleWheel3D = $WheelRR

func _ready():
    _setup_wheels()

func _setup_wheels():
    # 前轮(转向)
    wheel_fl.use_as_steering = true
    wheel_fr.use_as_steering = true
    
    # 后轮(驱动)
    wheel_rl.use_as_traction = true
    wheel_rr.use_as_traction = true
    
    # 配置悬挂
    for wheel in [wheel_fl, wheel_fr, wheel_rl, wheel_rr]:
        wheel.suspension_stiffness = 50.0
        wheel.suspension_travel = 0.2
        wheel.damping_compression = 2.0
        wheel.damping_relaxation = 3.0
        wheel.wheel_friction_slip = 2.0

func _physics_process(delta):
    _update_speed()
    _process_input()
    _update_rpm(delta)
    _auto_shift()

func _update_speed():
    speed_kmh = linear_velocity.length() * 3.6

func _process_input():
    # 油门/刹车
    var throttle = Input.get_axis("brake", "accelerate")
    
    if throttle > 0:
        engine_force = throttle * max_engine_force * _get_gear_multiplier()
        brake = 0.0
    elif throttle < 0:
        if speed_kmh > 5:
            engine_force = 0.0
            brake = -throttle * max_brake_force
        else:
            # 低速时允许倒车
            engine_force = throttle * max_engine_force * 0.5
            brake = 0.0
    else:
        engine_force = 0.0
        brake = 0.0
    
    # 转向
    var steer_input = Input.get_axis("steer_right", "steer_left")
    var steer_speed = 5.0 if speed_kmh < 30 else 2.0
    steering = lerp(steering, steer_input * deg_to_rad(max_steer_angle), steer_speed * get_physics_process_delta_time())
    
    # 手刹
    if Input.is_action_pressed("handbrake"):
        brake = max_brake_force

func _get_gear_multiplier() -> float:
    if current_gear < 0 or current_gear >= gear_ratios.size():
        return 0.0
    return gear_ratios[current_gear] * final_drive_ratio

func _update_rpm(delta):
    if current_gear == 1:  # 空挡
        current_rpm = lerp(current_rpm, idle_rpm, delta * 2.0)
        return
    
    # 根据车速和档位计算RPM
    var wheel_rpm = abs(linear_velocity.length()) * 60.0 / (2.0 * PI * 0.35)  # 假设轮胎半径0.35m
    var target_rpm = wheel_rpm * abs(_get_gear_multiplier())
    target_rpm = clamp(target_rpm, idle_rpm, shift_rpm + 500)
    
    current_rpm = lerp(current_rpm, target_rpm, delta * 5.0)

func _auto_shift():
    if current_gear <= 1:
        return
    
    # 升挡
    if current_rpm > shift_rpm and current_gear < gear_ratios.size() - 1:
        shift_up()
    
    # 降挡
    if current_rpm < idle_rpm + 500 and current_gear > 2:
        shift_down()

func shift_up():
    if current_gear < gear_ratios.size() - 1:
        current_gear += 1

func shift_down():
    if current_gear > 0:
        current_gear -= 1

## 获取车辆信息
func get_vehicle_info() -> Dictionary:
    return {
        "speed_kmh": speed_kmh,
        "rpm": current_rpm,
        "gear": current_gear,
        "engine_force": engine_force
    }

29.8.2 自定义载具物理

# custom_vehicle.gd
# 自定义载具物理(用于更精细的控制)

extends RigidBody3D

## 载具参数
@export_group("Vehicle Settings")
@export var max_motor_torque: float = 500.0
@export var max_steering_angle: float = 30.0
@export var wheel_base: float = 2.5
@export var track_width: float = 1.6
@export var center_of_mass_offset: Vector3 = Vector3(0, -0.5, 0)

## 轮胎参数
@export_group("Tire Settings")
@export var tire_friction: float = 1.5
@export var tire_stiffness: float = 20000.0
@export var tire_damping: float = 2000.0

## 空气动力学
@export_group("Aerodynamics")
@export var drag_coefficient: float = 0.35
@export var frontal_area: float = 2.2
@export var downforce_coefficient: float = 0.5

## 轮子数据
class WheelData:
    var raycast: RayCast3D
    var is_grounded: bool = false
    var contact_point: Vector3
    var contact_normal: Vector3
    var compression: float = 0.0
    var suspension_force: float = 0.0
    var slip_angle: float = 0.0
    var slip_ratio: float = 0.0

var wheels: Array[WheelData] = []
var steering_input: float = 0.0
var throttle_input: float = 0.0
var brake_input: float = 0.0

func _ready():
    # 设置质心
    center_of_mass = center_of_mass_offset
    
    # 初始化轮子
    _setup_wheels()

func _setup_wheels():
    var wheel_positions = [
        Vector3(-track_width/2, 0, wheel_base/2),   # FL
        Vector3(track_width/2, 0, wheel_base/2),    # FR
        Vector3(-track_width/2, 0, -wheel_base/2),  # RL
        Vector3(track_width/2, 0, -wheel_base/2)    # RR
    ]
    
    for i in range(4):
        var wheel = WheelData.new()
        wheel.raycast = RayCast3D.new()
        wheel.raycast.position = wheel_positions[i]
        wheel.raycast.target_position = Vector3(0, -0.8, 0)
        wheel.raycast.enabled = true
        add_child(wheel.raycast)
        wheels.append(wheel)

func _physics_process(delta):
    _get_input()
    _update_wheels(delta)
    _apply_forces(delta)
    _apply_aerodynamics()

func _get_input():
    throttle_input = Input.get_axis("brake", "accelerate")
    steering_input = Input.get_axis("steer_right", "steer_left")
    brake_input = Input.get_action_strength("handbrake")

func _update_wheels(delta):
    for i in range(wheels.size()):
        var wheel = wheels[i]
        var ray = wheel.raycast
        
        wheel.is_grounded = ray.is_colliding()
        
        if wheel.is_grounded:
            wheel.contact_point = ray.get_collision_point()
            wheel.contact_normal = ray.get_collision_normal()
            
            # 计算悬挂压缩
            var rest_length = abs(ray.target_position.y)
            var current_length = (ray.global_position - wheel.contact_point).length()
            wheel.compression = rest_length - current_length
            
            # 计算悬挂力
            wheel.suspension_force = wheel.compression * tire_stiffness
            
            # 添加阻尼
            var compression_velocity = _get_compression_velocity(wheel, delta)
            wheel.suspension_force -= compression_velocity * tire_damping
            wheel.suspension_force = max(wheel.suspension_force, 0)

func _get_compression_velocity(wheel: WheelData, delta: float) -> float:
    # 简化计算
    return 0.0

func _apply_forces(delta):
    for i in range(wheels.size()):
        var wheel = wheels[i]
        if not wheel.is_grounded:
            continue
        
        var force_position = to_local(wheel.contact_point)
        
        # 悬挂力
        var suspension_force = wheel.contact_normal * wheel.suspension_force
        apply_force(suspension_force, force_position)
        
        # 转向角度(只有前轮)
        var steer_angle = 0.0
        if i < 2:  # 前轮
            steer_angle = deg_to_rad(steering_input * max_steering_angle)
        
        # 计算轮子方向
        var wheel_direction = -global_transform.basis.z.rotated(Vector3.UP, steer_angle)
        var wheel_right = wheel_direction.cross(wheel.contact_normal).normalized()
        
        # 驱动力(只有后轮)
        if i >= 2:  # 后轮
            var motor_force = wheel_direction * throttle_input * max_motor_torque
            apply_force(motor_force, force_position)
        
        # 横向摩擦力
        var lateral_velocity = linear_velocity.project(wheel_right)
        var lateral_friction = -lateral_velocity * tire_friction * wheel.suspension_force / 1000.0
        apply_force(lateral_friction, force_position)
        
        # 制动力
        if brake_input > 0:
            var brake_force = -linear_velocity.normalized() * brake_input * 500.0
            apply_force(brake_force, force_position)

func _apply_aerodynamics():
    var velocity = linear_velocity
    var speed = velocity.length()
    
    if speed < 1:
        return
    
    # 空气阻力
    var air_density = 1.225
    var drag_force = 0.5 * air_density * drag_coefficient * frontal_area * speed * speed
    apply_central_force(-velocity.normalized() * drag_force)
    
    # 下压力
    var downforce = 0.5 * air_density * downforce_coefficient * frontal_area * speed * speed
    apply_central_force(Vector3.DOWN * downforce)

29.9 物理优化

29.9.1 性能优化策略

# physics_optimization.gd
# 物理系统优化

extends Node

## 优化设置
@export var enable_sleeping: bool = true
@export var enable_culling: bool = true
@export var cull_distance: float = 100.0
@export var simplified_collision_distance: float = 50.0

var camera: Camera3D
var physics_bodies: Array[RigidBody3D] = []

func _ready():
    camera = get_viewport().get_camera_3d()
    _collect_physics_bodies()

func _collect_physics_bodies():
    physics_bodies.clear()
    _find_bodies_recursive(get_tree().root)

func _find_bodies_recursive(node: Node):
    if node is RigidBody3D:
        physics_bodies.append(node)
    for child in node.get_children():
        _find_bodies_recursive(child)

func _physics_process(_delta):
    if not enable_culling:
        return
    
    for body in physics_bodies:
        if not is_instance_valid(body):
            continue
        
        var distance = body.global_position.distance_to(camera.global_position)
        
        # 距离剔除
        if distance > cull_distance:
            body.set_physics_process(false)
            body.sleeping = true
        else:
            body.set_physics_process(true)
            
            # 简化碰撞
            if distance > simplified_collision_distance:
                _use_simplified_collision(body)
            else:
                _use_full_collision(body)

func _use_simplified_collision(body: RigidBody3D):
    # 切换到更简单的碰撞形状
    for child in body.get_children():
        if child.name == "DetailedCollision":
            child.disabled = true
        elif child.name == "SimplifiedCollision":
            child.disabled = false

func _use_full_collision(body: RigidBody3D):
    for child in body.get_children():
        if child.name == "DetailedCollision":
            child.disabled = false
        elif child.name == "SimplifiedCollision":
            child.disabled = true

## 物理对象池
class PhysicsObjectPool:
    var pool: Array[RigidBody3D] = []
    var scene: PackedScene
    var parent: Node3D
    
    func _init(obj_scene: PackedScene, pool_parent: Node3D, initial_size: int = 10):
        scene = obj_scene
        parent = pool_parent
        
        for i in range(initial_size):
            var obj = scene.instantiate() as RigidBody3D
            obj.set_physics_process(false)
            obj.visible = false
            parent.add_child(obj)
            pool.append(obj)
    
    func get_object() -> RigidBody3D:
        for obj in pool:
            if not obj.visible:
                obj.visible = true
                obj.set_physics_process(true)
                obj.sleeping = false
                return obj
        
        # 池耗尽,创建新对象
        var obj = scene.instantiate() as RigidBody3D
        parent.add_child(obj)
        pool.append(obj)
        return obj
    
    func return_object(obj: RigidBody3D):
        obj.visible = false
        obj.set_physics_process(false)
        obj.sleeping = true
        obj.linear_velocity = Vector3.ZERO
        obj.angular_velocity = Vector3.ZERO

29.9.2 碰撞优化

# collision_optimization.gd
# 碰撞优化

extends Node

## 碰撞形状选择指南
## 
## 性能排序(从快到慢):
## 1. SphereShape3D - 最快
## 2. BoxShape3D - 很快
## 3. CapsuleShape3D - 快
## 4. CylinderShape3D - 中等
## 5. ConvexPolygonShape3D - 较慢(顶点数影响)
## 6. ConcavePolygonShape3D - 最慢(仅用于静态物体)

## 为模型生成优化的碰撞
static func generate_optimized_collision(
    mesh: Mesh,
    use_convex: bool = true,
    simplify: bool = true
) -> Shape3D:
    if use_convex:
        var shape = mesh.create_convex_shape(true, simplify)
        return shape
    else:
        # 凹形碰撞(仅静态物体)
        return mesh.create_trimesh_shape()

## 创建复合碰撞形状
static func create_compound_collision(meshes: Array[Mesh]) -> Array[Shape3D]:
    var shapes: Array[Shape3D] = []
    
    for mesh_data in meshes:
        var shape = mesh_data.create_convex_shape(true, true)
        shapes.append(shape)
    
    return shapes

## 碰撞层优化建议
static func get_optimized_layer_mask(body_type: String) -> Dictionary:
    # 返回推荐的层和遮罩配置
    var configs = {
        "player": {
            "layer": 1,
            "mask": 2 | 4 | 8  # 敌人、环境、拾取物
        },
        "enemy": {
            "layer": 2,
            "mask": 1 | 4 | 16  # 玩家、环境、子弹
        },
        "projectile": {
            "layer": 16,
            "mask": 2 | 4 | 128  # 敌人、环境、可破坏物
        },
        "pickup": {
            "layer": 8,
            "mask": 1  # 只与玩家碰撞
        },
        "environment": {
            "layer": 4,
            "mask": 0  # 不主动检测碰撞
        }
    }
    
    return configs.get(body_type, {"layer": 1, "mask": 0xFFFFFFFF})

29.10 实际案例:物理谜题系统

# physics_puzzle_system.gd
# 物理谜题系统

extends Node3D

## 谜题元素
class PressurePlate extends Area3D:
    signal activated
    signal deactivated
    
    var required_weight: float = 10.0
    var current_weight: float = 0.0
    var is_active: bool = false
    
    func _ready():
        body_entered.connect(_on_body_entered)
        body_exited.connect(_on_body_exited)
    
    func _on_body_entered(body: Node3D):
        if body is RigidBody3D:
            current_weight += body.mass
            _check_activation()
    
    func _on_body_exited(body: Node3D):
        if body is RigidBody3D:
            current_weight -= body.mass
            _check_activation()
    
    func _check_activation():
        var should_active = current_weight >= required_weight
        if should_active != is_active:
            is_active = should_active
            if is_active:
                emit_signal("activated")
            else:
                emit_signal("deactivated")

class PushableBlock extends RigidBody3D:
    @export var snap_to_grid: bool = true
    @export var grid_size: float = 1.0
    
    var is_being_pushed: bool = false
    
    func _physics_process(_delta):
        if snap_to_grid and sleeping:
            _snap_position()
    
    func _snap_position():
        var snapped = global_position.snapped(Vector3.ONE * grid_size)
        snapped.y = global_position.y  # 保持Y轴
        global_position = global_position.lerp(snapped, 0.1)

class Lever extends Node3D:
    signal toggled(state: bool)
    
    var is_on: bool = false
    var can_toggle: bool = true
    var cooldown: float = 0.5
    
    func interact():
        if not can_toggle:
            return
        
        is_on = not is_on
        emit_signal("toggled", is_on)
        
        # 动画
        var tween = create_tween()
        var target_rotation = deg_to_rad(45) if is_on else deg_to_rad(-45)
        tween.tween_property(self, "rotation:x", target_rotation, 0.3)
        
        # 冷却
        can_toggle = false
        await get_tree().create_timer(cooldown).timeout
        can_toggle = true

class BalanceBeam extends RigidBody3D:
    @export var pivot_point: Vector3 = Vector3.ZERO
    @export var balance_threshold: float = 5.0
    
    var left_weight: float = 0.0
    var right_weight: float = 0.0
    
    func _ready():
        # 固定为铰链运动
        var joint = HingeJoint3D.new()
        joint.global_position = global_position + pivot_point
        get_parent().add_child(joint)
        joint.node_a = get_path()
        
        # 限制旋转范围
        joint.set_flag(HingeJoint3D.FLAG_USE_LIMIT, true)
        joint.set_param(HingeJoint3D.PARAM_LIMIT_LOWER, deg_to_rad(-30))
        joint.set_param(HingeJoint3D.PARAM_LIMIT_UPPER, deg_to_rad(30))
    
    func update_weight(side: String, weight: float):
        if side == "left":
            left_weight = weight
        else:
            right_weight = weight
        
        # 检查平衡
        var difference = abs(left_weight - right_weight)
        if difference < balance_threshold:
            # 平衡达成
            pass

## 谜题管理器
var puzzle_elements: Dictionary = {}
var puzzle_solved: bool = false

signal puzzle_completed

func register_element(id: String, element: Node):
    puzzle_elements[id] = element
    
    # 连接信号
    if element is PressurePlate:
        element.activated.connect(_on_element_activated.bind(id))
        element.deactivated.connect(_on_element_deactivated.bind(id))
    elif element is Lever:
        element.toggled.connect(_on_lever_toggled.bind(id))

func _on_element_activated(id: String):
    print("Element activated: ", id)
    _check_puzzle_solution()

func _on_element_deactivated(id: String):
    print("Element deactivated: ", id)
    _check_puzzle_solution()

func _on_lever_toggled(state: bool, id: String):
    print("Lever ", id, " toggled: ", state)
    _check_puzzle_solution()

func _check_puzzle_solution():
    # 检查所有条件
    var all_plates_active = true
    var all_levers_on = true
    
    for id in puzzle_elements.keys():
        var element = puzzle_elements[id]
        if element is PressurePlate and not element.is_active:
            all_plates_active = false
        elif element is Lever and not element.is_on:
            all_levers_on = false
    
    if all_plates_active and all_levers_on and not puzzle_solved:
        puzzle_solved = true
        emit_signal("puzzle_completed")
        _on_puzzle_solved()

func _on_puzzle_solved():
    print("Puzzle solved!")
    # 开门、播放音效等

29.11 本章小结

本章全面讲解了Godot 4的3D物理系统:

  1. 物理系统概述:物理引擎架构、碰撞层与遮罩配置
  2. StaticBody3D:静态物体、移动平台、传送带实现
  3. RigidBody3D:刚体配置、高级控制、网络同步
  4. CharacterBody3D:完整角色控制器、高级移动能力
  5. Area3D:触发区域、伤害区域、力场实现
  6. 射线检测:RayCast3D、代码射线、形状投射
  7. 关节与约束:基础关节、绳索链条、布娃娃系统
  8. 载具物理:VehicleBody3D、自定义载具物理
  9. 物理优化:性能策略、碰撞优化、对象池
  10. 实际案例:物理谜题系统实现

物理系统是3D游戏交互的核心,合理使用物理引擎能够创造出真实有趣的游戏体验。下一章我们将学习Godot的3D动画系统,为游戏角色和物体添加生动的动画效果。

碰撞形状:

- BoxShape3D: 盒形

- SphereShape3D: 球形

- CapsuleShape3D: 胶囊形

- CylinderShape3D: 圆柱形

- ConvexPolygonShape3D: 凸多边形

- ConcavePolygonShape3D: 凹多边形(仅静态)

- HeightMapShape3D: 高度图(地形)

物理参数配置

func configurephysicssettings(): # 物理帧率(默认60Hz) Engine.physicsticksper_second = 60

# 物理插值(平滑显示) Engine.physicsjitterfix = 0.5

# 获取物理服务器 var physics_server = PhysicsServer3D

物理材质示例

class PhysicsMaterialExample: var friction: float = 1.0 # 摩擦力 var bounce: float = 0.0 # 弹性 var absorbent: bool = false # 是否吸收碰撞

func createphysicsmaterial() -> PhysicsMaterial: var material = PhysicsMaterial.new() material.friction = friction material.bounce = bounce material.absorbent = absorbent return material


### 29.1.2 碰撞层与遮罩

collision_layers.gd

碰撞层管理

extends Node

碰撞层定义(在项目设置中配置)

Layer 1: 玩家

Layer 2: 敌人

Layer 3: 静态环境

Layer 4: 可拾取物品

Layer 5: 子弹

Layer 6: 触发区域

Layer 7: 载具

Layer 8: 可破坏物

碰撞层常量

const LAYERPLAYER = 1 const LAYERENEMY = 2 const LAYERENVIRONMENT = 4 const LAYERPICKUP = 8 const LAYERPROJECTILE = 16 const LAYERTRIGGER = 32 const LAYERVEHICLE = 64 const LAYERDESTRUCTIBLE = 128

设置物体碰撞配置

static func setupcollision(body: CollisionObject3D, layer: int, mask: int): body.collisionlayer = layer body.collision_mask = mask

常用配置

static func setupplayer(body: CollisionObject3D): # 玩家在Layer 1,检测敌人、环境、拾取物、触发区域 body.collisionlayer = LAYERPLAYER body.collisionmask = LAYERENEMY | LAYERENVIRONMENT | LAYERPICKUP | LAYERTRIGGER

static func setupenemy(body: CollisionObject3D): # 敌人在Layer 2,检测玩家、环境、子弹 body.collisionlayer = LAYERENEMY body.collisionmask = LAYERPLAYER | LAYERENVIRONMENT | LAYER_PROJECTILE

static func setupprojectile(body: CollisionObject3D): # 子弹在Layer 5,检测敌人、环境、可破坏物 body.collisionlayer = LAYERPROJECTILE body.collisionmask = LAYERENEMY | LAYERENVIRONMENT | LAYER_DESTRUCTIBLE

动态修改碰撞层

static func setlayerbit(body: CollisionObject3D, bit: int, value: bool): body.setcollisionlayer_value(bit, value)

static func setmaskbit(body: CollisionObject3D, bit: int, value: bool): body.setcollisionmask_value(bit, value)


## 29.2 StaticBody3D 静态物体

### 29.2.1 静态物体基础

staticbodysetup.gd

静态物体配置

extends StaticBody3D

物理材质

@export var physics_material: PhysicsMaterial

平台类型

enum PlatformType { SOLID, # 实心平台 ONE_WAY, # 单向平台 MOVING, # 移动平台 CONVEYOR # 传送带 }

@export var platform_type: PlatformType = PlatformType.SOLID

传送带速度

@export var conveyor_velocity: Vector3 = Vector3.ZERO

func ready(): setupcollision() setup_platform()

func setupcollision(): if physicsmaterial: self.physicsmaterialoverride = physicsmaterial

# 设置为静态环境层 collision_layer = 4 # Layer 3

func setupplatform(): match platformtype: PlatformType.ONEWAY: # 单向平台配置 for child in get_children(): if child is CollisionShape3D: # 在Godot 4中通过碰撞层处理单向平台 pass

PlatformType.CONVEYOR: # 传送带配置 constantlinearvelocity = conveyor_velocity

创建碰撞形状

func addboxcollision(size: Vector3, offset: Vector3 = Vector3.ZERO): var collision = CollisionShape3D.new() var shape = BoxShape3D.new() shape.size = size collision.shape = shape collision.position = offset add_child(collision)

func addmeshcollision(mesh: Mesh): # 从网格创建碰撞形状 var collision = CollisionShape3D.new() collision.shape = mesh.createconvexshape() add_child(collision)

创建凹面碰撞(仅用于静态物体)

func addconcavecollision(mesh: Mesh): var collision = CollisionShape3D.new() collision.shape = mesh.createtrimeshshape() add_child(collision)


### 29.2.2 移动平台

moving_platform.gd

移动平台实现

extends AnimatableBody3D

移动类型

enum MoveType { LINEAR, # 直线移动 CIRCULAR, # 圆周运动 PATROL, # 路径巡逻 SINE_WAVE # 正弦波动 }

@export var movetype: MoveType = MoveType.LINEAR @export var speed: float = 2.0 @export var waittime: float = 1.0

直线移动参数

@exportgroup("Linear Movement") @export var targetoffset: Vector3 = Vector3(0, 5, 0)

圆周运动参数

@exportgroup("Circular Movement") @export var radius: float = 5.0 @export var rotationaxis: Vector3 = Vector3.UP

路径巡逻参数

@exportgroup("Patrol") @export var patrolpoints: Array[Vector3] = []

正弦波参数

@export_group("Sine Wave") @export var amplitude: Vector3 = Vector3(0, 2, 0) @export var frequency: float = 1.0

var startposition: Vector3 var currenttargetindex: int = 0 var direction: int = 1 var timeelapsed: float = 0.0 var waiting: bool = false var wait_timer: float = 0.0

func ready(): startposition = globalposition syncto_physics = true # 同步物理

func physicsprocess(delta): if waiting: waittimer -= delta if waittimer <= 0: waiting = false return

match movetype: MoveType.LINEAR: movelinear(delta) MoveType.CIRCULAR: movecircular(delta) MoveType.PATROL: movepatrol(delta) MoveType.SINEWAVE: movesine(delta)

func movelinear(delta): var target = startposition + targetoffset direction var newpos = globalposition.move_toward(target, speed delta)

# 使用moveandcollide保持与角色的正确交互 globalposition = newpos

if globalposition.isequalapprox(target): direction *= -1 start_wait()

func movecircular(delta): time_elapsed += delta * speed

var offset = Vector3( cos(timeelapsed) <em> radius, 0, sin(timeelapsed) radius )

# 应用旋转轴 offset = offset.rotated(rotation_axis.normalized(), 0)

globalposition = startposition + offset

func movepatrol(delta): if patrolpoints.isempty(): return

var target = startposition + patrolpoints[currenttargetindex] globalposition = globalposition.move_toward(target, speed * delta)

if globalposition.isequalapprox(target): currenttargetindex = (currenttargetindex + 1) % patrolpoints.size() startwait()

func movesine(delta): time_elapsed += delta frequency TAU

var offset = amplitude * sin(timeelapsed) globalposition = start_position + offset

func startwait(): if waittime > 0: waiting = true waittimer = wait_time

获取平台速度(用于角色移动补偿)

func getplatformvelocity() -> Vector3: return linear_velocity


## 29.3 RigidBody3D 刚体

### 29.3.1 刚体基础配置

rigidbodysetup.gd

刚体配置

extends RigidBody3D

刚体预设

enum RigidBodyPreset { DEFAULT, # 默认 HEAVY, # 重物 LIGHT, # 轻物 BOUNCY, # 弹性物 ROLLING, # 滚动物 FLOATING # 漂浮物 }

@export var preset: RigidBodyPreset = RigidBodyPreset.DEFAULT: set(value): preset = value applypreset()

自定义物理属性

@exportgroup("Custom Physics") @export var custommass: float = 1.0 @export var customfriction: float = 1.0 @export var custombounce: float = 0.0

func ready(): applypreset() setup_signals()

func applypreset(): match preset: RigidBodyPreset.DEFAULT: mass = 1.0 physicsmaterialoverride = null

RigidBodyPreset.HEAVY: mass = 50.0 creatematerial(0.8, 0.1) linear_damp = 0.5

RigidBodyPreset.LIGHT: mass = 0.1 creatematerial(0.3, 0.2) lineardamp = 1.0 angulardamp = 2.0

RigidBodyPreset.BOUNCY: mass = 1.0 creatematerial(0.2, 0.9)

RigidBodyPreset.ROLLING: mass = 2.0 creatematerial(0.5, 0.3) angular_damp = 0.1

RigidBodyPreset.FLOATING: mass = 0.5 gravityscale = 0.1 lineardamp = 2.0 angular_damp = 2.0

func creatematerial(friction: float, bounce: float): var mat = PhysicsMaterial.new() mat.friction = friction mat.bounce = bounce physicsmaterialoverride = mat

func setupsignals(): bodyentered.connect(onbodyentered) bodyexited.connect(onbodyexited) sleepingstatechanged.connect(onsleeping_changed)

func onbody_entered(body: Node): print("Collided with: ", body.name)

func onbody_exited(body: Node): print("Stopped colliding with: ", body.name)

func onsleeping_changed(): if sleeping: print("Rigid body went to sleep") else: print("Rigid body woke up")

应用力

func applyimpulseatcenter(impulse: Vector3): applycentral_impulse(impulse)

func applyforceatposition(force: Vector3, pos: Vector3): applyforce(force, pos - global_position)

应用爆炸力

func applyexplosionforce( origin: Vector3, force: float, radius: float ): var direction = global_position - origin var distance = direction.length()

if distance > radius: return

var falloff = 1.0 - (distance / radius) var impulse = direction.normalized() force falloff

applycentralimpulse(impulse)


### 29.3.2 高级刚体控制

advancedrigidbody.gd

高级刚体控制

extends RigidBody3D

冻结模式

@export var freezerotation: bool = false @export var freezepositionx: bool = false @export var freezepositiony: bool = false @export var freezeposition_z: bool = false

速度限制

@export var maxlinearspeed: float = 50.0 @export var maxangularspeed: float = 20.0

拾取设置

@export var canbepickedup: bool = true var isheld: bool = false var holder: Node3D = null

func physicsprocess(delta): applyconstraints() limitvelocities()

func applyconstraints(): if freezerotation: angularvelocity = Vector3.ZERO

var vel = linearvelocity if freezepositionx: vel.x = 0 if freezepositiony: vel.y = 0 if freezepositionz: vel.z = 0 linearvelocity = vel

func limitvelocities(): # 限制线速度 if linearvelocity.length() > maxlinearspeed: linearvelocity = linearvelocity.normalized() * maxlinear_speed

# 限制角速度 if angularvelocity.length() > maxangularspeed: angularvelocity = angularvelocity.normalized() * maxangular_speed

拾取系统

func pickup(newholder: Node3D): if not canbepickedup or isheld: return false

isheld = true holder = newholder

# 禁用物理 freeze = true

# 重新父级 var globaltrans = globaltransform getparent().removechild(self) newholder.addchild(self) globaltransform = globaltrans

# 设置相对位置 position = Vector3(0, 0, -2) # 在持有者前方 rotation = Vector3.ZERO

return true

func drop(throwvelocity: Vector3 = Vector3.ZERO): if not isheld: return

is_held = false

# 恢复物理 freeze = false

# 恢复父级 var globaltrans = globaltransform holder.removechild(self) gettree().currentscene.addchild(self) globaltransform = globaltrans

# 应用投掷速度 linearvelocity = throwvelocity

holder = null

func throw(direction: Vector3, force: float): drop(direction.normalized() * force)

状态保存/加载

func getphysicsstate() -> Dictionary: return { "position": globalposition, "rotation": globalrotation, "linearvelocity": linearvelocity, "angularvelocity": angularvelocity, "sleeping": sleeping }

func setphysicsstate(state: Dictionary): globalposition = state.position globalrotation = state.rotation linearvelocity = state.linearvelocity angularvelocity = state.angularvelocity sleeping = state.sleeping


### 29.3.3 刚体同步与联网

networkedrigidbody.gd

网络同步刚体

extends RigidBody3D

同步设置

@export var syncrate: float = 20.0 # 每秒同步次数 @export var interpolationspeed: float = 10.0

是否为主控端

var is_authority: bool = true

网络状态

var networkposition: Vector3 var networkrotation: Vector3 var networkvelocity: Vector3 var lastsync_time: float = 0.0

func ready(): setphysicsprocess(isauthority)

func physicsprocess(delta): if isauthority: # 主控端:发送状态 sendstate() else: # 从属端:插值到网络状态 interpolatetonetwork_state(delta)

func sendstate(): var currenttime = Time.getticksmsec() / 1000.0 if currenttime - lastsynctime < 1.0 / sync_rate: return

lastsynctime = current_time

var state = { "position": globalposition, "rotation": globalrotation, "velocity": linear_velocity }

# 在实际网络游戏中,这里会通过RPC发送 # rpc("receive_state", state)

func interpolatetonetworkstate(delta): # 位置插值 globalposition = globalposition.lerp( networkposition, interpolationspeed * delta )

# 旋转插值 var currentquat = Quaternion.fromeuler(globalrotation) var targetquat = Quaternion.fromeuler(networkrotation) var resultquat = currentquat.slerp(targetquat, interpolationspeed * delta) globalrotation = resultquat.get_euler()

接收网络状态

func receivestate(state: Dictionary): networkposition = state.position networkrotation = state.rotation networkvelocity = state.velocity

# 如果偏差过大,直接传送 if globalposition.distanceto(networkposition) > 5.0: globalposition = networkposition globalrotation = networkrotation linearvelocity = network_velocity


## 29.4 CharacterBody3D 角色物理

### 29.4.1 角色控制器

character_controller.gd

完整的角色控制器

extends CharacterBody3D

移动参数

@exportgroup("Movement") @export var walkspeed: float = 5.0 @export var runspeed: float = 10.0 @export var crouchspeed: float = 2.5 @export var acceleration: float = 10.0 @export var deceleration: float = 15.0

跳跃参数

@exportgroup("Jump") @export var jumpvelocity: float = 6.0 @export var doublejumpenabled: bool = true @export var maxjumps: int = 2 @export var jumpbuffertime: float = 0.15 @export var coyotetime: float = 0.1

重力参数

@exportgroup("Gravity") @export var gravitymultiplier: float = 2.5 @export var fallmultiplier: float = 3.0 @export var maxfall_speed: float = 50.0

斜坡参数

@exportgroup("Slopes") @export var maxslopeangle: float = 45.0 @export var snapto_ground: bool = true

状态变量

var currentspeed: float var jumpsremaining: int var jumpbuffertimer: float = 0.0 var coyotetimer: float = 0.0 var wason_floor: bool = false

移动状态

enum MoveState { IDLE, WALKING, RUNNING, CROUCHING, AIRBORNE } var move_state: MoveState = MoveState.IDLE

输入缓存

var inputdirection: Vector3 var wishjump: bool = false

func ready(): floormaxangle = degtorad(maxslopeangle) jumpsremaining = max_jumps

func physicsprocess(delta): processinput() updatetimers(delta) applygravity(delta) processmovement(delta) processjump() updatestate()

moveandslide()

postmovement()

func processinput(): # 获取输入方向 var inputdir = Input.getvector("moveleft", "moveright", "moveforward", "moveback") inputdirection = (transform.basis * Vector3(inputdir.x, 0, input_dir.y)).normalized()

# 跳跃输入缓冲 if Input.isactionjustpressed("jump"): wishjump = true jumpbuffertimer = jumpbuffertime

func updatetimers(delta): # 跳跃缓冲计时 if jumpbuffertimer > 0: jumpbuffertimer -= delta if jumpbuffertimer <= 0: wish_jump = false

# 土狼时间 if wasonfloor and not isonfloor(): coyotetimer = coyotetime

if coyotetimer > 0: coyotetimer -= delta

func applygravity(delta): if isonfloor(): return

var gravity = ProjectSettings.getsetting("physics/3d/defaultgravity")

# 根据下落状态调整重力 if velocity.y < 0: velocity.y -= gravity fallmultiplier </em> delta else: velocity.y -= gravity <em> gravitymultiplier delta

# 限制最大下落速度 velocity.y = max(velocity.y, -maxfallspeed)

func processmovement(delta): # 确定目标速度 match movestate: MoveState.RUNNING: currentspeed = runspeed MoveState.CROUCHING: currentspeed = crouchspeed : currentspeed = walkspeed

var targetvelocity = inputdirection * current_speed

# 平滑加减速 var accel = acceleration if input_direction.length() > 0 else deceleration

velocity.x = movetoward(velocity.x, targetvelocity.x, accel delta) velocity.z = movetoward(velocity.z, targetvelocity.z, accel delta)

func processjump(): if not wish_jump: return

var can_jump = false

# 地面跳跃 if isonfloor() or coyotetimer > 0: canjump = true jumpsremaining = maxjumps - 1 # 空中跳跃 elif doublejumpenabled and jumpsremaining > 0: canjump = true jumps_remaining -= 1

if canjump: velocity.y = jumpvelocity coyotetimer = 0.0 wishjump = false jumpbuffertimer = 0.0

func updatestate(): if not isonfloor(): movestate = MoveState.AIRBORNE elif Input.isactionpressed("crouch"): movestate = MoveState.CROUCHING elif Input.isactionpressed("sprint") and inputdirection.length() > 0: movestate = MoveState.RUNNING elif inputdirection.length() > 0: movestate = MoveState.WALKING else: move_state = MoveState.IDLE

func postmovement(): # 更新地面状态 wasonfloor = isonfloor()

# 重置跳跃次数 if isonfloor(): jumpsremaining = maxjumps

外部接口

func getmovestate() -> MoveState: return move_state

func is_moving() -> bool: return velocity.length() > 0.1

func gethorizontalspeed() -> float: return Vector2(velocity.x, velocity.z).length()


### 29.4.2 高级角色移动

advancedcharactermovement.gd

高级角色移动系统

extends CharacterBody3D

移动能力

@exportgroup("Abilities") @export var candash: bool = true @export var canwalljump: bool = true @export var canwallrun: bool = true @export var canslide: bool = true @export var canclimb: bool = true

冲刺参数

@exportgroup("Dash") @export var dashspeed: float = 20.0 @export var dashduration: float = 0.2 @export var dashcooldown: float = 1.0

墙跳参数

@exportgroup("Wall Jump") @export var walljumpforce: Vector3 = Vector3(8, 6, 0) @export var wallslide_speed: float = 2.0

滑铲参数

@exportgroup("Slide") @export var slidespeed: float = 12.0 @export var slideduration: float = 0.8 @export var slidefriction: float = 5.0

状态

var isdashing: bool = false var dashtimer: float = 0.0 var dashcooldowntimer: float = 0.0 var dash_direction: Vector3

var iswallsliding: bool = false var wall_normal: Vector3

var issliding: bool = false var slidetimer: float = 0.0 var slide_direction: Vector3

射线检测

@onready var wallcheckleft: RayCast3D = $WallCheckLeft @onready var wallcheckright: RayCast3D = $WallCheckRight @onready var ceiling_check: RayCast3D = $CeilingCheck

func physicsprocess(delta): updatecooldowns(delta) checkwall_contact()

processdash(delta) processwallslide(delta) process_slide(delta)

moveandslide()

func updatecooldowns(delta): if dashcooldowntimer > 0: dashcooldowntimer -= delta

func checkwallcontact(): iswall_sliding = false

if isonfloor(): return

if wallcheckleft.iscolliding(): iswallsliding = true wallnormal = wallcheckleft.getcollisionnormal() elif wallcheckright.iscolliding(): iswallsliding = true wallnormal = wallcheckright.getcollisionnormal()

冲刺

func startdash(): if not candash or isdashing or dashcooldown_timer > 0: return

isdashing = true dashtimer = dashduration dashcooldowntimer = dashcooldown

# 使用输入方向或面朝方向 dashdirection = -transform.basis.z var inputdir = Input.getvector("moveleft", "moveright", "moveforward", "moveback") if inputdir.length() > 0.1: dashdirection = (transform.basis * Vector3(inputdir.x, 0, input_dir.y)).normalized()

func processdash(delta): if not is_dashing: return

dash_timer -= delta

if dashtimer <= 0: isdashing = false return

velocity = dashdirection * dashspeed velocity.y = 0 # 保持水平

墙跳

func walljump(): if not canwalljump or not iswall_sliding: return

# 计算跳跃方向(远离墙壁) var jumpdirection = wallnormal

velocity = Vector3( jumpdirection.x <em> walljumpforce.x, walljumpforce.y, jumpdirection.z walljumpforce.x )

iswallsliding = false

func processwallslide(delta): if not iswall_sliding: return

# 限制下落速度 if velocity.y < -wallslidespeed: velocity.y = -wallslidespeed

# 检测墙跳输入 if Input.isactionjustpressed("jump"): walljump()

滑铲

func startslide(): if not canslide or issliding or not ison_floor(): return

issliding = true slidetimer = slideduration slidedirection = -transform.basis.z

# 降低碰撞体高度 setcrouch_collision(true)

func processslide(delta): if not is_sliding: return

slide_timer -= delta

# 检查是否可以站起 var canstand = not ceilingcheck.iscolliding() if ceilingcheck else true

if slidetimer <= 0 and canstand: issliding = false setcrouchcollision(false) return

# 滑铲移动 var currentslidespeed = slidespeed <em> (slidetimer / slideduration) velocity.x = slidedirection.x currentslidespeed velocity.z = slidedirection.z * currentslide_speed

func setcrouchcollision(crouching: bool): # 调整碰撞体形状 var collision = getnodeornull("CollisionShape3D") if collision and collision.shape is CapsuleShape3D: var capsule = collision.shape as CapsuleShape3D if crouching: capsule.height = 1.0 collision.position.y = 0.5 else: capsule.height = 2.0 collision.position.y = 1.0

攀爬检测

func canclimbledge() -> bool: if not can_climb: return false

# 检测前方是否有可攀爬的边缘 # 这里需要多个射线检测来确定边缘位置 return false # 简化实现

func climbledge(): if not canclimb_ledge(): return

# 执行攀爬动画和移动 pass


## 29.5 Area3D 区域检测

### 29.5.1 触发区域

trigger_area.gd

触发区域

extends Area3D

触发类型

enum TriggerType { ONCE, # 只触发一次 REPEATABLE, # 可重复触发 TOGGLE, # 切换状态 HOLD # 保持触发 }

@export var triggertype: TriggerType = TriggerType.REPEATABLE @export var triggerdelay: float = 0.0 @export var reset_delay: float = 1.0

过滤设置

@exportgroup("Filter") @export var triggerbyplayer: bool = true @export var triggerbyenemies: bool = false @export var triggerbyobjects: bool = false @export var requiredtag: String = ""

状态

var istriggered: bool = false var isactive: bool = false var bodies_inside: Array[Node3D] = []

信号

signal triggered(body: Node3D) signal untriggered(body: Node3D) signal state_changed(active: bool)

func ready(): bodyentered.connect(onbodyentered) bodyexited.connect(onbody_exited)

func onbodyentered(body: Node3D): if not should_trigger(body): return

bodies_inside.append(body)

match triggertype: TriggerType.ONCE: if not istriggered: activate(body) istriggered = true

TriggerType.REPEATABLE: _activate(body)

TriggerType.TOGGLE: if isactive: deactivate(body) else: _activate(body)

TriggerType.HOLD: if not isactive: activate(body)

func onbodyexited(body: Node3D): bodiesinside.erase(body)

if triggertype == TriggerType.HOLD: if bodiesinside.isempty() and isactive: _deactivate(body)

func shouldtrigger(body: Node3D) -> bool: # 检查标签 if requiredtag != "" and not body.isingroup(requiredtag): return false

# 检查类型 if body.isingroup("player") and triggerbyplayer: return true if body.isingroup("enemy") and triggerbyenemies: return true if triggerbyobjects and body is RigidBody3D: return true

return false

func activate(body: Node3D): if triggerdelay > 0: await gettree().createtimer(trigger_delay).timeout

isactive = true emitsignal("triggered", body) emitsignal("statechanged", true)

func deactivate(body: Node3D): if resetdelay > 0: await gettree().createtimer(reset_delay).timeout

isactive = false emitsignal("untriggered", body) emitsignal("statechanged", false)

手动控制

func forceactivate(): isactive = true emitsignal("statechanged", true)

func forcedeactivate(): isactive = false emitsignal("statechanged", false)

func reset(): istriggered = false isactive = false bodies_inside.clear()


### 29.5.2 伤害区域

damage_area.gd

伤害区域

extends Area3D

伤害设置

@exportgroup("Damage") @export var damage: float = 10.0 @export var damagetype: String = "default" @export var knockbackforce: float = 5.0 @export var knockbackdirection: Vector3 = Vector3.UP

伤害模式

enum DamageMode { INSTANT, # 进入时立即伤害 PERIODIC, # 周期性伤害 CONTINUOUS # 持续伤害 }

@export var damagemode: DamageMode = DamageMode.INSTANT @export var damageinterval: float = 1.0

效果

@exportgroup("Effects") @export var spawnparticles: bool = true @export var particle_scene: PackedScene

状态

var affectedbodies: Dictionary = {} # body: nextdamage_time

func ready(): bodyentered.connect(onbodyentered) bodyexited.connect(onbody_exited)

func physicsprocess(delta): if damagemode == DamageMode.PERIODIC or damagemode == DamageMode.CONTINUOUS: processperiodic_damage()

func onbodyentered(body: Node3D): if not can_damage(body): return

match damagemode: DamageMode.INSTANT: applydamage(body) DamageMode.PERIODIC, DamageMode.CONTINUOUS: affectedbodies[body] = Time.getticksmsec() if damagemode == DamageMode.PERIODIC: apply_damage(body) # 首次伤害

func onbodyexited(body: Node3D): affectedbodies.erase(body)

func processperiodicdamage(): var currenttime = Time.getticksmsec()

for body in affectedbodies.keys(): if not isinstancevalid(body): affectedbodies.erase(body) continue

var lastdamagetime = affectedbodies[body] var elapsed = (currenttime - lastdamagetime) / 1000.0

if damagemode == DamageMode.CONTINUOUS: # 持续伤害按帧计算 applydamage(body, damage * getphysicsprocessdeltatime()) elif elapsed >= damageinterval: applydamage(body) affectedbodies[body] = currenttime

func candamage(body: Node3D) -> bool: return body.hasmethod("takedamage") or body.has_signal("damaged")

func applydamage(body: Node3D, amount: float = -1): if amount < 0: amount = damage

# 调用伤害方法 if body.hasmethod("takedamage"): body.takedamage(amount, damagetype, self)

# 应用击退 if knockbackforce > 0 and body is CharacterBody3D: var direction = knockbackdirection if knockbackdirection == Vector3.ZERO: direction = (body.globalposition - globalposition).normalized() body.velocity += direction * knockbackforce

# 生成粒子效果 if spawnparticles and particlescene: var particles = particlescene.instantiate() gettree().currentscene.addchild(particles) particles.globalposition = body.globalposition

设置为一次性爆炸伤害

func explode(): var bodies = getoverlappingbodies() for body in bodies: if candamage(body): applydamage(body)

if spawnparticles and particlescene: var particles = particlescene.instantiate() gettree().currentscene.addchild(particles) particles.globalposition = globalposition

queue_free()


### 29.5.3 力场区域

force_field.gd

力场区域

extends Area3D

力场类型

enum ForceType { DIRECTIONAL, # 方向力 POINT, # 点力(吸引/排斥) VORTEX, # 漩涡力 WIND, # 风力 GRAVITY_ZONE # 重力区域 }

@export var forcetype: ForceType = ForceType.DIRECTIONAL @export var forcestrength: float = 10.0 @export var force_direction: Vector3 = Vector3.UP

点力参数

@export_group("Point Force") @export var attract: bool = false # true=吸引, false=排斥 @export var falloff: bool = true # 距离衰减

漩涡参数

@exportgroup("Vortex") @export var vortexaxis: Vector3 = Vector3.UP @export var tangential_force: float = 5.0

重力区域参数

@exportgroup("Gravity Zone") @export var customgravity: Vector3 = Vector3(0, -9.8, 0)

var affected_bodies: Array[RigidBody3D] = []

func ready(): bodyentered.connect(onbodyentered) bodyexited.connect(onbody_exited)

func physicsprocess(delta): for body in affectedbodies: if isinstancevalid(body): apply_force(body, delta)

func onbodyentered(body: Node3D): if body is RigidBody3D: affectedbodies.append(body)

# 重力区域特殊处理 if forcetype == ForceType.GRAVITYZONE: body.gravity_scale = 0 # 禁用默认重力

func onbodyexited(body: Node3D): if body is RigidBody3D: affectedbodies.erase(body)

# 恢复默认重力 if forcetype == ForceType.GRAVITYZONE: body.gravity_scale = 1

func applyforce(body: RigidBody3D, delta: float): var force = Vector3.ZERO

match forcetype: ForceType.DIRECTIONAL: force = forcedirection.normalized() * force_strength

ForceType.POINT: var direction = globalposition - body.globalposition var distance = direction.length() direction = direction.normalized()

if not attract: direction = -direction

var strength = forcestrength if falloff and distance > 0: strength = forcestrength / (distance * distance)

force = direction * strength

ForceType.VORTEX: var tocenter = globalposition - body.globalposition var distance = tocenter.length()

# 向心力 var centripetal = tocenter.normalized() * forcestrength

# 切向力 var tangent = vortexaxis.cross(tocenter).normalized() var tangential = tangent * tangential_force

force = centripetal + tangential

ForceType.WIND: # 风力带有随机扰动 var noise = Vector3( randfrange(-1, 1), randfrange(-1, 1), randfrange(-1, 1) ) <em> 0.2 force = (forcedirection.normalized() + noise) force_strength

ForceType.GRAVITYZONE: force = customgravity * body.mass

body.applycentralforce(force)


## 29.6 射线检测

### 29.6.1 RayCast3D使用

raycast_usage.gd

射线检测使用

extends Node3D

射线组件

@onready var groundray: RayCast3D = $GroundRay @onready var wallray: RayCast3D = $WallRay @onready var interact_ray: RayCast3D = $InteractRay

func ready(): configure_rays()

func configurerays(): # 地面检测射线 groundray.targetposition = Vector3(0, -1.5, 0) groundray.collisionmask = 4 # 环境层 groundray.hitfrominside = false groundray.hitbackfaces = false

# 墙壁检测射线 wallray.targetposition = Vector3(0, 0, -1) wallray.collisionmask = 4

# 交互检测射线 interactray.targetposition = Vector3(0, 0, -3) interactray.collisionmask = 8 # 可交互物品层

func physicsprocess(delta): checkground() checkwall() check_interaction()

func checkground(): if groundray.iscolliding(): var collisionpoint = groundray.getcollisionpoint() var collisionnormal = groundray.getcollisionnormal() var collider = groundray.getcollider()

# 计算斜坡角度 var slopeangle = radtodeg(acos(collisionnormal.dot(Vector3.UP))) print("Standing on: ", collider.name, " Slope: ", slope_angle, "°")

func checkwall(): if wallray.iscolliding(): var wallnormal = wallray.getcollisionnormal() print("Wall detected, normal: ", wall_normal)

func checkinteraction(): if interactray.iscolliding(): var interactable = interactray.getcollider() if interactable.has_method("highlight"): interactable.highlight()

if Input.isactionjustpressed("interact"): if interactable.hasmethod("interact"): interactable.interact()


### 29.6.2 代码射线检测

physics_raycast.gd

代码射线检测

extends Node3D

执行射线检测

func raycast( from: Vector3, to: Vector3, collisionmask: int = 0xFFFFFFFF, exclude: Array[RID] = [] ) -> Dictionary: var spacestate = getworld3d().directspacestate var query = PhysicsRayQueryParameters3D.create(from, to) query.collisionmask = collisionmask query.exclude = exclude

return spacestate.intersectray(query)

从相机发射射线(鼠标拾取)

func raycastfromcamera( camera: Camera3D, screenposition: Vector2, distance: float = 1000.0 ) -> Dictionary: var from = camera.projectrayorigin(screenposition) var to = from + camera.projectraynormal(screen_position) * distance

return raycast(from, to)

扇形射线检测

func raycastfan( origin: Vector3, direction: Vector3, angle: float, raycount: int, distance: float ) -> Array[Dictionary]: var results: Array[Dictionary] = [] var halfangle = degto_rad(angle / 2)

for i in range(raycount): var t = float(i) / float(raycount - 1) - 0.5 var current_angle = t * angle

# 旋转方向 var rotateddir = direction.rotated(Vector3.UP, degtorad(currentangle)) var endpoint = origin + rotateddir * distance

var result = raycast(origin, end_point) if result: results.append(result)

return results

球形射线检测(360度)

func raycastsphere( origin: Vector3, raycounthorizontal: int, raycount_vertical: int, distance: float ) -> Array[Dictionary]: var results: Array[Dictionary] = []

for i in range(raycountvertical): var pitch = (float(i) / float(raycountvertical - 1) - 0.5) * PI

for j in range(raycounthorizontal): var yaw = float(j) / float(raycounthorizontal) * TAU

var direction = Vector3( cos(pitch) sin(yaw), sin(pitch), cos(pitch) cos(yaw) )

var endpoint = origin + direction * distance var result = raycast(origin, endpoint) if result: results.append(result)

return results

穿透射线检测(检测所有碰撞)

func raycastpenetrate( from: Vector3, to: Vector3, maxhits: int = 10 ) -> Array[Dictionary]: var results: Array[Dictionary] = [] var exclude: Array[RID] = [] var current_from = from

for i in range(maxhits): var result = raycast(currentfrom, to, 0xFFFFFFFF, exclude) if result.is_empty(): break

results.append(result) exclude.append(result.rid) current_from = result.position + (to - from).normalized() * 0.01

return results


### 29.6.3 形状投射

shape_cast.gd

形状投射检测

extends Node3D

ShapeCast3D使用

@onready var shape_cast: ShapeCast3D = $ShapeCast3D

func ready(): # 配置形状投射 var sphere = SphereShape3D.new() sphere.radius = 0.5 shapecast.shape = sphere shapecast.targetposition = Vector3(0, 0, -5)

func physicsprocess(delta): if shapecast.iscolliding(): var collisioncount = shapecast.getcollisioncount() for i in range(collisioncount): var collider = shapecast.getcollider(i) var point = shapecast.getcollisionpoint(i) var normal = shapecast.getcollisionnormal(i) print("Hit: ", collider.name, " at ", point)

代码形状投射

func shapecastquery( shape: Shape3D, from: Transform3D, motion: Vector3, collisionmask: int = 0xFFFFFFFF ) -> Array[Dictionary]: var spacestate = getworld3d().directspacestate

var query = PhysicsShapeQueryParameters3D.new() query.shape = shape query.transform = from query.motion = motion query.collisionmask = collisionmask

return spacestate.intersectshape(query)

球形扫描

func sphere_sweep( radius: float, from: Vector3, to: Vector3 ) -> Array[Dictionary]: var sphere = SphereShape3D.new() sphere.radius = radius

var transform = Transform3D.IDENTITY transform.origin = from

return shapecastquery(sphere, transform, to - from)

盒形扫描

func box_sweep( size: Vector3, from: Transform3D, to: Vector3 ) -> Array[Dictionary]: var box = BoxShape3D.new() box.size = size

return shapecastquery(box, from, to - from.origin)

点查询(重叠检测)

func pointquery( point: Vector3, collisionmask: int = 0xFFFFFFFF ) -> Array[Dictionary]: var spacestate = getworld3d().directspace_state

var query = PhysicsPointQueryParameters3D.new() query.position = point query.collisionmask = collisionmask

return spacestate.intersectpoint(query)


## 29.7 关节与约束

### 29.7.1 基础关节

joints_setup.gd

关节系统

extends Node3D

创建固定关节

func createpinjoint( bodya: RigidBody3D, bodyb: RigidBody3D, anchor: Vector3 ) -> PinJoint3D: var joint = PinJoint3D.new() joint.global_position = anchor

addchild(joint) joint.nodea = bodya.getpath() joint.nodeb = bodyb.get_path()

return joint

创建铰链关节

func createhingejoint( bodya: RigidBody3D, bodyb: RigidBody3D, anchor: Vector3, axis: Vector3 ) -> HingeJoint3D: var joint = HingeJoint3D.new() joint.global_position = anchor

# 设置旋转轴 joint.look_at(anchor + axis, Vector3.UP)

addchild(joint) joint.nodea = bodya.getpath() joint.nodeb = bodyb.get_path()

# 配置限制 joint.setflag(HingeJoint3D.FLAGUSELIMIT, true) joint.setparam(HingeJoint3D.PARAMLIMITLOWER, degtorad(-90)) joint.setparam(HingeJoint3D.PARAMLIMITUPPER, degto_rad(90))

return joint

创建滑动关节

func createsliderjoint( bodya: RigidBody3D, bodyb: RigidBody3D, anchor: Vector3, slideaxis: Vector3 ) -> SliderJoint3D: var joint = SliderJoint3D.new() joint.globalposition = anchor

# 设置滑动轴 joint.lookat(anchor + slideaxis, Vector3.UP)

addchild(joint) joint.nodea = bodya.getpath() joint.nodeb = bodyb.get_path()

# 配置限制 joint.setparam(SliderJoint3D.PARAMLINEARLIMITLOWER, -5.0) joint.setparam(SliderJoint3D.PARAMLINEARLIMITUPPER, 5.0)

return joint

创建锥形扭转关节(用于布娃娃)

func createconetwistjoint( bodya: RigidBody3D, bodyb: RigidBody3D, anchor: Vector3 ) -> ConeTwistJoint3D: var joint = ConeTwistJoint3D.new() joint.globalposition = anchor

addchild(joint) joint.nodea = bodya.getpath() joint.nodeb = bodyb.get_path()

# 配置锥形限制 joint.setparam(ConeTwistJoint3D.PARAMSWINGSPAN, degtorad(45)) joint.setparam(ConeTwistJoint3D.PARAMTWISTSPAN, degtorad(30))

return joint

创建6自由度关节

func creategenericjoint( bodya: RigidBody3D, bodyb: RigidBody3D, anchor: Vector3 ) -> Generic6DOFJoint3D: var joint = Generic6DOFJoint3D.new() joint.global_position = anchor

addchild(joint) joint.nodea = bodya.getpath() joint.nodeb = bodyb.get_path()

# 锁定所有平移 joint.setflagx(Generic6DOFJoint3D.FLAGENABLELINEARLIMIT, true) joint.setflagy(Generic6DOFJoint3D.FLAGENABLELINEARLIMIT, true) joint.setflagz(Generic6DOFJoint3D.FLAGENABLELINEAR_LIMIT, true)

# 设置角度限制 joint.setflagx(Generic6DOFJoint3D.FLAGENABLEANGULARLIMIT, true) joint.setparamx(Generic6DOFJoint3D.PARAMANGULARLOWERLIMIT, degtorad(-45)) joint.setparamx(Generic6DOFJoint3D.PARAMANGULARUPPERLIMIT, degto_rad(45))

return joint


### 29.7.2 绳索与链条

rope_chain.gd

绳索和链条模拟

extends Node3D

绳索参数

@export var segmentcount: int = 10 @export var segmentlength: float = 0.5 @export var segmentmass: float = 0.1 @export var ropestiffness: float = 0.9

var segments: Array[RigidBody3D] = [] var joints: Array[PinJoint3D] = []

func ready(): createrope()

func createrope(): var previousbody: RigidBody3D = null

for i in range(segmentcount): # 创建段 var segment = createsegment(i) segment.position = Vector3(0, -i * segmentlength, 0) add_child(segment) segments.append(segment)

# 第一段固定 if i == 0: segment.freeze = true

# 创建关节连接相邻段 if previousbody: var joint = createjoint(previousbody, segment, i) joints.append(joint)

previous_body = segment

func createsegment(index: int) -> RigidBody3D: var segment = RigidBody3D.new() segment.mass = segmentmass segment.lineardamp = 0.5 segment.angular_damp = 0.5

# 碰撞形状 var collision = CollisionShape3D.new() var shape = CapsuleShape3D.new() shape.radius = 0.05 shape.height = segmentlength collision.shape = shape collision.rotationdegrees.x = 90 segment.add_child(collision)

# 可视化(可选) var meshinstance = MeshInstance3D.new() var cylinder = CylinderMesh.new() cylinder.topradius = 0.03 cylinder.bottomradius = 0.03 cylinder.height = segmentlength meshinstance.mesh = cylinder meshinstance.rotationdegrees.x = 90 segment.addchild(mesh_instance)

return segment

func createjoint(bodya: RigidBody3D, bodyb: RigidBody3D, index: int) -> PinJoint3D: var joint = PinJoint3D.new() joint.position = Vector3(0, -index * segmentlength + segmentlength / 2, 0)

addchild(joint) joint.nodea = bodya.getpath() joint.nodeb = bodyb.get_path()

return joint

在末端附加物体

func attachtoend(body: RigidBody3D): if segments.is_empty(): return

var lastsegment = segments[-1] var joint = PinJoint3D.new() joint.globalposition = lastsegment.globalposition - Vector3(0, segment_length / 2, 0)

addchild(joint) joint.nodea = lastsegment.getpath() joint.nodeb = body.getpath()

joints.append(joint)

##

← 返回目录