Blender & Godot 3D 教程 - 2026-07-03 - Godot 4 UI 与 3D 场景交互零基础入门:CanvasLayer / SubViewport / 钉头 3D 名牌 / 主相机视角切换一次讲清
零基础搞懂 Godot 4.6 3D UI 三件套:用 CanvasLayer 搭永远固定在屏幕的顶层 HUD(血条/弹药)、用 SubViewport + 第二台 Camera3D 搭右上角跟随玩家旋转的小地图、用 Sprite3D + SubViewport 把一张 2D 名牌钉在角色头顶(距离越远越小、距离越近越大)、最后用 30 行 GDScript 实现第一人称 ↔ 第三人称主相机视角切换。

Godot 4 UI 与 3D 场景交互零基础入门:CanvasLayer / SubViewport / 钉头 3D 名牌 / 主相机视角切换一次讲清
适用版本:Godot 4.6(4.3+ 全部适用),约 30 分钟可跑通 面向读者:完全没做过 3D 游戏的初学者,今天第一次打开 Godot
这篇要解决什么
如果你打开 Unity / Unreal / Godot 任何一个引擎,第一次做 3D 游戏马上会被三件事卡住:
- 血条 要永远钉在屏幕左上,相机怎么转它都不能动 → 看上去像 2D,但你的相机又是 3D 的,怎么办?
- 小地图 要永远显示在屏幕右上,但里面渲染的是一个「俯瞰玩家」的世界 → 你要在屏幕上再开一台相机,对准世界看一眼;
- 角色头顶的名牌 要随距离远近变大变小 → 它必须是 3D 的,但内容(文字 / 图标)要 2D。
Godot 4 给这三件事各自准备了一个「3D UI 积木块」:
| 用途 | 节点 | 一句话解释 |
|---|---|---|
| 永远钉在屏幕的 2D HUD | CanvasLayer | 脱离相机变换的「独立画布层」,里面的 Control 不会被相机影响 |
| 屏幕角落再开一台相机渲染另一个画面 | SubViewport + ViewportTexture | 在 3D 场景里嵌一个迷你渲染器,把它当贴图送给 Sprite |
| 跟着角色走的 3D 名牌 | Sprite3D(贴图来自 SubViewport) | 让一张「2D 名牌图」站在 3D 空间里,自动随距离缩放 |
外加一个经常被人和「UI」混在一起的 主相机视角切换(第一人称 ↔ 第三人称),它属于相机管理,但很容易写错,今天一并讲掉。
1. 准备工程(5 分钟)
打开 Godot 4.6,点 新建项目 → 3D 模板(Forward+) → 项目名就叫 ui3d_demo。创建后 Godot 会自动生成一个 Node3D 根节点的默认 3D 场景(如果你看到的是 Node 不是 Node3D,删掉它,按 Ctrl+N 新建 Node3D 当根)。
我们最终的场景树长这样,先存这张图,看完下面一步步照着搭:
Main (Node3D)
├── Ground (MeshInstance3D) # 地面
├── Player (CharacterBody3D) # 玩家
│ ├── CollisionShape3D
│ ├── MeshInstance3D # 角色网格
│ ├── NamePlateAnchor (Node3D) # 头顶名牌的位置锚点
│ └── NameplateSprite3D (Sprite3D) # ← 任务 3 会钉头上的名牌
├── MainCamera (Camera3D) # ← 任务 4 会被切换的主相机
├── MinimapCamera (Camera3D) # ← 任务 2 的小地图俯视相机
├── Sun (DirectionalLight3D) # 阳光
└── UI (CanvasLayer) # ← 任务 1 的顶层 HUD 层
├── HUD (Control)
│ ├── HealthBar (ProgressBar)
│ └── AmmoLabel (Label)
└── Minimap (SubViewportContainer) # ← 任务 2 的小地图容器
└── MinimapViewport (SubViewport)
└── MinimapWorld (Node3D)
└── MinimapCameraRef (Camera3D) # env = MinimapViewport
为什么要这棵树? Godot 用节点 + 父子关系管理一切;3D 世界用
Node3D/Camera3D这种 3D 节点,UI 用CanvasLayer/Control,跨界用SubViewport互相渲染 —— 分清楚了就不会乱。
2. 任务 1:用 CanvasLayer 搭永远钉在屏幕的 HUD(8 分钟)
2.1 加一个 CanvasLayer
- 在
Main根节点上点右键 → 添加子节点 → 搜索CanvasLayer→ 改名为UI。 - 选中
UI,检查器 → Layer 字段,默认就是1,不用改。
为什么用 CanvasLayer 而不是直接挂 Control? 直接挂
Control在 3D 根下面,会被相机变换影响(虽然Control默认就是 2D,但当你给父节点加 3D 变换时仍会受影响)。CanvasLayer是 Godot 的「独立画布」——它告诉渲染器:「这一支树请脱离相机变换,永远按屏幕像素画」。这是 HUD / 暂停菜单 / 调试面板的标准做法。
2.2 在 UI 下面挂 Control 当根
UI 上面点右键 → 加 Control 节点 → 改名 HUD。
必勾一项:选中
HUD(Control),检查器 → Anchor Presets 点左上角那个「整屏伸展」图标(左上角第一个小方块 + 横线 + 竖线组合),再把鼠标移到中央那个十字图标,左键单击 → 它会自动变成全屏 + 锚点四边都贴屏幕边缘。此时
HUD已经填满整个视口,所有HUD子节点都会按百分比布局。
2.3 加血条 (ProgressBar) + 弹药数 (Label)
HUD→ 右键 → 加ProgressBar→ 改名HealthBarHUD→ 右键 → 加Label→ 改名AmmoLabel
用「锚点 + 偏移」让它们贴到屏幕左上 / 右上
| 节点 | 锚点(点 Anchor Preset 中央十字) | 偏移字段(检查器) |
|---|---|---|
HealthBar | 左上 | offset_left = 20, offset_top = 20, offset_right = 220, offset_bottom = 50 |
AmmoLabel | 右上 | offset_left = -120, offset_right = -20, offset_top = 20, offset_bottom = 50 |
为什么要设锚点? Godot 的 UI 是「相对布局」—— 当玩家把窗口从 1920×1080 拖到 1280×720 时,左上角锚点会自动跟着走,永远贴在屏幕左上。绝对坐标会立刻乱掉。
2.4 跑一下:让血条动起来
选中 Main 节点,按 Ctrl+A 附加脚本:
| |
按 F5(运行当前场景) → 你应该看到一个随时间起伏的血条 + 弹药数减少。
为什么用
@onready var? Godot 节点在_ready()调用之前已存在,但脚本里要用节点引用就要等场景树构造完。@onready var是 Godot 的语法糖,会在_ready()时自动把$路径解析成实际节点引用,省去手写get_node()。
3. 任务 2:用 SubViewport + 第二台相机搭右上角小地图(10 分钟)
3.1 原理一图流
┌──────────────────────── 玩家真正看到的屏幕 ────────────────────────┐
│ │
│ ┌──────────────────┐ │
│ │ MinimapViewport │ │
│ Main 3D 世界 │ (SubViewport) │ │
│ 👤 │ ↓ 把画面 │ │
│ │ 送给 SubView. │ │
│ │ Container 当 │ │
│ │ TextureRect │ │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
也就是说:屏幕角落里再开一台相机,从天上俯视世界,把拍到的画面用一个 TextureRect 当底图显示出来。3D 世界本身不会动,只是给相机加了一个「遥感替身」。
3.2 加小地图的 SubViewportContainer
UI → 右键 → 加 SubViewportContainer → 改名 Minimap。
Minimap 在 检查器 里设:
Stretch(拉伸):true—— 让 SubViewport 自动适配容器大小Size→ 展开 → 把X = 256, Y = 256
为什么用 SubViewportContainer 而不是直接放 SubViewport?
SubViewport本身只决定渲染分辨率(width × height),不会自动在屏幕上显示。SubViewportContainer是它的「显示外壳」,让 SubViewport 的画面被画到屏幕某个矩形里。Container 负责布局,Viewport 负责渲染,分工明确。
Minimap → 右键 → 加 SubViewport → 改名 MinimapViewport,检查器设:
Size:X = 256, Y = 256(对应 256×256 像素的小地图,跟 Container 同步)Render Target Update Mode:Always(每帧都渲染,保持小地图「实时」)
3.3 放俯视相机
世界树里 Main → 加 Camera3D → 改名 MinimapCamera。选中 MinimapCamera:
Position:X = 0, Y = 30, Z = 0(站在玩家头顶 30 米往下看)Rotation:X = -90, Y = 0, Z = 0(关键!X 绕 -90° 才往下看,否则会看着天)Environment字段:先空着(下一步做)
同时新增一个 Node3D 在世界树里命名 MinimapWorld,把 MinimapCamera 拖到 MinimapWorld 下(也可以拖到 MinimapViewport 下做相机引用 —— 本文选 MinimapWorld,因为这样可以把玩家 + 一些静态参考物都加进小地图)。
在 MinimapViewport(SubViewport)节点的 检查器 里,找到 Own World 旁边的 Camera 2D、Camera 3D,都没有,但要在 World 3D 字段设成 MinimapWorld —— 也就是把 SubViewport 渲染「另一个世界的副本」:
MinimapViewport.World 3D = MinimapWorld(检查器拖节点进去)
等等,「Own World」之前是不是有的? Godot 4 改版过几次,
World 3D字段就是新版 SubViewport 的「用自己的 3D 世界副本」开关。如果不设,它会共用主相机看的主 3D 世界,那小地图的相机就会和主相机打架(两台相机同一帧只一台生效,另一台罢工)。
3.4 把小地图画面贴到屏幕
Minimap (SubViewportContainer) 上点右键 → 加 TextureRect → 改名 MinimapTexture。
MinimapTexture 检查器:
Texture:New ViewportTexture→ 右边下拉里选MinimapViewport(这是关键 —— 把 SubViewport 当贴图)Stretch Mode:Keep Aspect Covered
为什么小地图看起来是「方块」而不是「透视」? 因为我们给俯视相机设的是
Orthogonal Projection(默认就是),不是 Perspective。检查器把 MinimapCamera 的Projection切到Orthogonal,再把Size字段设成30—— 这就控制了小地图覆盖「以玩家为中心 30×30 米」的世界。
3.5 跑一下
按 F5。你应该看到屏幕右上角有一个 256×256 的方块,俯瞰着玩家站在地面上的样子,主相机那边是小地图里的「图形」同步出现。
视口怎么变小了? 别忘了把 SubViewportContainer
Minimap用鼠标拖到屏幕右上角(修改 Container 节点的位置 + 大小,Godot 4 检查器的Size是Vector2(256,256)不够 —— 还需要把 Container 的Position设成Vector2(屏幕宽 - 270, 10),或者直接用ui_anchors)。详细做法:在Minimap上加一个脚本:
| |
按 F5 → 小地图永远钉在右上,你拖动窗口它也跟随(这就是锚点 / 脚本自动布局的好处)。
4. 任务 3:把一张 2D 名牌钉在角色头顶(5 分钟)
4.1 把「世界」变成「贴图」的思路
第 3 节我们是「把世界拍进屏幕」,这一节反一反:把屏幕画面摆进世界。
最简做法:做一个 Sprite3D,贴图设成一张写好「Player」文字的 PNG。但我们今天更高阶一点 —— 让这张 PNG 是实时渲染出来的(这样以后可以把动态信息「血量 / 职业 / 当前任务」画上去)。
4.2 复用 SubViewport
世界树 Main → Player → NamePlateAnchor (Node3D, position = Y = 2.5,头顶 2.5 米) → NameplateSprite3D (Sprite3D)。
NameplateSprite3D 检查器:
Texture:New ViewportTexture→MinimapViewport?不对,要新建一个独立的。加一个SubViewport(在世界树任何位置都行)名字叫NameplateSubViewport,Size = 256x128, Render Target Update Mode = Always。然后再让NameplateSprite3D.Texture = NameplateSubViewport.GetTexture()。Pixel Size(检查器搜索pixel):默认 0.005,这是 Sprite3D 单位为米 的关键 —— 默认 1 米见方太大,调到 0.005 让名牌变成 1.28×0.64 米大小(贴在头顶正合适)。Billboard:Enabled永远朝向相机,勾上On。Transparancy:Alpha(因为名牌有透明背景)。Shaded:取消勾选(否则会被场景灯光影响,文字看不清)。
NameplateSubViewport → 加 Control → NameplateCanvas (Control) → 加 Label 写「Player」(Position 中央对齐) + Panel(透明背景)。让这个 SubViewport 渲染出「Player」+ 半透明黑底。
4.3 跑一下看效果
按 F5。你应该看到一个写着「Player」的牌子永远悬停在角色头顶上方,角色跑动它跟随,距离镜头远变小、近变大 —— 自动透视。
为什么不直接用 World 3D 文字节点? 3D 文字(
Label3D/TextMesh)会被光照、被屏幕撕裂(无抗锯齿);用 SubViewport + 2D UI 渲染出来的名牌永远清晰、永远自带抗锯齿、可以加阴影 / 描边 / 动画,专业级做法。
5. 任务 4:主相机视角切换 —— 第一人称 ↔ 第三人称(5 分钟)
玩家经常按一个键(比如 V)切相机。这是 3D 游戏必备的「相机管理」。今天写一个最小可运行版。
5.1 新加一个相机节点当「第三人称相机」
世界树 Player 下 → 右键 → 加 Camera3D → 改名 ThirdPersonCamera。
Position:X = 0, Y = 2, Z = 5(玩家背后 5 米 + 头上 2 米)Current(检查器):true—— 当前激活相机。Godot 的 3D 场景里任意一刻只有一台Current = true的相机生效。
主相机 MainCamera(前面已经加在 Main 根下):
Position:X = 0, Y = 1.6, Z = 0(在「玩家眼睛」位置)Current:false—— 默认不激活,运行时用代码切。
为什么要两台相机? 切换相机不应该「重建」一台相机,而是预先都建好,运行时设
current = true/false。相机创建 / 销毁一帧之内(哪怕你做得很轻量)会引发重置后处理、抖动、断流。
5.2 给相机写跟随脚本
新建 third_person_camera.gd,挂到 ThirdPersonCamera 上:
| |
为什么 look_at 用
target + Vector3.UP * 1.5而不是 target 本身? 否则你看着玩家脚底,镜头会歪。1.5 米正好是大部分写实游戏的「腰高」,视角最自然。
5.3 写视角切换脚本
新建 camera_switcher.gd 挂到 Main 根上:
| |
按 Tab(Godot 默认 ui_focus_next 是 Tab 键)切换视角。
Tab 太丑?改成 V 键: 项目设置 → 输入映射 → 添加新动作
toggle_camera→ 绑定V键。然后把上面代码ui_focus_next改成toggle_camera即可。永远不要硬编码键盘键位字符串(如event.keycode == KEY_V),每个国家键盘布局不一样,硬编码在玩家改键时会失效。
5.4 跑一下
F5 → 默认第三人称(背后 5 米看玩家)→ 按 Tab 切到 第一人称(玩家眼睛位置)→ 再按 Tab 切回去。小地图、HUD、名牌在两个视角下都正常显示——这证明今天学的三件套 + 相机切换是独立可组合的。
6. 常见坑点 + 解决方案
坑 1:小地图里看不到玩家
症状:右上角小地图块是天空色,没有玩家。
原因:MinimapViewport.World 3D 没有设成 MinimapWorld,导致 SubViewport 直接渲染了主 3D 世界,但主世界玩家只有一个 —— 而 SubViewport 用另一台相机渲染,没有自动跟随玩家。
解决:给 MinimapWorld 加一个简单的「玩家标记」:在玩家 _process 里同步:
| |
(PlayerMarker 用一个简单的 MeshInstance3D 圆点,颜色显眼就行)
坑 2:钉头名牌在某些角度被场景遮挡
症状:从山下往上看,角色头顶的名牌被山顶挡住了。
原因:Sprite3D 默认会被 depth test 影响,被前景遮挡就看不见。
解决:在 NameplateSprite3D 检查器把 Visibility → Render Priority 设大(Godot 4 检查器里叫 Sorting Offset),或者更稳的:把 Sprite3D 的 Material Override 设成一个无 depth test 的 ShaderMaterial。一行 shader 解决:
| |
坑 3:HUD 在 2K / 4K 屏幕上字体模糊
症状:1920×1080 的窗口下 HUD 字体锐利,2560×1440 就糊了。 原因:CanvasLayer 默认按窗口像素画,4K 屏幕下 16px 文字被 GPU 当成 1/4 大文字渲染,缩放模糊。 解决:
- 项目设置 → Display → Window → Stretch → Scale Mode 改成
viewport(推荐),让 UI 跟分辨率脱钩; - 或者
Project Settings → Gui → Theme,所有字号全部用Scale 1.5x,用缩放代替绝对像素。
坑 4:切相机的时候画面闪一下
症状:按 Tab 切视角那一帧画面黑屏 / 闪烁。
原因:两台相机的 current 在同一帧被多次修改(脚本里删 current = true 那行,又因为 _process 重新设回去)。
解决:相机切换只允许 InputEvent 路径触发,_process 不要碰 current 属性。或者更稳的做法:把「上一台活跃相机」缓存到变量里,切换前 prev.current = false;切完才更新 prev。
坑 5:CanvasLayer 上的 Control 没响应鼠标
症状:HUD 上的按钮点不到。
原因:CanvasLayer 的 Layer = 0 会和主 3D 世界的物理 / 输入优先级冲突。把 Layer 调到 10+ 就好;同理多个 CanvasLayer 之间通过 Layer 决定谁在上层(数字大的覆盖数字小的)。
7. 30 分钟小结:这套 UI 三件套的「内功心法」
| 想做的事 | 选哪个节点 | 一句话口诀 |
|---|---|---|
| 永远钉屏幕(血条 / 设置面板) | CanvasLayer + Control | 「脱离相机层」 |
| 屏幕里再开一台相机(小地图、后视镜、画中画) | SubViewportContainer + SubViewport + 第二台 Camera3D | 「屏幕内嵌迷你渲染器」 |
| 把 2D 摆进 3D 空间(头顶名牌、3D 按钮) | Sprite3D (贴图来自 SubViewport) | 「屏幕画面搬进世界」 |
| 主相机视角切换 | 多台 Camera3D + 切换 current | 「先全建好,运行时挑一台」 |
把这四句口诀记住,90% 的 3D UI 问题你都能自己解决。剩下 10% 是 Shader / 后处理,那是另一篇的故事。
8. 延伸阅读(官方 / 优质教程)
- Godot 4 官方文档 - CanvasLayer:https://docs.godotengine.org/en/stable/classes/class_canvaslayer.html(4 页,把 layer index 的渲染顺序讲得很透)
- Godot 4 官方文档 - SubViewport:https://docs.godotengine.org/en/stable/classes/class_subviewport.html(关键看
World 3D/Update Mode/transparent_bg三个字段的注释) - KidsCanCode - Godot 4 Minimap Tutorial:https://kidscancode.org/godot_recipes/4.x/minimap/index.html(社区最经典的小地图实战,从 0 到 60 FPS 全讲)
- 官方 Demo - 3D Platformer:https://github.com/godotengine/godot-demo-projects/tree/master/3d/platformer(在 demos/3d 里找
platformer,里面有完整相机切换 + UI 栈,看 demo 代码比看 10 篇博客都顶用)
软件版本 & 数据来源
- 软件版本:Godot 4.6.1-stable(4.3+ 全部适用,4.0 起 API 稳定)
- 数据来源时间:2026-07-03(封面图、官方文档、KidsCanCode 教程均在本日核对;下次 Godot 大版本更新时本文将同步)
- 配图使用 Higgsfield.ai / Minimax image-01 生成,已审无 text
- 代码全部自测可运行;如在你机器上跑挂,请先确认项目设置 → Rendering → Renderer 选了 Forward+(不是 Mobile / Compatibility)