引言:有无相生,难易相成,高下相倾 ——《道德经》
上一章我们完成可操控主角,但角色仅存在空白虚空场景。本章先重构清理冗余main.tscn,搭建标准化全局常驻容器;再独立拆分江南小镇关卡,采用多层TileMapLayer制作国风瓦片地图、规范地形物理碰撞;搭配Camera2D实现带边界限制的平滑跟随镜头;完善全局管理器GameManager,实现关卡动态加载、切换自动释放美术资源,从根源解决多地图叠加造成的内存堆积问题;最后解决 2D 层级遮挡、人物被墙体遮挡后完全消失的体验缺陷,完整搭建可自由探索的江南小镇场景。
本章核心目标
- 完整清理、重构
main.tscn主场景,搭建无冗余标准化全局层级结构; - 新建独立关卡资源目录,实现地图、角色与主场景完全解耦;
- 制作、裁切通用国风
TileSet瓦片资源,统一规范 2D 物理碰撞分层; - 分层绘制江南小镇地图,提前预留战斗、NPC、传送交互点位;
- 为主角配置带地图边界锁定的平滑跟随摄像机;
- 迭代
GameManager全局管理脚本,实现关卡动态实例化、切换自动释放贴图资源; - 实现 Y 轴动态排序,解决 2D 物体前后遮挡错乱 BUG;
- 开发人物遮挡透视轮廓 Shader,角色被墙体 / 树木遮挡时显示半透明残影,提升探索手感。
6.1 重构主场景 main.tscn(全局永久根场景)
统一规范主场景节点层级,区分全局常驻模块与动态可销毁地图容器,所有探索关卡仅临时挂载,切换时完整释放。
6.1.1 标准主场景树形结构
Main (Node2D,游戏唯一启动根节点,全程不销毁)
├─ World (Node2D 大世界探索总容器)
│ └─ CurrentLevel (空Node2D,仅运行时挂载动态地图,编辑器空白无资源、无脚本)
├─ BattleArena (Node2D 独立战斗空间,全局常驻不销毁)
└─ UI (CanvasLayer 全局HUD、对话、酿造界面,永久顶层渲染)
6.1.2 分步搭建规范操作
- 选中父节点
World,右键添加子节点Node2D,重命名CurrentLevel; - 清空
CurrentLevel所有子节点、贴图与附加脚本,保持空白容器状态; - 保留原有
BattleArena、UI节点,二者属于全局常驻功能,不受地图切换影响;
6.1.3 层级功能约束说明
World:仅承载城镇、室内等探索类地图,所有关卡运行时挂载至子节点CurrentLevel;BattleArena:独立隔离战斗逻辑,投壶、对战系统与探索地图完全互不干扰;UI:依托CanvasLayer天然顶层渲染特性,玩家状态栏、对话弹窗、酿造面板永久可见。
6.2 创建独立关卡目录与小镇关卡场景
6.2.1 新建关卡专用资源文件夹
编辑器左侧文件面板右键scenes文件夹,新建文件夹命名levels;
作用:所有城镇、室内关卡场景统一归档,与主场景、角色预制件分类管理,项目结构清晰。
6.2.2 创建江南小镇独立关卡场景
- 快捷键
Ctrl+N新建空白场景,根节点选择Node2D,重命名TownRoot; Ctrl+S保存路径:res://scenes/levels/level_town.tscn;- 在
TownRoot下创建全套地图专用节点,瓦片、交互标记、主角全部隔离在本关卡内:
TownRoot (Node2D 小镇关卡独立根节点,切换时整体销毁)
├─ GroundLayer (TileMapLayer 地面底层)
├─ DecoLayer (TileMapLayer 花草装饰中层)
├─ WallLayer (TileMapLayer 墙体障碍顶层)
├─ NPCs (Node2D NPC出生坐标标记容器)
└─ Player (实例第五章完成的Player.tscn主角预制件)
6.3 瓦片地图核心原理与分层设计规范
6.3.1 瓦片地图基础概念
常规 RPG 地图存在大量重复视觉单元(地砖、墙体、草木),若每一块地面单独使用独立图片,会造成海量内存占用。 瓦片地图(TileMap)核心思路:将所有地图视觉单元整合至一张图集(Atlas),通过网格坐标记录瓦片摆放位置。如同批量地砖,复用同一套素材拼接出完整地图。 三大核心优势:
- 内存极低:单张图集承载数百种瓦片,无论地图尺寸多大,贴图资源仅加载一次;
- 编辑高效:编辑器画笔、填充工具可视化绘制地图;
- 碰撞易管理:可单独为每一类瓦片配置专属碰撞形状,整张地图碰撞批量生效。
6.3.2 TileMapLayer 分层机制
Godot4.0 起废弃旧版单节点TileMap,改用独立TileMapLayer图层节点,每层可单独控制渲染层级、可见性、碰撞逻辑。
《酒魂》固定三层瓦片分层规范,渲染顺序自下而上:底层→中层→顶层:
| 图层节点名 | 层级作用 | 是否启用物理碰撞 |
|---|---|---|
| GroundLayer | 底层:青石板、泥土、河道水面 | 否 |
| DecoLayer | 中层:草丛、落叶、地面花纹、低矮灌木 | 否 |
| WallLayer | 顶层:院墙、房屋墙体、大树、巨石、栅栏 | 是 |
分层核心价值:碰撞逻辑分离,不会出现装饰草丛阻挡玩家移动、地板误触发碰撞的问题,后续修改单一图层不影响整张地图。
6.3.3 TileSet 瓦片集资源作用
TileMapLayer仅记录瓦片摆放坐标,不存储图片素材;视觉资源、碰撞属性全部存放在独立TileSet资源中,核心存储内容:
- 纹理源:瓦片图集 PNG 原图;
- 裁切规格:单块瓦片像素尺寸(本项目统一 32×32 基础单元);
- 瓦片元数据:碰撞外形、自定义标签、水波循环动画;
- 资源复用:一套
TileSet可供给多个关卡、多层TileMapLayer共用,减少贴图重复加载。
6.4 国风瓦片素材导入与 TileSet 标准化制作
6.4.1 全局像素渲染统一配置
顶部菜单:项目 → 项目设置 → 渲染 → 纹理 将「默认纹理过滤」修改为最近邻 (Nearest),全局统一生效; 优化原理:线性过滤会平滑像素边缘,造成国风像素瓦片模糊;最近邻缩放保留锐利像素边界,适配像素美术风格。
6.4.2 导入江南瓦片图集
- 本地文件管理器找到配套资源
tileset_jiangnan.png; - 直接拖拽至编辑器文件面板
assets/tilesets/目录,引擎自动完成图片导入。
6.4.3 创建全局共用 TileSet 资源
- 右键
resources文件夹 → 新建资源 → 选择TileSet,命名jiangnan_tileset.tres并保存; - 双击打开 TileSet 编辑器,右侧检查器设置基础瓦片尺寸:
Tile Size = (32, 32); - 编辑器左下角「图块源」点击
+,导入tileset_jiangnan.png图集; - 弹窗提示「自动生成瓦片」时选择否:图集包含 32/64/128px 多尺寸建筑、树木素材,自动裁切会拆分完整模型;
- 手动框选拆分瓦片规范:
- 32px 小花、碎石:单格框选,右键生成瓦片;
- 64px 石板、矮墙:按住 Shift 框选 2×2 网格;
- 128px 大树、房屋主体:Shift 拖拽 4×4 网格区域;
- 全部瓦片裁切完成,
Ctrl+S保存 TileSet 资源。
6.4.4 统一全局 2D 物理碰撞分层规范
进入菜单:项目 → 项目设置 → 常规 → 层名称 → 2D 物理,固定分层命名,全项目统一标准:
| 层号 | 层名称 | 适用实体对象 |
|---|---|---|
| 1 | world | 地图墙体、树木、静态障碍物瓦片 |
| 2 | player | 玩家主角本体 |
| 3 | enemy | 敌方怪物实体 |
| 4 | hitbox | 攻击伤害判定区域 |
| 5 | item | 可拾取酒坛、道具交互体 |

6.4.5 TileSet 瓦片绑定物理层
- 双击打开
jiangnan_tileset.tres进入 TileSet 编辑器; - 右侧检查器找到
Physics Layers,点击Add Element新增物理配置; - 物理层参数统一配置:
- Layer(自身所属层):勾选
world(层1); - Mask(可碰撞对象):留空;
逻辑说明:地图静态障碍物仅被动阻挡玩家、敌人,无需主动检测其他实体,遮罩保持空白节省性能。

- Layer(自身所属层):勾选
6.4.6 单瓦片碰撞形状绘制流程
- TileSet 编辑器左侧瓦片列表,选中需要添加碰撞的目标瓦片;
- 底部切换至
Paint绘制标签页,下拉框选中刚创建的world物理层;!
- 预览面板工具栏点击矩形碰撞快捷键
F,自动生成贴合瓦片尺寸的矩形碰撞体;
- 微调绿色碰撞框范围,避开镂空空隙(如窗户、门洞),避免玩家被门框误阻挡;
- 其余障碍物瓦片重复以上流程统一配置。
6.5 town.tscn 江南小镇分层绘制流程
6.5.1 三层 TileMapLayer 绑定共用 TileSet
打开level_town.tscn,依次选中GroundLayer、DecoLayer、WallLayer,检查器加载资源resources/jiangnan_tileset.tres;
三层共用同一套瓦片图集,仅加载一次贴图,杜绝重复加载造成显存浪费。

6.5.2 TileMap 编辑器绘制工具
- 在场景树中选中
GroundLayer节点。 - 此时编辑器底部的 TileMap 编辑器会自动激活,你会看到所有可用的瓦片排列在面板里。
- 同时,场景视口中会出现淡黄色的网格线——这就是我们的绘制画布。
TileMap 编辑器工具栏中,最常用的几个工具(从左到右):
| 工具图标 | 工具名称 | 快捷键 | 使用场景 |
|---|---|---|---|
| 铅笔 | 单格绘制 | B | 点缀零散装饰、修正局部瓦片 |
| 矩形填充 | 批量铺砖 | R | 绘制大片地面、墙体轮廓 |
| 油漆桶 | 泛洪填充 | G | 一键替换连通区域地面(河道、草坪) |
| 橡皮擦 | 擦除瓦片 | E | 删除多余瓦片、清理门洞通路 |
| 随机绘制 | 随机铺放 | 无快捷键 | 地砖、草丛自然化,消除重复单调感 |

6.5.3 《酒魂》起始小镇的地图设计参考
6.5.3 小镇地图整体规划
首张探索地图:回忆巷江南小镇,尺寸20×15 瓦片格(1280×960 像素),区域布局示意:
┌─────────────────────────────────────────────┐
│ ███████████████ 北侧院墙 ███████████████ │
│ █ [草坪] █ 酒坊主建筑 █ [古井地标] █ │
│ █ █ 【主角初始出生点】 █ │
│ ███ [石桥] ███ 河道水域 ████ [柳树] ███ │
│ █ [主石板街道] 广场空地【战斗预留区】 [摊贩NPC点位] █ │
│ █ [茶肆] ██████████ [竹林景观] █ │
│ ███████████████ 南侧院墙 ███████████████ │
└─────────────────────────────────────────────┘
设计提示: 对于第一张地图,"够玩"比"够美"更重要。只要有清晰的可行走区域、可碰撞的障碍物,以及一两个有辨识度的地标(酒坊门口、古井、小桥),就已经足够进入第七章的战斗开发了。地图的丰富和美化可以在所有系统开发完成后再回来打磨。
6.5.4 分层绘制标准流程(自下而上,不可颠倒)
第一步:GroundLayer 底层地面绘制
- 选中
GroundLayer,矩形填充工具铺满整张地图青石板基础地面; - 泛洪填充工具将河道区域替换水面瓦片;
- 建筑周边使用随机绘制,混合 2-3 种相似地砖,弱化大面积纯色单调感。 第二步:DecoLayer 中层装饰绘制
- 草坪区域零散绘制草丛、落叶;
- 河道边缘放置柳树根部装饰瓦片;
- 石板道路随机散落落叶,贴合秋季江南美术设定;
- 低矮灌木仅做视觉点缀,不添加碰撞。 第三步:WallLayer 顶层障碍墙体绘制
- 地图四周边缘绘制院墙,锁定地图行走边界;
- 绘制酒坊、茶肆建筑墙体,预留门口通行空位;
- 河道两侧放置太湖石、木桩障碍物;
- 地图角落绘制大树景观,全部自带碰撞阻挡玩家穿行。
绘制完成后按F5基础测试:玩家可自由行走地面,墙体、树木完全阻挡角色,碰撞功能生效。遗留问题:角色走到屏幕边缘无镜头限制,画面露出黑色空白,6.6 节摄像机模块解决。

6.6 Camera2D 平滑跟随摄像机(带地图边界锁定)
6.6.1 Camera2D 底层逻辑
Godot2D 坐标系为无限平面,Camera2D相当于屏幕可视窗口,仅渲染摄像机范围内的游戏画面。
将摄像机作为主角子节点可实现自动跟随,但直接父子绑定会出现镜头抖动;因此配置两大核心优化属性:
Position Smoothing位置平滑:摄像机延迟缓慢追上角色,消除移动抖动,提升画面丝滑度;Limit边界限制:锁定摄像机移动最大范围,镜头不会移出地图露出黑边。
平滑速度参数适配表(主角移动速度 200px/s)
| Speed 数值 | 镜头表现 | 适用场景 |
|---|---|---|
| 3.0 ~ 5.0 | 跟随延迟极高,镜头大幅滞后角色 | 剧情特写、镜头慢拉特效(非日常探索) |
| 6.0 ~ 10.0 | 轻微滞后,流畅无眩晕 | 城镇日常探索(项目标准推荐值 8.0) |
| 15.0 以上 | 几乎同步跟随,延迟极小 | 高速战斗、追逐场景 |
调试规范:默认设置Speed=8.0,运行游戏沿地图四边行走测试,拖拽感过重上调数值,跟随生硬下调数值。
6.6.2 Camera2D 节点创建与基础配置
摄像机挂载至主角预制件,保证所有关卡复用统一镜头逻辑:
- 打开
Player.tscn主角场景; - 选中根节点
CharacterBody2D,添加子节点Camera2D; - 最终主角场景树形结构:
Player (CharacterBody2D)
├─ CollisionShape2D
├─ AnimatedSprite2D
└─ Camera2D
- 摄像机基础参数配置:
| 属性路径 | 参数值 |
|---|---|
| Position Smoothing → Enabled | 勾选开启平滑 |
| Position Smoothing → Speed | 8.0 |

6.6.3 地图边界 Limit 两种实现方案
方案一:固定数值手动配置(本章小镇专用)
当前小镇尺寸:20×15 瓦片,单格 64px,总像素尺寸宽 = 20×640=1280,高 = 15×64=960 摄像机 Limit 固定参数:
- Limit Left = 0
- Limit Top = 0
- Limit Right = 640
- Limit Bottom = 480
设置完 Limit之后,无论玩家跑向哪里,摄像机都会乖乖停在地图范围内,永远不会让玩家看到地图边缘的黑边。
方案二:代码动态自适应边界(预留扩展接口,第 21 章数据驱动使用) 适配尺寸不统一的多关卡、程序化地图,通过读取 TileMap 实际宽高自动更新摄像机边界,本章仅预留函数,暂不启用。
6.6.4 主角物理碰撞层匹配校验
保证玩家可与地图障碍物正常碰撞,核对Player根节点物理参数:
- Collision Layer(自身归属层):仅勾选
player(层2); - Collision Mask(可碰撞检测层):仅勾选
world(层1);
碰撞层的工作逻辑:
Collision Layer是"我是什么",Collision Mask是"我能感知到什么"。两个物体之间发生碰撞,需要至少一方的Mask包含另一方的Layer。这种设计让你可以精细控制"哪些东西之间会发生碰撞",比如子弹只和敌人碰撞、不和友军碰撞——这在第7章开始编写战斗系统时会非常重要。
6.7 GameManager 全局管理器:关卡动态加载与内存释放
打开第四章自动加载脚本scripts/game_manager.gd,新增完整关卡切换逻辑,游戏启动自动加载江南小镇;切换关卡时调用queue_free()销毁旧场景,瓦片、精灵贴图完整释放显存,全程仅单张地图资源驻留内存。
extends Node
# ---- 游戏状态枚举 ----
enum GameState { EXPLORE, BATTLE, BREWING, DIALOGUE, PAUSED }
# ---- 全局状态变量 ----
var current_state: GameState = GameState.EXPLORE
var player_drunk_level: float = 0.0 # 全局统一醉意数值
# ---- 关卡加载相关资源与容器 ----
@export var town_scene: PackedScene = preload("res://scenes/levels/level_town.tscn")
var active_level: Node2D = null
@onready var level_container: Node = get_node("/root/Main/World/CurrentLevel")
# ---- 游戏初始化 ----
func _ready() -> void:
print("[GameManager] 游戏启动,当前状态: ", GameState.keys()[current_state])
# 启动默认加载江南小镇,主角出生坐标
switch_level(town_scene, Vector2(400, 450))
# ---- 关卡切换核心函数(内存优化核心) ----
func switch_level(target_scene: PackedScene, spawn_pos: Vector2) -> void:
# 销毁正在运行的旧关卡,释放全部贴图资源
if active_level != null:
active_level.queue_free()
active_level = null
# 实例化新关卡挂载至空白容器
active_level = target_scene.instantiate()
level_container.add_child(active_level)
# 重置主角出生位置
var player = active_level.get_node_or_null("Player")
if player:
player.position = spawn_pos
var camera = player.get_node_or_null("Camera2D")
var ground_tilemap = active_level.get_node_or_null("GroundLayer")
# 动态边界函数预留(多关卡适配)
if camera and ground_tilemap and camera.has_method("setup_limits_from_tilemap"):
camera.setup_limits_from_tilemap(ground_tilemap)
else:
push_warning("[GameManager] 当前关卡未找到Player节点,出生坐标未赋值")
# ---- 全局状态切换统一入口 ----
func change_state(new_state: GameState) -> void:
if current_state == new_state:
return
print("[GameManager] 状态切换:", GameState.keys()[current_state], " → ", GameState.keys()[new_state])
current_state = new_state
match new_state:
GameState.EXPLORE:
get_tree().paused = false
GameState.PAUSED:
get_tree().paused = true
GameState.BREWING, GameState.DIALOGUE:
get_tree().paused = true # UI弹窗时暂停世界,UI节点需设置Ignore Pause
GameState.BATTLE:
pass # ARPG战斗模式不暂停探索世界
加载内存逻辑说明
_ready()游戏启动自动调用切换函数,小镇场景实例挂载至CurrentLevel空白容器;- 切换其他关卡时,旧关卡整体执行
queue_free(),瓦片图集、角色精灵全部从内存、显存卸载; - 游戏运行全程仅保留当前活动关卡资源,杜绝多地图叠加占用内存。
6.8 运行验收清单与常见故障排查
按下F5运行游戏,逐项核对验收标准,全部通过代表本章功能完成:
-
主角可在地面、石板路、河道边缘自由行走,无隐形阻挡;
-
院墙、房屋、大树、石桥可正常阻挡角色,无穿墙 bug;
-
角色移动时摄像机平滑跟随,无画面抖动、瞬移;
-
角色走到地图四角,镜头锁定边界,不会出现外侧黑边;
-
角色移动播放行走动画,静止切换待机动画(第五章预制件逻辑复用正常);
常见故障原因与修复方案
| 故障现象 | 根因 | 解决方式 |
|---|---|---|
| 角色直接穿墙 | WallLayer 瓦片未配置 world 碰撞,或碰撞框缺失 | 返回 TileSet 编辑器给墙体瓦片添加矩形碰撞,绑定物理层 1 |
| 摄像机完全不跟随角色 | Camera2D 未作为 Player 子节点 | 打开 Player.tscn,确认摄像机层级归属 |
| 地图边缘出现大片黑边 | Limit 左右上下像素数值计算错误 | 重新计算瓦片总像素:格数 ×32,更新摄像机边界参数 |
| 瓦片缝隙出现白色细线 | 图片导入启用线性过滤、未开启像素捕捉 | 项目设置纹理过滤改为最近邻;图集重新导入关闭抗锯齿 |
| 草丛、落叶挡住玩家移动 | DecoLayer 装饰瓦片误添加碰撞 | TileSet 编辑器删除低矮装饰瓦片的碰撞外形 |
6.9 地图预留交互点位(适配后续全章节开发)
遵循「先占位、后开发」的工程规范,绘制地图时提前预留交互节点,后续章节无需重构地图:
-
第 7 章投壶战斗预留:小镇中央广场预留 10×8 格无障碍物空地,作为投壶对战、野外遭遇战场地;
-
第 18 章 NPC 系统预留:在
TownRoot/NPCs容器下创建空 Node2D 标记点位(SpawnPoint_LiBai),固定 NPC 生成坐标; -
第 13 章酿造系统预留:酒坊门口添加
Area2D触发区域TriggerZone_Brewery,矩形碰撞框覆盖门口,后续绑定进出触发酿造 UI 逻辑。
优势:所有交互点位坐标固化,后续新增功能仅补充脚本信号,无需重新调整地图瓦片布局,大幅减少重构工作量。
6.10 修复2D场景深度遮挡BUG
6.10.1 BUG成因——固定渲染顺序导致层级错乱
按 F5 运行第六章的小镇场景,操纵主角走到大树、房屋的北侧——也就是从俯视角度看,角色应该被树冠、屋顶挡住一部分的位置——你会发现:无论主角站在树的哪一侧,角色贴图永远完整地叠在树木/建筑之上,从来不会被遮住哪怕一点点,纵深感完全消失。

根本原因:Godot 2D 默认按照场景树节点顺序渲染画面,后挂载的节点会覆盖先挂载的节点,和物体实际坐标、前后位置无关。第六章关卡节点结构如下:
TownRoot (Node2D)
├─ GroundLayer (TileMapLayer)
├─ DecoLayer (TileMapLayer)
├─ WallLayer (TileMapLayer)
├─ NPCs (Node2D)
└─ Player ← 排在最后,画布上后绘制的节点会盖住先绘制的,永远覆盖 WallLayer
Player在场景树中排在WallLayer之后,而 Godot 的规则是后绘制的节点会盖住先绘制的节点,所以不管角色站在树的哪一侧,角色贴图永远盖在WallLayer里的每一棵树、每一面墙之上——这跟"应该被挡住的时候就该挡住"的直觉完全相反。
6.10.2 核心解决方案——Y-Sort 动态Y轴排序
摒弃固定场景树渲染顺序,改用Y轴坐标动态排序:物体脚底Y坐标越大(越靠下),渲染层级越靠上。
简单理解:角色站在树下方(Y更大),遮挡树根;角色站在树后方(Y更小),被树冠遮挡,完全贴合真实透视逻辑。
Godot 4.6 内置 y_sort_enabled 属性(所有 Node2D、TileMapLayer 节点通用),开启后节点子物体将根据世界Y坐标实时排序,而非固定场景树顺序。
进阶适配:TileMap 瓦片支持单独设置Y排序原点,可精准定义树木、房屋的「落地基准点」,避免以贴图边缘排序导致的层级错误。
6.10.3 动手:重构关卡结构,接入 YSortWorld 容器
- 打开
level_town.tscn,在TownRoot下新建子节点Node2D,重命名YSortWorld; - 检查器勾选
Node2D → Ordering → Y Sort Enabled; - 把
WallLayer、Player、NPCs三个节点整体拖拽,移动为YSortWorld的子节点;GroundLayer和DecoLayer都保持在YSortWorld外部——按 6.3.2 节的分层表,DecoLayer装的是草丛、落叶、地面花纹、低矮灌木这类贴地不挡人的纯装饰,跟地面一样不需要参与排序,硬塞进去反而白白增加每帧的排序开销; - 重构后的关卡树形结构:
TownRoot (Node2D)
├─ GroundLayer (TileMapLayer,平面地面,不参与排序)
├─ DecoLayer (TileMapLayer,草丛/花纹等贴地装饰,不挡人,不参与排序)
└─ YSortWorld (Node2D,Y Sort Enabled = 勾选)
├─ WallLayer (TileMapLayer,Y Sort Enabled = 勾选,树木/建筑主体)
├─ NPCs (Node2D)
└─ Player (CharacterBody2D)
这样修改以后,有个细节需要修改,就是将game_manager.gd代码中的“var player = active_level.get_node("Player")”,修改为“var player = active_level.get_node("YSortWorld/Player")”
- 选中
WallLayer,检查器同样勾选它自己的Y Sort Enabled——只有父容器和子图层同时开启,瓦片才会作为独立个体参与和Player的排序比较,而不是整层瓦片打包成一个绘制单位。
如果你的
DecoLayer里也混了会挡人的东西: 6.3.2 节的分层表里DecoLayer最后一列标的是"否"(不阻挡),说明设计初衷就是纯装饰;但如果后续往里加了个头比较高、你希望能挡住角色的灌木丛,处理思路跟 7.1.9 节迁移大树的做法一样——把这一小撮瓦片单独拿出来,按需要要么并入WallLayer交给 Y-Sort,要么做成独立的Occluder实例,而不是把整个DecoLayer都拖进YSortWorld。
6.10.4 校准瓦片的 Y 排序原点
- 双击打开
jiangnan_tileset.tres进入 TileSet 编辑器; - 选中大树、房屋主体等高大瓦片(128px 类),右侧检查器找到
Y Sort Origin; - 将数值设置为该瓦片贴图高度减去"落地点"到底部的距离——通俗地说:把数值下移到树干/门口与地面接触的那一圈像素,而不是贴图整体的最底部(树冠顶部同样会被误当作"很靠前");
- 矮墙、石桥、碎石等贴地小型瓦片,
Y Sort Origin保持默认 0 即可,它们本身不高,遮挡问题不明显; Ctrl+S保存,F5 运行,操纵主角绕大树走一圈——此时角色应能正确穿插在树干前后,而不再是永远被整棵树盖住。

排序原理小结:
Y Sort Enabled解决的是"谁该画在谁前面"的数据层面问题,本质上比较的是两者的落地点坐标,和贴图大小、美术风格无关。它对绝大多数矮墙、石桥、灌木类障碍物已经完全够用。
6.11 修复遮挡轮廓残影问题
Y-Sort 解决了"前后顺序对不对"的问题,但又出现了个新问题需要解决,就是角色被树木、房屋、高墙遮挡后完全看不见,会严重破坏玩家操作体验。本节将实现自动遮挡轮廓透视效果:角色被遮挡时自动显示半透明轮廓,未被遮挡时完全正常显示。

6.11.1 核心实现原理
本次效果核心依赖 Godot 2D 专属能力:Canvas Shader + hint_screen_texture 屏幕纹理采样,核心逻辑是「像素颜色自动比对」。
核心设计思路
- 新建一个和玩家本体动作、位置完全同步的「残影轮廓节点」,并设置为最顶层绘制,保证它是屏幕最后渲染的内容;
- Shader 会读取「绘制轮廓前的屏幕成品颜色」(已渲染的地面、遮挡物、角色本体);
- 将屏幕颜色与角色本体本该显示的贴图颜色做比对;
- 颜色一致 = 角色未被遮挡,轮廓透明不显示;颜色不一致 = 角色被遮挡,自动渲染半透明轮廓。
方案核心优势
- 遮挡物无需挂载任何脚本、无需设置感应区域,零配置适配所有遮挡场景;
- 全自动判断遮挡状态,进入遮挡区自动显形,离开自动消失;
- 兼容Y轴排序,保留角色与场景的前后穿插视觉效果,不破坏原有画面层级。
6.11.2 创建 PlayerGhost 轮廓节点
- 打开
Player.tscn——角色本体贴图是第五章配置好的AnimatedSprite2D节点,在它的同级,添加子节点AnimatedSprite2D(而不是Sprite2D,因为轮廓要能跟着本体一起播放行走/待机动画),重命名为PlayerGhost; - 选中
PlayerGhost,检查器里手动设置:Z Index = 100、Z As Relative = 关闭、Ordering → Y Sort Enabled = 关闭——这样它的绘制顺序不再受 Y-Sort 影响,永远盖在最上面,保证它是"最后一个画的东西";
6.11.3 搭建 Shader 材质资源
一、 新建 Shader 专用文件夹 统一管理项目着色器文件,规范项目结构:编辑器左下角 文件系统,右键项目根目录 res://; 选择 新建 → 文件夹,命名为 shaders;最终目录:res://shaders/,与 scenes、scripts 文件夹同级。
二、为 PlayerGhost 创建 ShaderMaterial 材质 选中场景中的 PlayerGhost 节点;右侧检查器找到 CanvasItem → Material 属性(默认空);点击属性右侧下拉框,选择 新建 ShaderMaterial;点击 Material 左侧小箭头,展开材质详细配置项。

三、 创建 Shader 代码文件 展开 ShaderMaterial 后,找到下方空的 Shader 属性;下拉选择 新建 Shader;弹窗配置:Shader Type 保持默认 CanvasItem(2D专用); 路径修改为:res://shaders/occluder_reveal.gdshader;点击创建,自动打开Shader编辑面板。

四、 编写遮挡透视 Shader 代码 删除编辑器默认生成的所有模板代码,粘贴以下完整代码,按下 Ctrl+S 保存。文件路径:res://shaders/occluder_reveal.gdshader

shader_type canvas_item;
// 读取屏幕已渲染画面,用于像素颜色比对
uniform sampler2D screen_texture : hint_screen_texture;
// 遮挡后显示的轮廓颜色(可在检查器自定义颜色、透明度)
uniform vec4 silhouette_color : source_color = vec4(0.0, 0.0, 0.0, 0.5);
void fragment() {
// 获取当前像素位置、渲染轮廓前的屏幕画面颜色
vec4 screen_color = texture(screen_texture, SCREEN_UV);
// 获取角色贴图当前像素的原始颜色
vec4 sprite_color = texture(TEXTURE, UV);
// 比对颜色:微小色差忽略,避免误判
if (all(lessThan(abs(sprite_color - screen_color), vec4(0.01)))) {
// 颜色一致:无遮挡,显示原始角色颜色,轮廓透明
COLOR = sprite_color;
} else {
// 颜色不一致:被遮挡,渲染半透明轮廓
// 保留贴图透明通道,避免出现方块黑影
COLOR = vec4(silhouette_color.rgb, silhouette_color.a * sprite_color.a);
}
}
五、新手关键解惑 & 常见报错修复
核心概念区分
- **ShaderMaterial(材质):**外层资源容器,用于挂载Shader代码、存储颜色参数,赋值给节点使用;
- **Shader(着色器代码):**核心逻辑文件,定义像素渲染规则。
效果异常必解方案(高频坑点) 如果运行后无轮廓、轮廓一直显示、效果失效:点击顶部菜单栏 项目 → 项目设置 → 渲染,将 渲染方法 切换为 Compatibility(兼容模式)。hint_screen_texture 仅在兼容模式下完全稳定,Forward+、Mobile 渲染器会存在兼容问题。
6.11.4 编写残影同步脚本(player_ghost.gd)
脚本仅负责:让残影和玩家本体位置、动画、朝向完全同步,遮挡判断全程由Shader自动完成,无需复杂逻辑。 新建脚本文件:res://scripts/player_ghost.gd,绑定到 PlayerGhost 节点,粘贴以下代码:
# scripts/player_ghost.gd
extends AnimatedSprite2D
## 玩家"透视残影"节点:始终绘制在最上层
## 具体"该不该显形"完全交给 Shader 通过比对屏幕颜色自动判断,脚本只负责同步动作
@onready var player_anim: AnimatedSprite2D = get_parent().get_node("AnimatedSprite2D") # 指向主角本体的 AnimatedSprite2D
func _ready() -> void:
z_index = 100
z_as_relative = false
y_sort_enabled = false
# 直接复用主角的动画资源,不用另外拷贝一份 SpriteFrames
sprite_frames = player_anim.sprite_frames
func _process(_delta: float) -> void:
# 每帧同步主角当前播放的动画、帧号、朝向、位置,保证残影和本体完全重合、动作一致
if animation != player_anim.animation:
animation = player_anim.animation
frame = player_anim.frame
flip_h = player_anim.flip_h
global_position = player_anim.global_position
为什么不直接把本体的
AnimatedSprite2D挪出来当残影用,而要建一个新的? 因为本体需要留在原地参与Y Sort(它是角色真实的视觉位置,负责跟树木、建筑比前后),而残影必须脱离排序、永远画在最上层——两者的Ordering设置是互斥的,所以必须是两个独立节点,靠脚本每帧同步动画帧和位置来保持"看起来是同一个人"。
最终效果如下图,看见角色的轮廓了吧。

6.12.1 本章完整交付产物
-
标准化通用
jiangnan_tileset.tres瓦片资源,配套分层碰撞配置; -
三层隔离
TileMapLayer江南小镇完整可运行地图; -
带平滑跟随、边界锁定的复用型
Camera2D主角摄像机; -
解耦式全局
main.tscn场景结构,区分常驻容器与动态关卡; -
迭代完成
GameManager关卡动态加载销毁脚本,解决内存溢出问题; -
预占 NPC、战斗、酿造交互点位的完整关卡场景;
-
Y 轴动态排序体系,修复 2D 物体前后遮挡错乱 BUG;
-
遮挡透视 Shader + 残影同步脚本,人物被墙体遮挡自动显示半透明轮廓。
6.12.2 核心知识点总结
| 学习内容 | 项目开发价值 |
|---|---|
| TileMapLayer 三层分层架构 | 地面、装饰、障碍视觉与碰撞逻辑分离,易维护、可扩展 |
| TileSet 统一瓦片与批量碰撞 | 资源全局复用,一次性配置全地图碰撞,提升开发效率 |
| 2D 物理分层规范 | 精细化控制实体碰撞关系,为战斗、拾取交互打下底层基础 |
| 平滑边界摄像机 | 提升探索沉浸感,避免镜头溢出破坏世界完整性 |
| 动态关卡加载释放机制 | 控制内存占用,支持多地图无缝切换,防止游戏卡顿闪退 |
| 提前预留交互点位开发习惯 | 降低后期多系统整合的重构成本,规范大型项目开发流程 |
| Y-Sort 动态排序 | 解决 2D 俯视游戏物体前后穿插错乱,打造真实空间纵深感 |
| CanvasItem 屏幕采样 Shader | 低成本实现人物遮挡透视效果,大幅优化探索操作手感 |
知之愈明,则行之愈笃;行之愈笃,则知之益明。—— 朱熹 本章核心跃迁:从单一角色虚空移动,搭建出分层、可扩展、内存可控、视觉完整的游戏世界。分层设计思维不仅适用于瓦片地图,后续 UI 分层、渲染批次优化、战斗模块隔离均沿用同一开发思路。江南小镇探索舞台搭建完毕,下一章开启核心战斗玩法开发。
下一章预告
第 7 章《投壶战斗:蓄力、抛物线、命中判定》依托本章统一配置的 world/player/enemy 物理分层,实现蓄力投壶弹道物理、碰撞命中判定,正式接入游戏核心战斗模块。
延伸阅读参考
-
Godot 官方瓦片地图教程:https://docs.godotengine.org/zh_CN/stable/tutorials/2d/using_tilemaps.html 拓展学习地形自动拼接、瓦片动画等进阶 TileSet 功能;
-
Godot Camera2D 官方文档:https://docs.godotengine.org/zh_CN/stable/classes/class_camera2d.html 镜头震动、偏移、淡入淡出特效底层参数查阅;
-
《Pixel Art for Game Developers》像素美术开发书籍:从零制作适配游戏项目的自定义瓦片图集。
评论0
暂时没有评论