Files
spellforge/docs/handbook/03_asset_replacement.md
T
2026-07-20 10:56:52 +08:00

282 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 03 资源替换指南 (Asset Replacement Guide)
> 当前项目使用**程序化占位**(几何体 + 蜂鸣音),所有接口已就绪。本文说明如何无缝替换为正式美术资源,**不需要修改任何业务逻辑代码**。
---
## A. 音效替换
### 当前状态
`AudioManager``res://audio/sfx/<id>.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)即可,立体声也支持 |
| 时长 | 音效 < 3sBGM 循环 |
| 响度 | 建议 -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], # 新增:频率 600Hz80ms
}
```
然后在需要播放的地方调用:
```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 datadamage_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/<id>.ogg` |
| BGM | `res://audio/bgm/<track>.ogg` |
| 玩家精灵 | `res://assets/sprites/player.png` |
| 敌人 Atlas | `res://assets/sprites/atlas_enemies.png` |
| VFX 场景 | `res://scenes/vfx/<id>.tscn` |
| UI Theme | `res://assets/ui/main_theme.tres` |
| UI 场景 | `res://scenes/ui/<name>.tscn` |
| 字体 | `res://assets/fonts/<name>.ttf` |
</content>