42 KiB
42 KiB
AI Town Game 开发者技术文档
📋 目录
项目概述
技术栈
客户端:
- 游戏引擎: 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 # 任务列表
技术架构
整体架构图
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:
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:
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:
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 可用磁盘空间
快速配置
- 克隆项目:
git clone <repository-url>
cd ai_community
- 配置 Godot:
# 下载并安装 Godot 4.5.1
# 导入项目: 选择 project.godot 文件
- 配置服务器:
cd server
yarn install
yarn build
- 启动开发环境:
# 终端 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:
{
"godot_tools.editor_path": "/path/to/godot",
"typescript.preferences.importModuleSpecifier": "relative",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
}
核心系统详解
网络系统
WebSocket 连接管理
客户端连接流程:
# 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)
服务器连接处理:
// 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);
});
});
消息协议实现
消息序列化:
# 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
消息路由:
// 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}`);
}
}
}
状态管理系统
游戏状态机
# 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)
数据持久化
客户端数据保存:
# 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")
服务器数据持久化:
// 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);
}
}
角色系统
角色控制器
# 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)
角色状态同步
客户端同步:
# 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()
服务器状态广播:
// 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();
}
对话系统
对话管理
# 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)
对话气泡系统
# 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
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
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
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
身份验证:
// 请求
{
"type": "auth_request",
"data": {
"username": string
},
"timestamp": number
}
// 响应
{
"type": "auth_response",
"data": {
"success": boolean,
"clientId": string,
"message"?: string
},
"timestamp": number
}
角色创建:
// 请求
{
"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
}
角色移动:
// 客户端 -> 服务器
{
"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 (管理接口)
系统状态:
GET /api/status
Authorization: Bearer <admin_token>
Response:
{
"status": "healthy",
"uptime": 3600,
"connections": 5,
"characters": 12,
"memory": {
"used": 45.2,
"total": 512
}
}
备份管理:
POST /api/backup
Authorization: Bearer <admin_token>
Response:
{
"success": true,
"backupId": "backup_1234567890",
"timestamp": 1234567890
}
数据库设计
数据模型
Character 模型
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 模型
interface WorldState {
sceneId: string; // 场景 ID
characters: Character[]; // 所有角色
timestamp: number; // 状态时间戳
}
Message 模型
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/
数据验证:
// 角色数据验证
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'
);
}
网络协议
连接生命周期
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
错误处理
网络错误:
{
"type": "error",
"data": {
"code": "E001",
"message": "Connection timeout",
"details": "Server did not respond within 10 seconds"
},
"timestamp": number
}
业务逻辑错误:
{
"type": "character_create",
"data": {
"success": false,
"message": "Character name already exists",
"code": "G001"
},
"timestamp": number
}
心跳机制
客户端心跳:
# 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)
服务器心跳检查:
// 每 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);
测试框架
单元测试
测试结构:
# 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)
属性测试
属性测试框架:
# 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)
}
集成测试
场景测试:
# 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))
部署指南
开发环境部署
本地开发:
# 1. 启动服务器
cd server
yarn dev
# 2. 启动 Godot 客户端
# 在 Godot 编辑器中按 F5
# 3. 运行测试
# 在 Godot 中打开 tests/RunAllTests.tscn,按 F6
生产环境部署
服务器部署
使用 PM2:
# 安装 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
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"]
# 构建镜像
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 导出:
# 在 Godot 编辑器中
1. 项目 -> 导出
2. 添加 "HTML5" 导出预设
3. 配置导出选项:
- 线程支持: 启用
- 导出路径: build/web/
4. 导出项目
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;
}
}
监控和维护
系统监控:
# 查看服务状态
pm2 status
# 查看系统资源
pm2 monit
# 重启服务
pm2 restart ai-town-server
# 查看错误日志
pm2 logs ai-town-server --err
数据备份:
# 手动备份
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. 添加新的消息类型
客户端:
# 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)
服务器:
// 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 组件
# 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. 添加新的游戏系统
# 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
性能优化
客户端优化
对象池:
# 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)
空间分区:
# 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)
)
服务器优化
消息批处理:
// 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; // 清空数组
}
}
}
}
内存管理:
// 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);
}
}
}
}
故障排除
常见问题诊断
网络连接问题
症状: 客户端无法连接服务器 诊断步骤:
- 检查服务器是否运行:
pm2 status - 检查端口是否开放:
netstat -an | grep 8080 - 检查防火墙设置
- 查看服务器日志:
pm2 logs ai-town-server
解决方案:
# 重启服务器
pm2 restart ai-town-server
# 检查配置
cat server/src/server.ts | grep PORT
# 测试连接
curl -I http://localhost:8080
性能问题
症状: 游戏卡顿,帧率低 诊断工具:
# 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")
优化建议:
- 减少同时显示的角色数量
- 使用对象池减少内存分配
- 优化渲染设置
- 检查是否有内存泄漏
数据同步问题
症状: 角色位置不同步 调试代码:
# 在 CharacterController.gd 中添加调试信息
func set_position_smooth(new_position: Vector2, duration: float = 0.2):
print("Position update: ", character_id, " from ", global_position, " to ", new_position)
# ... 原有代码
检查清单:
- 网络连接是否稳定
- 服务器是否正确广播位置更新
- 客户端是否正确处理位置消息
- 是否存在消息丢失
日志分析
服务器日志格式:
[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
日志分析脚本:
#!/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"
调试工具
网络调试:
# 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
性能分析器:
# 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 参考、部署指南和扩展开发。开发者可以根据这份文档:
- 快速上手: 通过环境配置和快速开始指南
- 深入理解: 通过核心系统详解和架构图
- 扩展功能: 通过扩展开发指南添加新功能
- 优化性能: 通过性能优化建议提升游戏体验
- 解决问题: 通过故障排除指南快速定位和解决问题
项目采用模块化设计,具有良好的可扩展性和可维护性。所有核心系统都经过充分测试,并提供了完整的 API 文档和使用示例。
如有任何技术问题或改进建议,欢迎通过项目 GitHub 页面提交 Issue 或 Pull Request。