forked from datawhale/whale-town-front
- 重构README文档,参考后端结构优化内容组织 - 添加模块开发指南,详细说明模块化开发流程 - 创建场景设计规范,规范场景架构和最佳实践 - 建立贡献者名单,记录团队成员和贡献统计 - 完善技术栈介绍和功能特性说明 - 优化文档结构和导航,提升开发者体验
617 lines
16 KiB
Markdown
617 lines
16 KiB
Markdown
# 场景设计规范
|
||
|
||
本文档定义了 Whale Town 项目中场景设计的标准和最佳实践。
|
||
|
||
## 🎯 设计原则
|
||
|
||
### 核心原则
|
||
1. **功能独立** - 每个场景都是独立的功能单元
|
||
2. **职责单一** - 一个场景只负责一个主要功能
|
||
3. **可复用性** - 场景组件应该能够在其他场景中复用
|
||
4. **标准化** - 统一的场景结构和命名规范
|
||
5. **性能优先** - 优化场景性能,避免不必要的资源消耗
|
||
|
||
### 场景分类
|
||
- **主要场景** - 游戏的核心功能场景(主菜单、游戏场景、设置等)
|
||
- **UI场景** - 纯界面场景(对话框、HUD、菜单等)
|
||
- **游戏场景** - 包含游戏逻辑的场景(关卡、战斗、探索等)
|
||
- **工具场景** - 开发和测试用的场景
|
||
|
||
## 🏗️ 场景结构
|
||
|
||
### 标准目录结构
|
||
```
|
||
scenes/
|
||
├── main_scene.tscn # 主场景
|
||
├── auth_scene.tscn # 认证场景
|
||
├── game_scene.tscn # 游戏场景
|
||
├── settings_scene.tscn # 设置场景
|
||
└── prefabs/ # 预制体组件
|
||
├── ui/ # UI组件
|
||
│ ├── button.tscn
|
||
│ ├── dialog.tscn
|
||
│ └── menu.tscn
|
||
├── characters/ # 角色组件
|
||
│ ├── player.tscn
|
||
│ └── npc.tscn
|
||
├── effects/ # 特效组件
|
||
│ ├── particle_effect.tscn
|
||
│ └── animation_effect.tscn
|
||
└── items/ # 物品组件
|
||
├── collectible.tscn
|
||
└── interactive.tscn
|
||
```
|
||
|
||
### 场景命名规范
|
||
- **主场景**: `scene_name.tscn` (snake_case)
|
||
- **预制体**: `component_name.tscn` (snake_case)
|
||
- **脚本文件**: `SceneName.gd` (PascalCase)
|
||
- **节点名称**: `NodeName` (PascalCase) 或 `nodeName` (camelCase)
|
||
|
||
## 📝 场景设计模板
|
||
|
||
### 主场景结构模板
|
||
|
||
```
|
||
SceneName (Control/Node2D)
|
||
├── Background (TextureRect/Sprite2D) # 背景
|
||
├── UI (CanvasLayer) # UI层
|
||
│ ├── HUD (Control) # 游戏HUD
|
||
│ ├── Menu (Control) # 菜单界面
|
||
│ └── Dialog (Control) # 对话框
|
||
├── Game (Node2D) # 游戏内容层
|
||
│ ├── Player (CharacterBody2D) # 玩家
|
||
│ ├── NPCs (Node2D) # NPC容器
|
||
│ ├── Items (Node2D) # 物品容器
|
||
│ └── Effects (Node2D) # 特效容器
|
||
├── Audio (Node) # 音频管理
|
||
│ ├── BGM (AudioStreamPlayer) # 背景音乐
|
||
│ └── SFX (AudioStreamPlayer2D) # 音效
|
||
└── Systems (Node) # 系统组件
|
||
├── CameraController (Node) # 相机控制
|
||
├── InputHandler (Node) # 输入处理
|
||
└── StateManager (Node) # 状态管理
|
||
```
|
||
|
||
### 场景脚本模板
|
||
|
||
```gdscript
|
||
# SceneName.gd
|
||
extends Control # 或 Node2D,根据场景类型选择
|
||
class_name SceneName
|
||
|
||
# 场景信息
|
||
const SCENE_NAME = "SceneName"
|
||
const SCENE_VERSION = "1.0.0"
|
||
|
||
# 场景状态
|
||
enum SceneState {
|
||
LOADING,
|
||
READY,
|
||
ACTIVE,
|
||
PAUSED,
|
||
TRANSITIONING
|
||
}
|
||
|
||
var current_state: SceneState = SceneState.LOADING
|
||
|
||
# 节点引用
|
||
@onready var background: TextureRect = $Background
|
||
@onready var ui_layer: CanvasLayer = $UI
|
||
@onready var game_layer: Node2D = $Game
|
||
@onready var audio_manager: Node = $Audio
|
||
@onready var systems: Node = $Systems
|
||
|
||
# 场景数据
|
||
var scene_data: Dictionary = {}
|
||
var is_initialized: bool = false
|
||
|
||
# 信号定义
|
||
signal scene_ready
|
||
signal scene_state_changed(old_state: SceneState, new_state: SceneState)
|
||
signal scene_data_updated(key: String, value)
|
||
|
||
func _ready():
|
||
"""场景初始化"""
|
||
print("初始化场景: ", SCENE_NAME)
|
||
|
||
# 初始化场景
|
||
await initialize_scene()
|
||
|
||
# 设置场景状态
|
||
change_state(SceneState.READY)
|
||
|
||
# 发送场景就绪信号
|
||
scene_ready.emit()
|
||
|
||
func initialize_scene():
|
||
"""初始化场景组件"""
|
||
# 加载场景数据
|
||
await load_scene_data()
|
||
|
||
# 初始化UI
|
||
initialize_ui()
|
||
|
||
# 初始化游戏组件
|
||
initialize_game_components()
|
||
|
||
# 初始化音频
|
||
initialize_audio()
|
||
|
||
# 初始化系统
|
||
initialize_systems()
|
||
|
||
# 连接信号
|
||
connect_signals()
|
||
|
||
is_initialized = true
|
||
|
||
func load_scene_data():
|
||
"""加载场景数据"""
|
||
# 从配置文件或网络加载场景数据
|
||
var data_path = "res://data/scenes/%s.json" % SCENE_NAME.to_lower()
|
||
if FileAccess.file_exists(data_path):
|
||
var file = FileAccess.open(data_path, FileAccess.READ)
|
||
if file:
|
||
var json_string = file.get_as_text()
|
||
file.close()
|
||
|
||
var json = JSON.new()
|
||
if json.parse(json_string) == OK:
|
||
scene_data = json.data
|
||
else:
|
||
print("警告: 场景数据JSON格式错误: ", data_path)
|
||
|
||
# 等待一帧确保所有节点都已初始化
|
||
await get_tree().process_frame
|
||
|
||
func initialize_ui():
|
||
"""初始化UI组件"""
|
||
if ui_layer:
|
||
# 初始化UI组件
|
||
for child in ui_layer.get_children():
|
||
if child.has_method("initialize"):
|
||
child.initialize()
|
||
|
||
func initialize_game_components():
|
||
"""初始化游戏组件"""
|
||
if game_layer:
|
||
# 初始化游戏组件
|
||
for child in game_layer.get_children():
|
||
if child.has_method("initialize"):
|
||
child.initialize()
|
||
|
||
func initialize_audio():
|
||
"""初始化音频"""
|
||
if audio_manager:
|
||
# 设置背景音乐
|
||
if scene_data.has("bgm"):
|
||
play_bgm(scene_data.bgm)
|
||
|
||
func initialize_systems():
|
||
"""初始化系统组件"""
|
||
if systems:
|
||
# 初始化系统组件
|
||
for child in systems.get_children():
|
||
if child.has_method("initialize"):
|
||
child.initialize()
|
||
|
||
func connect_signals():
|
||
"""连接信号"""
|
||
# 连接场景内部信号
|
||
# 连接全局事件
|
||
EventSystem.connect_event("game_paused", _on_game_paused)
|
||
EventSystem.connect_event("game_resumed", _on_game_resumed)
|
||
|
||
func change_state(new_state: SceneState):
|
||
"""改变场景状态"""
|
||
if current_state == new_state:
|
||
return
|
||
|
||
var old_state = current_state
|
||
current_state = new_state
|
||
|
||
print("场景状态变更: %s -> %s" % [SceneState.keys()[old_state], SceneState.keys()[new_state]])
|
||
|
||
# 处理状态变更
|
||
_handle_state_change(old_state, new_state)
|
||
|
||
# 发送状态变更信号
|
||
scene_state_changed.emit(old_state, new_state)
|
||
|
||
func _handle_state_change(old_state: SceneState, new_state: SceneState):
|
||
"""处理状态变更"""
|
||
match new_state:
|
||
SceneState.LOADING:
|
||
_on_enter_loading_state()
|
||
SceneState.READY:
|
||
_on_enter_ready_state()
|
||
SceneState.ACTIVE:
|
||
_on_enter_active_state()
|
||
SceneState.PAUSED:
|
||
_on_enter_paused_state()
|
||
SceneState.TRANSITIONING:
|
||
_on_enter_transitioning_state()
|
||
|
||
func _on_enter_loading_state():
|
||
"""进入加载状态"""
|
||
# 显示加载界面
|
||
pass
|
||
|
||
func _on_enter_ready_state():
|
||
"""进入就绪状态"""
|
||
# 场景准备完成
|
||
pass
|
||
|
||
func _on_enter_active_state():
|
||
"""进入活跃状态"""
|
||
# 开始游戏逻辑
|
||
pass
|
||
|
||
func _on_enter_paused_state():
|
||
"""进入暂停状态"""
|
||
# 暂停游戏逻辑
|
||
get_tree().paused = true
|
||
|
||
func _on_enter_transitioning_state():
|
||
"""进入转换状态"""
|
||
# 场景转换中
|
||
pass
|
||
|
||
func play_bgm(bgm_path: String):
|
||
"""播放背景音乐"""
|
||
var bgm_player = audio_manager.get_node("BGM") as AudioStreamPlayer
|
||
if bgm_player and FileAccess.file_exists(bgm_path):
|
||
var audio_stream = load(bgm_path)
|
||
bgm_player.stream = audio_stream
|
||
bgm_player.play()
|
||
|
||
func play_sfx(sfx_path: String, position: Vector2 = Vector2.ZERO):
|
||
"""播放音效"""
|
||
var sfx_player = audio_manager.get_node("SFX") as AudioStreamPlayer2D
|
||
if sfx_player and FileAccess.file_exists(sfx_path):
|
||
var audio_stream = load(sfx_path)
|
||
sfx_player.stream = audio_stream
|
||
if position != Vector2.ZERO:
|
||
sfx_player.global_position = position
|
||
sfx_player.play()
|
||
|
||
func update_scene_data(key: String, value):
|
||
"""更新场景数据"""
|
||
scene_data[key] = value
|
||
scene_data_updated.emit(key, value)
|
||
|
||
func get_scene_data(key: String, default_value = null):
|
||
"""获取场景数据"""
|
||
return scene_data.get(key, default_value)
|
||
|
||
func cleanup():
|
||
"""清理场景资源"""
|
||
print("清理场景: ", SCENE_NAME)
|
||
|
||
# 断开信号连接
|
||
EventSystem.disconnect_event("game_paused", _on_game_paused)
|
||
EventSystem.disconnect_event("game_resumed", _on_game_resumed)
|
||
|
||
# 清理组件
|
||
if ui_layer:
|
||
for child in ui_layer.get_children():
|
||
if child.has_method("cleanup"):
|
||
child.cleanup()
|
||
|
||
if game_layer:
|
||
for child in game_layer.get_children():
|
||
if child.has_method("cleanup"):
|
||
child.cleanup()
|
||
|
||
if systems:
|
||
for child in systems.get_children():
|
||
if child.has_method("cleanup"):
|
||
child.cleanup()
|
||
|
||
# 停止音频
|
||
if audio_manager:
|
||
var bgm_player = audio_manager.get_node("BGM") as AudioStreamPlayer
|
||
if bgm_player:
|
||
bgm_player.stop()
|
||
|
||
# 事件处理
|
||
func _on_game_paused():
|
||
"""游戏暂停事件"""
|
||
if current_state == SceneState.ACTIVE:
|
||
change_state(SceneState.PAUSED)
|
||
|
||
func _on_game_resumed():
|
||
"""游戏恢复事件"""
|
||
if current_state == SceneState.PAUSED:
|
||
change_state(SceneState.ACTIVE)
|
||
get_tree().paused = false
|
||
|
||
# 输入处理
|
||
func _input(event):
|
||
"""处理输入事件"""
|
||
if current_state != SceneState.ACTIVE:
|
||
return
|
||
|
||
# 处理场景特定的输入
|
||
_handle_scene_input(event)
|
||
|
||
func _handle_scene_input(event):
|
||
"""处理场景特定输入"""
|
||
# 在子类中重写此方法
|
||
pass
|
||
|
||
# 生命周期方法
|
||
func _process(delta):
|
||
"""每帧更新"""
|
||
if current_state == SceneState.ACTIVE:
|
||
_update_scene(delta)
|
||
|
||
func _update_scene(delta):
|
||
"""更新场景逻辑"""
|
||
# 在子类中重写此方法
|
||
pass
|
||
|
||
func _physics_process(delta):
|
||
"""物理更新"""
|
||
if current_state == SceneState.ACTIVE:
|
||
_physics_update_scene(delta)
|
||
|
||
func _physics_update_scene(delta):
|
||
"""物理更新场景逻辑"""
|
||
# 在子类中重写此方法
|
||
pass
|
||
```
|
||
|
||
## 🎨 UI设计规范
|
||
|
||
### UI层级结构
|
||
```
|
||
UI (CanvasLayer)
|
||
├── Background (Control) # UI背景
|
||
├── MainContent (Control) # 主要内容
|
||
│ ├── Header (Control) # 头部区域
|
||
│ ├── Body (Control) # 主体区域
|
||
│ └── Footer (Control) # 底部区域
|
||
├── Overlay (Control) # 覆盖层
|
||
│ ├── Loading (Control) # 加载界面
|
||
│ ├── Dialog (Control) # 对话框
|
||
│ └── Toast (Control) # 提示消息
|
||
└── Debug (Control) # 调试信息
|
||
```
|
||
|
||
### UI组件规范
|
||
1. **响应式设计** - 使用Anchor和Margin实现自适应布局
|
||
2. **主题统一** - 使用统一的主题资源
|
||
3. **动画效果** - 添加适当的过渡动画
|
||
4. **无障碍支持** - 考虑键盘导航和屏幕阅读器
|
||
|
||
### UI脚本模板
|
||
```gdscript
|
||
# UIComponent.gd
|
||
extends Control
|
||
class_name UIComponent
|
||
|
||
signal ui_action(action_name: String, data: Dictionary)
|
||
|
||
@export var auto_initialize: bool = true
|
||
@export var animation_duration: float = 0.3
|
||
|
||
var is_visible: bool = false
|
||
var is_initialized: bool = false
|
||
|
||
func _ready():
|
||
if auto_initialize:
|
||
initialize()
|
||
|
||
func initialize():
|
||
"""初始化UI组件"""
|
||
if is_initialized:
|
||
return
|
||
|
||
# 设置初始状态
|
||
modulate.a = 0.0
|
||
visible = false
|
||
|
||
# 连接信号
|
||
_connect_signals()
|
||
|
||
is_initialized = true
|
||
|
||
func show_ui(animated: bool = true):
|
||
"""显示UI"""
|
||
if is_visible:
|
||
return
|
||
|
||
visible = true
|
||
is_visible = true
|
||
|
||
if animated:
|
||
var tween = create_tween()
|
||
tween.tween_property(self, "modulate:a", 1.0, animation_duration)
|
||
else:
|
||
modulate.a = 1.0
|
||
|
||
func hide_ui(animated: bool = true):
|
||
"""隐藏UI"""
|
||
if not is_visible:
|
||
return
|
||
|
||
is_visible = false
|
||
|
||
if animated:
|
||
var tween = create_tween()
|
||
tween.tween_property(self, "modulate:a", 0.0, animation_duration)
|
||
await tween.finished
|
||
visible = false
|
||
else:
|
||
modulate.a = 0.0
|
||
visible = false
|
||
|
||
func _connect_signals():
|
||
"""连接信号"""
|
||
# 在子类中重写
|
||
pass
|
||
```
|
||
|
||
## 🎮 游戏场景规范
|
||
|
||
### 游戏场景结构
|
||
```
|
||
GameScene (Node2D)
|
||
├── Background (ParallaxBackground) # 背景层
|
||
├── Environment (Node2D) # 环境层
|
||
│ ├── Terrain (TileMap) # 地形
|
||
│ ├── Props (Node2D) # 道具
|
||
│ └── Decorations (Node2D) # 装饰
|
||
├── Entities (Node2D) # 实体层
|
||
│ ├── Player (CharacterBody2D) # 玩家
|
||
│ ├── NPCs (Node2D) # NPC
|
||
│ ├── Enemies (Node2D) # 敌人
|
||
│ └── Items (Node2D) # 物品
|
||
├── Effects (Node2D) # 特效层
|
||
│ ├── Particles (Node2D) # 粒子效果
|
||
│ └── Animations (Node2D) # 动画效果
|
||
└── Camera (Camera2D) # 相机
|
||
```
|
||
|
||
### 性能优化
|
||
1. **对象池** - 重用频繁创建销毁的对象
|
||
2. **视锥剔除** - 只渲染可见区域的对象
|
||
3. **LOD系统** - 根据距离调整细节级别
|
||
4. **批量处理** - 合并相似的渲染调用
|
||
|
||
## 🔧 场景管理
|
||
|
||
### 场景切换
|
||
```gdscript
|
||
# 使用SceneManager进行场景切换
|
||
SceneManager.change_scene("game_scene", {
|
||
"level": 1,
|
||
"player_data": player_data
|
||
})
|
||
|
||
# 带过渡效果的场景切换
|
||
SceneManager.change_scene_with_transition("battle_scene", {
|
||
"enemy_data": enemy_data
|
||
}, "fade")
|
||
```
|
||
|
||
### 场景数据传递
|
||
```gdscript
|
||
# 发送场景数据
|
||
var scene_data = {
|
||
"player_level": 10,
|
||
"inventory": player_inventory,
|
||
"quest_progress": quest_data
|
||
}
|
||
SceneManager.change_scene("next_scene", scene_data)
|
||
|
||
# 接收场景数据
|
||
func _on_scene_data_received(data: Dictionary):
|
||
if data.has("player_level"):
|
||
player_level = data.player_level
|
||
if data.has("inventory"):
|
||
load_inventory(data.inventory)
|
||
```
|
||
|
||
## 🧪 场景测试
|
||
|
||
### 测试场景创建
|
||
```gdscript
|
||
# TestScene.gd
|
||
extends "res://scenes/BaseScene.gd"
|
||
|
||
func _ready():
|
||
super._ready()
|
||
|
||
# 设置测试环境
|
||
setup_test_environment()
|
||
|
||
# 运行测试用例
|
||
run_test_cases()
|
||
|
||
func setup_test_environment():
|
||
"""设置测试环境"""
|
||
# 创建测试数据
|
||
# 设置测试状态
|
||
pass
|
||
|
||
func run_test_cases():
|
||
"""运行测试用例"""
|
||
test_scene_initialization()
|
||
test_ui_interactions()
|
||
test_game_logic()
|
||
|
||
func test_scene_initialization():
|
||
"""测试场景初始化"""
|
||
assert(is_initialized, "场景应该已初始化")
|
||
assert(current_state == SceneState.READY, "场景状态应该为READY")
|
||
|
||
func test_ui_interactions():
|
||
"""测试UI交互"""
|
||
# 模拟用户输入
|
||
# 验证UI响应
|
||
pass
|
||
|
||
func test_game_logic():
|
||
"""测试游戏逻辑"""
|
||
# 测试游戏规则
|
||
# 验证状态变化
|
||
pass
|
||
```
|
||
|
||
### 性能测试
|
||
```gdscript
|
||
# 性能监控
|
||
func _process(delta):
|
||
super._process(delta)
|
||
|
||
# 监控帧率
|
||
var fps = Engine.get_frames_per_second()
|
||
if fps < 30:
|
||
print("警告: 帧率过低: ", fps)
|
||
|
||
# 监控内存使用
|
||
var memory_usage = OS.get_static_memory_usage_by_type()
|
||
if memory_usage > 100 * 1024 * 1024: # 100MB
|
||
print("警告: 内存使用过高: ", memory_usage / 1024 / 1024, "MB")
|
||
```
|
||
|
||
## 📚 最佳实践
|
||
|
||
### 代码组织
|
||
1. **单一职责** - 每个场景只负责一个主要功能
|
||
2. **模块化** - 将复杂功能拆分为独立组件
|
||
3. **可测试** - 设计易于测试的场景结构
|
||
4. **文档化** - 为场景添加详细的文档说明
|
||
|
||
### 性能优化
|
||
1. **延迟加载** - 按需加载资源和组件
|
||
2. **对象复用** - 使用对象池管理频繁创建的对象
|
||
3. **批量操作** - 合并相似的操作减少开销
|
||
4. **内存管理** - 及时释放不需要的资源
|
||
|
||
### 用户体验
|
||
1. **响应式设计** - 支持不同分辨率和设备
|
||
2. **流畅动画** - 添加适当的过渡效果
|
||
3. **错误处理** - 优雅处理异常情况
|
||
4. **加载提示** - 为长时间操作提供进度反馈
|
||
|
||
## 🔍 常见问题
|
||
|
||
### Q: 如何处理场景间的数据传递?
|
||
A: 使用SceneManager的场景切换方法传递数据,或通过全局单例存储共享数据。
|
||
|
||
### Q: 场景性能如何优化?
|
||
A: 使用对象池、视锥剔除、LOD系统,避免在_process中执行重复计算。
|
||
|
||
### Q: 如何调试场景问题?
|
||
A: 使用Godot的远程调试器,添加性能监控,编写场景测试用例。
|
||
|
||
### Q: 场景切换如何添加过渡效果?
|
||
A: 使用SceneManager的过渡系统,或自定义Tween动画实现过渡效果。
|
||
|
||
---
|
||
|
||
**记住:良好的场景设计是游戏体验的基础!** |