HOME / 第 5 部分 · 3D 游戏开发 / 第 54 课
MODULE 5 · 3D 游戏开发

第 54 课 · 第三人称角色控制

⏱ 预计时长 45 分钟📊 难度 进阶📚 模块 第 5 部分 · 3D 游戏开发

🎯 本课学习目标

动作冒险游戏常用的第三人称:角色在屏幕中间,镜头在后上方环绕。本课实现它。

📖 详细教学步骤

场景结构搭建

结构规划:

Player (CharacterBody3D)
├── CollisionShape3D (胶囊)
├── Model (Node3D, 存放角色模型,可旋转)
│   └── MeshInstance3D (人或盒子)
└── CameraRig (Node3D, 相机支架)
    └── Camera3D (放在后上方)

移动代码代码

角色移动(同第一人称的 WASD):

GDSCRIPTextends CharacterBody3D

@export var speed: float = 6.0
@export var gravity = 9.8
@onready var model: Node3D = $Model

func _physics_process(delta):
    if not is_on_floor():
        velocity.y -= gravity * delta

    var input_dir = Input.get_vector("move_left", "move_right",
                                     "move_forward", "move_back")
    var dir = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
    velocity.x = dir.x * speed
    velocity.z = dir.z * speed
    move_and_slide()

    # 朝向移动方向
    if dir != Vector3.ZERO:
        model.rotation.y = lerp_angle(model.rotation.y,
            atan2(-dir.x, -dir.z), 0.2)

相机环绕(肩后视角)代码

相机用鼠标控制环绕:

GDSCRIPTextends Node3D   # CameraRig 脚本

@export var camera_speed: float = 3.0
@export var cam_offset: Vector3 = Vector3(0, 2.2, 4.0)
@onready var cam: Camera3D = $Camera3D

func _physics_process(delta):
    # 相机保持在角色后上方
    var target = get_parent().global_position + cam_offset
    global_position = global_position.lerp(target, camera_speed * delta)
    # 看向角色
    look_at(get_parent().global_position, Vector3.UP)

func _unhandled_input(event):
    if event is InputEventMouseMotion:
        # 让相机绕 Y 轴旋转(视角环绕)
        rotate_y(-event.relative.x * 0.003)

鼠标隐藏与操作手感实操

第三人称通常也捕获鼠标:

GDSCRIPTfunc _ready():
    Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

func _unhandled_input(event):
    if event.is_action_pressed("ui_cancel"):
        if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
            Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
        else:
            Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

相机碰撞(防穿墙)进阶

角色贴墙时相机不该穿进墙里:

GDSCRIPTfunc _physics_process(delta):
    var player = get_parent()
    # 从玩家向相机方向做射线
    var from = player.global_position + Vector3.UP * 1.5
    var to = from + (global_position - from).normalized() * cam_offset.length()
    var query = PhysicsRayQueryParameters3D.create(from, to)
    query.exclude = [player.get_rid()]
    var hit = get_world_3d().direct_space_state.intersect_ray(query)
    if hit:
        cam.global_position = hit.position   # 相机被墙挡住就拉近
    else:
        cam.global_position = global_position
    cam.look_at(player.global_position + Vector3.UP, Vector3.UP)

测试测试

  1. 场景放地面和障碍
  2. F5:WASD 移动、角色朝向移动方向、鼠标环绕视角
  3. 贴墙时相机自动拉近

🎉 标准第三人称达成!

⚠️
角色不朝向移动方向?

用 lerp_angle + atan2 计算朝向角并平滑插值;确认改的是 Model 子节点而不是根节点。

⚠️
相机乱转?

相机 Rig 的 rotate_y 只在鼠标移动时触发,别在 _physics_process 里每帧转。

🧪 动手试一试

🏋️ 课后练习

在第三人称基础上:按 Shift 奔跑(速度×1.6),并让相机跟随跑步产生轻微晃动。

sprint 动作 + 速度条件;相机 cam_offset 加 sin(time) 小抖动。

💡

lerp_angle 平滑旋转角度,避免瞬间转向。

💡

相机碰撞用射线 + exclude 排除玩家自身。

🎓 本课小结

第三人称 = CharacterBody3D 移动 + Model 朝向移动方向 + CameraRig 环绕 + 射线防穿墙;lerp_angle 平滑转身。