forked from datawhale/whale-town-front
新的目录结构: 01-项目入门/ # 新人必读,项目基础 02-开发规范/ # 编码标准和规范 03-技术实现/ # 具体开发指导 04-高级开发/ # 进阶开发技巧 05-部署运维/ # 发布和部署 06-功能模块/ # 特定功能文档 新增导航文档: - docs/README.md - 完整的文档导航和使用指南 - 各目录下的README.md - 分类说明和使用指导 优化效果: - 开发者可以按阶段快速定位需要的文档 - 新人有清晰的学习路径 - 不同角色有针对性的文档推荐 - 提供了问题导向的快速查找功能
388 lines
11 KiB
Markdown
388 lines
11 KiB
Markdown
# 实现细节规范
|
||
|
||
本文档详细说明了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
|
||
```
|
||
|
||
---
|
||
|
||
**记住:遵循这些实现细节规范可以确保游戏对象的一致性和性能!** |