# 03 资源替换指南 (Asset Replacement Guide) > 当前项目使用**程序化占位**(几何体 + 蜂鸣音),所有接口已就绪。本文说明如何无缝替换为正式美术资源,**不需要修改任何业务逻辑代码**。 --- ## A. 音效替换 ### 当前状态 `AudioManager` 在 `res://audio/sfx/.ogg`(或 `.wav` / `.mp3`)**不存在**时自动回退到程序化蜂鸣。文件存在即优先加载真实音效。 ### 替换步骤 **1. 创建目录并放入文件** ``` res://audio/sfx/ hit.ogg ← 子弹命中音效 kill.ogg ← 敌人死亡 hurt.ogg ← 玩家受击 wave_complete.ogg ← 波次结算 boss.ogg ← Boss 入场 shop.ogg ← 商店开启 buy.ogg ← 购买法术 level_up.ogg ← 升级 ``` **2. 确认格式要求** | 属性 | 推荐值 | |:---|:---| | 格式 | OGG Vorbis(体积小)/ WAV PCM(低延迟) | | 采样率 | 44100 Hz 或 22050 Hz | | 声道 | 单声道(Mono)即可,立体声也支持 | | 时长 | 音效 < 3s;BGM 循环 | | 响度 | 建议 -14 LUFS 标准化后导入 | **3. Godot 导入设置**(在编辑器 FileSystem 面板右键文件 → Import) | 设置 | 推荐值 | |:---|:---| | Loop | 音效:`Off`;BGM:`On` | | Compression Mode | `quality` OGG / `PCM` WAV | | Max Texture Size | 不适用(音频) | **4. 添加新音效 ID** 在 `AudioManager._SFX_DEFS` 中追加一行(定义占位蜂鸣,有真实文件时忽略): ```gdscript # audio_manager.gd const _SFX_DEFS: Dictionary = { # ... 已有 ... "spell_cast": [600.0, 80.0], # 新增:频率 600Hz,80ms } ``` 然后在需要播放的地方调用: ```gdscript AudioManager.play("spell_cast", spawn_pos) ``` ### BGM 系统(预留接口) 当前无 BGM 系统,添加时建议在 `AudioManager` 中扩展: ```gdscript # 建议扩展方向(未实现,供参考) func play_bgm(track_id: String, fade_sec: float = 1.0) -> void: var path = "res://audio/bgm/%s.ogg" % track_id # 跨淡 + loop... ``` --- ## B. 敌人 / 玩家 精灵替换 ### 当前状态 - **玩家**:`combat_s2.gd` 中程序化 `Polygon2D`(青色方块) - **敌人**:`EnemyManager.sync_multimesh()` 驱动 `MultiMeshInstance2D`,使用单位 `QuadMesh` + 颜色覆盖 ### 替换玩家精灵 `combat_s2.gd` → `_setup_world_view()`,找到玩家标记部分替换: ```gdscript # 当前(几何占位) var marker := Polygon2D.new() marker.polygon = PackedVector2Array([...]) marker.color = Color(0.35, 0.9, 1.0) _player_node.add_child(marker) # 替换为精灵(修改为) var marker := Sprite2D.new() marker.texture = load("res://assets/sprites/player.png") marker.scale = Vector2(0.5, 0.5) # 按实际尺寸调整 _player_node.add_child(marker) ``` ### 替换敌人渲染(MultiMesh + Atlas) 敌人使用 `MultiMeshInstance2D`,单次 Draw Call 渲染所有实例。替换步骤: **步骤 1:准备 Atlas 贴图** 将所有敌人原型排列在一张贴图上(建议 512×512): ``` atlas_enemies.png ┌────┬────┬────┐ │BASIC│FAST│ARM.│ 每格 64×64 px ├────┼────┼────┤ │ELITE│MIN.│BOSS│ └────┴────┴────┘ ``` **步骤 2:修改 MultiMesh 使用 Atlas** `combat_s2.gd` → `_setup_world_view()` 中的敌人 MMI 部分: ```gdscript # 原:使用 QuadMesh 纯色 var quad := QuadMesh.new() quad.size = Vector2(1.0, 1.0) _enemy_mm.mesh = quad # 替换:使用 PlaneMesh + Atlas 材质 var mesh := PlaneMesh.new() mesh.size = Vector2(1.0, 1.0) var mat := CanvasItemMaterial.new() # 或使用 ShaderMaterial + 自定义 Shader 实现 UV 偏移(按 enemy_type 选择 Atlas 区域) _enemy_mm.mesh = mesh var mmi := MultiMeshInstance2D.new() mmi.multimesh = _enemy_mm mmi.texture = load("res://assets/sprites/atlas_enemies.png") add_child(mmi) ``` **步骤 3(进阶):Shader 按 instance_custom_data 选 UV** ```glsl // res://shaders/enemy_atlas.gdshader shader_type canvas_item; uniform sampler2D atlas_tex; uniform int cols = 3; // atlas 列数 void fragment() { // INSTANCE_CUSTOM.x 存储 enemy_type (0-5) float etype = INSTANCE_CUSTOM.x; float row = floor(etype / float(cols)); float col = mod(etype, float(cols)); vec2 uv = (UV + vec2(col, row)) / vec2(float(cols), 2.0); COLOR = texture(atlas_tex, uv) * COLOR; } ``` 在 `EnemyManager.sync_multimesh()` 中写入 custom data: ```gdscript # enemy_manager.gd sync_multimesh() 内,在 set_instance_transform_2d 之后 mm.set_instance_custom_data(i, Color(float(etype) / 255.0, 0, 0, 0)) ``` --- ## C. 子弹精灵替换 子弹同样使用 `MultiMeshInstance2D`(`_bullet_mm`)。替换为 Atlas 方式与敌人相同,但子弹通常使用 `damage_type` 区分外观。 `BulletManager.sync_multimesh()` 中写入 custom data(damage_type): ```gdscript # bullet_manager.gd sync_multimesh() 内追加 mm.set_instance_custom_data(i, Color(float(_data[base + 8]) / 255.0, 0, 0, 0)) ``` --- ## D. VFX 特效替换 ### 当前状态 `VFXManager` 使用程序化彩色矩形作为特效占位(`ColorRect`/`Sprite2D`)。 ### 替换为粒子特效 `scripts/autoloads/vfx_manager.gd` → `_instantiate_vfx()` 中修改 `_vfx_scenes` 映射: **步骤 1:制作粒子场景** 为每个特效 ID 建一个场景文件(`GPUParticles2D` 为根节点,`one_shot = true`): ``` res://scenes/vfx/ hit_spark.tscn ← 命中火花 status_burn.tscn ← 燃烧特效 status_poison.tscn ← 中毒特效 cast_flash.tscn ← 施法闪光 death_burst.tscn ← 敌人死亡 zone_expire.tscn ← 地面效果消散 ``` 每个场景设置: - `GPUParticles2D.one_shot = true`(**必须**,P6-N72) - `GPUParticles2D.emitting = false`(代码控制时机) - `lifetime` = 特效持续时长 **步骤 2:修改 `_vfx_scenes` 预加载表** ```gdscript # vfx_manager.gd func _ready() -> void: _vfx_scenes = { "hit_spark": preload("res://scenes/vfx/hit_spark.tscn"), "status_burn": preload("res://scenes/vfx/status_burn.tscn"), # ... 其他 ... } ``` **步骤 3:让 VFX 播放时自动启动粒子** > **重要**:`one_shot = true` 和 `emitting = false` 须在**场景文件内**就设好(Godot 编辑器中设置属性),这是 P6-N72 强制要求。运行时 `VFXManager.play()` 只负责把节点拖出池并触发 `emitting = true`。 ```gdscript # vfx_manager.gd → play() 内,instance 创建后追加: if node is GPUParticles2D: node.emitting = true # 启动粒子(one_shot 已在场景中设置,这里不必覆盖) ``` ### 特效 ID 参考 | ID | 触发时机 | |:---|:---| | `"hit_spark"` | 子弹命中敌人 | | `"status_burn"` | 施加 BURN 状态 | | `"status_poison"` | 施加 POISON 状态 | | `"cast_flash"` | 施法时玩家周围 | | `"death_burst"` | 敌人死亡 | | `"zone_expire"` | 地面效果消失 | --- ## E. UI 界面图片替换 当前 UI 全为程序化构建(`ColorRect` + `Label` + `Button`),无独立 `.tscn` 场景。替换为美术 UI 的推荐路径: **选项 A:保留程序化,只替换 Theme** 在 `combat_s2.gd` 的各 `_setup_*` 函数中,为 `Button`/`Label` 应用自定义 Theme: ```gdscript var theme := load("res://assets/ui/main_theme.tres") # 你制作的 Theme 资源 btn.theme = theme ``` **选项 B:迁移到独立 .tscn 场景**(推荐,长期) 1. 为商店、背包、结算屏分别建 `.tscn` 2. 在 `_setup_shop_ui()` 等函数中替换为 `load("res://scenes/ui/shop.tscn").instantiate()` 3. 通过信号 / Callable 将原有事件(买法术、刷新等)连接到新场景节点 > **注意**:迁移 UI 时须保留对 `_shop_btns`、`_reroll_btn` 等引用的赋值,否则商店逻辑失效。 --- ## F. 资源路径约定 | 类型 | 约定路径 | |:---|:---| | 音效 | `res://audio/sfx/.ogg` | | BGM | `res://audio/bgm/.ogg` | | 玩家精灵 | `res://assets/sprites/player.png` | | 敌人 Atlas | `res://assets/sprites/atlas_enemies.png` | | VFX 场景 | `res://scenes/vfx/.tscn` | | UI Theme | `res://assets/ui/main_theme.tres` | | UI 场景 | `res://scenes/ui/.tscn` | | 字体 | `res://assets/fonts/.ttf` |