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

第 55 课 · 3D 射击:射线与命中检测

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

🎯 本课学习目标

FPS 射击本质是「从枪口发射一条射线,看它撞到谁」。本课实现完整射击。

📖 详细教学步骤

射击原理概念

大多数 FPS 的「子弹」不是真实物体,而是瞬间射线检测

  1. 从相机中心(准星)向正前方发射一条射线
  2. 射线撞到第一个物体 = 命中
  3. 调用被命中物体的受击函数

好处:无需子弹物理,零延迟、永不脱靶(指哪打哪)。

开火检测代码

在玩家脚本里加射击:

GDSCRIPTextends CharacterBody3D

@onready var cam: Camera3D = $CameraRig/Camera3D

func _unhandled_input(event):
    if event.is_action_pressed("fire"):
        shoot()

func shoot():
    var center = get_viewport().get_visible_rect().size / 2
    var from = cam.project_ray_origin(center)
    var to = from + cam.project_ray_normal(center) * 1000.0
    var query = PhysicsRayQueryParameters3D.create(from, to)
    var hit = get_world_3d().direct_space_state.intersect_ray(query)
    if hit:
        print("命中:", hit.collider.name, " 位置:", hit.position)
        # 通知受击对象
        var body = hit.collider
        if body.has_method("take_damage"):
            body.take_damage(25)

子弹延迟(弹道时间)进阶

想要「子弹飞一会儿」的打击感,用延时:

GDSCRIPTfunc shoot():
    var from = cam.project_ray_origin(...)
    var to = from + cam.project_ray_normal(...) * 1000.0
    # 延迟 0.05 秒后检测(模拟飞行)
    await get_tree().create_timer(0.05).timeout
    var query = PhysicsRayQueryParameters3D.create(from, to)
    var hit = get_world_3d().direct_space_state.intersect_ray(query)
    ...

准星实操

  1. 在 HUD(CanvasLayer)加一个 Label/TextureRect 做准星
  2. 居中放置(锚点:居中)
  3. 文字用一个「+」或圆点
  4. UI 永远在 3D 之上,天然当准星

敌人受击反馈代码

敌人脚本:

GDSCRIPTextends CharacterBody3D

@export var hp: int = 100

func take_damage(amount: int):
    hp -= amount
    # 受击闪白
    $MeshInstance3D.material_override.albedo_color = Color(1, 0.5, 0.5)
    await get_tree().create_timer(0.1).timeout
    $MeshInstance3D.material_override.albedo_color = Color.WHITE
    if hp <= 0:
        die()

func die():
    print("敌人被击杀")
    queue_free()

命中特效(火花/弹孔)进阶

命中位置加粒子特效:

GDSCRIPTfunc spawn_hit_effect(pos: Vector3):
    var fx = hit_fx_scene.instantiate()   # 火花粒子场景
    get_tree().current_scene.add_child(fx)
    fx.global_position = pos
    fx.emitting = true
⚠️
射线穿不过某些物体?

检查物体是否有 CollisionShape3D;查询可加 collision_mask 过滤层。

⚠️
命中自己?

射线 query 加 exclude 排除玩家自身 RID。

🧪 动手试一试

🏋️ 课后练习

做一个会掉血的靶子(HP 100),用射线射击:命中减 25,血空后消失并打印击杀。

玩家 shoot() 射线检测 → 靶子 take_damage()。

💡

射线命中反馈(火花/声音/受击闪白)是射击手感的灵魂。

💡

PhysicsRayQueryParameters3D.create + intersect_ray 是 3D 交互的万能钥匙。

🎓 本课小结

射击 = 从相机中心发射射线 → intersect_ray 命中检测 → 调用受击函数;配合准星、命中特效与受击反馈形成完整射击体验。