RustyWarfare 开发者文档
欢迎来到 RustyWarfare 开发者文档!
项目简介
RustyWarfare 是一款面向未来题材的实时策略游戏的 Rust 核心引擎。项目实现了:
- 权威服务端架构:所有游戏逻辑在服务端执行,杜绝作弊
- 网络同步:基于 Lightyear 的客户端预测、插值和复制
- ECS 架构:使用 Bevy ECS 管理游戏状态
- 数据驱动:通过 TOML 配置文件定义游戏内容
- 跨平台:支持 Windows、Linux、Android 等平台
- Godot 前端:通过 GDExtension 接入 Godot 引擎
技术栈
| 类别 | 技术 |
|---|---|
| 核心语言 | Rust 2024 Edition |
| 游戏框架 | Bevy 0.18.1 |
| 网络同步 | Lightyear 0.26.4 |
| 前端引擎 | Godot 4.x (通过 GDExtension) |
| 序列化 | Serde, TOML |
| 构建系统 | Cargo Workspace |
运行模式
项目支持三种运行模式,所有模式使用统一的游戏规则:
- 单人模式:本地 server + 本地 client
- 主机模式:本地 server + 本地 client + 远程 clients
- 远程模式:远程 server + 本地 client
Workspace 结构
rusty_warfare/
├── content/ # 内容包加载与验证
├── protocol/ # 网络协议定义
├── server/ # 权威服务端逻辑
├── client/ # 客户端核心
├── runtime_core/ # 运行时编排
├── gdextension/ # Godot 接入层
├── builder/ # 构建工具
├── game_domain/ # 游戏领域模型
└── launcher/godot/ # Godot 前端项目
文档导航
获取帮助
- 查看 常见问题
- 阅读项目根目录的
README.md和CONTRIBUTING.md - 参考
docs/目录下的架构文档
环境配置
本章介绍如何搭建 RustyWarfare 的开发环境。
系统要求
必需工具
- Rust 工具链 (推荐 stable 最新版本)
- Git (用于克隆仓库和管理子模块)
- Godot 4.x (用于前端开发和测试)
可选工具
- mdBook (用于构建本文档)
- Visual Studio Code 或 RustRover (推荐的 IDE)
- Android SDK (如需 Android 平台开发)
安装 Rust
如果尚未安装 Rust,请访问 rustup.rs 并按照说明安装:
# Linux / macOS
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Windows
# 下载并运行 rustup-init.exe
验证安装:
rustc --version
cargo --version
克隆仓库
git clone <repository-url> rusty_warfare
cd rusty_warfare
# 初始化子模块 (包含 Godot 前端)
git submodule update --init --recursive
配置 IDE
Visual Studio Code
推荐安装以下扩展:
- rust-analyzer - Rust 语言服务器
- CodeLLDB - 调试器
- Even Better TOML - TOML 语法支持
RustRover
RustRover 开箱即用,无需额外配置。
验证环境
运行以下命令验证环境配置正确:
# 检查所有 crate 是否能通过编译
cargo check --workspace
# 运行测试
cargo test --workspace
如果所有命令成功执行,说明环境配置完成!
下一步
继续阅读 构建项目 了解如何编译和运行项目。
构建项目
本章介绍如何构建 RustyWarfare 的各个组件。
Workspace 结构
RustyWarfare 使用 Cargo Workspace 组织,包含以下子包:
├── content/ # 内容包加载
├── protocol/ # 网络协议
├── server/ # 服务端
├── client/ # 客户端
├── runtime_core/ # 运行时核心
├── gdextension/ # Godot 接入
├── builder/ # 构建工具
└── game_domain/ # 游戏领域模型
构建所有组件
# 构建整个 workspace
cargo build --workspace
# Release 构建 (优化)
cargo build --workspace --release
构建特定模块
# 只构建内容包系统
cargo build -p content
# 只构建服务端
cargo build -p server
# 只构建 GDExtension
cargo build -p gdextension
构建 GDExtension 动态库
GDExtension 是连接 Rust 核心和 Godot 的桥梁。使用 builder 工具自动构建和部署:
cargo run -p builder
builder 会:
- 编译 gdextension 为动态库
- 将产物复制到
launcher/godot/addons/rusty_core/bin/ - 生成正确的 .gdextension 配置文件
验证构建
# 运行所有测试
cargo test --workspace
# 运行特定包的测试
cargo test -p content
cargo test -p server
# 代码风格检查
cargo fmt --check
# Clippy 静态分析
cargo clippy --workspace --all-targets
构建时间优化
如果构建时间过长,可以尝试:
使用 sccache
# 安装 sccache
cargo install sccache
# 配置 Cargo 使用 sccache
export RUSTC_WRAPPER=sccache
并行编译
# 设置并行编译任务数
export CARGO_BUILD_JOBS=8
增量编译 (开发构建)
# 增量编译默认开启,如需禁用:
export CARGO_INCREMENTAL=0
常见构建问题
问题:链接错误
如果遇到链接器错误,可能是缺少系统依赖。参考 环境配置 确保所有工具已安装。
问题:子模块未初始化
git submodule update --init --recursive
问题:Bevy 编译慢
首次编译 Bevy 可能需要较长时间 (5-10 分钟),这是正常现象。后续增量编译会快很多。
下一步
继续阅读 运行与调试 了解如何启动项目。
运行与调试
本章介绍如何运行和调试 RustyWarfare。
运行 Godot 客户端
RustyWarfare 的主要运行方式是通过 Godot 前端:
- 构建 GDExtension:
cargo run -p builder
- 打开 Godot 项目:
使用 Godot 4.x 打开 launcher/godot/ 目录
- 运行场景:
在 Godot 编辑器中按 F5 或点击播放按钮
运行模式
RustyWarfare 支持三种运行模式:
单人模式
在 Godot 中选择"单人游戏",本地同时启动 server 和 client。
主机模式
- 选择"创建房间"
- 系统启动本地 server 并自动连接
- 其他玩家可通过 IP 加入
客户端模式
- 选择"加入房间"
- 输入服务器地址
- 连接到远程 server
调试技巧
启用日志
项目使用 tracing 进行日志记录。在启动前设置环境变量:
# Windows (PowerShell)
$env:RUST_LOG="debug"
# Linux / macOS
export RUST_LOG=debug
日志级别:
error- 仅错误warn- 警告及以上info- 信息及以上 (推荐)debug- 调试及以上trace- 所有日志 (非常详细)
调试特定模块
# 只显示 server 模块的调试日志
export RUST_LOG=server=debug
# 多个模块
export RUST_LOG=server=debug,client=info,runtime_core=trace
使用调试器
VS Code
创建 .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug gdextension",
"cargo": {
"args": ["build", "-p", "gdextension"]
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}
RustRover
右键点击 gdextension/src/lib.rs -> Debug 'gdextension'
Godot 控制台输出
GDExtension 的 Rust 日志会输出到 Godot 控制台。在 Godot 编辑器底部查看输出面板。
网络调试
启用 Lightyear 的诊断功能:
export RUST_LOG=lightyear=debug
这会显示:
- 网络延迟
- 丢包率
- 预测命中率
- 回滚次数
性能分析
使用 cargo flamegraph
# 安装
cargo install flamegraph
# 生成火焰图
cargo flamegraph -p server
使用 perf (Linux)
perf record --call-graph dwarf cargo run -p server --release
perf report
常见运行问题
GDExtension 未加载
症状:Godot 报告找不到 RustyCore 类
解决:
- 确认
cargo run -p builder执行成功 - 检查
launcher/godot/addons/rusty_core/bin/下是否有动态库 - 重启 Godot 编辑器
连接超时
症状:客户端无法连接到服务器
解决:
- 检查防火墙设置
- 确认服务器端口已开放 (默认 5000)
- 验证 IP 地址正确
内容包加载失败
症状:启动时报告内容包错误
解决:
- 检查
launcher/godot/assets/content_packages/official/manifest.toml存在 - 验证内容包格式正确
- 查看详细错误日志
下一步
了解项目架构,请阅读 总体架构。
总体架构
RustyWarfare 采用权威服务端架构,结合 Bevy ECS 和 Lightyear 网络同步,实现了单人、主机、远程三种模式下的统一游戏逻辑。
架构原则
- 服务端权威:所有游戏规则在服务端执行,客户端只负责输入和显示
- 单一规则集:单人和多人使用相同的游戏逻辑,无重复代码
- 数据驱动:游戏内容通过 TOML 配置定义,支持 Mod 和内容包
- 前端分离:Godot 仅负责渲染和输入,不包含游戏规则
- 严格分层:模块间依赖单向,防止循环依赖
技术架构图
┌─────────────────────────────────────────────┐
│ Godot 前端 (GDScript) │
│ 渲染、UI、输入采集 │
└──────────────────┬──────────────────────────┘
│ GDExtension FFI
┌──────────────────▼──────────────────────────┐
│ gdextension (Rust) │
│ Godot ↔ Rust 类型转换和接口 │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ runtime_core (Rust) │
│ 运行模式编排、生命周期管理、前端投影 │
└─────────┬──────────────────────┬────────────┘
│ │
┌─────────▼─────────┐ ┌────────▼───────────┐
│ client (Rust) │ │ server (Rust) │
│ 预测、插值、校正 │ │ 权威逻辑、ECS世界 │
└─────────┬─────────┘ └────────┬───────────┘
│ │
└──────────┬───────────┘
│
┌────────────▼─────────────┐
│ protocol (Rust) │
│ 网络消息、共享组件定义 │
└────────────┬─────────────┘
│
┌────────────▼─────────────┐
│ content (Rust) │
│ 内容包加载、验证、数据库 │
└──────────────────────────┘
依赖关系
gdextension
└── runtime_core
├── client ──┐
│ ├── protocol
└── server ──┤
└── content
关键约束:
protocol和content是底层库,不依赖任何上层模块server和client平行,互不依赖runtime_core编排 server 和 client,但不实现游戏规则gdextension仅做类型转换,不包含业务逻辑
核心流程
启动流程
1. Godot 启动
2. 加载 gdextension 动态库
3. GDScript 调用 RustyCore.initialize()
4. runtime_core 读取配置
5. content 加载并验证内容包
6. 根据模式启动 server/client
7. 进入游戏循环
游戏循环 (单人模式)
每帧:
1. Godot 采集输入 → GDScript
2. GDScript → gdextension.send_command()
3. gdextension → runtime_core.update()
4. runtime_core → client.process_input()
5. client → server (本地通道)
6. server 执行权威逻辑 (Bevy ECS)
7. server → client (复制组件)
8. client 进行预测和校正
9. client → runtime_core.get_snapshot()
10. runtime_core → gdextension (Dictionary)
11. gdextension → GDScript
12. GDScript 更新 Godot 节点
游戏循环 (远程模式)
每帧:
1-4. [同单人模式]
5. client → 远程 server (UDP)
6. 本地 client 立即预测
7. 远程 server 执行权威逻辑
8. server → client (网络复制)
9. client reconcile (对比预测和权威结果)
10-12. [同单人模式]
为什么这样设计?
为什么保留 runtime_core?
如果没有 runtime_core,gdextension 会直接管理:
- Godot 类型转换
- Server 生命周期
- Client 生命周期
- 网络配置
- 错误处理
这会让 gdextension 变得臃肿且难以测试。runtime_core 提供纯 Rust API,使得:
- 可以编写纯 Rust 测试
- 未来可支持非 Godot 前端
- GDExtension 层保持简洁
为什么 client 和 server 分离?
- 明确职责:server 只关心规则,client 只关心显示
- 防止泄漏:防止客户端逻辑影响服务端
- 独立测试:可以单独测试 server 逻辑
- 专用服务器:未来可构建无 client 的专用服务器
为什么 protocol 独立?
protocol 定义 server 和 client 的"语言":
- 网络消息格式
- 复制组件结构
- 共享常量
这些定义必须双方一致,因此独立为底层库。
下一步
分层设计
RustyWarfare 采用严格的分层架构,每层有明确的职责边界。
分层概览
┌─────────────────────────────────────┐
│ Layer 8: Godot 前端 (GDScript) │ 表现层
├─────────────────────────────────────┤
│ Layer 7: gdextension (Rust) │ 接入层
├─────────────────────────────────────┤
│ Layer 6: runtime_core (Rust) │ 编排层
├─────────────────────────────────────┤
│ Layer 5: client (Rust) │ 客户端层
├─────────────────────────────────────┤
│ Layer 4: server (Rust) │ 服务端层
├─────────────────────────────────────┤
│ Layer 3: protocol (Rust) │ 协议层
├─────────────────────────────────────┤
│ Layer 2: content (Rust) │ 内容层
├─────────────────────────────────────┤
│ Layer 1: game_domain (Rust) │ 领域模型层
└─────────────────────────────────────┘
Layer 1: game_domain - 领域模型层
职责:定义游戏领域的基础类型和概念
包含:
- 坐标系统 (Position, Vec2)
- 单位ID、玩家ID
- 资源类型
- 游戏状态枚举
特点:
- 无依赖,纯数据结构
- 可被所有上层使用
- 不包含业务逻辑
Layer 2: content - 内容层
职责:内容包的加载、验证和管理
包含:
- 内容包 manifest 解析
- 单位模板、地图模板
- Schema 验证
- Fingerprint 计算
- 跨引用检查
- ContentDatabase
特点:
- 只依赖
game_domain - 不依赖 Bevy、Godot
- 纯文件系统操作
核心类型:
#![allow(unused)] fn main() { pub struct ContentDatabase { units: HashMap<ContentId, UnitTemplate>, maps: HashMap<ContentId, MapTemplate>, // ... } }
Layer 3: protocol - 协议层
职责:定义 client 和 server 的共享协议
包含:
- 网络消息定义
- Lightyear 复制组件
- 玩家输入命令
- 同步常量 (tick rate 等)
特点:
- 被
client和server共同依赖 - 不能依赖
client或server - 定义数据契约,不实现逻辑
核心类型:
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize, Component)] pub struct Position { pub x: f32, pub y: f32, } #[derive(Serialize, Deserialize)] pub enum PlayerInput { Move { target: Position }, Attack { target: EntityId }, } }
Layer 4: server - 服务端层
职责:权威游戏逻辑
包含:
- Bevy ECS World
- 游戏规则系统
- 地图加载
- 单位生成
- 战斗结算
- 资源生产
- 胜负判定
依赖:
protocol(共享组件)content(加载游戏数据)
核心系统:
#![allow(unused)] fn main() { fn combat_system( mut query: Query<(&mut Health, &Damage)>, ) { // 权威战斗逻辑 } }
Layer 5: client - 客户端层
职责:客户端状态管理和预测
包含:
- 输入缓冲
- 客户端预测
- 服务器状态接收
- Reconciliation (校正)
- 插值
- 前端 Snapshot 生成
依赖:
protocol(共享组件)
不包含:
- 权威规则
- Godot 类型
核心流程:
#![allow(unused)] fn main() { impl Client { pub fn predict_input(&mut self, input: PlayerInput) { // 本地预测 } pub fn reconcile(&mut self, server_state: ServerSnapshot) { // 对比预测和服务器状态,校正 } pub fn get_frontend_snapshot(&self) -> FrontendSnapshot { // 生成前端可消费的数据 } } }
Layer 6: runtime_core - 编排层
职责:运行时生命周期管理和模式编排
包含:
- Runtime 结构
- 运行模式管理 (单人/主机/远程)
- Server/Client 启动和关闭
- 前端 Snapshot 投影
- 错误处理
依赖:
clientserverprotocolcontent
核心 API:
#![allow(unused)] fn main() { pub struct Runtime { mode: RuntimeMode, server: Option<Server>, client: Option<Client>, } impl Runtime { pub fn start_singleplayer(&mut self) -> Result<()>; pub fn update(&mut self, delta: f64) -> Result<()>; pub fn get_snapshot(&self) -> FrontendSnapshot; } }
Layer 7: gdextension - 接入层
职责:Godot ↔ Rust 边界适配
包含:
- GodotClass 定义
- Rust ↔ Godot 类型转换
- RustyCore 接口暴露
- Dictionary/Array 转换
依赖:
runtime_core(唯一依赖)godotcrate (Godot 绑定)
核心实现:
#![allow(unused)] fn main() { #[derive(GodotClass)] #[class(base=Node)] pub struct RustyCore { runtime: Runtime, } #[godot_api] impl RustyCore { #[func] pub fn initialize(&mut self, config: Dictionary) { // 转换 Godot 类型 → Rust 类型 } #[func] pub fn get_snapshot(&self) -> Dictionary { // 转换 Rust 类型 → Godot 类型 } } }
Layer 8: Godot 前端
职责:渲染和用户交互
包含:
- UI 场景
- 输入处理
- 节点更新
- 资源加载 (贴图、音频)
特点:
- 不包含游戏规则
- 通过
RustyCore接口访问 Rust - 只读取 Snapshot,不直接访问 ECS
依赖规则
允许的依赖方向
上层 → 下层 ✅
同层平行 ✅ (client ↔ server 除外)
下层 → 上层 ❌ 禁止
具体禁止项
- ❌
protocol不得依赖client或server - ❌
content不得依赖server - ❌
server不得依赖client - ❌
client不得依赖server - ❌
runtime_core不得包含游戏规则 - ❌
gdextension不得包含游戏规则
为什么严格分层?
- 可测试性:每层可独立测试
- 复用性:底层可被多个上层使用
- 可维护性:修改某层不影响其他层
- 防止循环依赖:Rust 禁止循环依赖
- 清晰职责:每层职责明确,不会混淆
实践示例
添加新单位
- Layer 2 (content):定义单位模板 TOML
- Layer 4 (server):实现单位生成和行为系统
- Layer 3 (protocol):添加复制组件 (如需同步)
- Layer 5 (client):处理预测逻辑 (如需)
- Layer 8 (Godot):添加贴图和渲染节点
添加新网络消息
- Layer 3 (protocol):定义消息结构
- Layer 4 (server):处理接收逻辑
- Layer 5 (client):发送逻辑
修改 UI
- Layer 8 (Godot):修改 GDScript 和场景
- 无需修改 Rust 代码
下一步
了解不同运行模式下的差异:运行模式
运行模式
RustyWarfare 支持三种运行模式,所有模式使用相同的游戏规则,只是 server 和 client 的部署位置不同。
模式概览
| 模式 | Server 位置 | Client 位置 | 通信方式 | 用途 |
|---|---|---|---|---|
| 单人模式 | 本地 | 本地 | 内存通道 | 单人游戏 |
| 主机模式 | 本地 | 本地+远程 | 本地内存+UDP | 局域网/在线主机 |
| 远程模式 | 远程 | 本地 | UDP | 加入他人房间 |
| 专用服务器 | 独立进程 | 远程 | UDP | 持久化服务器 |
单人模式 (Singleplayer)
架构
┌──────────────────────────────────┐
│ Godot + gdextension │
│ ┌────────────────────────────┐ │
│ │ runtime_core │ │
│ │ ┌──────────┐ ┌─────────┐ │ │
│ │ │ Client │←→│ Server │ │ │
│ │ └──────────┘ └─────────┘ │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
单一进程
特点
- 低延迟:无网络开销
- 离线可玩:不需要网络连接
- 完整预测:仍然使用预测机制,保持架构一致性
- 调试友好:所有代码在同一进程
数据流
1. Godot 采集输入
2. gdextension → runtime_core
3. client.process_input()
4. client → server (内存通道,CrossbeamIo)
5. server 执行权威逻辑
6. server → client (内存复制)
7. client.reconcile()
8. runtime_core.get_snapshot()
9. gdextension → Godot
启动代码
#![allow(unused)] fn main() { impl Runtime { pub fn start_singleplayer(&mut self) -> Result<()> { // 加载内容包 let content_db = self.load_content()?; // 创建本地 server let server = Server::new_local(content_db)?; // 创建本地 client let client = Client::new_local()?; // 建立内存通道 let (tx, rx) = crossbeam_channel::unbounded(); server.connect_channel(rx); client.connect_channel(tx); self.server = Some(server); self.client = Some(client); self.mode = RuntimeMode::Singleplayer; Ok(()) } } }
主机模式 (Host)
架构
本机玩家:
┌────────────────────────────────────┐
│ Godot + gdextension │
│ ┌──────────────────────────────┐ │
│ │ runtime_core │ │
│ │ ┌──────────┐ ┌───────────┐ │ │
│ │ │ Client │←→│ Server │ │ │
│ │ └──────────┘ └─────┬─────┘ │ │
│ └────────────────────────┼──────┘ │
└────────────────────────────┼────────┘
│ UDP
↓
┌──────────────┐
│ 远程 Client │
└──────────────┘
特点
- 混合通信:本机用内存通道,远程用 UDP
- 主机优势:本机玩家零延迟
- 灵活性:可随时开始/停止接受连接
数据流
本机玩家:
同单人模式 (内存通道)
远程玩家:
1. 远程 Client 发送输入 → UDP → 本机 Server
2. 本机 Server 执行权威逻辑
3. 本机 Server → UDP → 远程 Client (复制状态)
4. 远程 Client reconcile 并显示
启动代码
#![allow(unused)] fn main() { impl Runtime { pub fn start_host(&mut self, port: u16) -> Result<()> { let content_db = self.load_content()?; // 创建网络 server (监听端口) let server = Server::new_host(content_db, port)?; // 创建本地 client let client = Client::new_local()?; // 本地连接用内存通道 let (tx, rx) = crossbeam_channel::unbounded(); server.add_local_client(rx); client.connect_channel(tx); // Server 同时监听 UDP 端口 self.server = Some(server); self.client = Some(client); self.mode = RuntimeMode::Host { port }; Ok(()) } } }
远程模式 (Client)
架构
本地:
┌────────────────────────────┐
│ Godot + gdextension │
│ ┌──────────────────────┐ │
│ │ runtime_core │ │
│ │ ┌──────────┐ │ │
│ │ │ Client │ │ │
│ │ └─────┬────┘ │ │
│ └────────┼─────────────┘ │
└────────────┼───────────────┘
│ UDP
↓
┌─────────────┐
│远程 Server │
└─────────────┘
特点
- 轻量级:不运行 server
- 预测 + 插值:掩盖网络延迟
- 自动校正:服务器权威,客户端接受校正
数据流
1. Godot 采集输入
2. Client 立即本地预测
3. Client → UDP → Server (发送输入)
4. [网络延迟...]
5. Server 执行权威逻辑
6. Server → UDP → Client (复制状态)
7. Client reconcile (对比预测和权威)
8. Client interpolate (平滑过渡)
9. 显示最终状态
预测与校正
时刻 t=0: 用户点击移动
时刻 t=0: Client 预测 → 单位立即开始移动
时刻 t=0: 发送输入到 Server
时刻 t=50ms: Server 收到输入
时刻 t=50ms: Server 执行权威移动
时刻 t=50ms: Server 返回权威状态
时刻 t=100ms: Client 收到权威状态
时刻 t=100ms: Client 对比预测和权威
时刻 t=100ms: 如有偏差,平滑校正
启动代码
#![allow(unused)] fn main() { impl Runtime { pub fn start_client(&mut self, server_addr: &str) -> Result<()> { // 仅创建 client let client = Client::new_remote(server_addr)?; self.client = Some(client); self.server = None; // 无本地 server self.mode = RuntimeMode::Client { server_addr: server_addr.to_string(), }; Ok(()) } } }
专用服务器 (Dedicated Server)
架构
专用服务器 (独立进程):
┌────────────────────┐
│ runtime_core │
│ ┌──────────────┐ │
│ │ Server │ │
│ └──────┬───────┘ │
└─────────┼──────────┘
│ UDP
┌─────┴─────┐
↓ ↓
┌────────┐ ┌────────┐
│Client 1│ │Client 2│
└────────┘ └────────┘
特点
- 持久化:不依赖玩家在线
- 无 GUI:无 Godot,纯 Rust
- 高性能:无渲染开销
- 独立部署:可部署到云服务器
组成模块
dedicated_server
└── runtime_core
└── server
└── protocol
└── content
不包含:
- ❌
client - ❌
gdextension - ❌ Godot
启动代码
// 独立的 dedicated_server 二进制 fn main() { let mut runtime = Runtime::new(); runtime.start_dedicated_server( port: 5000, max_players: 8, )?; loop { runtime.update(TICK_DURATION)?; thread::sleep(TICK_DURATION); } }
模式对比
包含的模块
| 模块 | 单人 | 主机 | 远程 | 专用 |
|---|---|---|---|---|
| gdextension | ✅ | ✅ | ✅ | ❌ |
| runtime_core | ✅ | ✅ | ✅ | ✅ |
| client | ✅ | ✅ | ✅ | ❌ |
| server | ✅ | ✅ | ❌ | ✅ |
| protocol | ✅ | ✅ | ✅ | ✅ |
| content | ✅ | ✅ | ❌ | ✅ |
延迟特性
| 模式 | 本地延迟 | 网络延迟 | 预测 | 插值 |
|---|---|---|---|---|
| 单人 | <1ms | 无 | 有(架构一致) | 无 |
| 主机 | <1ms | 20-100ms | 有 | 有 |
| 远程 | - | 20-100ms | 有 | 有 |
| 专用 | - | 20-100ms | - | - |
切换模式
在运行时不支持模式切换。必须:
- 调用
runtime.shutdown() - 调用新模式的
start_*()方法
#![allow(unused)] fn main() { // 从单人模式切换到主机模式 runtime.shutdown()?; runtime.start_host(5000)?; }
下一步
了解详细的数据流动:数据流
数据流
本章详细说明数据在系统各层之间的流动。
单人模式数据流
┌─────────────────────────────────────────────┐
│ Godot (GDScript) │
│ 1. 用户点击 → get_global_mouse_position() │
│ 2. 调用 rusty_core.send_command() │
└──────────────────┬──────────────────────────┘
│ Dictionary
↓
┌─────────────────────────────────────────────┐
│ gdextension (Rust) │
│ 3. Dictionary → PlayerInput │
│ 4. runtime.send_input() │
└──────────────────┬──────────────────────────┘
│ PlayerInput
↓
┌─────────────────────────────────────────────┐
│ runtime_core (Rust) │
│ 5. 分发到 client │
└──────────────────┬──────────────────────────┘
│ PlayerInput
↓
┌─────────────────────────────────────────────┐
│ client (Rust) │
│ 6. 本地预测 → 立即更新显示状态 │
│ 7. 发送到 server (内存通道) │
└──────────────────┬──────────────────────────┘
│ PlayerInput
↓
┌─────────────────────────────────────────────┐
│ server (Rust) │
│ 8. 处理输入命令 │
│ 9. 执行权威逻辑 (Bevy ECS) │
│ 10. 更新 ECS World │
│ 11. 复制组件 → client │
└──────────────────┬──────────────────────────┘
│ Replicated Components
↓
┌─────────────────────────────────────────────┐
│ client (Rust) │
│ 12. 接收权威状态 │
│ 13. Reconciliation (校正预测) │
│ 14. 生成 FrontendSnapshot │
└──────────────────┬──────────────────────────┘
│ FrontendSnapshot
↓
┌─────────────────────────────────────────────┐
│ runtime_core (Rust) │
│ 15. 转发 snapshot │
└──────────────────┬──────────────────────────┘
│ FrontendSnapshot
↓
┌─────────────────────────────────────────────┐
│ gdextension (Rust) │
│ 16. FrontendSnapshot → Dictionary │
└──────────────────┬──────────────────────────┘
│ Dictionary
↓
┌─────────────────────────────────────────────┐
│ Godot (GDScript) │
│ 17. 读取 snapshot["units"] │
│ 18. 更新 Node2D 位置 │
│ 19. 渲染到屏幕 │
└─────────────────────────────────────────────┘
远程模式数据流
客户端 → 服务器
Godot → gdextension → runtime_core → client
↓
┌──────────────┐
│ 本地预测 │ (立即反馈)
└──────────────┘
↓
┌──────────────┐
│ UDP Socket │
└──────┬───────┘
│ 网络延迟 (50-100ms)
↓
┌──────────────┐
│远程 Server │
└──────────────┘
服务器 → 客户端
┌──────────────┐
│远程 Server │
│执行权威逻辑 │
└──────┬───────┘
│ 网络延迟 (50-100ms)
↓
┌──────────────┐
│ UDP Socket │
└──────┬───────┘
↓
client ← runtime_core ← gdextension ← Godot
↓
┌──────────────┐
│ Reconcile │ (对比预测和权威)
└──────┬───────┘
↓
┌──────────────┐
│ Interpolate │ (平滑过渡)
└──────┬───────┘
↓
FrontendSnapshot → 显示
启动时数据流
1. Godot 启动
└─> 加载 gdextension.dll
2. gdextension 初始化
└─> 注册 RustyCore 类
3. GDScript 调用 RustyCore.initialize(config)
└─> Dictionary → RuntimeConfig
└─> runtime_core 初始化
4. runtime_core.initialize()
└─> 加载内容包
└─> content::ContentLoader.load_package()
└─> 读取 manifest.toml
└─> 解析 units/*.toml
└─> 验证 schema
└─> 构建 ContentDatabase
5. GDScript 调用 RustyCore.start_singleplayer()
└─> runtime_core.start_singleplayer()
├─> 创建 Server (使用 ContentDatabase)
│ └─> 初始化 Bevy App
│ └─> 加载地图
│ └─> 生成初始单位
└─> 创建 Client
└─> 建立与 server 的内存通道
6. 进入游戏循环
游戏循环数据流
每帧流程 (60 FPS)
Godot _process(delta):
├─> rusty_core.update(delta)
│ └─> runtime_core.update()
│ ├─> server.update()
│ │ └─> bevy_app.update()
│ │ ├─> movement_system
│ │ ├─> combat_system
│ │ ├─> resource_system
│ │ └─> replication_system
│ └─> client.update()
│ ├─> 接收复制组件
│ ├─> reconcile
│ └─> interpolate
│
├─> var snapshot = rusty_core.get_frontend_snapshot()
│ └─> client.get_frontend_snapshot()
│ └─> 遍历 ECS → 构建 FrontendSnapshot
│ └─> Dictionary
│
└─> render_snapshot(snapshot)
└─> 更新 Godot 节点
关键数据结构转换
Rust → Godot
FrontendSnapshot (Rust)
├─> units: Vec<FrontendUnit>
│ └─> [
│ FrontendUnit { id, position, health, ... },
│ FrontendUnit { ... },
│ ]
└─> resources: HashMap<u64, PlayerResources>
↓ (gdextension 转换)
Dictionary (Godot)
├─> "units": Array[Dictionary]
│ └─> [
│ { "id": 1, "position": Vector2(100, 200), ... },
│ { "id": 2, ... },
│ ]
└─> "resources": Dictionary
└─> { 0: 5000, 1: 4500 }
Godot → Rust
Dictionary (Godot)
├─> "type": "move"
└─> "target": Vector2(300, 400)
↓ (gdextension 转换)
PlayerInput (Rust)
└─> PlayerInput::Move {
target: Position { x: 300.0, y: 400.0 }
}
状态同步机制
Lightyear 复制流程
Server:
Entity(123)
├─> Position { x: 100.0, y: 200.0 } [Replicated]
├─> Health { current: 80, max: 100 } [Replicated]
└─> Velocity { ... } [Replicated]
↓ (Lightyear 自动序列化和发送)
Client:
Entity(123) (镜像实体)
├─> Position { x: 100.0, y: 200.0 } [从 server 接收]
├─> Health { current: 80, max: 100 } [从 server 接收]
└─> Velocity { ... } [从 server 接收]
下一步
了解核心概念详解:
Content - 内容包系统
content crate 负责游戏数据的加载、验证和管理。
职责
- 解析内容包 manifest
- 加载单位模板、地图模板、资源定义
- Schema 验证
- 计算内容包 fingerprint
- 跨引用检查
- 构建 ContentDatabase
核心概念
ContentId
所有内容使用命名空间 ID:
#![allow(unused)] fn main() { pub struct ContentId { namespace: String, // "official", "mod_name" name: String, // "tank", "builder" } // 示例: // - official:tank // - official:builder // - mega_builders:mega_builder }
ContentDatabase
运行时内容数据库:
#![allow(unused)] fn main() { pub struct ContentDatabase { pub units: HashMap<ContentId, UnitTemplate>, pub maps: HashMap<ContentId, MapTemplate>, pub projectiles: HashMap<ContentId, ProjectileTemplate>, // ... } }
内容包结构
official_base_game/
manifest.toml # 包元数据
units/
tank.toml
builder.toml
engineer.toml
maps/
duel_fields/
map.toml
projectiles/
tank_shell.toml
resources/
credits.toml
factions/
human.toml
加载流程
1. 读取 manifest.toml
2. 验证版本和依赖
3. 加载所有 TOML 文件
4. Schema 验证
5. 解析跨引用
6. 构建 ContentDatabase
7. 计算 fingerprint
使用示例
#![allow(unused)] fn main() { use content::{ContentLoader, ContentDatabase}; // 加载内容包 let loader = ContentLoader::new(); let db = loader.load_package("path/to/official_base_game")?; // 查询单位 let tank = db.get_unit(&ContentId::parse("official:tank")?)?; println!("Tank HP: {}", tank.health); // 加载地图 let map = db.get_map(&ContentId::parse("official:dev_test_map")?)?; println!("Map size: {}x{}", map.width, map.height); }
配置文件格式
manifest.toml
id = "official_base_game"
title = "Official Base Game"
version = "0.1.0"
content_version = 1
[entry]
units = "units"
maps = "maps"
单位模板 (units/tank.toml)
id = "tank"
display_name = "Tank"
[stats]
health = 100
speed = 2.0
cost = 300
[combat]
damage = 15
range = 150
projectile = "official:tank_shell"
下一步
Protocol - 网络协议
protocol crate 定义 client 和 server 的共享协议。
职责
- 定义网络消息
- 定义 Lightyear 复制组件
- 定义玩家输入命令
- 定义同步常量
核心组件
复制组件
使用 Lightyear 的 #[component] 宏:
#![allow(unused)] fn main() { use lightyear::prelude::*; #[derive(Component, Serialize, Deserialize, Clone)] pub struct Position { pub x: f32, pub y: f32, } #[derive(Component, Serialize, Deserialize, Clone)] pub struct Health { pub current: i32, pub max: i32, } }
玩家输入
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize, Clone)] pub enum PlayerInput { Move { target: Position }, Attack { entity_id: u64 }, Build { unit_type: String, position: Position }, Stop, } }
网络消息
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize)] pub enum ClientMessage { Input(PlayerInput), RequestSnapshot, } #[derive(Serialize, Deserialize)] pub enum ServerMessage { Snapshot(GameSnapshot), EntitySpawned { id: u64, template: String }, } }
Lightyear 配置
#![allow(unused)] fn main() { pub const TICK_RATE: f64 = 20.0; // 20 ticks/秒 pub const REPLICATION_INTERVAL: Duration = Duration::from_millis(50); }
为什么独立?
protocol 不依赖 client 或 server,因为:
- Server 和 client 都需要相同的定义
- 防止循环依赖
- 便于版本管理和兼容性检查
使用示例
#![allow(unused)] fn main() { // Server 侧 use protocol::{Position, Health, PlayerInput}; fn spawn_unit(world: &mut World, template: &UnitTemplate) { world.spawn(( Position { x: 0.0, y: 0.0 }, Health { current: 100, max: 100 }, )); } // Client 侧 fn send_move_command(client: &mut Client, target: Position) { client.send(PlayerInput::Move { target }); } }
下一步
Server - 权威服务端
server crate 实现游戏的权威逻辑。
职责
- 维护权威游戏状态 (Bevy ECS World)
- 加载地图和生成单位
- 处理玩家输入命令
- 执行游戏规则 (移动、战斗、资源生产)
- 判断胜负条件
- 向客户端复制状态
架构
Server
├── Bevy App
│ ├── ECS World (权威状态)
│ └── Systems (游戏规则)
├── Command Handler (处理玩家输入)
└── Replication (同步到客户端)
核心系统
移动系统
#![allow(unused)] fn main() { fn movement_system( time: Res<Time>, mut query: Query<(&mut Position, &Velocity)>, ) { for (mut pos, vel) in query.iter_mut() { pos.x += vel.x * time.delta_seconds(); pos.y += vel.y * time.delta_seconds(); } } }
战斗系统
#![allow(unused)] fn main() { fn combat_system( mut commands: Commands, mut targets: Query<(&mut Health, &Position)>, attackers: Query<(&Weapon, &Position, &Target)>, ) { for (weapon, attacker_pos, target) in attackers.iter() { if let Ok((mut health, target_pos)) = targets.get_mut(target.entity) { let distance = attacker_pos.distance(target_pos); if distance <= weapon.range { health.current -= weapon.damage; if health.current <= 0 { commands.entity(target.entity).despawn(); } } } } } }
资源生产系统
#![allow(unused)] fn main() { fn resource_production_system( time: Res<Time>, mut players: Query<(&mut Resources, &Buildings)>, extractors: Query<&Extractor>, ) { for (mut resources, buildings) in players.iter_mut() { for building in buildings.iter() { if let Ok(extractor) = extractors.get(*building) { resources.credits += extractor.rate * time.delta_seconds(); } } } } }
命令处理
#![allow(unused)] fn main() { impl Server { pub fn handle_input(&mut self, player_id: u64, input: PlayerInput) { match input { PlayerInput::Move { target } => { self.execute_move_command(player_id, target); } PlayerInput::Attack { entity_id } => { self.execute_attack_command(player_id, entity_id); } PlayerInput::Build { unit_type, position } => { self.execute_build_command(player_id, unit_type, position); } PlayerInput::Stop => { self.execute_stop_command(player_id); } } } } }
状态复制
使用 Lightyear 自动复制组件:
#![allow(unused)] fn main() { // 标记需要复制的组件 app.add_plugins(ReplicationPlugins) .replicate::<Position>() .replicate::<Health>() .replicate::<Velocity>(); }
地图加载
#![allow(unused)] fn main() { impl Server { pub fn load_map(&mut self, map_id: &ContentId) -> Result<()> { let map = self.content_db.get_map(map_id)?; // 生成地形 for layer in &map.terrain_layers { self.spawn_terrain(layer)?; } // 生成初始单位 for unit in &map.initial_units { self.spawn_unit(unit)?; } // 设置队伍和资源 for team in &map.teams { self.setup_team(team)?; } Ok(()) } } }
下一步
- 了解客户端实现:Client
- 了解运行时编排:Runtime Core
Client - 客户端核心
client crate 负责客户端状态管理和预测。
职责
- 接收玩家输入
- 本地预测 (立即反馈)
- 接收服务器权威状态
- Reconciliation (校正预测)
- 插值 (平滑显示)
- 生成前端 Snapshot
预测机制
为什么需要预测?
网络延迟 (50-100ms) 会导致操作感觉"卡顿"。预测让玩家感觉操作是即时的。
时刻 t=0: 玩家点击移动
时刻 t=0: Client 立即预测 → 单位开始移动 (预测)
时刻 t=0: 发送输入到 Server
时刻 t=50ms: Server 收到输入
时刻 t=50ms: Server 执行权威移动
时刻 t=50ms: Server 发送权威状态
时刻 t=100ms: Client 收到权威状态
时刻 t=100ms: Client 校正 (如有偏差)
预测实现
#![allow(unused)] fn main() { impl Client { pub fn predict_input(&mut self, input: PlayerInput) { // 1. 保存输入到历史 self.input_history.push(input.clone()); // 2. 立即在本地模拟 match input { PlayerInput::Move { target } => { self.predict_move(target); } PlayerInput::Attack { entity_id } => { self.predict_attack(entity_id); } _ => {} } // 3. 发送到服务器 self.send_to_server(input); } } }
Reconciliation (校正)
#![allow(unused)] fn main() { impl Client { pub fn reconcile(&mut self, server_state: ServerSnapshot) { for entity in server_state.entities { // 获取本地预测的位置 let predicted_pos = self.get_predicted_position(entity.id); // 获取服务器权威位置 let authoritative_pos = entity.position; // 计算误差 let error = predicted_pos.distance(authoritative_pos); if error > RECONCILE_THRESHOLD { // 误差过大,平滑校正 self.smooth_correct(entity.id, authoritative_pos); } else { // 误差可接受,接受预测 self.accept_prediction(entity.id); } } } } }
插值 (Interpolation)
对于非本地玩家控制的实体,使用插值平滑显示:
#![allow(unused)] fn main() { fn interpolate_position( current: Position, target: Position, alpha: f32, // 0.0 到 1.0 ) -> Position { Position { x: current.x + (target.x - current.x) * alpha, y: current.y + (target.y - current.y) * alpha, } } }
前端 Snapshot
Client 生成前端可消费的数据:
#![allow(unused)] fn main() { pub struct FrontendSnapshot { pub units: Vec<FrontendUnit>, pub resources: HashMap<u64, PlayerResources>, pub game_time: f64, } pub struct FrontendUnit { pub id: u64, pub position: (f32, f32), pub health: (i32, i32), // (current, max) pub unit_type: String, pub team_id: u8, } impl Client { pub fn get_frontend_snapshot(&self) -> FrontendSnapshot { // 将 ECS 组件转换为前端格式 FrontendSnapshot { units: self.extract_units(), resources: self.extract_resources(), game_time: self.game_time, } } } }
输入缓冲
#![allow(unused)] fn main() { pub struct InputBuffer { inputs: VecDeque<(Tick, PlayerInput)>, last_acknowledged: Tick, } impl InputBuffer { pub fn add(&mut self, tick: Tick, input: PlayerInput) { self.inputs.push_back((tick, input)); } pub fn acknowledge(&mut self, tick: Tick) { // 服务器确认收到,移除旧输入 self.inputs.retain(|(t, _)| *t > tick); self.last_acknowledged = tick; } } }
使用示例
#![allow(unused)] fn main() { use client::Client; use protocol::PlayerInput; let mut client = Client::new("127.0.0.1:5000")?; // 玩家输入 let input = PlayerInput::Move { target: Position { x: 100.0, y: 200.0 } }; // 预测并发送 client.predict_input(input); // 每帧更新 client.update(delta_time)?; // 获取显示数据 let snapshot = client.get_frontend_snapshot(); }
下一步
- 了解运行时编排:Runtime Core
- 了解 Godot 接入:GDExtension
Runtime Core - 运行时核心
runtime_core crate 负责运行模式编排和生命周期管理。
职责
- 管理运行模式 (单人/主机/远程)
- 启动和关闭 server/client
- 协调 server 和 client 的更新
- 前端 Snapshot 投影
- 错误处理和状态管理
核心类型
Runtime
#![allow(unused)] fn main() { pub struct Runtime { mode: RuntimeMode, server: Option<Server>, client: Option<Client>, content_db: Option<ContentDatabase>, } }
RuntimeMode
#![allow(unused)] fn main() { pub enum RuntimeMode { None, Singleplayer, Host { port: u16 }, Client { server_addr: String }, } }
RuntimeConfig
#![allow(unused)] fn main() { pub struct RuntimeConfig { pub assets_root: PathBuf, pub content_package_path: PathBuf, } }
核心 API
初始化
#![allow(unused)] fn main() { impl Runtime { pub fn new() -> Self { Runtime { mode: RuntimeMode::None, server: None, client: None, content_db: None, } } pub fn initialize(&mut self, config: RuntimeConfig) -> Result<()> { // 加载内容包 let loader = ContentLoader::new(); self.content_db = Some(loader.load_package(&config.content_package_path)?); Ok(()) } } }
启动模式
#![allow(unused)] fn main() { impl Runtime { pub fn start_singleplayer(&mut self) -> Result<()> { let content_db = self.content_db.as_ref() .ok_or(RuntimeError::NotInitialized)?; // 创建本地 server let server = Server::new_local(content_db.clone())?; // 创建本地 client let client = Client::new_local()?; // 建立内存通道 self.connect_local(&mut server, &mut client)?; self.server = Some(server); self.client = Some(client); self.mode = RuntimeMode::Singleplayer; Ok(()) } pub fn start_host(&mut self, port: u16) -> Result<()> { // 类似单人模式,但 server 监听网络端口 // ... } pub fn start_client(&mut self, server_addr: String) -> Result<()> { // 仅创建 client,连接远程 server // ... } } }
更新循环
#![allow(unused)] fn main() { impl Runtime { pub fn update(&mut self, delta_seconds: f64) -> Result<()> { match self.mode { RuntimeMode::Singleplayer => { // 更新 server if let Some(server) = &mut self.server { server.update(delta_seconds)?; } // 更新 client if let Some(client) = &mut self.client { client.update(delta_seconds)?; } } RuntimeMode::Host { .. } => { // 同上 } RuntimeMode::Client { .. } => { // 仅更新 client if let Some(client) = &mut self.client { client.update(delta_seconds)?; } } RuntimeMode::None => { return Err(RuntimeError::NotStarted); } } Ok(()) } } }
前端 Snapshot
#![allow(unused)] fn main() { impl Runtime { pub fn get_frontend_snapshot(&self) -> Option<FrontendSnapshot> { self.client.as_ref().map(|c| c.get_frontend_snapshot()) } } }
关闭
#![allow(unused)] fn main() { impl Runtime { pub fn shutdown(&mut self) -> Result<()> { if let Some(mut server) = self.server.take() { server.shutdown()?; } if let Some(mut client) = self.client.take() { client.shutdown()?; } self.mode = RuntimeMode::None; Ok(()) } } }
错误处理
#![allow(unused)] fn main() { #[derive(Debug, thiserror::Error)] pub enum RuntimeError { #[error("Runtime not initialized")] NotInitialized, #[error("Runtime not started")] NotStarted, #[error("Server error: {0}")] ServerError(#[from] ServerError), #[error("Client error: {0}")] ClientError(#[from] ClientError), #[error("Content error: {0}")] ContentError(#[from] ContentError), } }
为什么需要 runtime_core?
如果没有 runtime_core,gdextension 会直接管理:
- Server 和 client 的生命周期
- 网络配置
- 内容加载
- 模式切换
这会让 gdextension 变得臃肿,且难以测试。
runtime_core 提供纯 Rust API,使得:
- 可以编写不依赖 Godot 的测试
- 未来可支持其他前端 (如纯 Bevy 渲染)
- GDExtension 层只做类型转换
使用示例
#![allow(unused)] fn main() { use runtime_core::{Runtime, RuntimeConfig}; let mut runtime = Runtime::new(); // 初始化 runtime.initialize(RuntimeConfig { assets_root: PathBuf::from("assets"), content_package_path: PathBuf::from("assets/official"), })?; // 启动单人模式 runtime.start_singleplayer()?; // 游戏循环 loop { runtime.update(0.016)?; // 60 FPS if let Some(snapshot) = runtime.get_frontend_snapshot() { // 渲染 snapshot } } // 关闭 runtime.shutdown()?; }
下一步
- 了解 Godot 接入:GDExtension
- 了解构建流程:Builder
GDExtension - Godot接入
gdextension crate 负责 Rust ↔ Godot 的边界适配。
职责
- 注册 Godot 类 (RustyCore)
- Rust 类型 ↔ Godot 类型转换
- 暴露 Rust API 给 GDScript
- 处理跨语言调用
核心类型
RustyCore
#![allow(unused)] fn main() { use godot::prelude::*; #[derive(GodotClass)] #[class(base=Node)] pub struct RustyCore { runtime: Runtime, #[base] base: Base<Node>, } }
暴露的 API
初始化
#![allow(unused)] fn main() { #[godot_api] impl RustyCore { #[func] pub fn initialize(&mut self, config: Dictionary) { let rust_config = RuntimeConfig { assets_root: PathBuf::from( config.get("assets_root") .unwrap() .to::<GString>() .to_string() ), content_package_path: PathBuf::from( config.get("content_package_path") .unwrap() .to::<GString>() .to_string() ), }; self.runtime.initialize(rust_config).unwrap(); } } }
启动游戏
#![allow(unused)] fn main() { #[godot_api] impl RustyCore { #[func] pub fn start_singleplayer(&mut self) { self.runtime.start_singleplayer().unwrap(); } #[func] pub fn start_host(&mut self, port: i32) { self.runtime.start_host(port as u16).unwrap(); } #[func] pub fn start_client(&mut self, server_addr: GString) { self.runtime.start_client(server_addr.to_string()).unwrap(); } } }
更新和获取数据
#![allow(unused)] fn main() { #[godot_api] impl RustyCore { #[func] pub fn update(&mut self, delta: f64) { self.runtime.update(delta).unwrap(); } #[func] pub fn get_frontend_snapshot(&self) -> Dictionary { let snapshot = self.runtime.get_frontend_snapshot(); snapshot_to_dictionary(snapshot) } } }
发送命令
#![allow(unused)] fn main() { #[godot_api] impl RustyCore { #[func] pub fn send_command(&mut self, command: Dictionary) { let input = dictionary_to_player_input(command); self.runtime.send_input(input).unwrap(); } } }
类型转换
FrontendSnapshot → Dictionary
#![allow(unused)] fn main() { fn snapshot_to_dictionary(snapshot: FrontendSnapshot) -> Dictionary { let mut dict = Dictionary::new(); // 转换单位列表 let mut units_array = Array::new(); for unit in snapshot.units { let mut unit_dict = Dictionary::new(); unit_dict.set("id", unit.id); unit_dict.set("position", Vector2::new(unit.position.0, unit.position.1)); unit_dict.set("health", unit.health.0); unit_dict.set("max_health", unit.health.1); unit_dict.set("unit_type", GString::from(unit.unit_type)); units_array.push(unit_dict); } dict.set("units", units_array); // 转换资源 let mut resources_dict = Dictionary::new(); for (player_id, res) in snapshot.resources { resources_dict.set(player_id, res.credits); } dict.set("resources", resources_dict); dict } }
Dictionary → PlayerInput
#![allow(unused)] fn main() { fn dictionary_to_player_input(dict: Dictionary) -> PlayerInput { let cmd_type = dict.get("type").unwrap().to::<GString>().to_string(); match cmd_type.as_str() { "move" => { let target = dict.get("target").unwrap().to::<Vector2>(); PlayerInput::Move { target: Position { x: target.x, y: target.y, } } } "attack" => { let entity_id = dict.get("entity_id").unwrap().to::<u64>(); PlayerInput::Attack { entity_id } } _ => panic!("Unknown command type"), } } }
GDScript 使用示例
extends Node
var rusty_core: RustyCore
func _ready():
rusty_core = RustyCore.new()
add_child(rusty_core)
# 初始化
rusty_core.initialize({
"assets_root": "res://assets",
"content_package_path": "res://assets/official"
})
# 启动单人游戏
rusty_core.start_singleplayer()
func _process(delta):
# 更新游戏逻辑
rusty_core.update(delta)
# 获取数据
var snapshot = rusty_core.get_frontend_snapshot()
render_snapshot(snapshot)
func _input(event):
if event is InputEventMouseButton and event.pressed:
# 发送移动命令
rusty_core.send_command({
"type": "move",
"target": get_global_mouse_position()
})
构建和部署
使用 builder 工具自动构建:
cargo run -p builder
产物位置:
launcher/godot/addons/rusty_core/bin/
windows/libgdextension.dll
linux/libgdextension.so
macos/libgdextension.dylib
注意事项
线程安全
Godot 在主线程调用 Rust,确保:
- 不在 Rust 中持有 Godot 对象的可变引用
- 使用
Gd<T>而非&T
错误处理
GDScript 没有 Result,错误需要转换:
#![allow(unused)] fn main() { #[func] pub fn start_singleplayer(&mut self) -> bool { match self.runtime.start_singleplayer() { Ok(_) => true, Err(e) => { godot_error!("Failed to start: {}", e); false } } } }
下一步
Builder - 构建工具
builder crate 负责跨平台编译和部署 GDExtension。
职责
- 编译 gdextension 为动态库
- 复制产物到 Godot 项目
- 生成 .gdextension 配置文件
- 处理跨平台路径
使用方法
cargo run -p builder
构建流程
1. 检测当前平台
2. 编译 gdextension (release)
3. 复制动态库到目标目录
4. 生成 .gdextension 文件
实现示例
fn main() -> Result<()> { let target_dir = detect_platform_target()?; // 编译 gdextension println!("Building gdextension..."); let status = Command::new("cargo") .args(&["build", "-p", "gdextension", "--release"]) .status()?; if !status.success() { return Err("Build failed".into()); } // 复制产物 let lib_name = get_lib_name(); let src = format!("target/release/{}", lib_name); let dst = format!("launcher/godot/addons/rusty_core/bin/{}/{}", target_dir, lib_name); fs::copy(&src, &dst)?; println!("Copied {} -> {}", src, dst); // 生成配置 generate_gdextension_file()?; Ok(()) } fn get_lib_name() -> &'static str { if cfg!(target_os = "windows") { "gdextension.dll" } else if cfg!(target_os = "macos") { "libgdextension.dylib" } else { "libgdextension.so" } }
产物结构
launcher/godot/addons/rusty_core/
rusty_core.gdextension # 配置文件
bin/
windows/
libgdextension.dll
linux/
libgdextension.so
macos/
libgdextension.dylib
android/
arm64-v8a/
libgdextension.so
.gdextension 配置
[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.2
[libraries]
windows.debug.x86_64 = "res://addons/rusty_core/bin/windows/libgdextension.dll"
windows.release.x86_64 = "res://addons/rusty_core/bin/windows/libgdextension.dll"
linux.debug.x86_64 = "res://addons/rusty_core/bin/linux/libgdextension.so"
linux.release.x86_64 = "res://addons/rusty_core/bin/linux/libgdextension.so"
macos.debug = "res://addons/rusty_core/bin/macos/libgdextension.dylib"
macos.release = "res://addons/rusty_core/bin/macos/libgdextension.dylib"
跨平台构建
Windows → Android
# 安装目标
rustup target add aarch64-linux-android
# 配置 NDK 路径
export ANDROID_NDK_HOME=/path/to/ndk
# 构建
cargo build -p gdextension --target aarch64-linux-android --release
交叉编译
# Linux → Windows
cargo build -p gdextension --target x86_64-pc-windows-gnu --release
# macOS → iOS
cargo build -p gdextension --target aarch64-apple-ios --release
下一步
权威服务端
权威服务端 (Authoritative Server) 是 RustyWarfare 的核心架构原则。
核心原则
所有游戏规则在服务端执行,客户端只负责输入和显示。
为什么需要权威服务端?
防止作弊
如果客户端能决定游戏结果:
- 玩家可以修改内存,让单位无敌
- 玩家可以修改资源数量
- 玩家可以修改移动速度
权威服务端架构下:
- 客户端只发送"我想移动到这里"
- 服务端检查是否合法
- 服务端执行移动并告诉客户端结果
- 客户端无法直接修改游戏状态
保证一致性
多人游戏中,所有玩家必须看到相同的游戏状态。
权威服务端保证:
- 只有一个真实状态(服务端)
- 所有客户端同步到这个状态
- 不会出现"我这里他死了,他那里还活着"
架构对比
❌ 点对点架构 (P2P)
Client A ←→ Client B ←→ Client C
问题:
- 每个客户端维护自己的状态
- 网络延迟导致状态不一致
- 容易作弊
✅ 权威服务端架构
Server (权威)
↙ ↓ ↘
Client A B C
优点:
- Server 维护唯一真实状态
- Client 只是"视图"
- 无法作弊
实现细节
服务端职责
#![allow(unused)] fn main() { // 服务端决定战斗结果 fn combat_system( mut targets: Query<&mut Health>, attackers: Query<(&Damage, &Target)>, ) { for (damage, target) in attackers.iter() { if let Ok(mut health) = targets.get_mut(target.entity) { health.current -= damage.value; // 服务端权威扣血 } } } }
客户端职责
#![allow(unused)] fn main() { // 客户端只能"请求" fn send_attack_command(client: &mut Client, target_id: u64) { client.send(PlayerInput::Attack { entity_id: target_id }); // 客户端无法直接扣血,只能请求 } }
客户端预测
权威服务端 ≠ 卡顿。通过客户端预测掩盖延迟:
t=0ms: 玩家点击移动
t=0ms: 客户端立即预测 (单位开始移动)
t=0ms: 发送请求到服务端
t=50ms: 服务端收到请求
t=50ms: 服务端验证并执行
t=50ms: 服务端发送结果
t=100ms: 客户端收到权威结果
t=100ms: 对比预测和权威
t=100ms: 如有偏差,平滑校正
玩家感觉:即时响应 (因为 t=0ms 就有预测)
实际情况:服务端权威 (t=100ms 会校正错误预测)
单人模式也是权威服务端
单人模式不是特例,架构完全相同:
#![allow(unused)] fn main() { // 单人模式 Godot → Client (预测) → Server (权威) → Client (校正) → Godot // 多人模式 Godot → Client (预测) → Server (权威) → Client (校正) → Godot ↓ 其他玩家的 Client }
区别仅在于:
- 单人:Server 在本地进程
- 多人:Server 在远程
权威检查示例
移动检查
#![allow(unused)] fn main() { fn validate_move(server: &Server, unit_id: u64, target: Position) -> bool { let unit = server.get_unit(unit_id)?; // 检查是否属于该玩家 if unit.owner != player_id { return false; } // 检查距离是否合理 let distance = unit.position.distance(target); if distance > unit.speed * server.tick_duration() { return false; } // 检查地形是否可通行 if !server.map.is_walkable(target) { return false; } true } }
攻击检查
#![allow(unused)] fn main() { fn validate_attack(server: &Server, attacker_id: u64, target_id: u64) -> bool { let attacker = server.get_unit(attacker_id)?; let target = server.get_unit(target_id)?; // 检查是否是敌人 if attacker.team == target.team { return false; } // 检查距离 let distance = attacker.position.distance(target.position); if distance > attacker.weapon.range { return false; } true } }
优势总结
- 防作弊:客户端无法修改游戏状态
- 一致性:所有玩家看到相同状态
- 简化逻辑:只需实现一套规则(服务端)
- 可扩展:容易添加观战、回放等功能
下一步
ECS架构
RustyWarfare 使用 Bevy ECS (Entity Component System) 管理游戏状态。
什么是 ECS?
ECS 是一种数据驱动的架构模式,将游戏对象拆分为:
- Entity (实体):唯一 ID,代表游戏中的"事物"
- Component (组件):纯数据,描述实体的"属性"
- System (系统):逻辑,处理特定组件的"行为"
核心概念
Entity (实体)
#![allow(unused)] fn main() { // 实体只是一个 ID let tank_entity = world.spawn_empty().id(); // Entity(123) }
Component (组件)
#![allow(unused)] fn main() { #[derive(Component)] pub struct Position { pub x: f32, pub y: f32, } #[derive(Component)] pub struct Health { pub current: i32, pub max: i32, } #[derive(Component)] pub struct Velocity { pub x: f32, pub y: f32, } }
组合实体
#![allow(unused)] fn main() { // 创建一个坦克 world.spawn(( Position { x: 100.0, y: 200.0 }, Health { current: 100, max: 100 }, Velocity { x: 0.0, y: 0.0 }, Tank, // 标记组件 )); }
System (系统)
#![allow(unused)] fn main() { fn movement_system( time: Res<Time>, mut query: Query<(&mut Position, &Velocity)>, ) { for (mut pos, vel) in query.iter_mut() { pos.x += vel.x * time.delta_seconds(); pos.y += vel.y * time.delta_seconds(); } } }
为什么使用 ECS?
传统 OOP 的问题
#![allow(unused)] fn main() { // OOP: 继承层级复杂 class Unit { ... } class LandUnit extends Unit { ... } class Tank extends LandUnit { ... } class HoverTank extends Tank { ... } // 会飞的坦克怎么办? }
ECS 的灵活性
#![allow(unused)] fn main() { // 坦克 world.spawn((Position, Health, LandMovement, Turret)); // 会飞的坦克?加个组件即可 world.spawn((Position, Health, AirMovement, Turret)); // 两栖坦克? world.spawn((Position, Health, LandMovement, WaterMovement, Turret)); }
性能优势
ECS 将相同类型的组件存储在连续内存:
传统 OOP:
Tank1 [Position, Health, ...] → 内存位置 A
Tank2 [Position, Health, ...] → 内存位置 B (可能很远)
Tank3 [Position, Health, ...] → 内存位置 C (可能很远)
ECS:
所有 Position: [Pos1, Pos2, Pos3, ...] → 连续内存
所有 Health: [HP1, HP2, HP3, ...] → 连续内存
CPU 缓存友好 = 更快!
RustyWarfare 中的应用
单位组件
#![allow(unused)] fn main() { // 所有单位共有 Position Health Team // 可移动单位 Velocity MovementType (Land/Air/Water) // 战斗单位 Weapon Target // 建筑 Building ProductionQueue }
系统调度
#![allow(unused)] fn main() { fn configure_systems(app: &mut App) { app .add_systems(Update, ( movement_system, combat_system, production_system, resource_system, ).chain()); // 按顺序执行 } }
Query 查询
基础查询
#![allow(unused)] fn main() { // 查询所有有 Position 和 Health 的实体 fn display_system( query: Query<(&Position, &Health)>, ) { for (pos, health) in query.iter() { println!("Entity at ({}, {}) has {} HP", pos.x, pos.y, health.current); } } }
可变查询
#![allow(unused)] fn main() { // 修改组件 fn damage_system( mut query: Query<&mut Health>, ) { for mut health in query.iter_mut() { health.current -= 10; } } }
过滤查询
#![allow(unused)] fn main() { // 只查询敌方单位 fn target_enemy_system( my_team: Res<MyTeam>, query: Query<(Entity, &Position), With<Enemy>>, ) { for (entity, pos) in query.iter() { // 只处理敌人 } } }
Commands 延迟操作
#![allow(unused)] fn main() { fn spawn_projectile_system( mut commands: Commands, query: Query<(&Position, &Weapon)>, ) { for (pos, weapon) in query.iter() { // 延迟生成,不立即执行 commands.spawn(( Position { x: pos.x, y: pos.y }, Projectile, Velocity { x: 10.0, y: 0.0 }, )); } } // 系统结束后才真正生成 }
Resources 全局数据
#![allow(unused)] fn main() { #[derive(Resource)] pub struct GameTime { pub elapsed: f64, } fn time_system( time: Res<Time>, mut game_time: ResMut<GameTime>, ) { game_time.elapsed += time.delta_seconds(); } }
实践示例
完整的战斗系统
#![allow(unused)] fn main() { fn combat_system( mut commands: Commands, time: Res<Time>, mut targets: Query<(Entity, &mut Health, &Position)>, attackers: Query<(&Position, &Weapon, &Target)>, ) { for (attacker_pos, weapon, target) in attackers.iter() { if let Ok((entity, mut health, target_pos)) = targets.get_mut(target.entity) { // 检查距离 let distance = attacker_pos.distance(target_pos); if distance > weapon.range { continue; } // 检查冷却 if weapon.cooldown_remaining > 0.0 { continue; } // 造成伤害 health.current -= weapon.damage; // 死亡检查 if health.current <= 0 { commands.entity(entity).despawn(); } } } } }
优势总结
- 灵活组合:通过组件组合创建复杂实体
- 高性能:数据连续存储,缓存友好
- 并行友好:不同系统可并行执行
- 清晰职责:每个系统只处理特定逻辑
下一步
网络同步
RustyWarfare 使用 Lightyear 实现客户端-服务端状态同步。
核心机制
1. 复制 (Replication)
标记需要同步的组件:
#![allow(unused)] fn main() { use lightyear::prelude::*; #[derive(Component, Serialize, Deserialize)] pub struct Position { x: f32, y: f32 } // 注册复制 app.replicate::<Position>(); }
服务端的 Position 组件会自动同步到客户端。
2. 复制组
#![allow(unused)] fn main() { // 服务端 let entity = world.spawn(( Position { x: 100.0, y: 200.0 }, Health { current: 100, max: 100 }, Replicated, // 标记为需要复制的实体 )); }
3. 客户端接收
#![allow(unused)] fn main() { // 客户端自动创建镜像实体 fn on_replicate( mut commands: Commands, query: Query<(Entity, &Position, &Health), Added<Replicated>>, ) { for (entity, pos, health) in query.iter() { println!("New entity {} at ({}, {})", entity, pos.x, pos.y); } } }
同步策略
全量同步 vs 增量同步
全量同步:每次发送完整状态
#![allow(unused)] fn main() { struct Snapshot { entities: Vec<EntityState>, // 所有实体 } }
增量同步:只发送变化
#![allow(unused)] fn main() { struct Delta { changed: Vec<(EntityId, Component)>, // 只有变化的组件 removed: Vec<EntityId>, } }
Lightyear 默认使用增量同步。
可靠性设置
#![allow(unused)] fn main() { #[derive(Component, Serialize, Deserialize)] #[replicate(reliability = Reliable)] // 保证送达 pub struct Health { ... } #[derive(Component, Serialize, Deserialize)] #[replicate(reliability = Unreliable)] // 允许丢包 pub struct Velocity { ... } }
- Reliable:重要数据(生命值、位置)
- Unreliable:可丢失数据(速度、临时特效)
Tick 系统
固定时间步
#![allow(unused)] fn main() { pub const SERVER_TICK_RATE: f64 = 20.0; // 20 ticks/秒 pub const TICK_DURATION: f64 = 1.0 / 20.0; // 50ms }
服务端以固定频率更新:
#![allow(unused)] fn main() { fn fixed_update(world: &mut World) { // 每 50ms 执行一次 world.run_schedule(FixedUpdate); } }
Tick 编号
#![allow(unused)] fn main() { pub struct Tick(pub u32); // 服务端 let current_tick = server.tick(); // Tick(100) // 客户端 let server_tick = client.server_tick(); // Tick(98) (延迟 2 tick) }
兴趣管理 (Interest Management)
只同步玩家"感兴趣"的实体:
#![allow(unused)] fn main() { fn update_interest( mut clients: Query<&mut ClientInterest>, players: Query<&Position, With<Player>>, entities: Query<(Entity, &Position)>, ) { for (mut interest, player_pos) in clients.iter_mut() { interest.clear(); // 只同步玩家视野内的实体 for (entity, pos) in entities.iter() { if player_pos.distance(pos) < VISION_RADIUS { interest.add(entity); } } } } }
减少带宽:
- 不同步战场另一端的单位
- 迷雾中的单位不同步
输入同步
客户端发送输入
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize)] pub struct PlayerInput { tick: Tick, command: Command, } // 客户端 client.send_input(PlayerInput { tick: client.tick(), command: Command::Move { target: pos }, }); }
服务端处理输入
#![allow(unused)] fn main() { fn process_inputs( mut events: EventReader<InputEvent>, mut units: Query<&mut Position>, ) { for event in events.read() { if let Command::Move { target } = event.input.command { if let Ok(mut pos) = units.get_mut(event.entity) { // 执行移动 *pos = target; } } } } }
输入缓冲
服务端缓冲一小段时间的输入,处理乱序:
#![allow(unused)] fn main() { struct InputBuffer { buffer: BTreeMap<Tick, Vec<PlayerInput>>, oldest_tick: Tick, } impl InputBuffer { fn add(&mut self, input: PlayerInput) { self.buffer.entry(input.tick) .or_insert_with(Vec::new) .push(input); } fn drain_up_to(&mut self, tick: Tick) -> Vec<PlayerInput> { self.buffer.range(..=tick) .flat_map(|(_, inputs)| inputs.clone()) .collect() } } }
带宽优化
1. 组件优先级
#![allow(unused)] fn main() { #[replicate(priority = High)] pub struct Position { ... } #[replicate(priority = Low)] pub struct Cosmetic { ... } }
带宽不足时,优先发送 High 优先级的组件。
2. 更新频率
#![allow(unused)] fn main() { #[replicate(send_frequency = 10.0)] // 每秒 10 次 pub struct Position { ... } #[replicate(send_frequency = 2.0)] // 每秒 2 次 pub struct Health { ... } }
3. 压缩
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize)] pub struct CompressedPosition { x: i16, // -32768 到 32767 y: i16, } impl From<Position> for CompressedPosition { fn from(pos: Position) -> Self { Self { x: (pos.x * 10.0) as i16, // 精度 0.1 y: (pos.y * 10.0) as i16, } } } }
延迟补偿
服务端延迟补偿
#![allow(unused)] fn main() { fn lag_compensation( input: &PlayerInput, history: &StateHistory, ) -> ServerState { // 回退到客户端发送输入时的状态 let client_tick = input.tick; let state_at_input_time = history.get(client_tick); // 在该状态下验证输入 validate_input(input, state_at_input_time) } }
客户端插值
#![allow(unused)] fn main() { fn interpolate_entities( query: Query<(&NetworkedEntity, &mut Position)>, time: Res<InterpolationTime>, ) { for (networked, mut pos) in query.iter_mut() { let t0 = networked.snapshot_t0; let t1 = networked.snapshot_t1; let alpha = (time.current - t0) / (t1 - t0); *pos = Position::lerp(networked.pos_t0, networked.pos_t1, alpha); } } }
实战示例
完整的同步流程
1. 服务端 Tick 100
- 执行游戏逻辑
- 标记变化的组件
2. Lightyear 收集变化
- Position 组件改变
- Health 组件未变
3. 打包并发送
- 只发送 Position
- 添加 Tick 编号
4. 客户端接收 (Tick 98)
- 解包数据
- 更新镜像实体
5. 客户端插值 (Tick 98 → 100)
- 平滑过渡
- 显示到屏幕
诊断工具
网络统计
#![allow(unused)] fn main() { let stats = client.network_stats(); println!("RTT: {}ms", stats.rtt); println!("Packet loss: {:.1}%", stats.packet_loss * 100.0); println!("Bandwidth: {} KB/s", stats.bandwidth_usage / 1024.0); }
可视化
#![allow(unused)] fn main() { fn debug_networking( mut gizmos: Gizmos, clients: Query<&NetworkStats>, ) { for stats in clients.iter() { // 绘制延迟图表 gizmos.line( Vec2::ZERO, Vec2::new(stats.rtt as f32, 0.0), Color::RED, ); } } }
下一步
客户端预测
客户端预测是掩盖网络延迟、提供即时反馈的关键技术。
问题:网络延迟
典型网络延迟:50-100ms
没有预测的体验:
t=0ms: 玩家点击移动
t=0ms: 发送到服务器
t=50ms: 服务器收到并处理
t=100ms: 客户端收到结果,单位才开始移动
玩家感觉:卡顿!点击后要等 100ms 才有反应
解决方案:立即预测
有预测的体验:
t=0ms: 玩家点击移动
t=0ms: 客户端立即预测 → 单位立即移动
t=0ms: 同时发送到服务器
t=50ms: 服务器处理
t=100ms: 客户端收到权威结果 → 校正(如有偏差)
玩家感觉:即时响应!
预测流程
1. 输入阶段
#![allow(unused)] fn main() { fn on_click(client: &mut Client, target: Position) { let input = PlayerInput::Move { target }; // 1. 保存到历史 client.input_history.push(input.clone()); // 2. 立即本地模拟(预测) client.predict_move(target); // 3. 发送到服务器 client.send_to_server(input); } }
2. 预测阶段
#![allow(unused)] fn main() { fn predict_move(&mut self, target: Position) { // 获取选中单位 for entity in self.selected_units.iter() { if let Some(unit) = self.predicted_state.get_mut(entity) { // 立即更新预测状态 unit.target = Some(target); unit.is_moving = true; } } } }
3. 校正阶段
#![allow(unused)] fn main() { fn reconcile(&mut self, server_snapshot: ServerSnapshot) { for entity in server_snapshot.entities { let predicted = self.predicted_state.get(entity.id); let authoritative = entity.position; let error = predicted.distance(authoritative); if error < 0.1 { // 预测准确! self.accept_prediction(entity.id); } else if error < 5.0 { // 小偏差,平滑校正 self.smooth_correct(entity.id, authoritative); } else { // 大偏差,直接跳转 self.hard_correct(entity.id, authoritative); } } } }
预测什么?
✅ 应该预测
- 移动:路径确定,容易预测
- 转向:即时反馈重要
- 动画状态:行走、攻击动画
❌ 不应该预测
- 战斗结果:依赖双方状态,难以准确预测
- 资源生产:服务端权威
- 单位死亡:必须等待服务端确认
预测准确性
准确预测示例
玩家:移动到 (100, 200)
预测:单位沿直线移动到 (100, 200)
服务端:确认,单位确实到达 (100, 200)
结果:预测完全准确,无需校正 ✅
需要校正的情况
玩家:移动到 (100, 200)
预测:单位沿直线移动
服务端:路径被敌人阻挡,单位停在 (80, 180)
结果:预测偏差 20 像素,平滑校正 ⚠️
严重错误的情况
玩家:攻击单位 A
预测:单位 A 立即死亡
服务端:单位 A 存活(护盾吸收伤害)
结果:预测完全错误,硬校正 ❌
校正策略
平滑校正 (Smooth Correction)
小误差用插值平滑过渡:
#![allow(unused)] fn main() { fn smooth_correct(&mut self, entity_id: u64, target: Position) { let current = self.get_position(entity_id); let correction_speed = 5.0; // 每秒校正 5 单位 self.correction_targets.insert(entity_id, CorrectionTarget { from: current, to: target, speed: correction_speed, }); } fn update_corrections(&mut self, delta: f32) { for (entity_id, correction) in self.correction_targets.iter_mut() { let current = self.get_position(entity_id); let direction = (correction.to - current).normalize(); let step = direction * correction.speed * delta; let new_pos = current + step; self.set_position(entity_id, new_pos); if current.distance(correction.to) < 0.5 { // 校正完成 self.correction_targets.remove(entity_id); } } } }
硬校正 (Hard Correction)
严重错误直接跳转:
#![allow(unused)] fn main() { fn hard_correct(&mut self, entity_id: u64, authoritative: Position) { self.set_position(entity_id, authoritative); // 可能播放"传送"特效 } }
输入历史
保存输入历史用于回放:
#![allow(unused)] fn main() { pub struct InputHistory { inputs: VecDeque<(Tick, PlayerInput)>, last_acked: Tick, } impl InputHistory { pub fn add(&mut self, tick: Tick, input: PlayerInput) { self.inputs.push_back((tick, input)); } pub fn acknowledge(&mut self, server_tick: Tick) { // 服务端确认到某个 tick,清理旧输入 self.inputs.retain(|(t, _)| *t > server_tick); self.last_acked = server_tick; } pub fn replay_from(&self, tick: Tick) -> Vec<PlayerInput> { // 回放从某个 tick 之后的所有输入 self.inputs.iter() .filter(|(t, _)| *t > tick) .map(|(_, input)| input.clone()) .collect() } } }
预测 + 回滚
当服务端状态与预测差异较大时,需要回滚:
1. 客户端预测了 tick 100-110 的输入
2. 服务端返回 tick 105 的权威状态
3. 客户端发现 tick 105 的预测错误
4. 回滚到 tick 105
5. 重新应用 tick 106-110 的输入(基于正确的 105 状态)
#![allow(unused)] fn main() { fn rollback_and_replay(&mut self, server_tick: Tick, server_state: ServerSnapshot) { // 1. 恢复到服务器 tick self.state = server_state; // 2. 获取之后的所有输入 let inputs_to_replay = self.input_history.replay_from(server_tick); // 3. 重新模拟 for input in inputs_to_replay { self.simulate_input(input); } } }
优化技巧
1. 预测置信度
#![allow(unused)] fn main() { pub enum PredictionConfidence { High, // 移动等确定性操作 Medium, // 受其他单位影响的操作 Low, // 战斗结果等不确定操作 } }
低置信度的预测可以:
- 延迟显示
- 显示"预测中"标记
- 更快校正
2. 部分预测
#![allow(unused)] fn main() { // 只预测位置,不预测战斗结果 fn predict_move(&mut self, input: PlayerInput::Move) { self.update_position(input.target); // 不预测沿途的战斗 } }
3. 客户端插值
对于非本地玩家,使用插值而非预测:
#![allow(unused)] fn main() { fn interpolate_remote_players(&mut self, delta: f32) { for entity in self.remote_entities.iter() { let last = entity.last_server_pos; let next = entity.next_server_pos; let alpha = entity.interpolation_progress; let pos = last.lerp(next, alpha); self.set_position(entity.id, pos); entity.interpolation_progress += delta / INTERPOLATION_DELAY; } } }
总结
| 方面 | 无预测 | 有预测 |
|---|---|---|
| 延迟感 | 100ms 延迟 | 即时响应 |
| 实现复杂度 | 简单 | 复杂 |
| 需要校正 | 不需要 | 需要 |
| 用户体验 | 卡顿 | 流畅 |
下一步
内容包系统
内容包系统是 RustyWarfare 的数据驱动核心,支持官方内容、地图和 Mod。
设计理念
游戏规则和游戏数据分离
- 规则:Rust 代码实现(移动、战斗、生产)
- 数据:TOML 配置定义(单位属性、地图布局)
好处:
- 修改数据无需重新编译
- 支持 Mod
- 多人游戏内容一致性验证
内容包结构
official_base_game/
manifest.toml # 包元数据
units/ # 单位定义
tank.toml
builder.toml
maps/ # 地图
duel_fields/
map.toml
projectiles/ # 弹丸
tank_shell.toml
resources/ # 资源类型
credits.toml
使用示例
#![allow(unused)] fn main() { use content::ContentLoader; // 加载内容包 let loader = ContentLoader::new(); let db = loader.load_package("assets/official")?; // 查询单位 let tank = db.get_unit(&ContentId::parse("official:tank")?)?; // 计算 fingerprint let fingerprint = db.fingerprint(); }
多人一致性
通过 fingerprint 保证所有玩家使用相同的内容包:
#![allow(unused)] fn main() { // 客户端连接时验证 if client_fingerprint != server_fingerprint { return Err("Content mismatch"); } }
下一步
- 了解配置格式:配置文件格式
- 了解内容加载:Content 模块
贡献约定
欢迎为 RustyWarfare 贡献代码!
基本流程
- Fork 仓库
- 创建功能分支
- 提交更改
- 创建 Pull Request
代码检查
cargo fmt --check
cargo clippy --workspace
cargo test --workspace
Commit 规范
feat(server): 添加单位生产系统
fix(client): 修复预测错误
docs(readme): 更新安装说明
架构约定
遵循分层设计,禁止反向依赖:
✅ gdextension → runtime_core
✅ server → protocol, content
❌ protocol → server
❌ content → server
下一步
代码规范
Rust 代码风格
使用 rustfmt 和 clippy:
cargo fmt
cargo clippy --workspace
命名规范
- 类型:
PascalCase - 函数/变量:
snake_case - 常量:
SCREAMING_SNAKE_CASE - 生命周期:
'a,'b
错误处理
使用 Result 和 thiserror:
#![allow(unused)] fn main() { #[derive(Debug, thiserror::Error)] pub enum MyError { #[error("Invalid input: {0}")] InvalidInput(String), } }
文档注释
#![allow(unused)] fn main() { /// 简要描述 /// /// # Arguments /// * `x` - 参数说明 /// /// # Returns /// 返回值说明 pub fn my_function(x: i32) -> Result<()> { // ... } }
测试指南
运行测试
# 所有测试
cargo test --workspace
# 特定包
cargo test -p content
cargo test -p server
# 特定测试
cargo test test_content_loading
单元测试
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[test] fn test_example() { let result = my_function(42); assert!(result.is_ok()); } } }
集成测试
#![allow(unused)] fn main() { // tests/integration_test.rs use my_crate::*; #[test] fn test_full_flow() { let runtime = Runtime::new(); runtime.initialize(config).unwrap(); // ... } }
测试覆盖率
cargo install cargo-tarpaulin
cargo tarpaulin --workspace
调试技巧
日志调试
设置日志级别:
# Windows PowerShell
$env:RUST_LOG="debug"
# Linux / macOS
export RUST_LOG=debug
日志级别:
error- 仅错误warn- 警告及以上info- 信息及以上debug- 调试及以上trace- 所有日志
特定模块日志
export RUST_LOG=server=debug,client=info
使用调试器
VS Code
安装 CodeLLDB 扩展,创建 .vscode/launch.json。
RustRover
直接点击运行按钮旁的调试按钮。
网络调试
export RUST_LOG=lightyear=debug
查看:
- 网络延迟
- 丢包率
- 预测命中率
性能分析
cargo install flamegraph
cargo flamegraph -p server
API文档
查看各模块的 Rust 文档:
cargo doc --workspace --open
主要模块
content- 内容包系统protocol- 网络协议server- 权威服务端client- 客户端核心runtime_core- 运行时编排gdextension- Godot 接入
在线文档
生成后访问:target/doc/content/index.html
配置文件格式
RustyWarfare 使用 TOML 格式定义游戏内容。
单位模板
id = "tank"
display_name = "Tank"
[stats]
health = 100
speed = 2.0
cost = 300
[combat]
damage = 15
range = 150.0
projectile = "official:tank_shell"
[visual]
sprite = "units/tank.png"
地图模板
id = "duel_fields"
title = "Duel Fields"
[dimensions]
width = 64
height = 64
[[teams]]
id = 0
name = "Player 1"
[[spawns]]
team = 0
x = 120.0
y = 240.0
units = ["official:builder"]
下一步
- 查看 Content 模块
- 查看 内容包系统
常见问题
环境配置
Q: 编译失败,提示链接错误
A: 确保安装了所有系统依赖。Windows 需要 Visual Studio Build Tools。
Q: Bevy 编译时间很长
A: 首次编译 Bevy 需要 5-10 分钟,这是正常现象。后续增量编译会快很多。
运行问题
Q: GDExtension 未加载
A:
- 运行
cargo run -p builder - 检查
launcher/godot/addons/rusty_core/bin/目录 - 重启 Godot 编辑器
Q: 连接超时
A:
- 检查防火墙设置
- 确认端口开放 (默认 5000)
- 验证 IP 地址正确
Q: 内容包加载失败
A: 检查 manifest.toml 是否存在,验证 TOML 格式正确。
开发问题
Q: 如何添加新单位?
A: 在 content/units/ 创建 .toml 文件,参考现有单位模板。
Q: 如何调试网络问题?
A: 设置 RUST_LOG=lightyear=debug 查看详细网络日志。
Q: 如何修改架构?
A: 先阅读架构文档,理解分层设计和依赖规则,再提出修改建议。