docs: 补充开发规范相关文档
新增文档: - docs/输入映射配置.md - 游戏输入配置指南 - docs/架构与通信规范.md - 项目架构和组件通信规范 - docs/实现细节规范.md - 游戏对象具体实现要求 - docs/开发哲学与最佳实践.md - 开发理念和编程最佳实践 覆盖内容: - 输入映射的配置方法和验证 - EventSystem事件系统使用规范 - 玩家、NPC、TileMap的实现标准 - 代码质量标准和审查清单 - 性能优化和资源管理指导 这些文档补充了开发规范.md中提到但在docs目录中缺失的内容
This commit is contained in:
388
docs/实现细节规范.md
Normal file
388
docs/实现细节规范.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**记住:遵循这些实现细节规范可以确保游戏对象的一致性和性能!**
|
||||||
406
docs/开发哲学与最佳实践.md
Normal file
406
docs/开发哲学与最佳实践.md
Normal file
@@ -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: 这里的逻辑比较复杂,需要详细注释
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**记住:优秀的代码不仅能工作,更要易于理解、维护和扩展。追求代码质量是每个开发者的责任!**
|
||||||
272
docs/架构与通信规范.md
Normal file
272
docs/架构与通信规范.md
Normal file
@@ -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)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**记住:良好的架构设计是项目成功的基石!遵循这些通信规范可以确保代码的可维护性和扩展性。**
|
||||||
157
docs/输入映射配置.md
Normal file
157
docs/输入映射配置.md
Normal file
@@ -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中正确配置了手柄按钮映射。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**注意:输入映射配置是游戏正常运行的基础,请确保所有必需的输入动作都已正确配置!**
|
||||||
Reference in New Issue
Block a user