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

下一步