feat(network): 网络模块增强 - 预测、AOI、增量压缩 (#379)
- 添加 NetworkPredictionSystem 客户端预测系统 - 添加 NetworkAOISystem 兴趣区域管理 - 添加 StateDeltaCompressor 状态增量压缩 - 添加断线重连和状态恢复 - 增强协议支持时间戳、序列号、速度 - 添加中英文文档
This commit is contained in:
500
packages/framework/network/src/systems/NetworkAOISystem.ts
Normal file
500
packages/framework/network/src/systems/NetworkAOISystem.ts
Normal file
@@ -0,0 +1,500 @@
|
||||
/**
|
||||
* @zh 网络 AOI 系统
|
||||
* @en Network AOI System
|
||||
*
|
||||
* @zh 集成 AOI 兴趣区域管理,过滤网络同步数据
|
||||
* @en Integrates AOI interest management to filter network sync data
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, type Entity } from '@esengine/ecs-framework'
|
||||
import { NetworkIdentity } from '../components/NetworkIdentity'
|
||||
import { NetworkTransform } from '../components/NetworkTransform'
|
||||
import type { EntitySyncState } from '../protocol'
|
||||
|
||||
// =============================================================================
|
||||
// Types | 类型定义
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh AOI 事件类型
|
||||
* @en AOI event type
|
||||
*/
|
||||
export type NetworkAOIEventType = 'enter' | 'exit'
|
||||
|
||||
/**
|
||||
* @zh AOI 事件
|
||||
* @en AOI event
|
||||
*/
|
||||
export interface NetworkAOIEvent {
|
||||
/**
|
||||
* @zh 事件类型
|
||||
* @en Event type
|
||||
*/
|
||||
type: NetworkAOIEventType
|
||||
|
||||
/**
|
||||
* @zh 观察者网络 ID(玩家)
|
||||
* @en Observer network ID (player)
|
||||
*/
|
||||
observerNetId: number
|
||||
|
||||
/**
|
||||
* @zh 目标网络 ID(进入/离开视野的实体)
|
||||
* @en Target network ID (entity entering/exiting view)
|
||||
*/
|
||||
targetNetId: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh AOI 事件监听器
|
||||
* @en AOI event listener
|
||||
*/
|
||||
export type NetworkAOIEventListener = (event: NetworkAOIEvent) => void
|
||||
|
||||
/**
|
||||
* @zh 网络 AOI 配置
|
||||
* @en Network AOI configuration
|
||||
*/
|
||||
export interface NetworkAOIConfig {
|
||||
/**
|
||||
* @zh 网格单元格大小
|
||||
* @en Grid cell size
|
||||
*/
|
||||
cellSize: number
|
||||
|
||||
/**
|
||||
* @zh 默认视野范围
|
||||
* @en Default view range
|
||||
*/
|
||||
defaultViewRange: number
|
||||
|
||||
/**
|
||||
* @zh 是否启用 AOI 过滤
|
||||
* @en Whether to enable AOI filtering
|
||||
*/
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: NetworkAOIConfig = {
|
||||
cellSize: 100,
|
||||
defaultViewRange: 500,
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 观察者数据
|
||||
* @en Observer data
|
||||
*/
|
||||
interface ObserverData {
|
||||
netId: number
|
||||
position: { x: number; y: number }
|
||||
viewRange: number
|
||||
viewRangeSq: number
|
||||
cellKey: string
|
||||
visibleEntities: Set<number>
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NetworkAOISystem | 网络 AOI 系统
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 网络 AOI 系统
|
||||
* @en Network AOI system
|
||||
*
|
||||
* @zh 管理网络实体的兴趣区域,过滤同步数据
|
||||
* @en Manages network entities' areas of interest and filters sync data
|
||||
*/
|
||||
export class NetworkAOISystem extends EntitySystem {
|
||||
private readonly _config: NetworkAOIConfig
|
||||
private readonly _observers: Map<number, ObserverData> = new Map()
|
||||
private readonly _cells: Map<string, Set<number>> = new Map()
|
||||
private readonly _listeners: Set<NetworkAOIEventListener> = new Set()
|
||||
private readonly _entityNetIdMap: Map<Entity, number> = new Map()
|
||||
private readonly _netIdEntityMap: Map<number, Entity> = new Map()
|
||||
|
||||
constructor(config?: Partial<NetworkAOIConfig>) {
|
||||
super(Matcher.all(NetworkIdentity, NetworkTransform))
|
||||
this._config = { ...DEFAULT_CONFIG, ...config }
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取配置
|
||||
* @en Get configuration
|
||||
*/
|
||||
get config(): Readonly<NetworkAOIConfig> {
|
||||
return this._config
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 是否启用
|
||||
* @en Is enabled
|
||||
*/
|
||||
get enabled(): boolean {
|
||||
return this._config.enabled
|
||||
}
|
||||
|
||||
set enabled(value: boolean) {
|
||||
this._config.enabled = value
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 观察者数量
|
||||
* @en Observer count
|
||||
*/
|
||||
get observerCount(): number {
|
||||
return this._observers.size
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 观察者管理 | Observer Management
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 添加观察者(通常是玩家实体)
|
||||
* @en Add observer (usually player entity)
|
||||
*/
|
||||
addObserver(netId: number, x: number, y: number, viewRange?: number): void {
|
||||
if (this._observers.has(netId)) {
|
||||
this.updateObserverPosition(netId, x, y)
|
||||
return
|
||||
}
|
||||
|
||||
const range = viewRange ?? this._config.defaultViewRange
|
||||
const cellKey = this._getCellKey(x, y)
|
||||
const data: ObserverData = {
|
||||
netId,
|
||||
position: { x, y },
|
||||
viewRange: range,
|
||||
viewRangeSq: range * range,
|
||||
cellKey,
|
||||
visibleEntities: new Set(),
|
||||
}
|
||||
|
||||
this._observers.set(netId, data)
|
||||
this._addToCell(cellKey, netId)
|
||||
this._updateVisibility(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 移除观察者
|
||||
* @en Remove observer
|
||||
*/
|
||||
removeObserver(netId: number): boolean {
|
||||
const data = this._observers.get(netId)
|
||||
if (!data) return false
|
||||
|
||||
// Emit exit events for all visible entities
|
||||
for (const visibleNetId of data.visibleEntities) {
|
||||
this._emitEvent({
|
||||
type: 'exit',
|
||||
observerNetId: netId,
|
||||
targetNetId: visibleNetId,
|
||||
})
|
||||
}
|
||||
|
||||
this._removeFromCell(data.cellKey, netId)
|
||||
this._observers.delete(netId)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 更新观察者位置
|
||||
* @en Update observer position
|
||||
*/
|
||||
updateObserverPosition(netId: number, x: number, y: number): void {
|
||||
const data = this._observers.get(netId)
|
||||
if (!data) return
|
||||
|
||||
const newCellKey = this._getCellKey(x, y)
|
||||
if (newCellKey !== data.cellKey) {
|
||||
this._removeFromCell(data.cellKey, netId)
|
||||
data.cellKey = newCellKey
|
||||
this._addToCell(newCellKey, netId)
|
||||
}
|
||||
|
||||
data.position.x = x
|
||||
data.position.y = y
|
||||
this._updateVisibility(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 更新观察者视野范围
|
||||
* @en Update observer view range
|
||||
*/
|
||||
updateObserverViewRange(netId: number, viewRange: number): void {
|
||||
const data = this._observers.get(netId)
|
||||
if (!data) return
|
||||
|
||||
data.viewRange = viewRange
|
||||
data.viewRangeSq = viewRange * viewRange
|
||||
this._updateVisibility(data)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 实体管理 | Entity Management
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 注册网络实体
|
||||
* @en Register network entity
|
||||
*/
|
||||
registerEntity(entity: Entity, netId: number): void {
|
||||
this._entityNetIdMap.set(entity, netId)
|
||||
this._netIdEntityMap.set(netId, entity)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 注销网络实体
|
||||
* @en Unregister network entity
|
||||
*/
|
||||
unregisterEntity(entity: Entity): void {
|
||||
const netId = this._entityNetIdMap.get(entity)
|
||||
if (netId !== undefined) {
|
||||
// Remove from all observers' visible sets
|
||||
for (const [, data] of this._observers) {
|
||||
if (data.visibleEntities.has(netId)) {
|
||||
data.visibleEntities.delete(netId)
|
||||
this._emitEvent({
|
||||
type: 'exit',
|
||||
observerNetId: data.netId,
|
||||
targetNetId: netId,
|
||||
})
|
||||
}
|
||||
}
|
||||
this._netIdEntityMap.delete(netId)
|
||||
}
|
||||
this._entityNetIdMap.delete(entity)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 查询接口 | Query Interface
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 获取观察者能看到的实体网络 ID 列表
|
||||
* @en Get list of entity network IDs visible to observer
|
||||
*/
|
||||
getVisibleEntities(observerNetId: number): number[] {
|
||||
const data = this._observers.get(observerNetId)
|
||||
return data ? Array.from(data.visibleEntities) : []
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取能看到指定实体的观察者网络 ID 列表
|
||||
* @en Get list of observer network IDs that can see the entity
|
||||
*/
|
||||
getObserversOf(entityNetId: number): number[] {
|
||||
const observers: number[] = []
|
||||
for (const [, data] of this._observers) {
|
||||
if (data.visibleEntities.has(entityNetId)) {
|
||||
observers.push(data.netId)
|
||||
}
|
||||
}
|
||||
return observers
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查观察者是否能看到目标
|
||||
* @en Check if observer can see target
|
||||
*/
|
||||
canSee(observerNetId: number, targetNetId: number): boolean {
|
||||
const data = this._observers.get(observerNetId)
|
||||
return data?.visibleEntities.has(targetNetId) ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 过滤同步数据,只保留观察者能看到的实体
|
||||
* @en Filter sync data to only include entities visible to observer
|
||||
*/
|
||||
filterSyncData(observerNetId: number, entities: EntitySyncState[]): EntitySyncState[] {
|
||||
if (!this._config.enabled) {
|
||||
return entities
|
||||
}
|
||||
|
||||
const data = this._observers.get(observerNetId)
|
||||
if (!data) {
|
||||
return entities
|
||||
}
|
||||
|
||||
return entities.filter(entity => {
|
||||
// Always include the observer's own entity
|
||||
if (entity.netId === observerNetId) return true
|
||||
// Include entities in view
|
||||
return data.visibleEntities.has(entity.netId)
|
||||
})
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 事件系统 | Event System
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 添加事件监听器
|
||||
* @en Add event listener
|
||||
*/
|
||||
addListener(listener: NetworkAOIEventListener): void {
|
||||
this._listeners.add(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 移除事件监听器
|
||||
* @en Remove event listener
|
||||
*/
|
||||
removeListener(listener: NetworkAOIEventListener): void {
|
||||
this._listeners.delete(listener)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 系统生命周期 | System Lifecycle
|
||||
// =========================================================================
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
if (!this._config.enabled) return
|
||||
|
||||
// Update entity positions for AOI calculations
|
||||
for (const entity of entities) {
|
||||
const identity = this.requireComponent(entity, NetworkIdentity)
|
||||
const transform = this.requireComponent(entity, NetworkTransform)
|
||||
|
||||
// Register entity if not already registered
|
||||
if (!this._entityNetIdMap.has(entity)) {
|
||||
this.registerEntity(entity, identity.netId)
|
||||
}
|
||||
|
||||
// If this entity is an observer (has authority), update its position
|
||||
if (identity.bHasAuthority && this._observers.has(identity.netId)) {
|
||||
this.updateObserverPosition(
|
||||
identity.netId,
|
||||
transform.currentX,
|
||||
transform.currentY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update all observers' visibility based on entity positions
|
||||
this._updateAllObserversVisibility(entities)
|
||||
}
|
||||
|
||||
private _updateAllObserversVisibility(entities: readonly Entity[]): void {
|
||||
for (const [, data] of this._observers) {
|
||||
const newVisible = new Set<number>()
|
||||
|
||||
// Check all entities
|
||||
for (const entity of entities) {
|
||||
const identity = this.requireComponent(entity, NetworkIdentity)
|
||||
const transform = this.requireComponent(entity, NetworkTransform)
|
||||
|
||||
// Skip self
|
||||
if (identity.netId === data.netId) continue
|
||||
|
||||
// Check distance
|
||||
const dx = transform.currentX - data.position.x
|
||||
const dy = transform.currentY - data.position.y
|
||||
const distSq = dx * dx + dy * dy
|
||||
|
||||
if (distSq <= data.viewRangeSq) {
|
||||
newVisible.add(identity.netId)
|
||||
}
|
||||
}
|
||||
|
||||
// Find entities that entered view
|
||||
for (const netId of newVisible) {
|
||||
if (!data.visibleEntities.has(netId)) {
|
||||
this._emitEvent({
|
||||
type: 'enter',
|
||||
observerNetId: data.netId,
|
||||
targetNetId: netId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Find entities that exited view
|
||||
for (const netId of data.visibleEntities) {
|
||||
if (!newVisible.has(netId)) {
|
||||
this._emitEvent({
|
||||
type: 'exit',
|
||||
observerNetId: data.netId,
|
||||
targetNetId: netId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
data.visibleEntities = newVisible
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除所有数据
|
||||
* @en Clear all data
|
||||
*/
|
||||
clear(): void {
|
||||
this._observers.clear()
|
||||
this._cells.clear()
|
||||
this._entityNetIdMap.clear()
|
||||
this._netIdEntityMap.clear()
|
||||
}
|
||||
|
||||
protected override onDestroy(): void {
|
||||
this.clear()
|
||||
this._listeners.clear()
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 私有方法 | Private Methods
|
||||
// =========================================================================
|
||||
|
||||
private _getCellKey(x: number, y: number): string {
|
||||
const cellX = Math.floor(x / this._config.cellSize)
|
||||
const cellY = Math.floor(y / this._config.cellSize)
|
||||
return `${cellX},${cellY}`
|
||||
}
|
||||
|
||||
private _addToCell(cellKey: string, netId: number): void {
|
||||
let cell = this._cells.get(cellKey)
|
||||
if (!cell) {
|
||||
cell = new Set()
|
||||
this._cells.set(cellKey, cell)
|
||||
}
|
||||
cell.add(netId)
|
||||
}
|
||||
|
||||
private _removeFromCell(cellKey: string, netId: number): void {
|
||||
const cell = this._cells.get(cellKey)
|
||||
if (cell) {
|
||||
cell.delete(netId)
|
||||
if (cell.size === 0) {
|
||||
this._cells.delete(cellKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateVisibility(data: ObserverData): void {
|
||||
// This is called when an observer moves
|
||||
// The full visibility update happens in process() with all entities
|
||||
}
|
||||
|
||||
private _emitEvent(event: NetworkAOIEvent): void {
|
||||
for (const listener of this._listeners) {
|
||||
try {
|
||||
listener(event)
|
||||
} catch (e) {
|
||||
console.error('[NetworkAOISystem] Listener error:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 工厂函数 | Factory Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 创建网络 AOI 系统
|
||||
* @en Create network AOI system
|
||||
*/
|
||||
export function createNetworkAOISystem(
|
||||
config?: Partial<NetworkAOIConfig>
|
||||
): NetworkAOISystem {
|
||||
return new NetworkAOISystem(config)
|
||||
}
|
||||
@@ -1,11 +1,63 @@
|
||||
/**
|
||||
* @zh 网络输入系统
|
||||
* @en Network Input System
|
||||
*
|
||||
* @zh 收集本地玩家输入并发送到服务器,支持与预测系统集成
|
||||
* @en Collects local player input and sends to server, supports integration with prediction system
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher } from '@esengine/ecs-framework'
|
||||
import type { PlayerInput } from '../protocol'
|
||||
import type { NetworkService } from '../services/NetworkService'
|
||||
import type { NetworkPredictionSystem } from './NetworkPredictionSystem'
|
||||
|
||||
// =============================================================================
|
||||
// Types | 类型定义
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 输入配置
|
||||
* @en Input configuration
|
||||
*/
|
||||
export interface NetworkInputConfig {
|
||||
/**
|
||||
* @zh 发送输入的最小间隔(毫秒)
|
||||
* @en Minimum interval between input sends (milliseconds)
|
||||
*/
|
||||
sendInterval: number
|
||||
|
||||
/**
|
||||
* @zh 是否合并相同输入
|
||||
* @en Whether to merge identical inputs
|
||||
*/
|
||||
mergeIdenticalInputs: boolean
|
||||
|
||||
/**
|
||||
* @zh 最大输入队列长度
|
||||
* @en Maximum input queue length
|
||||
*/
|
||||
maxQueueLength: number
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: NetworkInputConfig = {
|
||||
sendInterval: 16, // ~60fps
|
||||
mergeIdenticalInputs: true,
|
||||
maxQueueLength: 10,
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 待发送输入
|
||||
* @en Pending input
|
||||
*/
|
||||
interface PendingInput {
|
||||
moveDir?: { x: number; y: number }
|
||||
actions?: string[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NetworkInputSystem | 网络输入系统
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 网络输入系统
|
||||
@@ -15,13 +67,52 @@ import type { NetworkService } from '../services/NetworkService'
|
||||
* @en Collects local player input and sends to server
|
||||
*/
|
||||
export class NetworkInputSystem extends EntitySystem {
|
||||
private _networkService: NetworkService
|
||||
private _frame: number = 0
|
||||
private _inputQueue: PlayerInput[] = []
|
||||
private readonly _networkService: NetworkService
|
||||
private readonly _config: NetworkInputConfig
|
||||
private _predictionSystem: NetworkPredictionSystem | null = null
|
||||
|
||||
constructor(networkService: NetworkService) {
|
||||
private _frame: number = 0
|
||||
private _inputSequence: number = 0
|
||||
private _inputQueue: PendingInput[] = []
|
||||
private _lastSendTime: number = 0
|
||||
private _lastMoveDir: { x: number; y: number } = { x: 0, y: 0 }
|
||||
|
||||
constructor(networkService: NetworkService, config?: Partial<NetworkInputConfig>) {
|
||||
super(Matcher.nothing())
|
||||
this._networkService = networkService
|
||||
this._config = { ...DEFAULT_CONFIG, ...config }
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取配置
|
||||
* @en Get configuration
|
||||
*/
|
||||
get config(): Readonly<NetworkInputConfig> {
|
||||
return this._config
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取当前帧号
|
||||
* @en Get current frame number
|
||||
*/
|
||||
get frame(): number {
|
||||
return this._frame
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取当前输入序列号
|
||||
* @en Get current input sequence
|
||||
*/
|
||||
get inputSequence(): number {
|
||||
return this._inputSequence
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置预测系统引用
|
||||
* @en Set prediction system reference
|
||||
*/
|
||||
setPredictionSystem(system: NetworkPredictionSystem): void {
|
||||
this._predictionSystem = system
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,11 +123,64 @@ export class NetworkInputSystem extends EntitySystem {
|
||||
if (!this._networkService.isConnected) return
|
||||
|
||||
this._frame++
|
||||
const now = Date.now()
|
||||
|
||||
while (this._inputQueue.length > 0) {
|
||||
const input = this._inputQueue.shift()!
|
||||
input.frame = this._frame
|
||||
this._networkService.sendInput(input)
|
||||
// Rate limiting
|
||||
if (now - this._lastSendTime < this._config.sendInterval) return
|
||||
|
||||
// If using prediction system, get input from there
|
||||
if (this._predictionSystem) {
|
||||
const predictedInput = this._predictionSystem.getInputToSend()
|
||||
if (predictedInput) {
|
||||
this._networkService.sendInput(predictedInput)
|
||||
this._lastSendTime = now
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise process queue
|
||||
if (this._inputQueue.length === 0) return
|
||||
|
||||
// Merge inputs if configured
|
||||
let mergedInput: PendingInput
|
||||
if (this._config.mergeIdenticalInputs && this._inputQueue.length > 1) {
|
||||
mergedInput = this._mergeInputs(this._inputQueue)
|
||||
this._inputQueue.length = 0
|
||||
} else {
|
||||
mergedInput = this._inputQueue.shift()!
|
||||
}
|
||||
|
||||
// Build and send input
|
||||
this._inputSequence++
|
||||
const input: PlayerInput = {
|
||||
seq: this._inputSequence,
|
||||
frame: this._frame,
|
||||
timestamp: mergedInput.timestamp,
|
||||
moveDir: mergedInput.moveDir,
|
||||
actions: mergedInput.actions,
|
||||
}
|
||||
|
||||
this._networkService.sendInput(input)
|
||||
this._lastSendTime = now
|
||||
}
|
||||
|
||||
private _mergeInputs(inputs: PendingInput[]): PendingInput {
|
||||
const allActions: string[] = []
|
||||
let lastMoveDir: { x: number; y: number } | undefined
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input.moveDir) {
|
||||
lastMoveDir = input.moveDir
|
||||
}
|
||||
if (input.actions) {
|
||||
allActions.push(...input.actions)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
moveDir: lastMoveDir,
|
||||
actions: allActions.length > 0 ? allActions : undefined,
|
||||
timestamp: inputs[inputs.length - 1].timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +189,24 @@ export class NetworkInputSystem extends EntitySystem {
|
||||
* @en Add move input
|
||||
*/
|
||||
public addMoveInput(x: number, y: number): void {
|
||||
this._inputQueue.push({
|
||||
frame: 0,
|
||||
moveDir: { x, y },
|
||||
})
|
||||
// Skip if same as last input
|
||||
if (
|
||||
this._config.mergeIdenticalInputs &&
|
||||
this._lastMoveDir.x === x &&
|
||||
this._lastMoveDir.y === y &&
|
||||
this._inputQueue.length > 0
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this._lastMoveDir = { x, y }
|
||||
|
||||
// Also set input on prediction system
|
||||
if (this._predictionSystem) {
|
||||
this._predictionSystem.setInput(x, y)
|
||||
}
|
||||
|
||||
this._addToQueue({ moveDir: { x, y }, timestamp: Date.now() })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,19 +214,70 @@ export class NetworkInputSystem extends EntitySystem {
|
||||
* @en Add action input
|
||||
*/
|
||||
public addActionInput(action: string): void {
|
||||
// Try to add to last input in queue
|
||||
const lastInput = this._inputQueue[this._inputQueue.length - 1]
|
||||
if (lastInput) {
|
||||
lastInput.actions = lastInput.actions || []
|
||||
lastInput.actions.push(action)
|
||||
} else {
|
||||
this._inputQueue.push({
|
||||
frame: 0,
|
||||
actions: [action],
|
||||
})
|
||||
this._addToQueue({ actions: [action], timestamp: Date.now() })
|
||||
}
|
||||
|
||||
// Also set on prediction system
|
||||
if (this._predictionSystem) {
|
||||
this._predictionSystem.setInput(
|
||||
this._lastMoveDir.x,
|
||||
this._lastMoveDir.y,
|
||||
[action]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private _addToQueue(input: PendingInput): void {
|
||||
this._inputQueue.push(input)
|
||||
|
||||
// Limit queue size
|
||||
while (this._inputQueue.length > this._config.maxQueueLength) {
|
||||
this._inputQueue.shift()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清空输入队列
|
||||
* @en Clear input queue
|
||||
*/
|
||||
public clearQueue(): void {
|
||||
this._inputQueue.length = 0
|
||||
this._lastMoveDir = { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置状态
|
||||
* @en Reset state
|
||||
*/
|
||||
public reset(): void {
|
||||
this._frame = 0
|
||||
this._inputSequence = 0
|
||||
this.clearQueue()
|
||||
}
|
||||
|
||||
protected override onDestroy(): void {
|
||||
this._inputQueue.length = 0
|
||||
this._predictionSystem = null
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 工厂函数 | Factory Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 创建网络输入系统
|
||||
* @en Create network input system
|
||||
*/
|
||||
export function createNetworkInputSystem(
|
||||
networkService: NetworkService,
|
||||
config?: Partial<NetworkInputConfig>
|
||||
): NetworkInputSystem {
|
||||
return new NetworkInputSystem(networkService, config)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* @zh 网络预测系统
|
||||
* @en Network Prediction System
|
||||
*
|
||||
* @zh 处理本地玩家的客户端预测和服务器校正
|
||||
* @en Handles client-side prediction and server reconciliation for local player
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, Time, type Entity } from '@esengine/ecs-framework'
|
||||
import { NetworkIdentity } from '../components/NetworkIdentity'
|
||||
import { NetworkTransform } from '../components/NetworkTransform'
|
||||
import type { SyncData, PlayerInput } from '../protocol'
|
||||
import {
|
||||
ClientPrediction,
|
||||
createClientPrediction,
|
||||
type IPredictor,
|
||||
type ClientPredictionConfig,
|
||||
type ITransformState,
|
||||
} from '../sync'
|
||||
|
||||
// =============================================================================
|
||||
// Types | 类型定义
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 移动输入
|
||||
* @en Movement input
|
||||
*/
|
||||
export interface MovementInput {
|
||||
x: number
|
||||
y: number
|
||||
actions?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 预测状态(位置 + 旋转)
|
||||
* @en Predicted state (position + rotation)
|
||||
*/
|
||||
export interface PredictedTransform extends ITransformState {
|
||||
velocityX: number
|
||||
velocityY: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 预测系统配置
|
||||
* @en Prediction system configuration
|
||||
*/
|
||||
export interface NetworkPredictionConfig extends Partial<ClientPredictionConfig> {
|
||||
/**
|
||||
* @zh 移动速度(单位/秒)
|
||||
* @en Movement speed (units/second)
|
||||
*/
|
||||
moveSpeed: number
|
||||
|
||||
/**
|
||||
* @zh 是否启用预测
|
||||
* @en Whether prediction is enabled
|
||||
*/
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: NetworkPredictionConfig = {
|
||||
moveSpeed: 200,
|
||||
enabled: true,
|
||||
maxUnacknowledgedInputs: 60,
|
||||
reconciliationThreshold: 0.5,
|
||||
reconciliationSpeed: 10,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 默认预测器 | Default Predictor
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 简单移动预测器
|
||||
* @en Simple movement predictor
|
||||
*/
|
||||
class SimpleMovementPredictor implements IPredictor<PredictedTransform, MovementInput> {
|
||||
constructor(private readonly _moveSpeed: number) {}
|
||||
|
||||
predict(state: PredictedTransform, input: MovementInput, deltaTime: number): PredictedTransform {
|
||||
const velocityX = input.x * this._moveSpeed
|
||||
const velocityY = input.y * this._moveSpeed
|
||||
|
||||
return {
|
||||
x: state.x + velocityX * deltaTime,
|
||||
y: state.y + velocityY * deltaTime,
|
||||
rotation: state.rotation,
|
||||
velocityX,
|
||||
velocityY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NetworkPredictionSystem | 网络预测系统
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 网络预测系统
|
||||
* @en Network prediction system
|
||||
*
|
||||
* @zh 处理本地玩家的输入预测和服务器状态校正
|
||||
* @en Handles local player input prediction and server state reconciliation
|
||||
*/
|
||||
export class NetworkPredictionSystem extends EntitySystem {
|
||||
private readonly _config: NetworkPredictionConfig
|
||||
private readonly _predictor: IPredictor<PredictedTransform, MovementInput>
|
||||
private _prediction: ClientPrediction<PredictedTransform, MovementInput> | null = null
|
||||
private _localPlayerNetId: number = -1
|
||||
private _currentInput: MovementInput = { x: 0, y: 0 }
|
||||
private _inputSequence: number = 0
|
||||
|
||||
constructor(config?: Partial<NetworkPredictionConfig>) {
|
||||
super(Matcher.all(NetworkIdentity, NetworkTransform))
|
||||
this._config = { ...DEFAULT_CONFIG, ...config }
|
||||
this._predictor = new SimpleMovementPredictor(this._config.moveSpeed)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取配置
|
||||
* @en Get configuration
|
||||
*/
|
||||
get config(): Readonly<NetworkPredictionConfig> {
|
||||
return this._config
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取当前输入序列号
|
||||
* @en Get current input sequence number
|
||||
*/
|
||||
get inputSequence(): number {
|
||||
return this._inputSequence
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取待确认输入数量
|
||||
* @en Get pending input count
|
||||
*/
|
||||
get pendingInputCount(): number {
|
||||
return this._prediction?.pendingInputCount ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 是否启用预测
|
||||
* @en Whether prediction is enabled
|
||||
*/
|
||||
get enabled(): boolean {
|
||||
return this._config.enabled
|
||||
}
|
||||
|
||||
set enabled(value: boolean) {
|
||||
this._config.enabled = value
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置本地玩家网络 ID
|
||||
* @en Set local player network ID
|
||||
*/
|
||||
setLocalPlayerNetId(netId: number): void {
|
||||
this._localPlayerNetId = netId
|
||||
this._prediction = createClientPrediction<PredictedTransform, MovementInput>(
|
||||
this._predictor,
|
||||
{
|
||||
maxUnacknowledgedInputs: this._config.maxUnacknowledgedInputs,
|
||||
reconciliationThreshold: this._config.reconciliationThreshold,
|
||||
reconciliationSpeed: this._config.reconciliationSpeed,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置移动输入
|
||||
* @en Set movement input
|
||||
*/
|
||||
setInput(x: number, y: number, actions?: string[]): void {
|
||||
this._currentInput = { x, y, actions }
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取下一个要发送的输入(带序列号)
|
||||
* @en Get next input to send (with sequence number)
|
||||
*/
|
||||
getInputToSend(): PlayerInput | null {
|
||||
if (!this._prediction) return null
|
||||
|
||||
const input = this._prediction.getInputToSend()
|
||||
if (!input) return null
|
||||
|
||||
return {
|
||||
seq: input.sequence,
|
||||
frame: 0,
|
||||
timestamp: input.timestamp,
|
||||
moveDir: { x: input.input.x, y: input.input.y },
|
||||
actions: input.input.actions,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 处理服务器同步数据进行校正
|
||||
* @en Process server sync data for reconciliation
|
||||
*/
|
||||
reconcileWithServer(data: SyncData): void {
|
||||
if (!this._prediction || this._localPlayerNetId < 0) return
|
||||
|
||||
// Find local player state in sync data
|
||||
const localState = data.entities.find(e => e.netId === this._localPlayerNetId)
|
||||
if (!localState || !localState.pos) return
|
||||
|
||||
const serverState: PredictedTransform = {
|
||||
x: localState.pos.x,
|
||||
y: localState.pos.y,
|
||||
rotation: localState.rot ?? 0,
|
||||
velocityX: localState.vel?.x ?? 0,
|
||||
velocityY: localState.vel?.y ?? 0,
|
||||
}
|
||||
|
||||
// Reconcile prediction with server state
|
||||
if (data.ackSeq !== undefined) {
|
||||
this._prediction.reconcile(
|
||||
serverState,
|
||||
data.ackSeq,
|
||||
(state) => ({ x: state.x, y: state.y }),
|
||||
Time.deltaTime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
if (!this._config.enabled || !this._prediction) return
|
||||
|
||||
const deltaTime = Time.deltaTime
|
||||
|
||||
for (const entity of entities) {
|
||||
const identity = this.requireComponent(entity, NetworkIdentity)
|
||||
|
||||
// Only process local player with authority
|
||||
if (!identity.bHasAuthority || identity.netId !== this._localPlayerNetId) continue
|
||||
|
||||
const transform = this.requireComponent(entity, NetworkTransform)
|
||||
|
||||
// Get current state
|
||||
const currentState: PredictedTransform = {
|
||||
x: transform.currentX,
|
||||
y: transform.currentY,
|
||||
rotation: transform.currentRotation,
|
||||
velocityX: 0,
|
||||
velocityY: 0,
|
||||
}
|
||||
|
||||
// Record input and get predicted state
|
||||
if (this._currentInput.x !== 0 || this._currentInput.y !== 0) {
|
||||
const predicted = this._prediction.recordInput(
|
||||
this._currentInput,
|
||||
currentState,
|
||||
deltaTime
|
||||
)
|
||||
|
||||
// Apply predicted position
|
||||
transform.currentX = predicted.x
|
||||
transform.currentY = predicted.y
|
||||
transform.currentRotation = predicted.rotation
|
||||
|
||||
// Update target to match (for rendering)
|
||||
transform.targetX = predicted.x
|
||||
transform.targetY = predicted.y
|
||||
transform.targetRotation = predicted.rotation
|
||||
|
||||
this._inputSequence = this._prediction.currentSequence
|
||||
}
|
||||
|
||||
// Apply correction offset smoothly
|
||||
const offset = this._prediction.correctionOffset
|
||||
if (Math.abs(offset.x) > 0.01 || Math.abs(offset.y) > 0.01) {
|
||||
transform.currentX += offset.x * deltaTime * 5
|
||||
transform.currentY += offset.y * deltaTime * 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置预测状态
|
||||
* @en Reset prediction state
|
||||
*/
|
||||
reset(): void {
|
||||
this._prediction?.clear()
|
||||
this._inputSequence = 0
|
||||
this._currentInput = { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
protected override onDestroy(): void {
|
||||
this._prediction?.clear()
|
||||
this._prediction = null
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 工厂函数 | Factory Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 创建网络预测系统
|
||||
* @en Create network prediction system
|
||||
*/
|
||||
export function createNetworkPredictionSystem(
|
||||
config?: Partial<NetworkPredictionConfig>
|
||||
): NetworkPredictionSystem {
|
||||
return new NetworkPredictionSystem(config)
|
||||
}
|
||||
@@ -1,10 +1,32 @@
|
||||
/**
|
||||
* @zh 网络同步系统
|
||||
* @en Network Sync System
|
||||
*
|
||||
* @zh 处理网络实体的状态同步、快照缓冲和插值
|
||||
* @en Handles state synchronization, snapshot buffering, and interpolation for networked entities
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, Time, type Entity } from '@esengine/ecs-framework'
|
||||
import { NetworkIdentity } from '../components/NetworkIdentity'
|
||||
import { NetworkTransform } from '../components/NetworkTransform'
|
||||
import type { SyncData, EntitySyncState } from '../protocol'
|
||||
import {
|
||||
SnapshotBuffer,
|
||||
createSnapshotBuffer,
|
||||
TransformInterpolator,
|
||||
createTransformInterpolator,
|
||||
type ITransformState,
|
||||
type ITransformStateWithVelocity,
|
||||
type IStateSnapshot,
|
||||
} from '../sync'
|
||||
|
||||
// =============================================================================
|
||||
// Types | 类型定义
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 同步消息接口
|
||||
* @en Sync message interface
|
||||
* @zh 同步消息接口(兼容旧版)
|
||||
* @en Sync message interface (for backwards compatibility)
|
||||
*/
|
||||
export interface SyncMessage {
|
||||
entities: Array<{
|
||||
@@ -14,25 +36,134 @@ export interface SyncMessage {
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 实体快照数据
|
||||
* @en Entity snapshot data
|
||||
*/
|
||||
interface EntitySnapshotData {
|
||||
buffer: SnapshotBuffer<ITransformStateWithVelocity>
|
||||
lastServerTime: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 同步系统配置
|
||||
* @en Sync system configuration
|
||||
*/
|
||||
export interface NetworkSyncConfig {
|
||||
/**
|
||||
* @zh 快照缓冲区大小
|
||||
* @en Snapshot buffer size
|
||||
*/
|
||||
bufferSize: number
|
||||
|
||||
/**
|
||||
* @zh 插值延迟(毫秒)
|
||||
* @en Interpolation delay in milliseconds
|
||||
*/
|
||||
interpolationDelay: number
|
||||
|
||||
/**
|
||||
* @zh 是否启用外推
|
||||
* @en Whether to enable extrapolation
|
||||
*/
|
||||
enableExtrapolation: boolean
|
||||
|
||||
/**
|
||||
* @zh 最大外推时间(毫秒)
|
||||
* @en Maximum extrapolation time in milliseconds
|
||||
*/
|
||||
maxExtrapolationTime: number
|
||||
|
||||
/**
|
||||
* @zh 使用赫尔米特插值(更平滑)
|
||||
* @en Use Hermite interpolation (smoother)
|
||||
*/
|
||||
useHermiteInterpolation: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: NetworkSyncConfig = {
|
||||
bufferSize: 30,
|
||||
interpolationDelay: 100,
|
||||
enableExtrapolation: true,
|
||||
maxExtrapolationTime: 200,
|
||||
useHermiteInterpolation: false,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NetworkSyncSystem | 网络同步系统
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 网络同步系统
|
||||
* @en Network sync system
|
||||
*
|
||||
* @zh 处理网络实体的状态同步和插值
|
||||
* @en Handles state synchronization and interpolation for networked entities
|
||||
* @zh 处理网络实体的状态同步和插值,支持快照缓冲、平滑插值和外推
|
||||
* @en Handles state synchronization and interpolation for networked entities,
|
||||
* supports snapshot buffering, smooth interpolation, and extrapolation
|
||||
*/
|
||||
export class NetworkSyncSystem extends EntitySystem {
|
||||
private _netIdToEntity: Map<number, number> = new Map()
|
||||
private readonly _netIdToEntity: Map<number, number> = new Map()
|
||||
private readonly _entitySnapshots: Map<number, EntitySnapshotData> = new Map()
|
||||
private readonly _interpolator: TransformInterpolator
|
||||
private readonly _config: NetworkSyncConfig
|
||||
|
||||
constructor() {
|
||||
private _serverTimeOffset: number = 0
|
||||
private _lastSyncTime: number = 0
|
||||
private _renderTime: number = 0
|
||||
|
||||
constructor(config?: Partial<NetworkSyncConfig>) {
|
||||
super(Matcher.all(NetworkIdentity, NetworkTransform))
|
||||
this._config = { ...DEFAULT_CONFIG, ...config }
|
||||
this._interpolator = createTransformInterpolator()
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 处理同步消息
|
||||
* @en Handle sync message
|
||||
* @zh 获取配置
|
||||
* @en Get configuration
|
||||
*/
|
||||
get config(): Readonly<NetworkSyncConfig> {
|
||||
return this._config
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取服务器时间偏移
|
||||
* @en Get server time offset
|
||||
*/
|
||||
get serverTimeOffset(): number {
|
||||
return this._serverTimeOffset
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取当前渲染时间
|
||||
* @en Get current render time
|
||||
*/
|
||||
get renderTime(): number {
|
||||
return this._renderTime
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 处理同步消息(新版,带时间戳)
|
||||
* @en Handle sync message (new version with timestamp)
|
||||
*/
|
||||
handleSyncData(data: SyncData): void {
|
||||
const serverTime = data.timestamp
|
||||
|
||||
// Update server time offset
|
||||
const clientTime = Date.now()
|
||||
this._serverTimeOffset = serverTime - clientTime
|
||||
this._lastSyncTime = clientTime
|
||||
|
||||
for (const state of data.entities) {
|
||||
this._processEntityState(state, serverTime)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 处理同步消息(兼容旧版)
|
||||
* @en Handle sync message (backwards compatible)
|
||||
*/
|
||||
handleSync(msg: SyncMessage): void {
|
||||
const now = Date.now()
|
||||
for (const state of msg.entities) {
|
||||
const entityId = this._netIdToEntity.get(state.netId)
|
||||
if (entityId === undefined) continue
|
||||
@@ -44,22 +175,133 @@ export class NetworkSyncSystem extends EntitySystem {
|
||||
if (transform && state.pos) {
|
||||
transform.setTarget(state.pos.x, state.pos.y, state.rot ?? 0)
|
||||
}
|
||||
|
||||
// Also add to snapshot buffer for interpolation
|
||||
this._processEntityState({
|
||||
netId: state.netId,
|
||||
pos: state.pos,
|
||||
rot: state.rot,
|
||||
}, now)
|
||||
}
|
||||
}
|
||||
|
||||
private _processEntityState(state: EntitySyncState, serverTime: number): void {
|
||||
const entityId = this._netIdToEntity.get(state.netId)
|
||||
if (entityId === undefined) return
|
||||
|
||||
// Get or create snapshot buffer
|
||||
let snapshotData = this._entitySnapshots.get(state.netId)
|
||||
if (!snapshotData) {
|
||||
snapshotData = {
|
||||
buffer: createSnapshotBuffer<ITransformStateWithVelocity>(
|
||||
this._config.bufferSize,
|
||||
this._config.interpolationDelay
|
||||
),
|
||||
lastServerTime: 0,
|
||||
}
|
||||
this._entitySnapshots.set(state.netId, snapshotData)
|
||||
}
|
||||
|
||||
// Create snapshot
|
||||
const transformState: ITransformStateWithVelocity = {
|
||||
x: state.pos?.x ?? 0,
|
||||
y: state.pos?.y ?? 0,
|
||||
rotation: state.rot ?? 0,
|
||||
velocityX: state.vel?.x ?? 0,
|
||||
velocityY: state.vel?.y ?? 0,
|
||||
angularVelocity: state.angVel ?? 0,
|
||||
}
|
||||
|
||||
const snapshot: IStateSnapshot<ITransformStateWithVelocity> = {
|
||||
timestamp: serverTime,
|
||||
state: transformState,
|
||||
}
|
||||
|
||||
snapshotData.buffer.push(snapshot)
|
||||
snapshotData.lastServerTime = serverTime
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
const deltaTime = Time.deltaTime
|
||||
const clientTime = Date.now()
|
||||
|
||||
// Calculate render time (current time adjusted for server offset)
|
||||
this._renderTime = clientTime + this._serverTimeOffset
|
||||
|
||||
for (const entity of entities) {
|
||||
const transform = this.requireComponent(entity, NetworkTransform)
|
||||
const identity = this.requireComponent(entity, NetworkIdentity)
|
||||
|
||||
if (!identity.bHasAuthority && transform.bInterpolate) {
|
||||
this._interpolate(transform, deltaTime)
|
||||
// Skip entities with authority (local player handles their own movement)
|
||||
if (identity.bHasAuthority) continue
|
||||
|
||||
if (transform.bInterpolate) {
|
||||
this._interpolateEntity(identity.netId, transform, deltaTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _interpolateEntity(
|
||||
netId: number,
|
||||
transform: NetworkTransform,
|
||||
deltaTime: number
|
||||
): void {
|
||||
const snapshotData = this._entitySnapshots.get(netId)
|
||||
|
||||
if (snapshotData && snapshotData.buffer.size >= 2) {
|
||||
// Use snapshot buffer for interpolation
|
||||
const result = snapshotData.buffer.getInterpolationSnapshots(this._renderTime)
|
||||
|
||||
if (result) {
|
||||
const [prev, next, t] = result
|
||||
const interpolated = this._interpolator.interpolate(prev.state, next.state, t)
|
||||
|
||||
transform.currentX = interpolated.x
|
||||
transform.currentY = interpolated.y
|
||||
transform.currentRotation = interpolated.rotation
|
||||
|
||||
// Update target for compatibility
|
||||
transform.targetX = next.state.x
|
||||
transform.targetY = next.state.y
|
||||
transform.targetRotation = next.state.rotation
|
||||
return
|
||||
}
|
||||
|
||||
// Extrapolation if enabled and we have velocity data
|
||||
if (this._config.enableExtrapolation) {
|
||||
const latest = snapshotData.buffer.getLatest()
|
||||
if (latest) {
|
||||
const timeSinceLastSnapshot = this._renderTime - latest.timestamp
|
||||
if (timeSinceLastSnapshot > 0 && timeSinceLastSnapshot < this._config.maxExtrapolationTime) {
|
||||
const extrapolated = this._interpolator.extrapolate(
|
||||
latest.state,
|
||||
timeSinceLastSnapshot / 1000
|
||||
)
|
||||
transform.currentX = extrapolated.x
|
||||
transform.currentY = extrapolated.y
|
||||
transform.currentRotation = extrapolated.rotation
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: simple lerp towards target
|
||||
this._simpleLerp(transform, deltaTime)
|
||||
}
|
||||
|
||||
private _simpleLerp(transform: NetworkTransform, deltaTime: number): void {
|
||||
const t = Math.min(1, transform.lerpSpeed * deltaTime)
|
||||
|
||||
transform.currentX += (transform.targetX - transform.currentX) * t
|
||||
transform.currentY += (transform.targetY - transform.currentY) * t
|
||||
|
||||
let angleDiff = transform.targetRotation - transform.currentRotation
|
||||
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2
|
||||
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2
|
||||
transform.currentRotation += angleDiff * t
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 注册网络实体
|
||||
* @en Register network entity
|
||||
@@ -74,6 +316,7 @@ export class NetworkSyncSystem extends EntitySystem {
|
||||
*/
|
||||
unregisterEntity(netId: number): void {
|
||||
this._netIdToEntity.delete(netId)
|
||||
this._entitySnapshots.delete(netId)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,19 +327,26 @@ export class NetworkSyncSystem extends EntitySystem {
|
||||
return this._netIdToEntity.get(netId)
|
||||
}
|
||||
|
||||
private _interpolate(transform: NetworkTransform, deltaTime: number): void {
|
||||
const t = Math.min(1, transform.lerpSpeed * deltaTime)
|
||||
/**
|
||||
* @zh 获取实体的快照缓冲区
|
||||
* @en Get entity's snapshot buffer
|
||||
*/
|
||||
getSnapshotBuffer(netId: number): SnapshotBuffer<ITransformStateWithVelocity> | undefined {
|
||||
return this._entitySnapshots.get(netId)?.buffer
|
||||
}
|
||||
|
||||
transform.currentX += (transform.targetX - transform.currentX) * t
|
||||
transform.currentY += (transform.targetY - transform.currentY) * t
|
||||
|
||||
let angleDiff = transform.targetRotation - transform.currentRotation
|
||||
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2
|
||||
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2
|
||||
transform.currentRotation += angleDiff * t
|
||||
/**
|
||||
* @zh 清空所有快照缓冲
|
||||
* @en Clear all snapshot buffers
|
||||
*/
|
||||
clearSnapshots(): void {
|
||||
for (const data of this._entitySnapshots.values()) {
|
||||
data.buffer.clear()
|
||||
}
|
||||
}
|
||||
|
||||
protected override onDestroy(): void {
|
||||
this._netIdToEntity.clear()
|
||||
this._entitySnapshots.clear()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user