第27章 实战:物理益智游戏
物理益智游戏要素示意图
弹球发射:
🧙 按住拖拽 → 松开发射
↗
●──────→ 弹球飞行方向(反向)
弹力球弹跳:
█ █
█ ●↗ █
█ ↘ █ 碰到墙壁反弹
█ ↘ █ bounce = 0.9(高弹性)
█────────█
关节约束:
PinJoint2D(铰链) DampedSpringJoint2D(弹簧)
● ●
│ │
◉ ← 旋转点 ∿∿∿ ← 弹簧连接
│ │
● ●
物体切割:
切割前 切割后
┌────┐ ┌──┐ ┌──┐
│ │ →→ │ │ │ │
│ │ └──┘ └──┘
└────┘ 上半 下半
消除类玩法:
┌─┬─┬─┬─┬─┬─┐
│🔴│🔵│🟢│🟡│🔴│🔵│ ← 3个相同颜色连线
├─┼─┼─┼─┼─┼─┤ → 消除
│🟢│🔴│🔵│🟢│🟡│🔴│ ← 上方方块下落填补
├─┼─┼─┼─┼─┼─┤
│🟡│🟢│🟡│🔴│🔵│🟢│
└─┴─┴─┴─┴─┴─┘
27.1 RigidBody2D 深入
extends RigidBody2D
# 常用属性
mass = 1.0 # 质量
gravity_scale = 1.0 # 重力倍率
linear_damp = 0.0 # 线性阻尼(空气阻力)
angular_damp = 0.0 # 角度阻尼
# 物理模式
freeze = false # 冻结(不参与物理)
freeze_mode = RigidBody2D.FREEZE_MODE_KINEMATIC # 冻结时作为运动学体
# 应用力
apply_central_force(Vector2(100, 0)) # 持续力(受质量影响)
apply_central_impulse(Vector2(500, -300)) # 冲量(一次性)
apply_force(Vector2(100, 0), Vector2(0, -20)) # 指定位置的力(产生旋转)
27.2 物理材质
# 创建弹力球
func create_bouncy_ball() -> RigidBody2D:
var ball := RigidBody2D.new()
var shape := CollisionShape2D.new()
var circle := CircleShape2D.new()
circle.radius = 15.0
shape.shape = circle
ball.add_child(shape)
var mat := PhysicsMaterial.new()
mat.bounce = 0.9 # 高弹性
mat.friction = 0.1 # 低摩擦
ball.physics_material_override = mat
return ball
# 创建冰块(低摩擦)
func create_ice_block() -> RigidBody2D:
var block := RigidBody2D.new()
var mat := PhysicsMaterial.new()
mat.bounce = 0.1
mat.friction = 0.02 # 极低摩擦
block.physics_material_override = mat
return block
# 创建橡胶球(高弹性)
func create_rubber_ball() -> RigidBody2D:
var ball := RigidBody2D.new()
var mat := PhysicsMaterial.new()
mat.bounce = 1.0 # 完美弹跳
mat.friction = 0.8
ball.physics_material_override = mat
return ball
27.3 关节约束
# PinJoint2D:铰链(旋转连接)
func create_hinge(body_a: Node2D, body_b: Node2D, pivot: Vector2) -> void:
var joint := PinJoint2D.new()
joint.node_a = body_a.get_path()
joint.node_b = body_b.get_path()
joint.global_position = pivot
add_child(joint)
# SpringJoint2D:弹簧连接
func create_spring(body_a: Node2D, body_b: Node2D, length: float, stiffness: float) -> void:
var joint := DampedSpringJoint2D.new()
joint.node_a = body_a.get_path()
joint.node_b = body_b.get_path()
joint.length = length
joint.stiffness = stiffness
joint.damping = 0.5
add_child(joint)
# GrooveJoint2D:滑槽约束
func create_groove(body_a: Node2D, body_b: Node2D, groove_start: Vector2, groove_end: Vector2) -> void:
var joint := GrooveJoint2D.new()
joint.node_a = body_a.get_path()
joint.node_b = body_b.get_path()
joint.global_position = groove_start
joint.groove_length = groove_start.distance_to(groove_end)
add_child(joint)
27.4 物理交互玩法
弹球发射
extends Node2D
@export var ball_scene: PackedScene
@export var launch_force: float = 800.0
var is_aiming: bool = false
var aim_start: Vector2 = Vector2.ZERO
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
is_aiming = true
aim_start = get_global_mouse_position()
else:
launch_ball(aim_start, get_global_mouse_position())
is_aiming = false
func launch_ball(from: Vector2, to: Vector2) -> void:
var direction := (from - to).normalized() # 反向发射
var ball: RigidBody2D = ball_scene.instantiate()
ball.global_position = from
ball.apply_central_impulse(direction * launch_force)
add_child(ball)
func _draw() -> void:
if is_aiming:
var mouse_pos := to_local(get_global_mouse_position())
var aim_dir := (aim_start - get_global_mouse_position()).normalized()
var from := to_local(aim_start)
draw_line(from, from + aim_dir * 100, Color.RED, 2.0)
物体切割
# 使用 Geometry2D 实现简单切割
func cut_polygon(polygon: PackedVector2Array, cut_start: Vector2, cut_end: Vector2) -> Array:
# 获取切割线与多边形的交点
var intersection := Geometry2D.segment_intersects_segment(
cut_start, cut_end, polygon[0], polygon[polygon.size() - 1]
)
if intersection == null:
return [polygon] # 没有交点,不切割
# 分割多边形(简化实现)
var upper_half: PackedVector2Array = []
var lower_half: PackedVector2Array = []
for point in polygon:
# 根据点在切割线的哪一侧分组
var cross := (cut_end - cut_start).cross(point - cut_start)
if cross > 0:
upper_half.append(point)
else:
lower_half.append(point)
return [upper_half, lower_half]
27.5 消除类玩法
extends Node2D
@export var block_scene: PackedScene
@export var grid_size: Vector2i = Vector2i(8, 10)
@export var cell_size: float = 40.0
var grid: Array = [] # 二维数组
func _ready() -> void:
init_grid()
fill_grid()
func init_grid() -> void:
grid.clear()
for x in grid_size.x:
grid.append([])
for y in grid_size.y:
grid[x].append(null)
func fill_grid() -> void:
for x in grid_size.x:
for y in grid_size.y:
spawn_block(x, y)
func spawn_block(x: int, y: int) -> void:
var block: RigidBody2D = block_scene.instantiate()
block.global_position = grid_to_world(x, y)
block.block_type = randi() % 4 # 4种颜色
add_child(block)
grid[x][y] = block
func grid_to_world(x: int, y: int) -> Vector2:
return Vector2(x * cell_size, y * cell_size) + Vector2(cell_size / 2, cell_size / 2)
func world_to_grid(pos: Vector2) -> Vector2i:
return Vector2i(int(pos.x / cell_size), int(pos.y / cell_size))
func check_matches() -> void:
# 检查3个或更多相同颜色的连线
var matches: Array[Vector2i] = []
# 水平检查
for y in grid_size.y:
var count := 1
for x in range(1, grid_size.x):
if grid[x][y] and grid[x-1][y] and grid[x][y].block_type == grid[x-1][y].block_type:
count += 1
else:
if count >= 3:
for i in count:
matches.append(Vector2i(x - 1 - i, y))
count = 1
# 消除匹配的方块
for pos in matches:
if grid[pos.x][pos.y]:
grid[pos.x][pos.y].queue_free()
grid[pos.x][pos.y] = null
# 下落填补空缺
apply_gravity()
func apply_gravity() -> void:
for x in grid_size.x:
var write_y := grid_size.y - 1
for read_y in range(grid_size.y - 1, -1, -1):
if grid[x][read_y] != null:
if read_y != write_y:
grid[x][write_y] = grid[x][read_y]
grid[x][read_y] = null
# 动画移动到新位置
var tween := create_tween()
tween.tween_property(grid[x][write_y], "global_position", grid_to_world(x, write_y), 0.2)
write_y -= 1
实操练习
练习:简单弹球物理场景
# 创建场景包含:
# 1. 一个可以拖拽发射的球
# 2. 几个不同材质的障碍物(弹力墙、冰面、粘性表面)
# 3. 一个目标区域(检测球进入)
# 4. 使用关节约束创建可摆动的障碍
# 5. 记录尝试次数和最佳成绩