Day 4 · 共 10 节
让村子里出现第一个会说话的人:靠近、按键、弹出对白。故事从一张嘴开始。
承接 Day3 可移动玩家。今天只加人说话,不做任务网。每节验收一个机关,最后焊成闭环。
前三天,你有了厨房、院子和会走的演员。可如果院子里只有你一个人,故事很难开始。NPC(非玩家角色)就是村里的其他人:店小二、捕快、说书人。他们不一定会走路打架,但他们能说话、给线索、让世界显得有人烟。
今天的完成定义请置顶:走进店小二附近,按下交互键,屏幕弹出至少一句对白;再按一次可关闭或翻到下一句。不做任务系统网、不做好感度、不做分支恋爱。先打通「靠近 → 按键 → 看见字」。
导演为什么要先做对话壳?因为 RPG 的「故事感」大量来自说话。战斗可以后天补,但一个会说话的人,立刻让 Demo 从技术验证变成可讲述的世界。AI 很擅长写对白,你要做的是搭好舞台机关,让字能出现在正确的时机。
NPC 交互本质是「触发器 + UI」。触发器用 Area2D 最常见;UI 用 Control/Label/Panel。别把对白写死在十个地方,先打通一条链路,再扩展多段文本。
承接检查:玩家能走、墙基本不穿。若移动不稳,先回 Day3 补课。对话系统建立在可站位的世界之上,站不稳就谈不了话。
把完成定义抄进备忘录。任何「顺便做任务栏」的冲动,都先写到以后。今日只为人民服务:让一个人开口。
记住:NPC 不是「更复杂的玩家」。他可以不会走、不会打,只要能在正确时机把话说出来。范围缩小,成功率上升。这是本课程反复训练的导演肌肉。
开工顺序固定:结构 → 检测 → 按键 → UI → 台词 → 联调。顺序乱了,排错成本会翻倍。把顺序贴在显示器下方也行。
还有一层意义:NPC 对话是你指挥 AI 写「内容」而不仅是「代码」的开始。以前你让 AI 写脚本,今天你让 AI 写台词,再把台词塞进机关。导演同时管技术组与编剧组,正是本课程的培养目标。
如果你害怕做 UI,把它想成手机底部弹一条通知。先丑后美,先有后好。丑而可用的对话框,胜过精美却永远「明天再接」的设计稿。
写下今日 Done 一句话。写不清就还没准备好开工——开口的标准,比台词华丽更重要。
一个最小会说话的 NPC,通常包含:根节点(Node2D 或 CharacterBody2D,今天静止可用 Node2D)+ Sprite2D(外观)+ Area2D(靠近检测)+ CollisionShape2D(挂在 Area 下,表示范围)。对话 UI 可以做在 NPC 里,但更推荐做成村庄/UI 层的通用对话框,被 NPC 调用。
为什么 Area2D?因为它擅长「有人进出我的范围就通知我」,不像 CharacterBody 那样负责走路。店小二今天可以站着不动;他是感应门 + 一张嘴。
命名建议:场景 npc_waiter.tscn,根名 NpcWaiter,子节点 Sprite2D、InteractArea(Area2D)。名字写给人看,也写给 AI 看。
交互范围的碰撞形状通常比精灵更大一点,让玩家不用脸贴脸才能说话。范围太小会「按了没反应」,被误判为脚本坏了。
外观素材:可复用 AI 出图流程(Day1),生成「古风店小二」PNG,放入 res://assets/characters/npc_waiter.png。没有完美图也可用色块占位,先通交互。
独立成场景再拖进村庄,和玩家同一套管理哲学:改蓝图,处处生效;炸了只影响 NPC 文件。
若你担心碰撞层概念,可以先让 AI 给「最懒但能用」的层配置,并要求它用中文注释每个勾选项。你不必一次成为物理层专家,但要留下可查的注释。
静止 NPC 用 Node2D 足够。若你以后想让他巡逻,再换成 CharacterBody2D 也不迟。今天把「会走」从需求里删掉,是为了保护「会说」。需求减法,是专业。
Area2D 的监控对象要勾对:监控身体(bodies)或面积。玩家是 CharacterBody2D 时,确保 Area 能监控到 body。这一勾错,信号永远沉默。
在纸上画出 NPC 节点树三层。画得出来,才配让 AI 开工——结构先于台词。
动手:新建 res://scenes/npcs/npc_waiter.tscn。根 Node2D → Sprite2D 挂图 → Area2D(命名 InteractArea)→ 其子 CollisionShape2D,Shape 用 CircleShape2D 或矩形,半径大约角色身高的 1.5–2 倍。保存后,把实例拖到村庄路边或村口内侧。
摆位原则:玩家路过自然遇见,不要藏死角;也不要堵死唯一村口。NPC 是路标,不是路障。Day2 的路是叙事动线,店小二就站在动线旁边。
运行游戏,确认能看见 NPC。还不能对话没关系——先「群众演员入画」。很多人一上来写信号连接,结果场景里根本没这个人。
Area2D 要能检测到玩家,双方的碰撞层/掩码需要匹配。若完全不懂,把场景树截图给 AI,说「让 InteractArea 能检测到 Player 的 CharacterBody2D」。今天以求通为先,层编号细节可后补文档。
备份项目。接下来要接信号与 UI,容易改乱。有备份,才敢让 AI「大胆一点」。
店小二素材没有时,用纯色矩形 Sprite 也成立。占位角色能帮你先验证系统。很多失败项目死于「等美术」。我们用占位换进度。
摆位后建议在编辑器里用玩家大概尺寸目测:走过去会不会自然停在范围边缘。太远要喊话、太近像脸贴脸,都尴尬。交互距离也是设计,不只是技术参数。
把 NPC 实例命名清楚,例如 NpcWaiter。村庄里以后会有多个实例,默认名 Node2D2 会让你和 AI 一起迷路。
F5 看见店小二站在村里。看不见就先别写对话——群众演员没到场,台词没用。
Area2D 提供信号:例如 body_entered / body_exited(检测到 CharacterBody 等物理身体时)。思路:玩家进入范围 → 记 player_in_range = true;离开 → 设回 false。只有为 true 时,按交互键才开对话。
在 NPC 脚本里连接信号(编辑器节点面板连,或代码 connect)。进入时还可以 print("有客官靠近") 做验收。输出出现这句,说明感应门工作了——这比直接做 UI 更能定位问题。
先做「靠近提示」再做对话框:例如进入范围时显示「按 E 交谈」Label。提示是廉价的用户体验,也能证明范围逻辑正确。
extends Node2D
var player_in_range := false
func _on_interact_area_body_entered(body: Node2D) -> void:
if body.is_in_group("player") or body.name == "Player":
player_in_range = true
print("有客官靠近")
func _on_interact_area_body_exited(body: Node2D) -> void:
if body.is_in_group("player") or body.name == "Player":
player_in_range = false
建议给玩家根节点加入组 player,检测更稳,不怕改名。组是剧组名册:谁是主演,一查便知。
检测用 body 还是 area,取决于玩家碰撞类型。CharacterBody2D 通常走 body_entered。若你连了 area_entered 却永远不触发,先怀疑信号种类选错——这比改三小时逻辑更常见。
调试时把 Area 的 CollisionShape 显示出来(调试可见碰撞)。看见圈,才知道自己到底站进没站进。看不见的范围等于不存在的范围。
走进走出范围,输出面板出现靠近/离开相关打印。通了再往下——感应门先亮,门铃后装。
在 _process 或 _unhandled_input 里检测按键:例如 E 或空格。仅当 player_in_range 为 true 时响应。推荐在 Input Map 新增动作 interact,绑定 E 与手柄键;脚本写 Input.is_action_just_pressed("interact")。
用 just_pressed 而不是 pressed,避免按住时一帧开一百次对话。这是交互系统的经典坑,写进 Prompt 要求 AI 必须 just_pressed。
对话打开时,可暂时禁止玩家移动(可选)。最小做法:对话框显示时忽略移动输入,或设 GameState.talking = true。今天若来不及,允许边走边说,但要知道这不优雅,Day6 可收拾。
先打印「开始交谈」,确认按键逻辑。很多人对话框写了半天,其实按键根本没进分支。分段验收再次救命。
提示文案「按 E 交谈」可在进入范围时显示、离开时隐藏。哪怕只是一个 Label,玩家也不会在 NPC 脸上盲按。
键位冲突:UI 的「点击」与游戏的「交谈」可能抢输入。若发现按 E 也被别的系统吃掉,改用 _unhandled_input 或调整处理顺序。把它当排错清单一项即可。
把 interact 写进口令本:键位、动作名、脚本判断行。明天战斗若也占用按键,你会需要这张对照表来做仲裁。
为什么单独开一节讲按键?因为「检测到人」和「愿意开口」是两件事。有人靠近不等于自动弹剧情——除非你做自动触发。本课程默认主动按键,玩家更有控制感,也更少误触。
自动触发适合机关与过场;交谈适合社交。类型选错,体验会怪。今天我们做社交。
靠近并按 E,输出出现「开始交谈」;远离再按不应触发。键位逻辑正确,本节过。
让字出现在屏幕上。常用结构:CanvasLayer → Panel(或 NinePatch)→ 角色名 Label → 正文 Label → 可选提示「按 E 继续」。CanvasLayer 保证 UI 叠在游戏画面之上,不跟着地图乱跑。
UI 可以做成独立场景 dialogue_box.tscn,提供方法 show_line(speaker, text) 与 hide_box()。NPC 只负责调用,不负责画面板。这是分工:化妆组与编剧分开,现场才不乱。
默认隐藏对话框(visible = false)。需要时再显示。打开时填充文本;关闭时清空或保留末句均可。别忘了在编辑器里把锚点放到底部,不同窗口尺寸更稳。
extends CanvasLayer
@onready var name_label: Label = $Panel/NameLabel
@onready var body_label: Label = $Panel/BodyLabel
func show_line(speaker: String, text: String) -> void:
name_label.text = speaker
body_label.text = text
visible = true
func hide_box() -> void:
visible = false
字体太小或颜色对比不足,会让人以为「没显示」。先用大字高对比,古风字体以后再说。可读,是 UI 的第一美德。
对话框请放在较高的 CanvasLayer layer,避免被 TileMap 或角色挡住。被挡住的字,玩家会发誓系统坏了。
用临时按钮或 _ready 里调用 show_line 测试,确认字能显示。UI 通了,再接 NPC——舞台灯光先亮。
最小存储:NPC 脚本里一个字符串数组,例如两条台词。按 E:若对话框关着就显示第 0 句;若已打开则索引 +1;超过末尾则关闭。这叫「翻页式对话」,足够 Day4。
进阶可把对白放到 .json / .tres,但今天禁止为了架构完美而延迟开口。先数组,后数据驱动。导演要的是今晚能演,不是明年能扩展到 MMO。
var lines := [
"客官,酒旗刚挂上。",
"听说竹林近来不太平……",
]
var index := 0
var talking := false
func try_talk() -> void:
if not talking:
talking = true
index = 0
dialogue.show_line("店小二", lines[index])
else:
index += 1
if index >= lines.size():
talking = false
dialogue.hide_box()
else:
dialogue.show_line("店小二", lines[index])
对白文本请让 AI 写「能读出口的短句」,避免说明书腔。古风点到为止:一个语气词胜过堆砌之乎者也。
把对话框节点引用拿稳:可用组、唯一名称、或导出节点路径。找不到 dialogue 是第二常见崩点。打印 dialogue 是否 null,再叫冤。
对白请避免一次塞一段作文。游戏阅读环境是「边玩边看」,短句更强。让 AI 改写时加约束:「可朗读、可截图发群」。
数组索引越界是翻页常见崩溃。关闭条件写清楚:index >= size 就 hide。让 AI 加保护,别假设玩家按键次数守规矩。
对白数组可以先写占位:「第一句」「第二句」,链路通后再换成好词。很多人对着空系统硬憋文案,又累又慢。先通水管,再决定流酒还是流茶。
若你想要说话人名字也变化,把数据结构改成字典列表:speaker + text。需要时再升级,不需要时别提前抽象。
不接 NPC 完整链路也行:写好 3 句数组。词库就位——没词,机关只是空壳。
现在把 Lesson 4–7 焊成一条管线:进入范围 → 显示「按 E 交谈」→ 按 E 调用 try_talk → 对话框显示/翻页/关闭 → 离开范围时若还在谈话可选择强制关闭。让 AI 按你当前场景树给出完整脚本,你负责粘贴与点连接。
验收时故意做坏事:远离再按 E(不应开);对话中途走开(定义你的规则并坚持);连按是否跳句正常。规则一致比规则完美重要。
信号没连、节点路径错、玩家不在检测层、UI 被别的 CanvasLayer 挡住——是四大元凶。排错时每次只验证一个假设,带着 print 与可见性检查前进。
接通后保存两份:场景与脚本。录 5 秒:走近、说话、翻页、离开。这是 Day4 的技术样片。
若时间不够做提示 Label,至少保证对话框能出字。提示是体验,出字是生死线。
接线完成后,不要连续大改三处。改 UI 就只改 UI,测一次;改检测就只改检测,测一次。混改是制造幽灵 Bug 的快捷方式。
联调成功后,故意改错一个节点名再改回来,观察报错长什么样。认识报错,是为了以后不怕报错。
联调时打开远程场景或确保对话框实例就在当前运行场景树里。脚本拿着路径找不到节点,是「我写了但没挂上」的典型。print 节点路径,比盯着代码发呆有效。
把成功录像或截图留存。第二天你改战斗输入时若弄坏对话,这些样片能告诉你「昨天好的标准长什么样」。
链路通后,把「interact + player_in_range + show_line」写成你自己的三行口诀。口诀能背,排错就快。
完整走通一次对话开合。通了才允许美化——先能演,再妆造。
把完成定义当成场记表,逐项勾选:
加一点导演润色(可选,不超过 15 分钟):对白提到竹林;NPC 站在石板路旁;提示文字用「按 E 交谈」。润色超时立刻停——完美是明日的敌人。
第二个 NPC 只是复制场景改对白与贴图。今天不必做。证明模板可复制,比堆角色重要。模板 = 生产能力。
备份项目。对话系统常在「再改一点」时崩。备份是彩排后的定妆照。
如果朋友试玩只会「乱按」,说明提示不够。加一行「按 E 交谈」往往比加一段教程动画更有效。提示是设计,不是示弱。
杀青标准是「可重复演示」。重启游戏再谈一次话,仍要成功。只成功一次的 Demo,叫巧合,不叫系统。
排练不止一次。重启游戏,换个方向走近,连按,慢按,走开放开。系统要在粗鲁操作下仍不崩溃。玩家不会按你的设计意图来按键,他们按自己的好奇心来。
勾完场记表后,才允许加第二个装饰性 NPC。第二个没有新系统,只是内容增量。系统未稳就增量,等于放大事故面。
演示给别人看时,先自己走位到 NPC 旁再交键盘。演示也是导演工作,不是纯技术工作。
场记表勾完后,把对话截图和三句台词一起塞进口令本。明天做战斗时,你可能暂时关掉对话输入,需要随时能找回「对话正常时的样子」。
若翻页偶发跳两句,检查是否同时在 _process 与按钮信号里处理了同一按键。只保留一条处理路径。
可重复演示三次,才算真正杀青。一次成功可能是运气。
场记表全勾,截图对话框与场景树。交得出样片,才算杀青。
第四天,世界开始说话。把链路收进词典:
你应该已经具备:
叙事系统最容易贪心:分支、任务、声望……AI 一鼓励,项目就胀死。导演要会说:「今天只让一个人说完三句话。」有限的说话,胜过无限的架构幻想。
明天 Day5 做战斗:世界将有冲突。对话给了动机,战斗给了代价。若今天对话不稳,明天别急着上刀,先让店小二能可靠地开口。
更新口令本:NPC 路径、对话框路径、interact 键、三句对白。档案在,剧组就还在。
四天回顾:可进引擎、有地、能走、能说话。这已经是可演示的竖切片雏形。后面战斗与包装,是让竖切片更刺激、更好看,而不是推倒重来。
若只完成 80%,优先保证出字与关闭。提示、动画、打字机效果都是皮肤。皮肤可缺,嘴不能哑。
请用四句话复盘:谁在说话、怎样才算靠近、哪个键开始、怎样结束。说得顺,说明模型在你脑子里建好了。说得卡,回去对着场景树再指一遍。
内容生产者的你,已经可以批量生成「村口闲聊」。但批量前请锁定模板。模板不锁,批量只是批量制造债务。
从今天起,你的 AI 口令可以更「编剧化」:指定说话人身份、信息目标、禁止剧透。技术口令与内容口令分流,团队才像团队。
完成定义再读一遍:靠近、按键、出字、翻页、关闭。全满足就点标记完成;缺一项就补一项,不要用「差不多」骗自己。
口令本更新完毕后,才点「标记为已完成」。档案先于荣誉。
明天见:刀光之前,先确保今天这张嘴还在。
向自己口述链路四步。说顺了,再点「标记为已完成」——说得清,才算真会。
1)保存三句对白与 interact 键位到口令本。
2)截图:提示/对话框/场景树。
3)写一句:「我的对话关闭条件是 ___。」
NPC = 感应门 + 嘴巴 + 对话框。胜利条件:村里有人能把话说完。