From 2998fd2d11cd6a4c76f2bbf3f5ce5872cad25f0f Mon Sep 17 00:00:00 2001 From: moyin <2443444649@qq.com> Date: Wed, 31 Dec 2025 17:50:19 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E7=9B=B8=E5=85=B3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增文档: - docs/输入映射配置.md - 游戏输入配置指南 - docs/架构与通信规范.md - 项目架构和组件通信规范 - docs/实现细节规范.md - 游戏对象具体实现要求 - docs/开发哲学与最佳实践.md - 开发理念和编程最佳实践 覆盖内容: - 输入映射的配置方法和验证 - EventSystem事件系统使用规范 - 玩家、NPC、TileMap的实现标准 - 代码质量标准和审查清单 - 性能优化和资源管理指导 这些文档补充了开发规范.md中提到但在docs目录中缺失的内容 --- docs/实现细节规范.md | 388 ++++++++++++++++++++++++++ docs/开发哲学与最佳实践.md | 406 ++++++++++++++++++++++++++++ docs/架构与通信规范.md | 272 +++++++++++++++++++ docs/输入映射配置.md | 157 +++++++++++ 4 files changed, 1223 insertions(+) create mode 100644 docs/实现细节规范.md create mode 100644 docs/开发哲学与最佳实践.md create mode 100644 docs/架构与通信规范.md create mode 100644 docs/输入映射配置.md diff --git a/docs/实现细节规范.md b/docs/实现细节规范.md new file mode 100644 index 0000000..5b3ddb7 --- /dev/null +++ b/docs/实现细节规范.md @@ -0,0 +1,388 @@ +# 实现细节规范 + +本文档详细说明了WhaleTown项目中各种游戏对象的具体实现要求和技术细节。 + +## 🎮 玩家实现规范 + +### 基础要求 +- **节点类型**: 必须使用 `CharacterBody2D` +- **移动系统**: 使用 `move_and_slide()` 方法 +- **相机集成**: 必须包含 `Camera2D` 子节点 + +### 玩家节点结构 +``` +Player (CharacterBody2D) +├── Sprite2D # 玩家精灵 +├── CollisionShape2D # 碰撞形状 +├── Camera2D # 玩家相机 +│ └── [相机配置] +├── InteractionArea (Area2D) # 交互检测区域 +│ └── CollisionShape2D +└── AnimationPlayer # 动画播放器 +``` + +### 相机配置要求 +```gdscript +# Player.gd 中的相机设置 +@onready var camera: Camera2D = $Camera2D + +func _ready() -> void: + # 必须启用位置平滑 + camera.position_smoothing_enabled = true + camera.position_smoothing_speed = 5.0 + + # 设置相机边界(基于TileMap) + _setup_camera_limits() + +func _setup_camera_limits() -> void: + # 获取当前场景的TileMap + var tilemap = get_tree().get_first_node_in_group("tilemap") + if tilemap and tilemap is TileMap: + var used_rect = tilemap.get_used_rect() + var tile_size = tilemap.tile_set.tile_size + + # 计算世界坐标边界 + camera.limit_left = used_rect.position.x * tile_size.x + camera.limit_top = used_rect.position.y * tile_size.y + camera.limit_right = used_rect.end.x * tile_size.x + camera.limit_bottom = used_rect.end.y * tile_size.y +``` + +### 移动实现模板 +```gdscript +extends CharacterBody2D +class_name Player + +@export var move_speed: float = 200.0 +@export var acceleration: float = 1000.0 +@export var friction: float = 1000.0 + +@onready var sprite: Sprite2D = $Sprite2D +@onready var camera: Camera2D = $Camera2D + +func _ready() -> void: + # 设置相机 + camera.position_smoothing_enabled = true + _setup_camera_limits() + +func _physics_process(delta: float) -> void: + _handle_movement(delta) + +func _handle_movement(delta: float) -> void: + # 获取输入方向 + var input_direction := Input.get_vector( + "move_left", "move_right", + "move_up", "move_down" + ) + + # 应用移动 + if input_direction != Vector2.ZERO: + velocity = velocity.move_toward(input_direction * move_speed, acceleration * delta) + else: + velocity = velocity.move_toward(Vector2.ZERO, friction * delta) + + move_and_slide() + + # 发送移动事件 + if velocity.length() > 0: + EventSystem.emit_event(EventNames.PLAYER_MOVED, { + "position": global_position, + "velocity": velocity + }) +``` + +## 🤖 NPC实现规范 + +### 基础要求 +- **节点类型**: 使用 `CharacterBody2D` 或 `StaticBody2D` +- **交互区域**: 必须包含名为 `InteractionArea` 的 `Area2D` +- **事件通信**: 通过 `EventSystem` 触发交互 + +### NPC节点结构 +``` +NPC (CharacterBody2D) +├── Sprite2D # NPC精灵 +├── CollisionShape2D # 物理碰撞 +├── InteractionArea (Area2D) # 交互检测区域 +│ └── CollisionShape2D # 交互碰撞形状 +├── DialogueComponent # 对话组件 +└── AnimationPlayer # 动画播放器 +``` + +### NPC实现模板 +```gdscript +extends CharacterBody2D +class_name NPC + +@export var npc_name: String = "NPC" +@export var dialogue_resource: DialogueResource + +@onready var interaction_area: Area2D = $InteractionArea +@onready var sprite: Sprite2D = $Sprite2D + +signal interaction_available(npc: NPC) +signal interaction_unavailable(npc: NPC) + +func _ready() -> void: + # 连接交互区域信号 + interaction_area.body_entered.connect(_on_interaction_area_entered) + interaction_area.body_exited.connect(_on_interaction_area_exited) + + # 监听交互事件 + EventSystem.connect_event(EventNames.INTERACT_PRESSED, _on_interact_pressed) + +func _on_interaction_area_entered(body: Node2D) -> void: + if body.is_in_group("player"): + interaction_available.emit(self) + # 显示交互提示 + _show_interaction_hint() + +func _on_interaction_area_exited(body: Node2D) -> void: + if body.is_in_group("player"): + interaction_unavailable.emit(self) + # 隐藏交互提示 + _hide_interaction_hint() + +func _on_interact_pressed(data: Dictionary = {}) -> void: + # 检查玩家是否在交互范围内 + if _is_player_in_range(): + start_dialogue() + +func start_dialogue() -> void: + EventSystem.emit_event(EventNames.NPC_TALKED, { + "npc": self, + "npc_name": npc_name, + "dialogue": dialogue_resource + }) + +func _is_player_in_range() -> bool: + var bodies = interaction_area.get_overlapping_bodies() + for body in bodies: + if body.is_in_group("player"): + return true + return false +``` + +## 🗺️ TileMap图层规范 + +### 图层配置要求 +TileMap必须按以下标准配置图层: + +#### 图层0:地面层 (Ground) +- **用途**: 地面纹理、道路、草地等 +- **碰撞**: 禁用物理层 +- **渲染**: 最底层渲染 +- **Y排序**: 禁用 + +```gdscript +# 设置地面层 +var ground_layer = tilemap.get_layer(0) +tilemap.set_layer_name(0, "Ground") +tilemap.set_layer_enabled(0, true) +tilemap.set_layer_y_sort_enabled(0, false) +# 不设置物理层 +``` + +#### 图层1:障碍层 (Obstacles) +- **用途**: 墙壁、树木、建筑等不可通过的障碍 +- **碰撞**: 启用物理层 +- **渲染**: 中间层 +- **Y排序**: 禁用 + +```gdscript +# 设置障碍层 +tilemap.set_layer_name(1, "Obstacles") +tilemap.set_layer_enabled(1, true) +tilemap.set_layer_y_sort_enabled(1, false) +# 设置物理层用于碰撞检测 +tilemap.set_layer_physics_enabled(1, true) +``` + +#### 图层2:装饰层 (Decoration) +- **用途**: 装饰物、前景元素 +- **碰撞**: 根据需要设置 +- **渲染**: 最上层 +- **Y排序**: 启用(重要!) + +```gdscript +# 设置装饰层 +tilemap.set_layer_name(2, "Decoration") +tilemap.set_layer_enabled(2, true) +tilemap.set_layer_y_sort_enabled(2, true) # 启用Y排序 +tilemap.set_layer_y_sort_origin(2, 16) # 设置排序原点 +``` + +### TileMap设置模板 +```gdscript +extends TileMap +class_name GameTileMap + +func _ready() -> void: + # 设置TileMap为tilemap组 + add_to_group("tilemap") + + # 配置图层 + _setup_layers() + + # 通知相机系统更新边界 + EventSystem.emit_event(EventNames.TILEMAP_READY, { + "tilemap": self, + "used_rect": get_used_rect() + }) + +func _setup_layers() -> void: + # 确保有足够的图层 + while get_layers_count() < 3: + add_layer(-1) + + # 配置地面层 (0) + set_layer_name(0, "Ground") + set_layer_y_sort_enabled(0, false) + + # 配置障碍层 (1) + set_layer_name(1, "Obstacles") + set_layer_y_sort_enabled(1, false) + set_layer_physics_enabled(1, true) + + # 配置装饰层 (2) + set_layer_name(2, "Decoration") + set_layer_y_sort_enabled(2, true) + set_layer_y_sort_origin(2, tile_set.tile_size.y / 2) +``` + +## 🎯 交互物实现规范 + +### 可收集物品 +```gdscript +extends Area2D +class_name CollectibleItem + +@export var item_name: String = "Item" +@export var item_value: int = 1 + +@onready var sprite: Sprite2D = $Sprite2D +@onready var collision: CollisionShape2D = $CollisionShape2D + +func _ready() -> void: + body_entered.connect(_on_body_entered) + +func _on_body_entered(body: Node2D) -> void: + if body.is_in_group("player"): + collect_item(body) + +func collect_item(collector: Node2D) -> void: + # 发送收集事件 + EventSystem.emit_event(EventNames.ITEM_COLLECTED, { + "item_name": item_name, + "item_value": item_value, + "collector": collector, + "position": global_position + }) + + # 播放收集动画 + _play_collect_animation() + +func _play_collect_animation() -> void: + var tween = create_tween() + tween.parallel().tween_property(self, "scale", Vector2.ZERO, 0.3) + tween.parallel().tween_property(self, "modulate:a", 0.0, 0.3) + await tween.finished + queue_free() +``` + +### 可交互对象 +```gdscript +extends StaticBody2D +class_name InteractableObject + +@export var interaction_text: String = "按E交互" +@export var can_interact: bool = true + +@onready var interaction_area: Area2D = $InteractionArea +@onready var sprite: Sprite2D = $Sprite2D + +var player_in_range: bool = false + +func _ready() -> void: + interaction_area.body_entered.connect(_on_interaction_area_entered) + interaction_area.body_exited.connect(_on_interaction_area_exited) + EventSystem.connect_event(EventNames.INTERACT_PRESSED, _on_interact_pressed) + +func _on_interaction_area_entered(body: Node2D) -> void: + if body.is_in_group("player") and can_interact: + player_in_range = true + _show_interaction_prompt() + +func _on_interaction_area_exited(body: Node2D) -> void: + if body.is_in_group("player"): + player_in_range = false + _hide_interaction_prompt() + +func _on_interact_pressed(data: Dictionary = {}) -> void: + if player_in_range and can_interact: + interact() + +func interact() -> void: + # 子类重写此方法实现具体交互逻辑 + print("与 ", name, " 交互") + + EventSystem.emit_event(EventNames.OBJECT_INTERACTED, { + "object": self, + "interaction_type": "default" + }) +``` + +## 🎨 资源过滤设置 + +### 纹理过滤规范 +所有像素艺术资源必须使用最近邻过滤: + +```gdscript +# 在导入设置中或代码中设置 +func _setup_pixel_perfect_texture(texture: Texture2D) -> void: + if texture is ImageTexture: + var image = texture.get_image() + image.generate_mipmaps(false) + # 在导入设置中设置Filter为Off +``` + +### 导入设置模板 +对于所有精灵资源,在导入设置中: +- **Filter**: Off (关闭) +- **Mipmaps**: Off (关闭) +- **Fix Alpha Border**: On (开启) + +## 🔧 性能优化要求 + +### 节点缓存 +```gdscript +# ✅ 正确:使用@onready缓存节点引用 +@onready var sprite: Sprite2D = $Sprite2D +@onready var collision: CollisionShape2D = $CollisionShape2D + +func _process(delta: float) -> void: + sprite.modulate.a = 0.5 # 使用缓存的引用 + +# ❌ 错误:在_process中重复获取节点 +func _process(delta: float) -> void: + $Sprite2D.modulate.a = 0.5 # 每帧都要查找节点 +``` + +### 事件频率控制 +```gdscript +# 控制事件发送频率 +var last_event_time: float = 0.0 +const EVENT_INTERVAL: float = 0.1 # 100ms间隔 + +func _process(delta: float) -> void: + var current_time = Time.get_time_dict_from_system() + if current_time - last_event_time >= EVENT_INTERVAL: + EventSystem.emit_event(EventNames.POSITION_UPDATE, { + "position": global_position + }) + last_event_time = current_time +``` + +--- + +**记住:遵循这些实现细节规范可以确保游戏对象的一致性和性能!** \ No newline at end of file diff --git a/docs/开发哲学与最佳实践.md b/docs/开发哲学与最佳实践.md new file mode 100644 index 0000000..76f50c9 --- /dev/null +++ b/docs/开发哲学与最佳实践.md @@ -0,0 +1,406 @@ +# 开发哲学与最佳实践 + +本文档阐述了WhaleTown项目的开发哲学和编程最佳实践,旨在指导团队创造高质量、可维护的代码。 + +## 🧘 开发哲学 + +### 核心理念 +- **用户体验至上** - 每个功能都要考虑用户感受 +- **代码即文档** - 代码应该自解释,清晰易懂 +- **简洁胜于复杂** - 优先选择简单直接的解决方案 +- **质量重于速度** - 宁可慢一点,也要做对 + +### 设计原则 + +#### 1. 流畅体验 (Juice or Death) +每个用户交互都必须有视觉反馈和动画效果: + +```gdscript +# ✅ 正确:为UI交互添加动画 +func show_dialog() -> void: + dialog.modulate.a = 0.0 + dialog.scale = Vector2(0.8, 0.8) + dialog.visible = true + + var tween = create_tween() + tween.parallel().tween_property(dialog, "modulate:a", 1.0, 0.3) + tween.parallel().tween_property(dialog, "scale", Vector2.ONE, 0.3) + tween.set_ease(Tween.EASE_OUT) + tween.set_trans(Tween.TRANS_BACK) + +# ❌ 错误:没有动画的生硬切换 +func show_dialog() -> void: + dialog.visible = true # 突然出现,体验差 +``` + +#### 2. 零魔法数字 (Zero Magic Numbers) +所有数值都应该有明确的含义和来源: + +```gdscript +# ✅ 正确:使用导出变量或配置文件 +@export var move_speed: float = 200.0 +@export var jump_height: float = 400.0 +@export var health_max: int = 100 + +# 或从配置文件加载 +const CONFIG_PATH = "res://data/player_config.json" +var config_data: Dictionary + +func _ready() -> void: + config_data = load_config(CONFIG_PATH) + move_speed = config_data.get("move_speed", 200.0) + +# ❌ 错误:硬编码的魔法数字 +func _physics_process(delta: float) -> void: + velocity.x = input_direction.x * 200 # 200是什么? + if position.y > 1000: # 1000代表什么? + respawn() +``` + +#### 3. 函数单一职责 +每个函数只做一件事,做好一件事: + +```gdscript +# ✅ 正确:职责分离 +func handle_player_input() -> void: + var input_direction = get_input_direction() + apply_movement(input_direction) + check_interaction_input() + +func get_input_direction() -> Vector2: + return Input.get_vector("move_left", "move_right", "move_up", "move_down") + +func apply_movement(direction: Vector2) -> void: + velocity = direction * move_speed + move_and_slide() + +func check_interaction_input() -> void: + if Input.is_action_just_pressed("interact"): + try_interact() + +# ❌ 错误:一个函数做太多事 +func handle_everything() -> void: + # 处理输入 + var direction = Input.get_vector("move_left", "move_right", "move_up", "move_down") + # 处理移动 + velocity = direction * move_speed + move_and_slide() + # 处理交互 + if Input.is_action_just_pressed("interact"): + # 检查交互对象 + var interactables = get_nearby_interactables() + # 执行交互 + for obj in interactables: + obj.interact() + # 更新UI + update_health_bar() + # 播放音效 + play_footstep_sound() +``` + +#### 4. 隐藏复杂性 +复杂的逻辑应该被封装,对外提供简洁的接口: + +```gdscript +# ✅ 正确:封装复杂逻辑 +class_name NetworkManager + +func login(username: String, password: String, callback: Callable) -> int: + return _make_request("POST", "/auth/login", { + "username": username, + "password": password + }, callback) + +func _make_request(method: String, endpoint: String, data: Dictionary, callback: Callable) -> int: + # 复杂的网络请求逻辑被隐藏 + var request = HTTPRequest.new() + var request_id = _generate_request_id() + + # 设置请求头、处理认证、错误重试等复杂逻辑 + _setup_request_headers(request) + _handle_authentication(request) + _setup_retry_logic(request, callback) + + return request_id + +# 使用时非常简洁 +func _on_login_button_pressed() -> void: + NetworkManager.login(username_input.text, password_input.text, _on_login_response) +``` + +## 📋 编码最佳实践 + +### 1. 类型安全 +始终使用严格的类型声明: + +```gdscript +# ✅ 正确:明确的类型声明 +var player_health: int = 100 +var move_speed: float = 200.0 +var player_name: String = "Player" +var inventory_items: Array[Item] = [] +var config_data: Dictionary = {} + +func calculate_damage(base_damage: int, multiplier: float) -> int: + return int(base_damage * multiplier) + +# ❌ 错误:缺少类型信息 +var health = 100 # 类型不明确 +var speed = 200 # 可能是int也可能是float + +func calculate_damage(base, mult): # 参数类型不明确 + return base * mult # 返回类型不明确 +``` + +### 2. 错误处理 +主动处理可能的错误情况: + +```gdscript +# ✅ 正确:完善的错误处理 +func load_save_file(file_path: String) -> Dictionary: + if not FileAccess.file_exists(file_path): + push_warning("存档文件不存在: " + file_path) + return {} + + var file = FileAccess.open(file_path, FileAccess.READ) + if file == null: + push_error("无法打开存档文件: " + file_path) + return {} + + var json_string = file.get_as_text() + file.close() + + if json_string.is_empty(): + push_warning("存档文件为空: " + file_path) + return {} + + var json = JSON.new() + var parse_result = json.parse(json_string) + if parse_result != OK: + push_error("存档文件JSON格式错误: " + file_path) + return {} + + return json.data + +# ❌ 错误:没有错误处理 +func load_save_file(file_path: String) -> Dictionary: + var file = FileAccess.open(file_path, FileAccess.READ) + var json_string = file.get_as_text() + file.close() + var json = JSON.new() + json.parse(json_string) + return json.data # 任何步骤出错都会崩溃 +``` + +### 3. 资源管理 +及时释放不需要的资源: + +```gdscript +# ✅ 正确:资源管理 +class_name AudioManager + +var audio_players: Array[AudioStreamPlayer] = [] +var max_concurrent_sounds: int = 10 + +func play_sound(sound: AudioStream, volume: float = 0.0) -> void: + # 清理已完成的音频播放器 + _cleanup_finished_players() + + # 限制并发音频数量 + if audio_players.size() >= max_concurrent_sounds: + _stop_oldest_player() + + var player = AudioStreamPlayer.new() + add_child(player) + player.stream = sound + player.volume_db = volume + player.finished.connect(_on_audio_finished.bind(player)) + player.play() + + audio_players.append(player) + +func _cleanup_finished_players() -> void: + audio_players = audio_players.filter(func(player): return player.playing) + +func _on_audio_finished(player: AudioStreamPlayer) -> void: + audio_players.erase(player) + player.queue_free() +``` + +### 4. 性能优化 +编写高效的代码: + +```gdscript +# ✅ 正确:性能优化的代码 +class_name EnemyManager + +var enemies: Array[Enemy] = [] +var update_timer: float = 0.0 +const UPDATE_INTERVAL: float = 0.1 # 每100ms更新一次 + +func _process(delta: float) -> void: + update_timer += delta + if update_timer >= UPDATE_INTERVAL: + _update_enemies(update_timer) + update_timer = 0.0 + +func _update_enemies(delta_time: float) -> void: + # 只更新屏幕附近的敌人 + var camera_pos = get_viewport().get_camera_2d().global_position + var screen_size = get_viewport().get_visible_rect().size + + for enemy in enemies: + if _is_enemy_near_screen(enemy, camera_pos, screen_size): + enemy.update_ai(delta_time) + +func _is_enemy_near_screen(enemy: Enemy, camera_pos: Vector2, screen_size: Vector2) -> bool: + var distance = enemy.global_position.distance_to(camera_pos) + var max_distance = screen_size.length() * 0.6 # 屏幕对角线的60% + return distance <= max_distance + +# ❌ 错误:性能问题 +func _process(delta: float) -> void: + # 每帧更新所有敌人,无论是否可见 + for enemy in enemies: + enemy.update_ai(delta) # 可能有数百个敌人 + # 每帧进行复杂计算 + var path = enemy.find_path_to_player() + enemy.follow_path(path) +``` + +## 🎯 代码审查标准 + +### 审查清单 +在提交代码前,请检查以下项目: + +#### 功能性 +- [ ] 代码实现了预期功能 +- [ ] 处理了边界情况和错误情况 +- [ ] 添加了必要的测试用例 + +#### 可读性 +- [ ] 变量和函数名称清晰明确 +- [ ] 代码结构逻辑清晰 +- [ ] 添加了必要的注释 + +#### 性能 +- [ ] 避免了不必要的计算 +- [ ] 正确管理了资源生命周期 +- [ ] 使用了合适的数据结构 + +#### 规范性 +- [ ] 遵循了项目命名规范 +- [ ] 使用了正确的类型声明 +- [ ] 符合架构设计原则 + +### 代码示例评分 + +#### 优秀代码示例 (A级) +```gdscript +extends CharacterBody2D +class_name Player + +## 玩家角色控制器 +## +## 负责处理玩家输入、移动和基础交互 +## 使用事件系统与其他组件通信 + +@export_group("Movement") +@export var move_speed: float = 200.0 +@export var acceleration: float = 1000.0 +@export var friction: float = 800.0 + +@export_group("Interaction") +@export var interaction_range: float = 50.0 + +@onready var sprite: Sprite2D = %Sprite2D +@onready var animation_player: AnimationPlayer = %AnimationPlayer +@onready var interaction_area: Area2D = %InteractionArea + +var _current_interactable: Interactable = null + +func _ready() -> void: + _setup_interaction_area() + _connect_signals() + +func _physics_process(delta: float) -> void: + _handle_movement(delta) + +func _input(event: InputEvent) -> void: + if event.is_action_pressed("interact"): + _try_interact() + +func _handle_movement(delta: float) -> void: + var input_direction := _get_movement_input() + _apply_movement(input_direction, delta) + _update_animation(input_direction) + +func _get_movement_input() -> Vector2: + return Input.get_vector("move_left", "move_right", "move_up", "move_down") + +func _apply_movement(direction: Vector2, delta: float) -> void: + if direction != Vector2.ZERO: + velocity = velocity.move_toward(direction * move_speed, acceleration * delta) + else: + velocity = velocity.move_toward(Vector2.ZERO, friction * delta) + + move_and_slide() + +func _update_animation(direction: Vector2) -> void: + if direction.length() > 0.1: + animation_player.play("walk") + sprite.flip_h = direction.x < 0 + else: + animation_player.play("idle") +``` + +#### 需要改进的代码 (C级) +```gdscript +extends CharacterBody2D + +var speed = 200 +var player +var enemies = [] + +func _ready(): + player = self + +func _process(delta): + var dir = Vector2() + if Input.is_action_pressed("ui_left"): + dir.x -= 1 + if Input.is_action_pressed("ui_right"): + dir.x += 1 + if Input.is_action_pressed("ui_up"): + dir.y -= 1 + if Input.is_action_pressed("ui_down"): + dir.y += 1 + + velocity = dir * speed + move_and_slide() + + for enemy in enemies: + if position.distance_to(enemy.position) < 100: + print("near enemy") +``` + +## 🚀 持续改进 + +### 重构指导原则 +1. **小步快跑** - 每次只重构一小部分 +2. **测试保护** - 重构前确保有测试覆盖 +3. **功能不变** - 重构不改变外部行为 +4. **逐步优化** - 持续改进代码质量 + +### 技术债务管理 +```gdscript +# 使用TODO注释标记技术债务 +# TODO: 重构这个函数,职责过多 +# FIXME: 这里有性能问题,需要优化 +# HACK: 临时解决方案,需要找到更好的方法 +# NOTE: 这里的逻辑比较复杂,需要详细注释 +``` + +--- + +**记住:优秀的代码不仅能工作,更要易于理解、维护和扩展。追求代码质量是每个开发者的责任!** \ No newline at end of file diff --git a/docs/架构与通信规范.md b/docs/架构与通信规范.md new file mode 100644 index 0000000..e54ebd4 --- /dev/null +++ b/docs/架构与通信规范.md @@ -0,0 +1,272 @@ +# 架构与通信规范 + +本文档定义了WhaleTown项目的架构设计原则和组件间通信规范。 + +## 🏛️ 架构设计原则 + +### 核心原则 +- **"信号向上,调用向下"** - 父节点调用子节点方法,子节点发出信号通知父节点 +- **高度解耦** - 通过事件系统实现组件间通信,避免直接依赖 +- **分层架构** - 严格的三层架构:框架层、游戏层、界面层 +- **单一职责** - 每个组件只负责一个明确的功能 + +### 分层架构详解 + +``` +┌─────────────────────────────────────┐ +│ UI Layer (界面层) │ +│ UI/Windows/, UI/HUD/ │ +├─────────────────────────────────────┤ +│ Scenes Layer (游戏层) │ +│ Scenes/Maps/, Scenes/Entities/ │ +├─────────────────────────────────────┤ +│ _Core Layer (框架层) │ +│ _Core/managers/, _Core/systems/ │ +└─────────────────────────────────────┘ +``` + +## 🔄 事件系统 (EventSystem) + +### 事件系统位置 +- **文件路径**: `_Core/systems/EventSystem.gd` +- **自动加载**: 必须设置为AutoLoad单例 +- **作用**: 全局事件总线,实现跨模块通信 + +### 事件命名规范 +所有事件名称必须在 `_Core/EventNames.gd` 中定义: + +```gdscript +# _Core/EventNames.gd +class_name EventNames + +# 玩家相关事件 +const PLAYER_MOVED = "player_moved" +const PLAYER_HEALTH_CHANGED = "player_health_changed" +const PLAYER_DIED = "player_died" + +# 交互事件 +const INTERACT_PRESSED = "interact_pressed" +const NPC_TALKED = "npc_talked" +const ITEM_COLLECTED = "item_collected" + +# UI事件 +const UI_BUTTON_CLICKED = "ui_button_clicked" +const DIALOG_OPENED = "dialog_opened" +const DIALOG_CLOSED = "dialog_closed" + +# 游戏状态事件 +const GAME_PAUSED = "game_paused" +const GAME_RESUMED = "game_resumed" +const SCENE_CHANGED = "scene_changed" +``` + +### 事件使用方法 + +#### 发送事件 +```gdscript +# 发送简单事件 +EventSystem.emit_event(EventNames.PLAYER_MOVED) + +# 发送带数据的事件 +EventSystem.emit_event(EventNames.PLAYER_HEALTH_CHANGED, { + "old_health": 80, + "new_health": 60, + "damage": 20 +}) +``` + +#### 监听事件 +```gdscript +func _ready() -> void: + # 连接事件监听 + EventSystem.connect_event(EventNames.PLAYER_DIED, _on_player_died) + EventSystem.connect_event(EventNames.ITEM_COLLECTED, _on_item_collected) + +func _on_player_died(data: Dictionary = {}) -> void: + print("玩家死亡,游戏结束") + # 处理玩家死亡逻辑 + +func _on_item_collected(data: Dictionary) -> void: + var item_name = data.get("item_name", "未知物品") + print("收集到物品: ", item_name) +``` + +#### 断开事件监听 +```gdscript +func _exit_tree() -> void: + # 节点销毁时断开事件监听 + EventSystem.disconnect_event(EventNames.PLAYER_DIED, _on_player_died) + EventSystem.disconnect_event(EventNames.ITEM_COLLECTED, _on_item_collected) +``` + +## 🎯 单例管理器 + +### 允许的自动加载单例 +项目中只允许以下三个单例: + +1. **GameManager** - 游戏状态管理 + - 路径: `_Core/managers/GameManager.gd` + - 职责: 游戏状态、场景数据、全局配置 + +2. **SceneManager** - 场景管理 + - 路径: `_Core/managers/SceneManager.gd` + - 职责: 场景切换、场景生命周期 + +3. **EventSystem** - 事件系统 + - 路径: `_Core/systems/EventSystem.gd` + - 职责: 全局事件通信 + +### 单例使用规范 +```gdscript +# ✅ 正确:高层组件可以访问单例 +func _ready() -> void: + var current_scene = SceneManager.get_current_scene() + var game_state = GameManager.get_game_state() + +# ❌ 错误:底层实体不应直接访问GameManager +# 在Player.gd或NPC.gd中避免这样做: +func _ready() -> void: + GameManager.register_player(self) # 不推荐 + +# ✅ 正确:使用事件系统 +func _ready() -> void: + EventSystem.emit_event(EventNames.PLAYER_SPAWNED, {"player": self}) +``` + +## 🔗 组件通信模式 + +### 1. 父子通信 +```gdscript +# 父节点调用子节点方法(向下调用) +func _on_button_pressed() -> void: + child_component.activate() + child_component.set_data(some_data) + +# 子节点发出信号通知父节点(向上信号) +# 在子节点中: +signal component_activated(data: Dictionary) +signal component_finished() + +func _some_action() -> void: + component_activated.emit({"status": "active"}) +``` + +### 2. 兄弟组件通信 +```gdscript +# 通过共同的父节点中转 +# 或使用事件系统 +func _notify_sibling() -> void: + EventSystem.emit_event(EventNames.COMPONENT_MESSAGE, { + "sender": self, + "message": "Hello sibling!" + }) +``` + +### 3. 跨场景通信 +```gdscript +# 使用事件系统进行跨场景通信 +func _change_scene_with_data() -> void: + EventSystem.emit_event(EventNames.SCENE_DATA_TRANSFER, { + "target_scene": "battle_scene", + "player_data": player_data + }) +``` + +## 🚫 禁止的通信模式 + +### 1. 直接节点引用 +```gdscript +# ❌ 错误:直接获取其他场景的节点 +func _bad_communication() -> void: + var other_scene = get_tree().get_first_node_in_group("other_scene") + other_scene.do_something() # 强耦合,难以维护 +``` + +### 2. 全局变量传递 +```gdscript +# ❌ 错误:使用全局变量传递状态 +# 在autoload中: +var global_player_data = {} # 避免这种做法 +``` + +### 3. 循环依赖 +```gdscript +# ❌ 错误:A依赖B,B又依赖A +# ComponentA.gd +var component_b: ComponentB + +# ComponentB.gd +var component_a: ComponentA # 循环依赖 +``` + +## 📋 通信最佳实践 + +### 1. 事件数据结构 +```gdscript +# 使用结构化的事件数据 +EventSystem.emit_event(EventNames.PLAYER_ATTACK, { + "attacker": self, + "target": target_enemy, + "damage": damage_amount, + "attack_type": "melee", + "timestamp": Time.get_time_dict_from_system() +}) +``` + +### 2. 错误处理 +```gdscript +func _on_event_received(data: Dictionary) -> void: + # 验证数据完整性 + if not data.has("required_field"): + push_error("事件数据缺少必需字段: required_field") + return + + # 安全地获取数据 + var value = data.get("optional_field", default_value) +``` + +### 3. 性能考虑 +```gdscript +# 避免在_process中频繁发送事件 +var last_position: Vector2 +func _process(delta: float) -> void: + if global_position.distance_to(last_position) > 10.0: + EventSystem.emit_event(EventNames.PLAYER_MOVED, { + "position": global_position + }) + last_position = global_position +``` + +## 🧪 测试通信系统 + +### 单元测试示例 +```gdscript +extends GutTest + +func test_event_emission(): + # 监听事件 + watch_signals(EventSystem) + + # 发送事件 + EventSystem.emit_event(EventNames.PLAYER_MOVED, {"x": 100, "y": 200}) + + # 验证事件发送 + assert_signal_emitted(EventSystem, "event_raised") + +func test_event_data(): + var received_data: Dictionary + + # 连接事件监听 + EventSystem.connect_event(EventNames.TEST_EVENT, func(data): received_data = data) + + # 发送测试数据 + var test_data = {"test": "value"} + EventSystem.emit_event(EventNames.TEST_EVENT, test_data) + + # 验证数据传递 + assert_eq(received_data, test_data) +``` + +--- + +**记住:良好的架构设计是项目成功的基石!遵循这些通信规范可以确保代码的可维护性和扩展性。** \ No newline at end of file diff --git a/docs/输入映射配置.md b/docs/输入映射配置.md new file mode 100644 index 0000000..3a72809 --- /dev/null +++ b/docs/输入映射配置.md @@ -0,0 +1,157 @@ +# 输入映射配置指南 + +本文档说明了WhaleTown项目的输入映射配置要求和设置方法。 + +## 🎮 必需的输入映射 + +### 基础移动控制 +- **`move_left`** - 向左移动 + - 推荐按键:A键、左方向键 +- **`move_right`** - 向右移动 + - 推荐按键:D键、右方向键 +- **`move_up`** - 向上移动 + - 推荐按键:W键、上方向键 +- **`move_down`** - 向下移动 + - 推荐按键:S键、下方向键 + +### 交互控制 +- **`interact`** - 交互动作 + - 推荐按键:E键、空格键 +- **`pause`** - 暂停游戏 + - 推荐按键:ESC键 + +## ⚙️ Godot编辑器配置步骤 + +### 1. 打开输入映射设置 +1. 在Godot编辑器中打开 `Project` → `Project Settings` +2. 切换到 `Input Map` 标签 + +### 2. 添加输入动作 +对于每个必需的输入动作: + +1. 在 `Action` 输入框中输入动作名称(如 `move_left`) +2. 点击 `Add` 按钮 +3. 点击新添加动作右侧的 `+` 按钮 +4. 按下对应的按键进行绑定 +5. 重复步骤3-4添加备用按键 + +### 3. 配置示例 + +``` +move_left: + - Key: A + - Key: Left Arrow + +move_right: + - Key: D + - Key: Right Arrow + +move_up: + - Key: W + - Key: Up Arrow + +move_down: + - Key: S + - Key: Down Arrow + +interact: + - Key: E + - Key: Space + +pause: + - Key: Escape +``` + +## 🔧 代码中的使用方法 + +### 移动输入检测 +```gdscript +func _physics_process(delta: float) -> void: + # 获取移动向量 + var direction := Input.get_vector( + "move_left", "move_right", + "move_up", "move_down" + ) + + # 应用移动 + velocity = direction * move_speed + move_and_slide() +``` + +### 交互输入检测 +```gdscript +func _input(event: InputEvent) -> void: + if event.is_action_pressed("interact"): + _handle_interaction() + + if event.is_action_pressed("pause"): + _toggle_pause() +``` + +### 连续输入检测 +```gdscript +func _process(delta: float) -> void: + # 检测持续按下的按键 + if Input.is_action_pressed("interact"): + _continuous_interaction(delta) +``` + +## 📱 手柄支持(可选) + +### 推荐手柄映射 +- **左摇杆** - 移动控制 +- **A按钮/X按钮** - 交互 +- **Start按钮** - 暂停 + +### 配置方法 +1. 在Input Map中为每个动作添加手柄输入 +2. 使用 `Joypad Button` 或 `Joypad Axis` 进行绑定 + +## ✅ 验证配置 + +### 测试脚本 +创建一个简单的测试脚本验证输入配置: + +```gdscript +extends Node + +func _ready() -> void: + print("输入映射测试开始...") + _test_input_actions() + +func _test_input_actions() -> void: + var required_actions = [ + "move_left", "move_right", "move_up", "move_down", + "interact", "pause" + ] + + for action in required_actions: + if InputMap.has_action(action): + print("✅ ", action, " - 已配置") + else: + print("❌ ", action, " - 未配置") + +func _input(event: InputEvent) -> void: + # 实时显示输入事件 + for action in InputMap.get_actions(): + if event.is_action_pressed(action): + print("按下: ", action) +``` + +## 🚨 常见问题 + +### Q: 输入没有响应怎么办? +A: 检查以下几点: +1. 确认输入动作名称拼写正确 +2. 验证按键是否正确绑定 +3. 检查代码中是否正确使用了动作名称 + +### Q: 如何添加自定义输入? +A: 按照相同步骤在Input Map中添加新的动作,并在代码中使用对应的动作名称。 + +### Q: 手柄不工作怎么办? +A: 确保手柄已连接,并在Input Map中正确配置了手柄按钮映射。 + +--- + +**注意:输入映射配置是游戏正常运行的基础,请确保所有必需的输入动作都已正确配置!** \ No newline at end of file