Files
whale-town/DEVELOPER_GUIDE.md
2025-12-05 19:00:14 +08:00

1758 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI Town Game 开发者技术文档
## 📋 目录
1. [项目概述](#项目概述)
2. [技术架构](#技术架构)
3. [开发环境配置](#开发环境配置)
4. [核心系统详解](#核心系统详解)
5. [API 参考](#api-参考)
6. [数据库设计](#数据库设计)
7. [网络协议](#网络协议)
8. [测试框架](#测试框架)
9. [部署指南](#部署指南)
10. [扩展开发](#扩展开发)
11. [性能优化](#性能优化)
12. [故障排除](#故障排除)
## 项目概述
### 技术栈
**客户端**:
- **游戏引擎**: Godot 4.5.1
- **编程语言**: GDScript
- **导出平台**: HTML5 (Web), Windows, macOS, Linux
- **UI 框架**: Godot 内置 UI 系统
**服务器**:
- **运行时**: Node.js 24.7.0+
- **编程语言**: TypeScript
- **网络协议**: WebSocket (ws 库)
- **数据存储**: JSON 文件系统
- **包管理**: Yarn 1.22.22+
**开发工具**:
- **版本控制**: Git
- **代码规范**: ESLint (TypeScript), GDScript 内置检查
- **测试框架**: 自定义 GDScript 测试框架
- **构建工具**: TypeScript Compiler, Godot Export
### 项目结构
```
ai_community/
├── project.godot # Godot 项目配置
├── scenes/ # 游戏场景文件
│ ├── Main.tscn # 主场景
│ ├── DatawhaleOffice.tscn # Datawhale 办公室场景
│ ├── PlayerCharacter.tscn # 玩家角色场景
│ ├── RemoteCharacter.tscn # 远程角色场景
│ └── TestGameplay.tscn # 测试场景
├── scripts/ # GDScript 脚本
│ ├── Main.gd # 主脚本
│ ├── NetworkManager.gd # 网络管理器
│ ├── GameStateManager.gd # 游戏状态管理器
│ ├── CharacterController.gd # 角色控制器
│ ├── DialogueSystem.gd # 对话系统
│ ├── InputHandler.gd # 输入处理器
│ ├── WorldManager.gd # 世界管理器
│ └── Utils.gd # 工具函数
├── assets/ # 游戏资源
│ ├── sprites/ # 精灵图像
│ ├── tilesets/ # 瓦片集
│ └── ui/ # UI 资源
├── tests/ # 测试文件
│ ├── RunAllTests.tscn # 测试运行器
│ ├── test_*.gd # 单元测试
│ └── test_property_*.gd # 属性测试
├── server/ # WebSocket 服务器
│ ├── src/ # TypeScript 源码
│ │ ├── server.ts # 主服务器文件
│ │ ├── api/ # API 模块
│ │ ├── backup/ # 备份管理
│ │ ├── logging/ # 日志管理
│ │ ├── maintenance/ # 维护管理
│ │ └── monitoring/ # 监控模块
│ ├── data/ # 数据存储
│ │ ├── characters.json # 角色数据
│ │ ├── logs/ # 日志文件
│ │ └── backups/ # 备份文件
│ ├── dist/ # 编译输出
│ ├── admin/ # Web 管理界面
│ ├── package.json # 依赖配置
│ └── tsconfig.json # TypeScript 配置
└── .kiro/specs/ # 项目规范文档
└── godot-ai-town-game/
├── requirements.md # 需求文档
├── design.md # 设计文档
└── tasks.md # 任务列表
```
## 技术架构
### 整体架构图
```mermaid
graph TB
subgraph "客户端 (Godot)"
A[Main Scene] --> B[NetworkManager]
A --> C[GameStateManager]
A --> D[UILayer]
A --> E[GameWorld]
B --> F[WebSocket Client]
C --> G[State Machine]
D --> H[UI Components]
E --> I[Characters]
E --> J[TileMap]
end
subgraph "网络层"
F <--> K[WebSocket Connection]
end
subgraph "服务器 (Node.js)"
K <--> L[WebSocket Server]
L --> M[Connection Manager]
L --> N[Message Router]
L --> O[World State]
L --> P[Data Persistence]
M --> Q[Authentication]
N --> R[Message Handlers]
O --> S[Character Manager]
P --> T[JSON Storage]
end
subgraph "管理系统"
L --> U[Health Monitor]
L --> V[Backup Manager]
L --> W[Log Manager]
U --> X[Admin API]
V --> Y[Auto Backup]
W --> Z[Log Analysis]
end
```
### 客户端架构
#### 场景树结构
```
Main (Node)
├── NetworkManager (Node)
├── GameStateManager (Node)
├── InputHandler (Node)
├── UILayer (CanvasLayer)
│ ├── LoginScreen (Control)
│ ├── CharacterCreation (Control)
│ ├── HUD (Control)
│ ├── DialogueBox (Control)
│ ├── ErrorNotification (Control)
│ └── LoadingIndicator (Control)
└── GameWorld (Node2D)
├── DatawhaleOffice (TileMap)
├── Characters (Node2D)
│ ├── PlayerCharacter (CharacterBody2D)
│ └── RemoteCharacter (CharacterBody2D) [多个]
└── Camera2D
```
#### 核心组件职责
**NetworkManager**:
- 管理 WebSocket 连接
- 处理消息序列化/反序列化
- 实现断线重连机制
- 维护心跳检测
**GameStateManager**:
- 管理游戏状态机
- 处理数据持久化
- 协调状态转换
- 发射状态变化信号
**InputHandler**:
- 处理键盘/触摸输入
- 设备类型检测
- 虚拟控件管理
- 输入事件分发
**WorldManager**:
- 管理游戏世界中的所有角色
- 处理角色生成/销毁
- 维护角色状态同步
- 提供空间查询功能
### 服务器架构
#### 模块设计
**ConnectionManager**:
```typescript
class ConnectionManager {
private clients: Map<string, WebSocket>
private heartbeats: Map<string, number>
addClient(clientId: string, ws: WebSocket): void
removeClient(clientId: string): void
broadcastMessage(message: any, excludeClient?: string): void
sendToClient(clientId: string, message: any): void
checkHeartbeats(): void
}
```
**MessageRouter**:
```typescript
class MessageRouter {
private handlers: Map<string, MessageHandler>
registerHandler(type: string, handler: MessageHandler): void
routeMessage(clientId: string, message: any): void
createResponse(type: string, data: any): any
}
```
**WorldState**:
```typescript
class WorldState {
private characters: Map<string, Character>
private scenes: Map<string, Scene>
addCharacter(character: Character): void
updateCharacter(characterId: string, updates: Partial<Character>): void
removeCharacter(characterId: string): void
getWorldSnapshot(): WorldSnapshot
}
```
## 开发环境配置
### 环境要求
**开发工具**:
- Godot 4.5.1+ (游戏引擎)
- Node.js 24.7.0+ (服务器运行时)
- Yarn 1.22.22+ (包管理器)
- Git (版本控制)
- VS Code (推荐编辑器)
**系统要求**:
- Windows 10+ / macOS 10.14+ / Ubuntu 18.04+
- 8GB RAM (推荐)
- 2GB 可用磁盘空间
### 快速配置
1. **克隆项目**:
```bash
git clone <repository-url>
cd ai_community
```
2. **配置 Godot**:
```bash
# 下载并安装 Godot 4.5.1
# 导入项目: 选择 project.godot 文件
```
3. **配置服务器**:
```bash
cd server
yarn install
yarn build
```
4. **启动开发环境**:
```bash
# 终端 1: 启动服务器
cd server
yarn dev
# 终端 2: 启动 Godot 编辑器
# 在 Godot 中按 F5 运行项目
```
### VS Code 配置
推荐的 VS Code 扩展:
- **godot-tools**: GDScript 语法支持
- **TypeScript Importer**: TypeScript 开发支持
- **GitLens**: Git 增强功能
- **Prettier**: 代码格式化
`.vscode/settings.json`:
```json
{
"godot_tools.editor_path": "/path/to/godot",
"typescript.preferences.importModuleSpecifier": "relative",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
}
```
## 核心系统详解
### 网络系统
#### WebSocket 连接管理
**客户端连接流程**:
```gdscript
# NetworkManager.gd
func connect_to_server(url: String) -> void:
_websocket = WebSocketPeer.new()
var error = _websocket.connect_to_url(url)
if error != OK:
emit_signal("connection_error", "Failed to connect: " + str(error))
return
_connection_state = ConnectionState.CONNECTING
_connection_timer = 0.0
emit_signal("connection_attempt_started")
func _process(delta: float) -> void:
if _websocket:
_websocket.poll()
var state = _websocket.get_ready_state()
match state:
WebSocketPeer.STATE_OPEN:
_handle_connected()
WebSocketPeer.STATE_CLOSED:
_handle_disconnected()
_handle_incoming_messages()
_update_heartbeat(delta)
```
**服务器连接处理**:
```typescript
// server.ts
wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
const clientId = generateClientId();
const clientInfo = {
id: clientId,
ws: ws,
lastHeartbeat: Date.now(),
authenticated: false,
characterId: null
};
clients.set(clientId, clientInfo);
console.log(`✅ Client connected: ${clientId}`);
ws.on('message', (data: Buffer) => {
handleMessage(clientId, data);
});
ws.on('close', () => {
handleDisconnection(clientId);
});
});
```
#### 消息协议实现
**消息序列化**:
```gdscript
# MessageProtocol.gd
static func create_message(type: String, data: Dictionary = {}) -> Dictionary:
return {
"type": type,
"data": data,
"timestamp": Time.get_unix_time_from_system()
}
static func serialize_message(message: Dictionary) -> String:
return JSON.stringify(message)
static func deserialize_message(json_string: String) -> Dictionary:
var json = JSON.new()
var parse_result = json.parse(json_string)
if parse_result != OK:
ErrorHandler.log_network_error("Failed to parse message", {"json": json_string})
return {}
return json.data
```
**消息路由**:
```typescript
// MessageRouter.ts
class MessageRouter {
private handlers = new Map<string, MessageHandler>();
constructor() {
this.registerHandler('auth_request', new AuthHandler());
this.registerHandler('character_create', new CharacterCreateHandler());
this.registerHandler('character_move', new CharacterMoveHandler());
this.registerHandler('dialogue_send', new DialogueHandler());
this.registerHandler('ping', new PingHandler());
}
routeMessage(clientId: string, message: any): void {
const handler = this.handlers.get(message.type);
if (handler) {
handler.handle(clientId, message.data);
} else {
console.warn(`Unknown message type: ${message.type}`);
}
}
}
```
### 状态管理系统
#### 游戏状态机
```gdscript
# GameStateManager.gd
enum GameState {
LOGIN,
CHARACTER_CREATION,
IN_GAME,
DISCONNECTED
}
var current_state: GameState = GameState.LOGIN
var player_data: Dictionary = {}
func change_state(new_state: GameState) -> void:
var old_state = current_state
current_state = new_state
print("State changed: ", GameState.keys()[old_state], " -> ", GameState.keys()[new_state])
match new_state:
GameState.LOGIN:
_show_login_screen()
GameState.CHARACTER_CREATION:
_show_character_creation()
GameState.IN_GAME:
_enter_game_world()
GameState.DISCONNECTED:
_show_disconnected_screen()
emit_signal("state_changed", old_state, new_state)
```
#### 数据持久化
**客户端数据保存**:
```gdscript
# GameStateManager.gd
func save_player_data() -> void:
var save_data = {
"player_id": player_data.get("id", ""),
"character_name": player_data.get("character_name", ""),
"last_position": player_data.get("position", {"x": 1000, "y": 750}),
"settings": player_data.get("settings", {}),
"timestamp": Time.get_unix_time_from_system()
}
var file = FileAccess.open("user://player_data.json", FileAccess.WRITE)
if file:
file.store_string(JSON.stringify(save_data))
file.close()
print("Player data saved successfully")
else:
ErrorHandler.log_game_error("Failed to save player data")
```
**服务器数据持久化**:
```typescript
// DataPersistence.ts
class DataPersistence {
private dataPath = './data/characters.json';
private backupInterval = 5 * 60 * 1000; // 5 minutes
async saveCharacters(characters: Character[]): Promise<void> {
try {
const data = JSON.stringify(characters, null, 2);
await fs.writeFile(this.dataPath, data, 'utf8');
console.log('💾 Characters data saved');
} catch (error) {
console.error('Failed to save characters:', error);
}
}
async loadCharacters(): Promise<Character[]> {
try {
const data = await fs.readFile(this.dataPath, 'utf8');
return JSON.parse(data);
} catch (error) {
console.log('📂 No existing character data found, starting fresh');
return [];
}
}
startAutoSave(): void {
setInterval(() => {
this.saveCharacters(Array.from(worldState.characters.values()));
}, this.backupInterval);
}
}
```
### 角色系统
#### 角色控制器
```gdscript
# CharacterController.gd
extends CharacterBody2D
class_name CharacterController
@export var character_id: String = ""
@export var character_name: String = ""
@export var is_online: bool = false
@export var move_speed: float = 200.0
var target_position: Vector2
var is_moving: bool = false
signal position_updated(new_position: Vector2)
signal animation_changed(animation_name: String)
func _ready():
target_position = global_position
_setup_animation()
_setup_collision()
func move_to(direction: Vector2) -> void:
if direction.length() > 0:
velocity = direction.normalized() * move_speed
is_moving = true
_play_animation("walk")
else:
velocity = Vector2.ZERO
is_moving = false
_play_animation("idle")
move_and_slide()
if global_position != target_position:
target_position = global_position
emit_signal("position_updated", global_position)
func set_position_smooth(new_position: Vector2, duration: float = 0.2) -> void:
var tween = create_tween()
tween.tween_property(self, "global_position", new_position, duration)
tween.tween_callback(func(): target_position = new_position)
```
#### 角色状态同步
**客户端同步**:
```gdscript
# WorldManager.gd
func update_character_state(character_id: String, state_data: Dictionary) -> void:
if not characters.has(character_id):
ErrorHandler.log_game_error("Character not found for update", {"id": character_id})
return
var character = characters[character_id]
# 更新位置
if state_data.has("position"):
var pos = state_data["position"]
character.set_position_smooth(Vector2(pos["x"], pos["y"]))
# 更新在线状态
if state_data.has("isOnline"):
character.set_online_status(state_data["isOnline"])
# 更新名称
if state_data.has("name"):
character.character_name = state_data["name"]
character.update_name_label()
```
**服务器状态广播**:
```typescript
// CharacterManager.ts
updateCharacterPosition(characterId: string, position: Position): void {
const character = this.characters.get(characterId);
if (!character) return;
character.position = position;
character.lastSeen = Date.now();
// 广播位置更新给所有客户端
const message = {
type: 'character_move',
data: {
characterId: characterId,
position: position
},
timestamp: Date.now()
};
connectionManager.broadcastMessage(message);
// 触发自动保存
this.scheduleAutoSave();
}
```
### 对话系统
#### 对话管理
```gdscript
# DialogueSystem.gd
extends Node
class_name DialogueSystem
var active_dialogues: Dictionary = {}
var dialogue_history: Array = []
signal dialogue_started(character_id: String)
signal dialogue_ended()
signal message_received(sender: String, message: String)
func start_dialogue(target_character_id: String) -> void:
if active_dialogues.has(target_character_id):
print("Dialogue already active with character: ", target_character_id)
return
var dialogue_data = {
"target_id": target_character_id,
"start_time": Time.get_unix_time_from_system(),
"messages": []
}
active_dialogues[target_character_id] = dialogue_data
emit_signal("dialogue_started", target_character_id)
# 显示对话界面
var dialogue_box = get_node("../UILayer/DialogueBox")
dialogue_box.show_dialogue(target_character_id)
func send_message(target_character_id: String, message: String) -> void:
if not active_dialogues.has(target_character_id):
ErrorHandler.log_game_error("No active dialogue with character", {"id": target_character_id})
return
# 验证消息内容
if message.strip_edges().is_empty():
return
if message.length() > 500:
message = message.substr(0, 500)
# 添加到对话历史
var message_data = {
"sender": "player",
"content": message,
"timestamp": Time.get_unix_time_from_system()
}
active_dialogues[target_character_id]["messages"].append(message_data)
dialogue_history.append(message_data)
# 发送到服务器
var network_message = MessageProtocol.create_message("dialogue_send", {
"receiverId": target_character_id,
"message": message
})
NetworkManager.send_message(network_message)
emit_signal("message_received", "player", message)
```
#### 对话气泡系统
```gdscript
# ChatBubble.gd
extends Control
class_name ChatBubble
@onready var label: Label = $Background/Label
@onready var background: NinePatchRect = $Background
@onready var timer: Timer = $Timer
var character_node: Node2D
var offset: Vector2 = Vector2(0, -60)
func show_bubble(character: Node2D, message: String, duration: float = 3.0) -> void:
character_node = character
label.text = message
# 调整气泡大小
var text_size = label.get_theme_font("font").get_string_size(
message,
HORIZONTAL_ALIGNMENT_LEFT,
-1,
label.get_theme_font_size("font_size")
)
var bubble_size = text_size + Vector2(20, 16)
background.size = bubble_size
size = bubble_size
# 设置位置
_update_position()
# 显示气泡
modulate.a = 0.0
visible = true
var tween = create_tween()
tween.tween_property(self, "modulate:a", 1.0, 0.2)
# 设置自动隐藏
timer.wait_time = duration
timer.start()
func _update_position() -> void:
if character_node:
global_position = character_node.global_position + offset
```
## API 参考
### 客户端 API
#### NetworkManager
```gdscript
class_name NetworkManager extends Node
# 信号
signal connected_to_server()
signal disconnected_from_server()
signal connection_error(error: String)
signal message_received(message: Dictionary)
# 方法
func connect_to_server(url: String) -> void
func disconnect_from_server() -> void
func send_message(message: Dictionary) -> void
func is_connected() -> bool
func get_connection_state() -> ConnectionState
```
#### GameStateManager
```gdscript
class_name GameStateManager extends Node
# 枚举
enum GameState { LOGIN, CHARACTER_CREATION, IN_GAME, DISCONNECTED }
# 信号
signal state_changed(old_state: GameState, new_state: GameState)
signal data_saved()
signal data_loaded(data: Dictionary)
# 属性
var current_state: GameState
var player_data: Dictionary
# 方法
func change_state(new_state: GameState) -> void
func save_player_data() -> void
func load_player_data() -> Dictionary
func get_current_state() -> GameState
```
#### CharacterController
```gdscript
class_name CharacterController extends CharacterBody2D
# 信号
signal position_updated(new_position: Vector2)
signal animation_changed(animation_name: String)
signal online_status_changed(is_online: bool)
# 属性
@export var character_id: String
@export var character_name: String
@export var is_online: bool
@export var move_speed: float
# 方法
func move_to(direction: Vector2) -> void
func set_position_smooth(target_pos: Vector2, duration: float = 0.2) -> void
func set_online_status(online: bool) -> void
func play_animation(anim_name: String) -> void
```
### 服务器 API
#### WebSocket 消息 API
**身份验证**:
```typescript
// 请求
{
"type": "auth_request",
"data": {
"username": string
},
"timestamp": number
}
// 响应
{
"type": "auth_response",
"data": {
"success": boolean,
"clientId": string,
"message"?: string
},
"timestamp": number
}
```
**角色创建**:
```typescript
// 请求
{
"type": "character_create",
"data": {
"name": string
},
"timestamp": number
}
// 响应
{
"type": "character_create",
"data": {
"success": boolean,
"character"?: {
"id": string,
"name": string,
"position": { "x": number, "y": number },
"isOnline": boolean
},
"message"?: string
},
"timestamp": number
}
```
**角色移动**:
```typescript
// 客户端 -> 服务器
{
"type": "character_move",
"data": {
"position": { "x": number, "y": number }
},
"timestamp": number
}
// 服务器 -> 所有客户端
{
"type": "character_move",
"data": {
"characterId": string,
"position": { "x": number, "y": number }
},
"timestamp": number
}
```
#### REST API (管理接口)
**系统状态**:
```http
GET /api/status
Authorization: Bearer <admin_token>
Response:
{
"status": "healthy",
"uptime": 3600,
"connections": 5,
"characters": 12,
"memory": {
"used": 45.2,
"total": 512
}
}
```
**备份管理**:
```http
POST /api/backup
Authorization: Bearer <admin_token>
Response:
{
"success": true,
"backupId": "backup_1234567890",
"timestamp": 1234567890
}
```
## 数据库设计
### 数据模型
#### Character 模型
```typescript
interface Character {
id: string; // UUID
name: string; // 角色名称 (2-20 字符)
ownerId: string; // 所属玩家 ID
position: { // 角色位置
x: number;
y: number;
};
isOnline: boolean; // 在线状态
appearance?: { // 外观设置 (可选)
sprite: string;
color: string;
};
createdAt: number; // 创建时间戳
lastSeen: number; // 最后在线时间戳
}
```
#### WorldState 模型
```typescript
interface WorldState {
sceneId: string; // 场景 ID
characters: Character[]; // 所有角色
timestamp: number; // 状态时间戳
}
```
#### Message 模型
```typescript
interface DialogueMessage {
senderId: string; // 发送者角色 ID
receiverId?: string; // 接收者角色 ID (可选,为空表示广播)
message: string; // 消息内容
timestamp: number; // 发送时间戳
}
```
### 数据存储
**文件结构**:
```
server/data/
├── characters.json # 角色数据
├── maintenance_tasks.json # 维护任务
├── logs/ # 日志文件
│ └── server_YYYY-MM-DD.log
└── backups/ # 备份文件
└── backup_<timestamp>/
├── backup_info.json
├── characters.json.gz
└── logs/
```
**数据验证**:
```typescript
// 角色数据验证
function validateCharacter(character: any): boolean {
return (
typeof character.id === 'string' &&
typeof character.name === 'string' &&
character.name.length >= 2 &&
character.name.length <= 20 &&
typeof character.ownerId === 'string' &&
typeof character.position === 'object' &&
typeof character.position.x === 'number' &&
typeof character.position.y === 'number' &&
typeof character.isOnline === 'boolean'
);
}
```
## 网络协议
### 连接生命周期
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
C->>S: WebSocket Connection
S->>C: Connection Established
C->>S: auth_request
S->>C: auth_response (success)
C->>S: character_create
S->>C: character_create (success)
S->>C: world_state (initial)
loop Game Loop
C->>S: character_move
S->>C: character_move (broadcast)
C->>S: ping
S->>C: pong
end
C->>S: Connection Close
S->>S: Update character offline
```
### 错误处理
**网络错误**:
```typescript
{
"type": "error",
"data": {
"code": "E001",
"message": "Connection timeout",
"details": "Server did not respond within 10 seconds"
},
"timestamp": number
}
```
**业务逻辑错误**:
```typescript
{
"type": "character_create",
"data": {
"success": false,
"message": "Character name already exists",
"code": "G001"
},
"timestamp": number
}
```
### 心跳机制
**客户端心跳**:
```gdscript
# NetworkManager.gd
func _update_heartbeat(delta: float) -> void:
_heartbeat_timer += delta
if _heartbeat_timer >= HEARTBEAT_INTERVAL:
_send_ping()
_heartbeat_timer = 0.0
func _send_ping() -> void:
var ping_message = MessageProtocol.create_message("ping")
send_message(ping_message)
```
**服务器心跳检查**:
```typescript
// 每 30 秒检查一次心跳
setInterval(() => {
const now = Date.now();
for (const [clientId, client] of clients) {
if (now - client.lastHeartbeat > HEARTBEAT_TIMEOUT) {
console.log(`⏰ Client ${clientId} heartbeat timeout`);
handleDisconnection(clientId);
}
}
}, 30000);
```
## 测试框架
### 单元测试
**测试结构**:
```gdscript
# test_example.gd
extends Node
var test_results: Array = []
func _ready():
run_all_tests()
print_results()
func run_all_tests():
test_basic_functionality()
test_edge_cases()
test_error_handling()
func test_basic_functionality():
var result = TestResult.new("Basic Functionality")
# 测试逻辑
var expected = "expected_value"
var actual = function_under_test()
if actual == expected:
result.pass("Function returns expected value")
else:
result.fail("Expected %s, got %s" % [expected, actual])
test_results.append(result)
```
### 属性测试
**属性测试框架**:
```gdscript
# PropertyTest.gd
class_name PropertyTest
const DEFAULT_ITERATIONS = 100
static func run_property_test(
property_name: String,
test_function: Callable,
iterations: int = DEFAULT_ITERATIONS
) -> PropertyTestResult:
var result = PropertyTestResult.new(property_name)
for i in range(iterations):
var test_data = generate_test_data()
var success = test_function.call(test_data)
if success:
result.add_success()
else:
result.add_failure(i, test_data)
return result
static func generate_test_data() -> Dictionary:
# 生成随机测试数据
return {
"character_id": "char_" + str(randi()),
"position": Vector2(randf_range(0, 2000), randf_range(0, 1500)),
"name": "Test" + str(randi() % 1000)
}
```
### 集成测试
**场景测试**:
```gdscript
# test_scene_integration.gd
extends Node
func test_character_spawning():
# 加载测试场景
var scene = preload("res://scenes/DatawhaleOffice.tscn").instantiate()
add_child(scene)
# 创建角色
var character_data = {
"id": "test_char",
"name": "Test Character",
"position": {"x": 1000, "y": 750}
}
var world_manager = scene.get_node("WorldManager")
world_manager.spawn_character(character_data)
# 验证角色是否正确生成
assert(world_manager.characters.has("test_char"))
var character = world_manager.characters["test_char"]
assert(character.character_name == "Test Character")
assert(character.global_position == Vector2(1000, 750))
```
## 部署指南
### 开发环境部署
**本地开发**:
```bash
# 1. 启动服务器
cd server
yarn dev
# 2. 启动 Godot 客户端
# 在 Godot 编辑器中按 F5
# 3. 运行测试
# 在 Godot 中打开 tests/RunAllTests.tscn按 F6
```
### 生产环境部署
#### 服务器部署
**使用 PM2**:
```bash
# 安装 PM2
npm install -g pm2
# 构建项目
cd server
yarn build
# 启动服务
pm2 start dist/server.js --name ai-town-server
# 查看日志
pm2 logs ai-town-server
# 设置开机自启
pm2 startup
pm2 save
```
**使用 Docker**:
```dockerfile
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# 复制依赖文件
COPY server/package*.json ./
COPY server/yarn.lock ./
# 安装依赖
RUN yarn install --frozen-lockfile
# 复制源码
COPY server/ ./
# 构建项目
RUN yarn build
# 暴露端口
EXPOSE 8080 8081
# 启动服务
CMD ["yarn", "start"]
```
```bash
# 构建镜像
docker build -t ai-town-server .
# 运行容器
docker run -d \
--name ai-town-server \
-p 8080:8080 \
-p 8081:8081 \
-v $(pwd)/data:/app/data \
ai-town-server
```
#### 客户端部署
**Web 导出**:
```bash
# 在 Godot 编辑器中
1. 项目 -> 导出
2. 添加 "HTML5" 导出预设
3. 配置导出选项:
- 线程支持: 启用
- 导出路径: build/web/
4. 导出项目
```
**Nginx 配置**:
```nginx
server {
listen 80;
server_name your-domain.com;
# 静态文件
location / {
root /var/www/ai-town/web;
index index.html;
try_files $uri $uri/ /index.html;
}
# WebSocket 代理
location /ws {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 管理 API 代理
location /api {
proxy_pass http://localhost:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
### 监控和维护
**系统监控**:
```bash
# 查看服务状态
pm2 status
# 查看系统资源
pm2 monit
# 重启服务
pm2 restart ai-town-server
# 查看错误日志
pm2 logs ai-town-server --err
```
**数据备份**:
```bash
# 手动备份
curl -X POST http://localhost:8081/api/backup \
-H "Authorization: Bearer admin123"
# 自动备份 (crontab)
0 2 * * * curl -X POST http://localhost:8081/api/backup -H "Authorization: Bearer admin123"
```
## 扩展开发
### 添加新功能
#### 1. 添加新的消息类型
**客户端**:
```gdscript
# MessageProtocol.gd
enum MessageType {
# ... 现有类型
NEW_FEATURE_REQUEST,
NEW_FEATURE_RESPONSE
}
static func create_new_feature_message(data: Dictionary) -> Dictionary:
return create_message("new_feature_request", data)
```
**服务器**:
```typescript
// MessageRouter.ts
constructor() {
// ... 现有处理器
this.registerHandler('new_feature_request', new NewFeatureHandler());
}
// NewFeatureHandler.ts
class NewFeatureHandler implements MessageHandler {
handle(clientId: string, data: any): void {
// 处理新功能请求
const response = {
type: 'new_feature_response',
data: { success: true },
timestamp: Date.now()
};
connectionManager.sendToClient(clientId, response);
}
}
```
#### 2. 添加新的 UI 组件
```gdscript
# NewUIComponent.gd
extends Control
class_name NewUIComponent
signal component_action(action: String, data: Dictionary)
@onready var button: Button = $Button
@onready var label: Label = $Label
func _ready():
button.pressed.connect(_on_button_pressed)
_setup_component()
func _setup_component():
# 组件初始化逻辑
pass
func _on_button_pressed():
emit_signal("component_action", "button_clicked", {})
func update_display(data: Dictionary):
# 更新显示内容
label.text = data.get("text", "")
```
#### 3. 添加新的游戏系统
```gdscript
# NewGameSystem.gd
extends Node
class_name NewGameSystem
signal system_event(event_type: String, data: Dictionary)
var system_data: Dictionary = {}
var is_initialized: bool = false
func _ready():
initialize_system()
func initialize_system():
# 系统初始化
system_data = load_system_data()
is_initialized = true
emit_signal("system_event", "initialized", {})
func process_system_update(delta: float):
if not is_initialized:
return
# 系统更新逻辑
pass
func handle_network_message(message: Dictionary):
# 处理网络消息
match message.type:
"system_update":
_handle_system_update(message.data)
func _handle_system_update(data: Dictionary):
# 处理系统更新
pass
```
### 性能优化
#### 客户端优化
**对象池**:
```gdscript
# ObjectPool.gd
class_name ObjectPool
var pool: Array = []
var scene_template: PackedScene
var max_size: int
func _init(template: PackedScene, size: int = 50):
scene_template = template
max_size = size
_populate_pool()
func get_object() -> Node:
if pool.is_empty():
return scene_template.instantiate()
return pool.pop_back()
func return_object(obj: Node):
if pool.size() < max_size:
obj.reset() # 假设对象有 reset 方法
pool.append(obj)
else:
obj.queue_free()
func _populate_pool():
for i in range(max_size / 2):
var obj = scene_template.instantiate()
pool.append(obj)
```
**空间分区**:
```gdscript
# SpatialGrid.gd
class_name SpatialGrid
var grid: Dictionary = {}
var cell_size: float = 100.0
func add_object(obj: Node2D, id: String):
var cell = _get_cell(obj.global_position)
if not grid.has(cell):
grid[cell] = {}
grid[cell][id] = obj
func get_nearby_objects(position: Vector2, radius: float) -> Array:
var nearby = []
var cells = _get_cells_in_radius(position, radius)
for cell in cells:
if grid.has(cell):
nearby.append_array(grid[cell].values())
return nearby
func _get_cell(position: Vector2) -> Vector2i:
return Vector2i(
int(position.x / cell_size),
int(position.y / cell_size)
)
```
#### 服务器优化
**消息批处理**:
```typescript
// MessageBatcher.ts
class MessageBatcher {
private batches = new Map<string, any[]>();
private batchInterval = 50; // 50ms
constructor() {
setInterval(() => this.flushBatches(), this.batchInterval);
}
addMessage(clientId: string, message: any): void {
if (!this.batches.has(clientId)) {
this.batches.set(clientId, []);
}
this.batches.get(clientId)!.push(message);
}
private flushBatches(): void {
for (const [clientId, messages] of this.batches) {
if (messages.length > 0) {
const batchMessage = {
type: 'batch',
data: { messages },
timestamp: Date.now()
};
connectionManager.sendToClient(clientId, batchMessage);
messages.length = 0; // 清空数组
}
}
}
}
```
**内存管理**:
```typescript
// MemoryManager.ts
class MemoryManager {
private cleanupInterval = 5 * 60 * 1000; // 5 minutes
constructor() {
setInterval(() => this.cleanup(), this.cleanupInterval);
}
cleanup(): void {
// 清理过期的连接
this.cleanupExpiredConnections();
// 清理旧的消息历史
this.cleanupMessageHistory();
// 强制垃圾回收
if (global.gc) {
global.gc();
}
}
private cleanupExpiredConnections(): void {
const now = Date.now();
const timeout = 10 * 60 * 1000; // 10 minutes
for (const [clientId, client] of clients) {
if (now - client.lastHeartbeat > timeout) {
clients.delete(clientId);
}
}
}
}
```
## 故障排除
### 常见问题诊断
#### 网络连接问题
**症状**: 客户端无法连接服务器
**诊断步骤**:
1. 检查服务器是否运行: `pm2 status`
2. 检查端口是否开放: `netstat -an | grep 8080`
3. 检查防火墙设置
4. 查看服务器日志: `pm2 logs ai-town-server`
**解决方案**:
```bash
# 重启服务器
pm2 restart ai-town-server
# 检查配置
cat server/src/server.ts | grep PORT
# 测试连接
curl -I http://localhost:8080
```
#### 性能问题
**症状**: 游戏卡顿,帧率低
**诊断工具**:
```gdscript
# PerformanceMonitor.gd
func _process(delta):
var fps = Engine.get_frames_per_second()
var memory = OS.get_static_memory_usage()
if fps < 30:
print("Low FPS detected: ", fps)
if memory > 100 * 1024 * 1024: # 100MB
print("High memory usage: ", memory / 1024 / 1024, "MB")
```
**优化建议**:
1. 减少同时显示的角色数量
2. 使用对象池减少内存分配
3. 优化渲染设置
4. 检查是否有内存泄漏
#### 数据同步问题
**症状**: 角色位置不同步
**调试代码**:
```gdscript
# 在 CharacterController.gd 中添加调试信息
func set_position_smooth(new_position: Vector2, duration: float = 0.2):
print("Position update: ", character_id, " from ", global_position, " to ", new_position)
# ... 原有代码
```
**检查清单**:
1. 网络连接是否稳定
2. 服务器是否正确广播位置更新
3. 客户端是否正确处理位置消息
4. 是否存在消息丢失
### 日志分析
**服务器日志格式**:
```
[2024-12-05 10:30:15] INFO: Server started on port 8080
[2024-12-05 10:30:20] INFO: ✅ Client connected: client_abc123
[2024-12-05 10:30:25] INFO: 🔐 Authentication successful: client_abc123
[2024-12-05 10:30:30] INFO: 👤 Character created: char_def456 (Hero)
[2024-12-05 10:30:35] ERROR: ❌ Invalid message format from client_abc123
```
**日志分析脚本**:
```bash
#!/bin/bash
# analyze_logs.sh
LOG_FILE="server/data/logs/server_$(date +%Y-%m-%d).log"
echo "=== Connection Statistics ==="
grep "Client connected" $LOG_FILE | wc -l
echo "Total connections today"
echo "=== Error Summary ==="
grep "ERROR" $LOG_FILE | cut -d' ' -f4- | sort | uniq -c | sort -nr
echo "=== Character Creation ==="
grep "Character created" $LOG_FILE | wc -l
echo "Characters created today"
```
### 调试工具
**网络调试**:
```gdscript
# NetworkDebugger.gd
extends Node
var message_log: Array = []
var max_log_size: int = 1000
func log_message(direction: String, message: Dictionary):
var log_entry = {
"timestamp": Time.get_unix_time_from_system(),
"direction": direction, # "sent" or "received"
"type": message.get("type", "unknown"),
"data": message.get("data", {}),
"size": JSON.stringify(message).length()
}
message_log.append(log_entry)
if message_log.size() > max_log_size:
message_log.pop_front()
print("[NET %s] %s: %s bytes" % [direction.to_upper(), log_entry.type, log_entry.size])
func get_message_statistics() -> Dictionary:
var stats = {
"total_messages": message_log.size(),
"sent_messages": 0,
"received_messages": 0,
"message_types": {}
}
for entry in message_log:
if entry.direction == "sent":
stats.sent_messages += 1
else:
stats.received_messages += 1
var type = entry.type
if not stats.message_types.has(type):
stats.message_types[type] = 0
stats.message_types[type] += 1
return stats
```
**性能分析器**:
```gdscript
# Profiler.gd
extends Node
var frame_times: Array = []
var function_times: Dictionary = {}
func start_profiling(function_name: String):
function_times[function_name] = Time.get_ticks_usec()
func end_profiling(function_name: String):
if function_times.has(function_name):
var elapsed = Time.get_ticks_usec() - function_times[function_name]
print("Function %s took %d microseconds" % [function_name, elapsed])
function_times.erase(function_name)
func _process(delta):
frame_times.append(delta)
if frame_times.size() > 60: # 保持最近 60 帧
frame_times.pop_front()
# 每秒输出一次平均帧时间
if Engine.get_process_frames() % 60 == 0:
var avg_frame_time = 0.0
for time in frame_times:
avg_frame_time += time
avg_frame_time /= frame_times.size()
var fps = 1.0 / avg_frame_time
print("Average FPS: %.1f" % fps)
```
---
## 总结
本技术文档涵盖了 AI Town Game 项目的所有技术细节包括架构设计、API 参考、部署指南和扩展开发。开发者可以根据这份文档:
1. **快速上手**: 通过环境配置和快速开始指南
2. **深入理解**: 通过核心系统详解和架构图
3. **扩展功能**: 通过扩展开发指南添加新功能
4. **优化性能**: 通过性能优化建议提升游戏体验
5. **解决问题**: 通过故障排除指南快速定位和解决问题
项目采用模块化设计,具有良好的可扩展性和可维护性。所有核心系统都经过充分测试,并提供了完整的 API 文档和使用示例。
如有任何技术问题或改进建议,欢迎通过项目 GitHub 页面提交 Issue 或 Pull Request。