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