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

运行模式

项目支持三种运行模式,所有模式使用统一的游戏规则:

  1. 单人模式:本地 server + 本地 client
  2. 主机模式:本地 server + 本地 client + 远程 clients
  3. 远程模式:远程 server + 本地 client

Workspace 结构

rusty_warfare/
├── content/         # 内容包加载与验证
├── protocol/        # 网络协议定义
├── server/          # 权威服务端逻辑
├── client/          # 客户端核心
├── runtime_core/    # 运行时编排
├── gdextension/     # Godot 接入层
├── builder/         # 构建工具
├── game_domain/     # 游戏领域模型
└── launcher/godot/  # Godot 前端项目

文档导航

获取帮助

  • 查看 常见问题
  • 阅读项目根目录的 README.mdCONTRIBUTING.md
  • 参考 docs/ 目录下的架构文档

环境配置

本章介绍如何搭建 RustyWarfare 的开发环境。

系统要求

必需工具

  • Rust 工具链 (推荐 stable 最新版本)
  • Git (用于克隆仓库和管理子模块)
  • Godot 4.x (用于前端开发和测试)

可选工具

  • mdBook (用于构建本文档)
  • Visual Studio CodeRustRover (推荐的 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 会:

  1. 编译 gdextension 为动态库
  2. 将产物复制到 launcher/godot/addons/rusty_core/bin/
  3. 生成正确的 .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 前端:

  1. 构建 GDExtension
cargo run -p builder
  1. 打开 Godot 项目

使用 Godot 4.x 打开 launcher/godot/ 目录

  1. 运行场景

在 Godot 编辑器中按 F5 或点击播放按钮

运行模式

RustyWarfare 支持三种运行模式:

单人模式

在 Godot 中选择"单人游戏",本地同时启动 server 和 client。

主机模式

  1. 选择"创建房间"
  2. 系统启动本地 server 并自动连接
  3. 其他玩家可通过 IP 加入

客户端模式

  1. 选择"加入房间"
  2. 输入服务器地址
  3. 连接到远程 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 类

解决

  1. 确认 cargo run -p builder 执行成功
  2. 检查 launcher/godot/addons/rusty_core/bin/ 下是否有动态库
  3. 重启 Godot 编辑器

连接超时

症状:客户端无法连接到服务器

解决

  1. 检查防火墙设置
  2. 确认服务器端口已开放 (默认 5000)
  3. 验证 IP 地址正确

内容包加载失败

症状:启动时报告内容包错误

解决

  1. 检查 launcher/godot/assets/content_packages/official/manifest.toml 存在
  2. 验证内容包格式正确
  3. 查看详细错误日志

下一步

了解项目架构,请阅读 总体架构

总体架构

RustyWarfare 采用权威服务端架构,结合 Bevy ECSLightyear 网络同步,实现了单人、主机、远程三种模式下的统一游戏逻辑。

架构原则

  1. 服务端权威:所有游戏规则在服务端执行,客户端只负责输入和显示
  2. 单一规则集:单人和多人使用相同的游戏逻辑,无重复代码
  3. 数据驱动:游戏内容通过 TOML 配置定义,支持 Mod 和内容包
  4. 前端分离:Godot 仅负责渲染和输入,不包含游戏规则
  5. 严格分层:模块间依赖单向,防止循环依赖

技术架构图

┌─────────────────────────────────────────────┐
│          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

关键约束

  • protocolcontent 是底层库,不依赖任何上层模块
  • serverclient 平行,互不依赖
  • 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_coregdextension 会直接管理:

  • 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 等)

特点

  • clientserver 共同依赖
  • 不能依赖 clientserver
  • 定义数据契约,不实现逻辑

核心类型

#![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 投影
  • 错误处理

依赖

  • client
  • server
  • protocol
  • content

核心 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 (唯一依赖)
  • godot crate (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 不得依赖 clientserver
  • content 不得依赖 server
  • server 不得依赖 client
  • client 不得依赖 server
  • runtime_core 不得包含游戏规则
  • gdextension 不得包含游戏规则

为什么严格分层?

  1. 可测试性:每层可独立测试
  2. 复用性:底层可被多个上层使用
  3. 可维护性:修改某层不影响其他层
  4. 防止循环依赖:Rust 禁止循环依赖
  5. 清晰职责:每层职责明确,不会混淆

实践示例

添加新单位

  1. Layer 2 (content):定义单位模板 TOML
  2. Layer 4 (server):实现单位生成和行为系统
  3. Layer 3 (protocol):添加复制组件 (如需同步)
  4. Layer 5 (client):处理预测逻辑 (如需)
  5. Layer 8 (Godot):添加贴图和渲染节点

添加新网络消息

  1. Layer 3 (protocol):定义消息结构
  2. Layer 4 (server):处理接收逻辑
  3. Layer 5 (client):发送逻辑

修改 UI

  1. Layer 8 (Godot):修改 GDScript 和场景
  2. 无需修改 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有(架构一致)
主机<1ms20-100ms
远程-20-100ms
专用-20-100ms--

切换模式

在运行时不支持模式切换。必须:

  1. 调用 runtime.shutdown()
  2. 调用新模式的 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
  • 了解服务端如何使用内容:Server

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 不依赖 clientserver,因为:

  • 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
  • 了解客户端实现:Client

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 - 客户端核心

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 - 运行时核心

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_coregdextension 会直接管理:

  • 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()?;
}

下一步

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
}
}

优势总结

  1. 防作弊:客户端无法修改游戏状态
  2. 一致性:所有玩家看到相同状态
  3. 简化逻辑:只需实现一套规则(服务端)
  4. 可扩展:容易添加观战、回放等功能

下一步

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();
            }
        }
    }
}
}

优势总结

  1. 灵活组合:通过组件组合创建复杂实体
  2. 高性能:数据连续存储,缓存友好
  3. 并行友好:不同系统可并行执行
  4. 清晰职责:每个系统只处理特定逻辑

下一步

网络同步

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");
}
}

下一步

贡献约定

欢迎为 RustyWarfare 贡献代码!

基本流程

  1. Fork 仓库
  2. 创建功能分支
  3. 提交更改
  4. 创建 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 代码风格

使用 rustfmtclippy

cargo fmt
cargo clippy --workspace

命名规范

  • 类型PascalCase
  • 函数/变量snake_case
  • 常量SCREAMING_SNAKE_CASE
  • 生命周期'a, 'b

错误处理

使用 Resultthiserror

#![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"]

下一步

常见问题

环境配置

Q: 编译失败,提示链接错误

A: 确保安装了所有系统依赖。Windows 需要 Visual Studio Build Tools。

Q: Bevy 编译时间很长

A: 首次编译 Bevy 需要 5-10 分钟,这是正常现象。后续增量编译会快很多。

运行问题

Q: GDExtension 未加载

A:

  1. 运行 cargo run -p builder
  2. 检查 launcher/godot/addons/rusty_core/bin/ 目录
  3. 重启 Godot 编辑器

Q: 连接超时

A:

  1. 检查防火墙设置
  2. 确认端口开放 (默认 5000)
  3. 验证 IP 地址正确

Q: 内容包加载失败

A: 检查 manifest.toml 是否存在,验证 TOML 格式正确。

开发问题

Q: 如何添加新单位?

A: 在 content/units/ 创建 .toml 文件,参考现有单位模板。

Q: 如何调试网络问题?

A: 设置 RUST_LOG=lightyear=debug 查看详细网络日志。

Q: 如何修改架构?

A: 先阅读架构文档,理解分层设计和依赖规则,再提出修改建议。

更多帮助