feat: add fixed-point math and network sync, fix docs links (#440)

- feat(math): add Fixed32, FixedMath, FixedVector2 for deterministic calculations
- feat(network): add FixedSnapshotBuffer and FixedClientPrediction for lockstep sync
- docs: fix relative links in behavior-tree, blueprint, guide docs
- docs: add missing sidebar items (cocos-editor, distributed)
- docs: add scene-manager and persistent-entity Chinese translations
This commit is contained in:
YHH
2026-01-05 22:17:30 +08:00
committed by GitHub
parent 12da6bd609
commit 30173f0764
49 changed files with 6073 additions and 103 deletions
+11
View File
@@ -0,0 +1,11 @@
---
"@esengine/ecs-framework-math": minor
---
feat(math): 添加定点数数学库 | Add fixed-point math library
**@esengine/ecs-framework-math** - 新增定点数支持 | Add fixed-point number support
- 新增 `Fixed32` 类:Q16.16 定点数实现 | Add `Fixed32` class: Q16.16 fixed-point implementation
- 新增 `FixedMath` 工具类:定点数数学运算 | Add `FixedMath` utility: fixed-point math operations
- 新增 `FixedVector2` 类:定点数二维向量 | Add `FixedVector2` class: fixed-point 2D vector
- 支持基本算术运算、三角函数、向量运算 | Support basic arithmetic, trigonometry, vector operations
+10
View File
@@ -0,0 +1,10 @@
---
"@esengine/network": minor
---
feat(network): 添加定点数网络同步支持 | Add fixed-point network sync support
**@esengine/network** - 新增定点数同步模块 | Add fixed-point sync module
- 新增 `FixedSnapshotBuffer`:定点数快照缓冲区 | Add `FixedSnapshotBuffer`: fixed-point snapshot buffer
- 新增 `FixedClientPrediction`:定点数客户端预测 | Add `FixedClientPrediction`: fixed-point client prediction
- 支持确定性帧同步和状态回滚 | Support deterministic lockstep and state rollback
+5
View File
@@ -100,6 +100,8 @@ export default defineConfig({
{ label: '最佳实践', slug: 'guide/scene/best-practices', translations: { en: 'Best Practices' } }, { label: '最佳实践', slug: 'guide/scene/best-practices', translations: { en: 'Best Practices' } },
], ],
}, },
{ label: '场景管理器', slug: 'guide/scene-manager', translations: { en: 'SceneManager' } },
{ label: '持久实体', slug: 'guide/persistent-entity', translations: { en: 'Persistent Entity' } },
{ {
label: '序列化', label: '序列化',
translations: { en: 'Serialization' }, translations: { en: 'Serialization' },
@@ -237,6 +239,7 @@ export default defineConfig({
items: [ items: [
{ label: '概述', slug: 'modules/blueprint', translations: { en: 'Overview' } }, { label: '概述', slug: 'modules/blueprint', translations: { en: 'Overview' } },
{ label: '编辑器使用指南', slug: 'modules/blueprint/editor-guide', translations: { en: 'Editor Guide' } }, { label: '编辑器使用指南', slug: 'modules/blueprint/editor-guide', translations: { en: 'Editor Guide' } },
{ label: 'Cocos Creator 编辑器', slug: 'modules/blueprint/cocos-editor', translations: { en: 'Cocos Creator Editor' } },
{ label: '虚拟机 API', slug: 'modules/blueprint/vm', translations: { en: 'VM API' } }, { label: '虚拟机 API', slug: 'modules/blueprint/vm', translations: { en: 'VM API' } },
{ label: '自定义节点', slug: 'modules/blueprint/custom-nodes', translations: { en: 'Custom Nodes' } }, { label: '自定义节点', slug: 'modules/blueprint/custom-nodes', translations: { en: 'Custom Nodes' } },
{ label: '内置节点', slug: 'modules/blueprint/nodes', translations: { en: 'Built-in Nodes' } }, { label: '内置节点', slug: 'modules/blueprint/nodes', translations: { en: 'Built-in Nodes' } },
@@ -275,7 +278,9 @@ export default defineConfig({
{ label: 'HTTP 路由', slug: 'modules/network/http', translations: { en: 'HTTP Routing' } }, { label: 'HTTP 路由', slug: 'modules/network/http', translations: { en: 'HTTP Routing' } },
{ label: '认证系统', slug: 'modules/network/auth', translations: { en: 'Authentication' } }, { label: '认证系统', slug: 'modules/network/auth', translations: { en: 'Authentication' } },
{ label: '速率限制', slug: 'modules/network/rate-limit', translations: { en: 'Rate Limiting' } }, { label: '速率限制', slug: 'modules/network/rate-limit', translations: { en: 'Rate Limiting' } },
{ label: '分布式房间', slug: 'modules/network/distributed', translations: { en: 'Distributed Rooms' } },
{ label: '状态同步', slug: 'modules/network/sync', translations: { en: 'State Sync' } }, { label: '状态同步', slug: 'modules/network/sync', translations: { en: 'State Sync' } },
{ label: '定点数同步', slug: 'modules/network/fixed-point', translations: { en: 'Fixed-Point Sync' } },
{ label: '客户端预测', slug: 'modules/network/prediction', translations: { en: 'Prediction' } }, { label: '客户端预测', slug: 'modules/network/prediction', translations: { en: 'Prediction' } },
{ label: 'AOI 兴趣区域', slug: 'modules/network/aoi', translations: { en: 'AOI' } }, { label: 'AOI 兴趣区域', slug: 'modules/network/aoi', translations: { en: 'AOI' } },
{ label: '增量压缩', slug: 'modules/network/delta', translations: { en: 'Delta Compression' } }, { label: '增量压缩', slug: 'modules/network/delta', translations: { en: 'Delta Compression' } },
@@ -359,6 +359,5 @@ class GoodScene extends Scene {
## Related Documentation ## Related Documentation
- [Scene](./scene) - Learn the basics of scenes - [Scene](/en/guide/scene/) - Learn the basics of scenes
- [SceneManager](./scene-manager) - Learn about scene transitions - [SceneManager](/en/guide/scene-manager/) - Learn about scene transitions
- [WorldManager](./world-manager) - Learn about multi-world management
@@ -16,7 +16,7 @@ The ECS framework provides a platform adapter interface that allows users to imp
## Supported Platforms ## Supported Platforms
### [Browser Adapter](./platform-adapter/browser/) ### [Browser Adapter](/en/guide/platform-adapter/browser/)
Supports all modern browser environments, including Chrome, Firefox, Safari, Edge, etc. Supports all modern browser environments, including Chrome, Firefox, Safari, Edge, etc.
@@ -30,7 +30,7 @@ Supports all modern browser environments, including Chrome, Firefox, Safari, Edg
--- ---
### [WeChat Mini Game Adapter](./platform-adapter/wechat-minigame/) ### [WeChat Mini Game Adapter](/en/guide/platform-adapter/wechat-minigame/)
Designed specifically for the WeChat Mini Game environment, handling special restrictions and APIs. Designed specifically for the WeChat Mini Game environment, handling special restrictions and APIs.
@@ -44,7 +44,7 @@ Designed specifically for the WeChat Mini Game environment, handling special res
--- ---
### [Node.js Adapter](./platform-adapter/nodejs/) ### [Node.js Adapter](/en/guide/platform-adapter/nodejs/)
Provides support for Node.js server environments, suitable for game servers and compute servers. Provides support for Node.js server environments, suitable for game servers and compute servers.
@@ -23,7 +23,7 @@ SceneManager is suitable for:
- Automatic ECS fluent API management - Automatic ECS fluent API management
- Automatic scene lifecycle handling - Automatic scene lifecycle handling
- Integrated with Core, auto-updated - Integrated with Core, auto-updated
- Supports [Persistent Entity](./persistent-entity) migration across scenes (v2.3.0+) - Supports [Persistent Entity](/en/guide/persistent-entity/) migration across scenes (v2.3.0+)
## Basic Usage ## Basic Usage
@@ -434,7 +434,6 @@ Core (Global Services)
## Related Documentation ## Related Documentation
- [Persistent Entity](./persistent-entity) - Learn how to keep entities across scene transitions - [Persistent Entity](/en/guide/persistent-entity/) - Learn how to keep entities across scene transitions
- [WorldManager](./world-manager) - Learn about advanced multi-world isolation features
SceneManager provides simple yet powerful scene management capabilities for most games. Through Core's static methods, you can easily manage scene transitions. SceneManager provides simple yet powerful scene management capabilities for most games. Through Core's static methods, you can easily manage scene transitions.
@@ -100,6 +100,6 @@ console.log('Current state:', runtime.state);
## Next Steps ## Next Steps
- [Core Concepts](./core-concepts/) - Understand nodes and execution - [Core Concepts](/en/modules/behavior-tree/core-concepts/) - Understand nodes and execution
- [Custom Actions](./custom-actions/) - Create your own nodes - [Custom Actions](/en/modules/behavior-tree/custom-actions/) - Create your own nodes
- [Editor Guide](./editor-guide/) - Visual tree creation - [Editor Guide](/en/modules/behavior-tree/editor-guide/) - Visual tree creation
@@ -0,0 +1,383 @@
---
title: "Cocos Creator Blueprint Editor"
description: "Using the blueprint visual scripting system in Cocos Creator"
---
This document explains how to install and use the blueprint visual scripting editor extension in Cocos Creator projects.
## Installation
### 1. Copy Extension to Project
Copy the `cocos-node-editor` extension to your Cocos Creator project's `extensions` directory:
```
your-project/
├── assets/
├── extensions/
│ └── cocos-node-editor/ # Blueprint editor extension
└── ...
```
### 2. Install Dependencies
Install dependencies in the extension directory:
```bash
cd extensions/cocos-node-editor
npm install
```
### 3. Enable Extension
1. Open Cocos Creator
2. Go to **Extensions → Extension Manager**
3. Find `cocos-node-editor` and enable it
## Opening the Blueprint Editor
Open the blueprint editor panel via menu **Panel → Node Editor**.
## Editor Interface
### Toolbar
| Button | Shortcut | Function |
|--------|----------|----------|
| New | - | Create empty blueprint |
| Load | - | Load blueprint from file |
| Save | `Ctrl+S` | Save blueprint to file |
| Undo | `Ctrl+Z` | Undo last operation |
| Redo | `Ctrl+Shift+Z` | Redo operation |
| Cut | `Ctrl+X` | Cut selected nodes |
| Copy | `Ctrl+C` | Copy selected nodes |
| Paste | `Ctrl+V` | Paste nodes |
| Delete | `Delete` | Delete selected items |
| Rescan | - | Rescan project for blueprint nodes |
### Canvas Operations
- **Right-click on canvas**: Open node addition menu
- **Drag nodes**: Move node position
- **Click node**: Select node
- **Ctrl+Click**: Multi-select nodes
- **Drag pin to pin**: Create connection
- **Scroll wheel**: Zoom canvas
- **Middle-click drag**: Pan canvas
### Node Menu
Right-clicking on canvas shows the node menu:
- Search box at top for quick node search
- Nodes grouped by category
- Press `Enter` to quickly add first search result
- Press `Esc` to close menu
## Blueprint File Format
Blueprints are saved as `.blueprint.json` files, fully compatible with runtime:
```json
{
"version": 1,
"type": "blueprint",
"metadata": {
"name": "My Blueprint",
"createdAt": 1704307200000,
"modifiedAt": 1704307200000
},
"variables": [],
"nodes": [
{
"id": "node-1",
"type": "PrintString",
"position": { "x": 100, "y": 200 },
"data": {}
}
],
"connections": [
{
"id": "conn-1",
"fromNodeId": "node-1",
"fromPin": "exec",
"toNodeId": "node-2",
"toPin": "exec"
}
]
}
```
## Running Blueprints in Game
Use ECS system to manage and execute blueprints.
### 1. Define Blueprint Component
```typescript
import { Component, ECSComponent, Property, Serialize } from '@esengine/ecs-framework';
import type { BlueprintAsset } from '@esengine/blueprint';
@ECSComponent('Blueprint')
export class BlueprintComponent extends Component {
@Serialize()
@Property({ type: 'asset', label: 'Blueprint Asset' })
blueprintPath: string = '';
@Serialize()
@Property({ type: 'boolean', label: 'Auto Start' })
autoStart: boolean = true;
// Runtime data (not serialized)
blueprintAsset: BlueprintAsset | null = null;
vm: BlueprintVM | null = null;
isStarted: boolean = false;
}
```
### 2. Create Blueprint Execution System
```typescript
import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
import {
BlueprintVM,
validateBlueprintAsset
} from '@esengine/blueprint';
import { BlueprintComponent } from './BlueprintComponent';
export class BlueprintExecutionSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(BlueprintComponent));
}
protected override process(entities: readonly Entity[]): void {
const dt = Time.deltaTime;
for (const entity of entities) {
const bp = entity.getComponent(BlueprintComponent)!;
// Skip entities without blueprint asset
if (!bp.blueprintAsset) continue;
// Initialize VM
if (!bp.vm) {
bp.vm = new BlueprintVM(bp.blueprintAsset, entity, this.scene!);
}
// Auto start
if (bp.autoStart && !bp.isStarted) {
bp.vm.start();
bp.isStarted = true;
}
// Update blueprint
if (bp.isStarted) {
bp.vm.tick(dt);
}
}
}
protected override onRemoved(entity: Entity): void {
const bp = entity.getComponent(BlueprintComponent);
if (bp?.vm && bp.isStarted) {
bp.vm.stop();
bp.vm = null;
bp.isStarted = false;
}
}
}
```
### 3. Load Blueprint and Add to Entity
```typescript
import { resources, JsonAsset } from 'cc';
import { validateBlueprintAsset } from '@esengine/blueprint';
// Load blueprint asset
async function loadBlueprint(path: string): Promise<BlueprintAsset | null> {
return new Promise((resolve) => {
resources.load(path, JsonAsset, (err, asset) => {
if (err || !asset) {
console.error('Failed to load blueprint:', err);
resolve(null);
return;
}
const data = asset.json;
if (validateBlueprintAsset(data)) {
resolve(data as BlueprintAsset);
} else {
console.error('Invalid blueprint format');
resolve(null);
}
});
});
}
// Create entity with blueprint
async function createBlueprintEntity(scene: IScene, blueprintPath: string): Promise<Entity> {
const entity = scene.createEntity('BlueprintEntity');
const bpComponent = entity.addComponent(BlueprintComponent);
bpComponent.blueprintPath = blueprintPath;
bpComponent.blueprintAsset = await loadBlueprint(blueprintPath);
return entity;
}
```
### 4. Register System to Scene
```typescript
// During scene initialization
scene.addSystem(new BlueprintExecutionSystem());
```
## Creating Custom Nodes
### Using Decorators for Components
Use decorators to automatically generate blueprint nodes from components:
```typescript
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { BlueprintExpose, BlueprintProperty, BlueprintMethod } from '@esengine/blueprint';
@ECSComponent('Health')
@BlueprintExpose({ displayName: 'Health Component' })
export class HealthComponent extends Component {
@BlueprintProperty({ displayName: 'Current Health', category: 'number' })
current: number = 100;
@BlueprintProperty({ displayName: 'Max Health', category: 'number' })
max: number = 100;
@BlueprintMethod({ displayName: 'Heal', isExec: true })
heal(amount: number): void {
this.current = Math.min(this.current + amount, this.max);
}
@BlueprintMethod({ displayName: 'Take Damage', isExec: true })
takeDamage(amount: number): void {
this.current = Math.max(this.current - amount, 0);
}
@BlueprintMethod({ displayName: 'Is Dead' })
isDead(): boolean {
return this.current <= 0;
}
}
```
### Register Component Nodes
```typescript
import { registerAllComponentNodes } from '@esengine/blueprint';
// Register all decorated components at application startup
registerAllComponentNodes();
```
### Manual Node Definition (Advanced)
For fully custom node logic:
```typescript
import {
BlueprintNodeTemplate,
INodeExecutor,
RegisterNode,
ExecutionContext,
ExecutionResult
} from '@esengine/blueprint';
const MyNodeTemplate: BlueprintNodeTemplate = {
type: 'MyCustomNode',
title: 'My Custom Node',
category: 'custom',
description: 'Custom node example',
inputs: [
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
{ name: 'value', type: 'number', direction: 'input', defaultValue: 0 }
],
outputs: [
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
{ name: 'result', type: 'number', direction: 'output' }
]
};
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const value = context.getInput<number>(node.id, 'value');
return {
outputs: { result: value * 2 },
nextExec: 'exec'
};
}
}
```
## Node Categories
| Category | Description | Color |
|----------|-------------|-------|
| `event` | Event nodes | Red |
| `flow` | Flow control | Gray |
| `entity` | Entity operations | Blue |
| `component` | Component access | Cyan |
| `math` | Math operations | Green |
| `logic` | Logic operations | Red |
| `variable` | Variable access | Purple |
| `time` | Time utilities | Cyan |
| `debug` | Debug utilities | Gray |
| `custom` | Custom nodes | Blue-gray |
## Best Practices
1. **File Organization**
- Place blueprint files in `assets/blueprints/` directory
- Use meaningful file names like `player-controller.blueprint.json`
2. **Component Design**
- Use `@BlueprintExpose` to mark components that should be exposed to blueprints
- Provide clear `displayName` for properties and methods
- Mark execution methods with `isExec: true`
3. **Performance Considerations**
- Avoid heavy computation in Tick events
- Use variables to cache intermediate results
- Pure function nodes automatically cache outputs
4. **Debugging Tips**
- Use Print nodes to output intermediate values
- Enable `vm.debug = true` to view execution logs
## FAQ
### Q: Node menu is empty?
A: Click the **Rescan** button to scan for blueprint node classes in your project. Make sure you have called `registerAllComponentNodes()`.
### Q: Blueprint doesn't execute?
A: Check:
1. Entity has `BlueprintComponent` added
2. `BlueprintExecutionSystem` is registered to scene
3. `blueprintAsset` is correctly loaded
4. `autoStart` is `true`
### Q: How to trigger custom events?
A: Trigger through VM:
```typescript
const bp = entity.getComponent(BlueprintComponent);
bp.vm?.triggerCustomEvent('OnPickup', { item: itemEntity });
```
## Related Documentation
- [Blueprint Runtime API](/en/modules/blueprint/) - BlueprintVM and core API
- [Custom Nodes](/en/modules/blueprint/custom-nodes) - Detailed node creation guide
- [Built-in Nodes](/en/modules/blueprint/nodes) - Built-in node reference
@@ -604,7 +604,7 @@ Use **Print** nodes to output variable values to the console.
## Next Steps ## Next Steps
- [ECS Node Reference](./nodes) - Complete node list - [ECS Node Reference](/en/modules/blueprint/nodes) - Complete node list
- [Custom Nodes](./custom-nodes) - Create custom nodes - [Custom Nodes](/en/modules/blueprint/custom-nodes) - Create custom nodes
- [Runtime Integration](./vm) - Blueprint VM API - [Runtime Integration](/en/modules/blueprint/vm) - Blueprint VM API
- [Examples](./examples) - More game logic examples - [Examples](/en/modules/blueprint/examples) - More game logic examples
@@ -604,6 +604,6 @@ Blueprint-defined variables automatically generate Get and Set nodes:
## Related Documentation ## Related Documentation
- [Blueprint Editor Guide](./editor-guide) - Learn how to use the editor - [Blueprint Editor Guide](/en/modules/blueprint/editor-guide) - Learn how to use the editor
- [Custom Nodes](./custom-nodes) - Create custom nodes - [Custom Nodes](/en/modules/blueprint/custom-nodes) - Create custom nodes
- [Blueprint VM](./vm) - Runtime API - [Blueprint VM](/en/modules/blueprint/vm) - Runtime API
@@ -0,0 +1,326 @@
---
title: "Fixed-Point Numbers"
description: "Deterministic fixed-point math library for lockstep games"
---
`@esengine/ecs-framework-math` provides deterministic fixed-point calculations designed for **Lockstep** architecture. Fixed-point numbers guarantee identical results across all platforms.
## Why Fixed-Point?
Floating-point numbers may produce different rounding results on different platforms:
```typescript
// Floating-point: may differ across platforms
const a = 0.1 + 0.2; // 0.30000000000000004 (some platforms)
// 0.3 (other platforms)
// Fixed-point: consistent everywhere
const x = Fixed32.from(0.1);
const y = Fixed32.from(0.2);
const z = x.add(y); // raw = 19661 (all platforms)
```
| Feature | Floating-Point | Fixed-Point |
|---------|----------------|-------------|
| Cross-platform consistency | ❌ May differ | ✅ Identical |
| Network sync mode | State sync | Lockstep |
| Game types | FPS, RPG | RTS, MOBA, Fighting |
## Installation
```bash
npm install @esengine/ecs-framework-math
```
## Fixed32 Fixed-Point Number
Q16.16 format: 16-bit integer + 16-bit fraction, range ±32767.99998.
### Creating Fixed-Point Numbers
```typescript
import { Fixed32 } from '@esengine/ecs-framework-math';
// From floating-point
const speed = Fixed32.from(5.5);
// From integer (no precision loss)
const count = Fixed32.fromInt(10);
// From raw value (after network receive)
const received = Fixed32.fromRaw(360448); // equals 5.5
// Predefined constants
Fixed32.ZERO // 0
Fixed32.ONE // 1
Fixed32.HALF // 0.5
Fixed32.PI // π
Fixed32.TWO_PI // 2π
Fixed32.HALF_PI // π/2
```
### Basic Operations
```typescript
const a = Fixed32.from(10);
const b = Fixed32.from(3);
const sum = a.add(b); // 13
const diff = a.sub(b); // 7
const prod = a.mul(b); // 30
const quot = a.div(b); // 3.333...
const mod = a.mod(b); // 1
const neg = a.neg(); // -10
const abs = neg.abs(); // 10
```
### Comparison Operations
```typescript
const x = Fixed32.from(5);
const y = Fixed32.from(3);
x.eq(y) // false - equal
x.ne(y) // true - not equal
x.lt(y) // false - less than
x.le(y) // false - less or equal
x.gt(y) // true - greater than
x.ge(y) // true - greater or equal
x.isZero() // false
x.isPositive() // true
x.isNegative() // false
```
### Math Functions
```typescript
// Square root (Newton's method, deterministic)
const sqrt = Fixed32.sqrt(Fixed32.from(16)); // 4
// Rounding
Fixed32.floor(Fixed32.from(3.7)) // 3
Fixed32.ceil(Fixed32.from(3.2)) // 4
Fixed32.round(Fixed32.from(3.5)) // 4
// Clamping
Fixed32.clamp(value, min, max)
// Linear interpolation
Fixed32.lerp(from, to, t)
// Min/Max
Fixed32.min(a, b)
Fixed32.max(a, b)
```
### Type Conversion
```typescript
const value = Fixed32.from(3.14159);
// To float (for rendering)
const float = value.toNumber(); // 3.14159
// Get raw value (for network)
const raw = value.toRaw(); // 205887
// To integer (floor)
const int = value.toInt(); // 3
```
## FixedVector2 Fixed-Point Vector
Immutable 2D vector, all operations return new instances.
### Creating Vectors
```typescript
import { FixedVector2, Fixed32 } from '@esengine/ecs-framework-math';
// From floating-point
const pos = FixedVector2.from(100, 200);
// From raw values (after network receive)
const received = FixedVector2.fromRaw(6553600, 13107200);
// From Fixed32
const vec = new FixedVector2(Fixed32.from(10), Fixed32.from(20));
// Predefined constants
FixedVector2.ZERO // (0, 0)
FixedVector2.ONE // (1, 1)
FixedVector2.RIGHT // (1, 0)
FixedVector2.LEFT // (-1, 0)
FixedVector2.UP // (0, 1)
FixedVector2.DOWN // (0, -1)
```
### Vector Operations
```typescript
const a = FixedVector2.from(3, 4);
const b = FixedVector2.from(1, 2);
// Basic operations
const sum = a.add(b); // (4, 6)
const diff = a.sub(b); // (2, 2)
const scaled = a.mul(Fixed32.from(2)); // (6, 8)
const divided = a.div(Fixed32.from(2)); // (1.5, 2)
// Vector products
const dot = a.dot(b); // 3*1 + 4*2 = 11
const cross = a.cross(b); // 3*2 - 4*1 = 2
// Length
const lenSq = a.lengthSquared(); // 25
const len = a.length(); // 5
// Normalize
const norm = a.normalize(); // (0.6, 0.8)
// Distance
const dist = a.distanceTo(b); // sqrt((3-1)² + (4-2)²)
```
### Rotation and Angles
```typescript
import { FixedMath } from '@esengine/ecs-framework-math';
const vec = FixedVector2.from(1, 0);
const angle = Fixed32.from(Math.PI / 2); // 90 degrees
// Rotate vector
const rotated = vec.rotate(angle); // (0, 1)
// Rotate around point
const center = FixedVector2.from(5, 5);
const around = vec.rotateAround(center, angle);
// Get vector angle
const vecAngle = vec.angle();
// Angle between vectors
const between = vec.angleTo(other);
// Create unit vector from angle
const dir = FixedVector2.fromAngle(angle);
// From polar coordinates
const polar = FixedVector2.fromPolar(length, angle);
```
### Type Conversion
```typescript
const pos = FixedVector2.from(100.5, 200.5);
// To float object (for rendering)
const obj = pos.toObject(); // { x: 100.5, y: 200.5 }
// To array
const arr = pos.toArray(); // [100.5, 200.5]
// Get raw values (for network)
const raw = pos.toRawObject(); // { x: 6586368, y: 13140992 }
```
## FixedMath Trigonometric Functions
Deterministic trigonometric functions using lookup tables.
```typescript
import { FixedMath, Fixed32 } from '@esengine/ecs-framework-math';
const angle = Fixed32.from(Math.PI / 6); // 30 degrees
// Trigonometric functions
const sin = FixedMath.sin(angle); // 0.5
const cos = FixedMath.cos(angle); // 0.866
const tan = FixedMath.tan(angle); // 0.577
// Inverse trigonometric
const atan = FixedMath.atan2(y, x);
const asin = FixedMath.asin(value);
const acos = FixedMath.acos(value);
// Normalize angle to [-π, π]
const normalized = FixedMath.normalizeAngle(angle);
// Angle difference (shortest path)
const delta = FixedMath.angleDelta(from, to);
// Angle interpolation (handles 360° wrap)
const lerped = FixedMath.lerpAngle(from, to, t);
// Radian/degree conversion
const deg = FixedMath.radToDeg(rad);
const rad = FixedMath.degToRad(deg);
```
## Best Practices
### 1. Use Fixed-Point Throughout
```typescript
// ✅ Correct: all game logic uses fixed-point
function calculateDamage(baseDamage: Fixed32, multiplier: Fixed32): Fixed32 {
return baseDamage.mul(multiplier);
}
// ❌ Wrong: mixing floating-point
function calculateDamage(baseDamage: number, multiplier: number): number {
return baseDamage * multiplier; // may be inconsistent
}
```
### 2. Only Convert to Float for Rendering
```typescript
// Game logic
const position: FixedVector2 = calculatePosition(input);
// Rendering
const { x, y } = position.toObject();
sprite.position.set(x, y);
```
### 3. Use Raw Values for Network
```typescript
// ✅ Correct: transmit raw integers
const raw = position.toRawObject();
send(JSON.stringify(raw));
// ❌ Wrong: transmit floats
const float = position.toObject();
send(JSON.stringify(float)); // may lose precision
```
### 4. Use FixedMath for Trigonometry
```typescript
// ✅ Correct: use lookup tables
const direction = FixedVector2.fromAngle(FixedMath.atan2(dy, dx));
// ❌ Wrong: use Math library
const angle = Math.atan2(dy.toNumber(), dx.toNumber()); // non-deterministic
```
## API Exports
```typescript
import {
Fixed32,
FixedVector2,
FixedMath,
type IFixed32,
type IFixedVector2
} from '@esengine/ecs-framework-math';
```
## Related Docs
- [State Sync](/en/modules/network/sync) - Fixed-point snapshot buffer
- [Client Prediction](/en/modules/network/prediction) - Fixed-point client prediction
@@ -252,3 +252,145 @@ if (predictionSystem) {
console.log('Current sequence:', predictionSystem.inputSequence); console.log('Current sequence:', predictionSystem.inputSequence);
} }
``` ```
---
## Fixed-Point Client Prediction (Lockstep)
Deterministic client prediction for **Lockstep** architecture.
> See [Fixed-Point Numbers](/en/modules/network/fixed-point) for math basics
### Basic Usage
```typescript
import {
FixedClientPrediction,
createFixedClientPrediction,
type IFixedPredictor,
type IFixedStatePositionExtractor
} from '@esengine/network';
import { Fixed32, FixedVector2 } from '@esengine/ecs-framework-math';
// Define game state
interface GameState {
position: FixedVector2;
velocity: FixedVector2;
}
// Implement predictor (must use fixed-point arithmetic)
const predictor: IFixedPredictor<GameState, PlayerInput> = {
predict(state: GameState, input: PlayerInput, deltaTime: Fixed32): GameState {
const speed = Fixed32.from(100);
const inputVec = FixedVector2.from(input.dx, input.dy);
const velocity = inputVec.normalize().mul(speed);
const displacement = velocity.mul(deltaTime);
return {
position: state.position.add(displacement),
velocity
};
}
};
// Create prediction
const prediction = createFixedClientPrediction(predictor, {
maxUnacknowledgedInputs: 60,
fixedDeltaTime: Fixed32.from(1 / 60),
reconciliationThreshold: Fixed32.from(0.001),
enableSmoothReconciliation: false // Usually disabled for lockstep
});
```
### Record Input
```typescript
function onUpdate(input: PlayerInput, currentState: GameState) {
// Record input and get predicted state
const predicted = prediction.recordInput(input, currentState);
// Render predicted state
const pos = predicted.position.toObject();
sprite.position.set(pos.x, pos.y);
// Send input
socket.send(JSON.stringify({
frame: prediction.currentFrame,
input
}));
}
```
### Server Reconciliation
```typescript
// Position extractor
const posExtractor: IFixedStatePositionExtractor<GameState> = {
getPosition(state: GameState): FixedVector2 {
return state.position;
}
};
// When receiving server state
function onServerState(serverState: GameState, serverFrame: number) {
const reconciled = prediction.reconcile(
serverState,
serverFrame,
posExtractor
);
}
```
### Rollback and Replay
```typescript
// Rollback when desync detected
const correctedState = prediction.rollbackAndResimulate(
serverFrame,
authoritativeState
);
// View historical state
const historicalState = prediction.getStateAtFrame(100);
```
### Preset Movement Predictor
```typescript
import {
createFixedMovementPredictor,
createFixedMovementPositionExtractor,
type IFixedMovementInput,
type IFixedMovementState
} from '@esengine/network';
// Create movement predictor (speed 100 units/sec)
const movePredictor = createFixedMovementPredictor(Fixed32.from(100));
const posExtractor = createFixedMovementPositionExtractor();
const prediction = createFixedClientPrediction<IFixedMovementState, IFixedMovementInput>(
movePredictor,
{ fixedDeltaTime: Fixed32.from(1 / 60) }
);
// Input format
const input: IFixedMovementInput = { dx: 1, dy: 0 };
```
### API Exports
```typescript
import {
FixedClientPrediction,
createFixedClientPrediction,
createFixedMovementPredictor,
createFixedMovementPositionExtractor,
type IFixedInputSnapshot,
type IFixedPredictedState,
type IFixedPredictor,
type IFixedStatePositionExtractor,
type FixedClientPredictionConfig,
type IFixedMovementInput,
type IFixedMovementState
} from '@esengine/network';
```
@@ -235,3 +235,139 @@ const corrected = prediction.reconcile(serverState, serverSeq, applyInput);
1. **Interpolation delay**: 100-150ms for typical networks 1. **Interpolation delay**: 100-150ms for typical networks
2. **Prediction**: Use only for local player, interpolate remote players 2. **Prediction**: Use only for local player, interpolate remote players
3. **Snapshot count**: Keep enough snapshots to handle network jitter 3. **Snapshot count**: Keep enough snapshots to handle network jitter
---
## Fixed-Point Sync (Lockstep)
For **Lockstep** architecture, use fixed-point numbers to ensure cross-platform determinism.
> See [Fixed-Point Numbers](/en/modules/network/fixed-point) for math basics
### FixedTransformState
Fixed-point transform state for network transmission:
```typescript
import {
FixedTransformState,
FixedTransformStateWithVelocity,
type IFixedTransformStateRaw
} from '@esengine/network';
// Create state
const state = FixedTransformState.from(100, 200, Math.PI / 4);
// Serialize (sender)
const raw: IFixedTransformStateRaw = state.toRaw();
socket.send(JSON.stringify({ type: 'sync', state: raw }));
// Deserialize (receiver)
const received = FixedTransformState.fromRaw(message.state);
// Use for rendering
const { x, y, rotation } = received.toFloat();
sprite.position.set(x, y);
```
State with velocity (for extrapolation):
```typescript
const state = FixedTransformStateWithVelocity.from(
100, 200, // position
0, // rotation
5, 3, // velocity
0.1 // angular velocity
);
```
### Fixed-Point Interpolators
```typescript
import {
createFixedTransformInterpolator,
createFixedHermiteTransformInterpolator
} from '@esengine/network';
import { Fixed32 } from '@esengine/ecs-framework-math';
// Linear interpolator
const interpolator = createFixedTransformInterpolator();
const from = FixedTransformState.from(0, 0, 0);
const to = FixedTransformState.from(100, 50, Math.PI);
const t = Fixed32.from(0.5);
const result = interpolator.interpolate(from, to, t);
// Hermite interpolator (smoother)
const hermite = createFixedHermiteTransformInterpolator(100);
```
### Fixed-Point Snapshot Buffer
Manages fixed-point state history for lockstep replay:
```typescript
import {
FixedSnapshotBuffer,
createFixedSnapshotBuffer
} from '@esengine/network';
// Create buffer (max 30 snapshots, 2 frame delay)
const buffer = createFixedSnapshotBuffer<FixedTransformState>(30, 2);
// Add snapshots
buffer.push({
frame: 100,
state: FixedTransformState.from(100, 200, 0)
});
// Get interpolation snapshots
const result = buffer.getInterpolationSnapshots(103);
if (result) {
const { from, to, t } = result;
const interpolated = interpolator.interpolate(from.state, to.state, t);
}
// Get latest/specific frame
const latest = buffer.getLatest();
const atFrame = buffer.getAtFrame(100);
// Rollback replay
const snapshotsToReplay = buffer.getSnapshotsAfter(98);
// Clean up old snapshots
buffer.removeSnapshotsBefore(95);
```
Sub-frame interpolation:
```typescript
// Use Fixed32 frame time (supports fractional frames)
const frameTime = Fixed32.from(102.5);
const result = buffer.getInterpolationSnapshotsFixed(frameTime);
```
### API Exports
```typescript
import {
// State classes
FixedTransformState,
FixedTransformStateWithVelocity,
type IFixedTransformStateRaw,
type IFixedTransformStateWithVelocityRaw,
// Interpolators
FixedTransformInterpolator,
FixedHermiteTransformInterpolator,
createFixedTransformInterpolator,
createFixedHermiteTransformInterpolator,
// Snapshot buffer
FixedSnapshotBuffer,
createFixedSnapshotBuffer,
type IFixedStateSnapshot,
type IFixedInterpolationResult
} from '@esengine/network';
```
+3 -3
View File
@@ -434,6 +434,6 @@ const found = hierarchySystem.findChild(parent, "Child");
## 下一步 ## 下一步
- 了解 [实体类](./entity/) 的其他功能 - 了解 [实体类](/guide/entity/) 的其他功能
- 了解 [场景管理](./scene/) 如何组织实体和系统 - 了解 [场景管理](/guide/scene/) 如何组织实体和系统
- 了解 [组件系统](./component/) 如何定义和使用组件 - 了解 [组件系统](/guide/component/) 如何定义和使用组件
@@ -0,0 +1,363 @@
---
title: "持久实体"
---
# 持久实体
> **版本**: v2.3.0+
持久实体是一种特殊类型的实体,在场景切换时会自动迁移到新场景。适用于需要跨场景保持状态的游戏对象,如玩家、游戏管理器、音频管理器等。
## 基本概念
在 ECS 框架中,实体有两种生命周期策略:
| 策略 | 描述 | 默认 |
|------|------|------|
| `SceneLocal` | 场景局部实体,场景切换时销毁 | ✓ |
| `Persistent` | 持久实体,场景切换时自动迁移 | |
## 快速开始
### 创建持久实体
```typescript
import { Scene } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// 创建持久玩家实体
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Position(100, 200));
player.addComponent(new PlayerData('Hero', 500));
// 创建普通敌人实体(场景切换时销毁)
const enemy = this.createEntity('Enemy');
enemy.addComponent(new Position(300, 200));
enemy.addComponent(new EnemyAI());
}
}
```
### 场景切换时的行为
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// 初始场景
class Level1Scene extends Scene {
protected initialize(): void {
// 玩家 - 持久实体,将迁移到下一个场景
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Position(0, 0));
player.addComponent(new Health(100));
// 敌人 - 场景局部实体,场景切换时销毁
const enemy = this.createEntity('Enemy');
enemy.addComponent(new Position(100, 100));
}
}
// 目标场景
class Level2Scene extends Scene {
protected initialize(): void {
// 新敌人
const enemy = this.createEntity('Boss');
enemy.addComponent(new Position(200, 200));
}
public onStart(): void {
// 玩家已自动迁移到此场景
const player = this.findEntity('Player');
console.log(player !== null); // true
// 位置和生命值数据完整保留
const position = player?.getComponent(Position);
const health = player?.getComponent(Health);
console.log(position?.x, position?.y); // 0, 0
console.log(health?.value); // 100
}
}
// 切换场景
Core.create({ debug: true });
Core.setScene(new Level1Scene());
// 稍后切换到 Level2
Core.loadScene(new Level2Scene());
// 玩家实体自动迁移,敌人实体被销毁
```
## API 参考
### 实体方法
#### setPersistent()
将实体标记为持久实体,防止在场景切换时被销毁。
```typescript
public setPersistent(): this
```
**返回值**: 返回实体本身,支持链式调用
**示例**:
```typescript
const player = scene.createEntity('Player')
.setPersistent();
player.addComponent(new Position(100, 200));
```
#### setSceneLocal()
将实体恢复为场景局部策略(默认)。
```typescript
public setSceneLocal(): this
```
**返回值**: 返回实体本身,支持链式调用
**示例**:
```typescript
// 动态取消持久性
player.setSceneLocal();
```
#### isPersistent
检查实体是否为持久实体。
```typescript
public get isPersistent(): boolean
```
**示例**:
```typescript
if (entity.isPersistent) {
console.log('这是一个持久实体');
}
```
#### lifecyclePolicy
获取实体的生命周期策略。
```typescript
public get lifecyclePolicy(): EEntityLifecyclePolicy
```
**示例**:
```typescript
import { EEntityLifecyclePolicy } from '@esengine/ecs-framework';
if (entity.lifecyclePolicy === EEntityLifecyclePolicy.Persistent) {
console.log('持久实体');
}
```
### 场景方法
#### findPersistentEntities()
查找场景中所有持久实体。
```typescript
public findPersistentEntities(): Entity[]
```
**返回值**: 持久实体数组
**示例**:
```typescript
const persistentEntities = scene.findPersistentEntities();
console.log(`场景中有 ${persistentEntities.length} 个持久实体`);
```
#### extractPersistentEntities()
提取并移除场景中所有持久实体(通常由框架内部调用)。
```typescript
public extractPersistentEntities(): Entity[]
```
**返回值**: 被提取的持久实体数组
#### receiveMigratedEntities()
接收迁移的实体(通常由框架内部调用)。
```typescript
public receiveMigratedEntities(entities: Entity[]): void
```
**参数**:
- `entities` - 要接收的实体数组
## 使用场景
### 1. 跨关卡的玩家实体
```typescript
class PlayerSetupScene extends Scene {
protected initialize(): void {
// 玩家在所有关卡中保持状态
const player = this.createEntity('Player').setPersistent();
player.addComponent(new Transform(0, 0));
player.addComponent(new Health(100));
player.addComponent(new Inventory());
player.addComponent(new PlayerStats());
}
}
class Level1 extends Scene { /* ... */ }
class Level2 extends Scene { /* ... */ }
class Level3 extends Scene { /* ... */ }
// 玩家实体在所有关卡之间自动迁移
Core.setScene(new PlayerSetupScene());
// ... 游戏进行
Core.loadScene(new Level1());
// ... 关卡完成
Core.loadScene(new Level2());
// 玩家数据(生命值、背包、属性)完整保留
```
### 2. 全局管理器
```typescript
class BootstrapScene extends Scene {
protected initialize(): void {
// 音频管理器 - 跨场景持久
const audioManager = this.createEntity('AudioManager').setPersistent();
audioManager.addComponent(new AudioController());
// 成就管理器 - 跨场景持久
const achievementManager = this.createEntity('AchievementManager').setPersistent();
achievementManager.addComponent(new AchievementTracker());
// 游戏设置 - 跨场景持久
const settings = this.createEntity('GameSettings').setPersistent();
settings.addComponent(new SettingsData());
}
}
```
### 3. 动态切换持久性
```typescript
class GameScene extends Scene {
protected initialize(): void {
// 初始创建为普通实体
const companion = this.createEntity('Companion');
companion.addComponent(new Transform(0, 0));
companion.addComponent(new CompanionAI());
// 监听招募事件
this.eventSystem.on('companion:recruited', () => {
// 招募后变为持久实体
companion.setPersistent();
console.log('同伴加入队伍,将跟随玩家跨场景');
});
// 监听解散事件
this.eventSystem.on('companion:dismissed', () => {
// 解散后恢复为场景局部实体
companion.setSceneLocal();
console.log('同伴离开队伍,不再跨场景持久');
});
}
}
```
## 最佳实践
### 1. 明确标识持久实体
```typescript
// 推荐:创建时立即标记
const player = this.createEntity('Player').setPersistent();
// 不推荐:创建后再标记(容易遗忘)
const player = this.createEntity('Player');
// ... 大量代码 ...
player.setPersistent(); // 容易忘记
```
### 2. 合理使用持久性
```typescript
// ✓ 适合持久化的实体
const player = this.createEntity('Player').setPersistent(); // 玩家
const gameManager = this.createEntity('GameManager').setPersistent(); // 全局管理器
const audioManager = this.createEntity('AudioManager').setPersistent(); // 音频系统
// ✗ 不应该持久化的实体
const bullet = this.createEntity('Bullet'); // 临时对象
const enemy = this.createEntity('Enemy'); // 关卡特定敌人
const particle = this.createEntity('Particle'); // 特效粒子
```
### 3. 检查迁移的实体
```typescript
class NewScene extends Scene {
public onStart(): void {
// 检查预期的持久实体是否存在
const player = this.findEntity('Player');
if (!player) {
console.error('玩家实体未正确迁移!');
// 处理错误情况
}
}
}
```
### 4. 避免循环引用
```typescript
// ✗ 避免:持久实体引用场景局部实体
class BadScene extends Scene {
protected initialize(): void {
const player = this.createEntity('Player').setPersistent();
const enemy = this.createEntity('Enemy');
// 危险:player 是持久的但 enemy 不是
// 场景切换后,enemy 被销毁,引用变为无效
player.addComponent(new TargetComponent(enemy));
}
}
// ✓ 推荐:使用 ID 引用或事件系统
class GoodScene extends Scene {
protected initialize(): void {
const player = this.createEntity('Player').setPersistent();
const enemy = this.createEntity('Enemy');
// 存储 ID 而非直接引用
player.addComponent(new TargetComponent(enemy.id));
// 或使用事件系统通信
}
}
```
## 重要说明
1. **已销毁的实体不会迁移**:如果实体在场景切换前被销毁,即使标记为持久也不会迁移。
2. **组件数据完整保留**:迁移过程中所有组件及其状态都会被保留。
3. **场景引用会更新**:迁移后,实体的 `scene` 属性将指向新场景。
4. **查询系统会更新**:迁移的实体会自动注册到新场景的查询系统中。
5. **延迟切换同样有效**:使用 `Core.loadScene()` 进行延迟切换时,持久实体同样会迁移。
## 相关文档
- [场景](/guide/scene/) - 了解场景基础知识
- [场景管理器](/guide/scene-manager/) - 了解场景切换
@@ -16,7 +16,7 @@ ECS框架提供了平台适配器接口,允许用户为不同的运行环境
## 支持的平台 ## 支持的平台
### 🌐 [浏览器适配器](./platform-adapter/browser/) ### 🌐 [浏览器适配器](/guide/platform-adapter/browser/)
支持所有现代浏览器环境,包括 Chrome、Firefox、Safari、Edge 等。 支持所有现代浏览器环境,包括 Chrome、Firefox、Safari、Edge 等。
@@ -30,7 +30,7 @@ ECS框架提供了平台适配器接口,允许用户为不同的运行环境
--- ---
### 📱 [微信小游戏适配器](./platform-adapter/wechat-minigame/) ### 📱 [微信小游戏适配器](/guide/platform-adapter/wechat-minigame/)
专为微信小游戏环境设计,处理微信小游戏的特殊限制和API。 专为微信小游戏环境设计,处理微信小游戏的特殊限制和API。
@@ -44,7 +44,7 @@ ECS框架提供了平台适配器接口,允许用户为不同的运行环境
--- ---
### 🖥️ [Node.js适配器](./platform-adapter/nodejs/) ### 🖥️ [Node.js适配器](/guide/platform-adapter/nodejs/)
为 Node.js 服务器环境提供支持,适用于游戏服务器和计算服务器。 为 Node.js 服务器环境提供支持,适用于游戏服务器和计算服务器。
@@ -0,0 +1,439 @@
---
title: "场景管理器"
---
# SceneManager
SceneManager 是 ECS Framework 提供的轻量级场景管理器,适用于 95% 的游戏应用。它提供简单直观的 API,支持场景切换和延迟加载。
## 适用场景
SceneManager 适用于:
- 单人游戏
- 简单多人游戏
- 移动游戏
- 需要场景切换的游戏(菜单、游戏、暂停等)
- 不需要多 World 隔离的项目
## 功能特性
- 轻量级,零额外开销
- 简单直观的 API
- 支持延迟场景切换(避免在帧中途切换)
- 自动 ECS 流式 API 管理
- 自动场景生命周期处理
- 与 Core 集成,自动更新
- 支持 [持久实体](/guide/persistent-entity/) 跨场景迁移(v2.3.0+
## 基本用法
### 推荐:使用 Core 的静态方法
这是最简单且推荐的方式,适用于大多数应用:
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
// 1. 初始化 Core
Core.create({ debug: true });
// 2. 创建并设置场景
class GameScene extends Scene {
protected initialize(): void {
this.name = "GameScene";
// 添加系统
this.addSystem(new MovementSystem());
this.addSystem(new RenderSystem());
// 创建初始实体
const player = this.createEntity("Player");
player.addComponent(new Transform(400, 300));
player.addComponent(new Health(100));
}
public onStart(): void {
console.log("游戏场景已启动");
}
}
// 3. 设置场景
Core.setScene(new GameScene());
// 4. 游戏循环(Core.update 自动更新场景)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 自动更新所有服务和场景
}
// Laya 引擎集成
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000;
Core.update(deltaTime);
});
// Cocos Creator 集成
update(deltaTime: number) {
Core.update(deltaTime);
}
```
### 进阶:直接使用 SceneManager
如果需要更多控制,可以直接使用 SceneManager
```typescript
import { Core, SceneManager, Scene } from '@esengine/ecs-framework';
// 初始化 Core
Core.create({ debug: true });
// 获取 SceneManager(已由 Core 自动创建并注册)
const sceneManager = Core.services.resolve(SceneManager);
// 设置场景
const gameScene = new GameScene();
sceneManager.setScene(gameScene);
// 游戏循环(仍然使用 Core.update
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // Core 自动调用 sceneManager.update()
}
```
**重要提示**:无论使用哪种方式,在游戏循环中只需调用 `Core.update()`。它会自动更新 SceneManager 和场景。无需手动调用 `sceneManager.update()`
## 场景切换
### 立即切换
使用 `Core.setScene()``sceneManager.setScene()` 立即切换场景:
```typescript
// 方法 1:使用 Core(推荐)
Core.setScene(new MenuScene());
// 方法 2:使用 SceneManager
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.setScene(new MenuScene());
```
### 延迟切换
使用 `Core.loadScene()``sceneManager.loadScene()` 进行延迟场景切换,在下一帧生效:
```typescript
// 方法 1:使用 Core(推荐)
Core.loadScene(new GameOverScene());
// 方法 2:使用 SceneManager
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.loadScene(new GameOverScene());
```
在 System 中切换场景时,使用延迟切换:
```typescript
class GameOverSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
const player = entities.find(e => e.name === 'Player');
const health = player?.getComponent(Health);
if (health && health.value <= 0) {
// 延迟切换到游戏结束场景(下一帧生效)
Core.loadScene(new GameOverScene());
// 当前帧继续执行,不会中断当前系统处理
}
}
}
```
## API 参考
### Core 静态方法(推荐)
#### Core.setScene()
立即切换场景。
```typescript
public static setScene<T extends IScene>(scene: T): T
```
**参数**:
- `scene` - 要设置的场景实例
**返回值**:
- 返回设置的场景实例
**示例**:
```typescript
const gameScene = Core.setScene(new GameScene());
console.log(gameScene.name);
```
#### Core.loadScene()
延迟场景加载(下一帧切换)。
```typescript
public static loadScene<T extends IScene>(scene: T): void
```
**参数**:
- `scene` - 要加载的场景实例
**示例**:
```typescript
Core.loadScene(new GameOverScene());
```
#### Core.scene
获取当前活动场景。
```typescript
public static get scene(): IScene | null
```
**返回值**:
- 当前场景实例,如果没有场景则返回 null
**示例**:
```typescript
const currentScene = Core.scene;
if (currentScene) {
console.log(`当前场景: ${currentScene.name}`);
}
```
### SceneManager 方法(进阶)
如果需要直接使用 SceneManager,通过服务容器获取:
```typescript
const sceneManager = Core.services.resolve(SceneManager);
```
#### setScene()
立即切换场景。
```typescript
public setScene<T extends IScene>(scene: T): T
```
#### loadScene()
延迟场景加载。
```typescript
public loadScene<T extends IScene>(scene: T): void
```
#### currentScene
获取当前场景。
```typescript
public get currentScene(): IScene | null
```
#### hasScene
检查是否有活动场景。
```typescript
public get hasScene(): boolean
```
#### hasPendingScene
检查是否有待处理的场景切换。
```typescript
public get hasPendingScene(): boolean
```
## 最佳实践
### 1. 使用 Core 的静态方法
```typescript
// 推荐:使用 Core 的静态方法
Core.setScene(new GameScene());
Core.loadScene(new MenuScene());
const currentScene = Core.scene;
// 不推荐:除非有特殊需求,否则不要直接使用 SceneManager
const sceneManager = Core.services.resolve(SceneManager);
sceneManager.setScene(new GameScene());
```
### 2. 只调用 Core.update()
```typescript
// 正确:只调用 Core.update()
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 自动更新所有服务和场景
}
// 错误:不要手动调用 sceneManager.update()
function gameLoop(deltaTime: number) {
Core.update(deltaTime);
sceneManager.update(); // 重复更新,会导致问题!
}
```
### 3. 使用延迟切换避免问题
在 System 中切换场景时,使用 `loadScene()` 而不是 `setScene()`
```typescript
// 推荐:延迟切换
class HealthSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
if (health.value <= 0) {
Core.loadScene(new GameOverScene());
// 当前帧继续处理其他实体
}
}
}
}
// 不推荐:立即切换可能导致问题
class HealthSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
for (const entity of entities) {
const health = entity.getComponent(Health);
if (health.value <= 0) {
Core.setScene(new GameOverScene());
// 场景立即切换,当前帧其他实体可能无法正确处理
}
}
}
}
```
### 4. 场景职责分离
每个场景应该只负责一个特定的游戏状态:
```typescript
// 好的设计 - 职责清晰
class MenuScene extends Scene {
// 只处理菜单相关逻辑
}
class GameScene extends Scene {
// 只处理游戏逻辑
}
class PauseScene extends Scene {
// 只处理暂停界面逻辑
}
// 避免这种设计 - 职责混杂
class MegaScene extends Scene {
// 包含菜单、游戏、暂停和所有其他逻辑
}
```
### 5. 资源管理
在场景的 `unload()` 方法中清理资源:
```typescript
class GameScene extends Scene {
private textures: Map<string, any> = new Map();
private sounds: Map<string, any> = new Map();
protected initialize(): void {
this.loadResources();
}
private loadResources(): void {
this.textures.set('player', loadTexture('player.png'));
this.sounds.set('bgm', loadSound('bgm.mp3'));
}
public unload(): void {
// 清理资源
this.textures.clear();
this.sounds.clear();
console.log('场景资源已清理');
}
}
```
### 6. 事件驱动的场景切换
使用事件系统触发场景切换,保持代码解耦:
```typescript
class GameScene extends Scene {
protected initialize(): void {
// 监听场景切换事件
this.eventSystem.on('goto:menu', () => {
Core.loadScene(new MenuScene());
});
this.eventSystem.on('goto:gameover', (data) => {
Core.loadScene(new GameOverScene());
});
}
}
// 在 System 中触发事件
class GameLogicSystem extends EntitySystem {
process(entities: readonly Entity[]): void {
if (levelComplete) {
this.scene.eventSystem.emitSync('goto:gameover', {
score: 1000,
level: 5
});
}
}
}
```
## 架构概览
SceneManager 在 ECS Framework 中的位置:
```
Core(全局服务)
└── SceneManager(场景管理,自动更新)
└── Scene(当前场景)
├── EntitySystem(系统)
├── Entity(实体)
└── Component(组件)
```
## 与 WorldManager 的比较
| 特性 | SceneManager | WorldManager |
|------|--------------|--------------|
| 适用场景 | 95% 的游戏应用 | 高级多世界隔离场景 |
| 复杂度 | 简单 | 复杂 |
| 场景数量 | 单场景(可切换) | 多个 World,每个包含多个场景 |
| 性能开销 | 最小 | 较高 |
| 使用方式 | `Core.setScene()` | `worldManager.createWorld()` |
**何时使用 SceneManager**
- 单人游戏
- 简单多人游戏
- 移动游戏
- 需要切换但不需要同时运行的场景
**何时使用 WorldManager**
- MMO 游戏服务器(每个房间一个 World)
- 游戏大厅系统(每个游戏房间完全隔离)
- 需要运行多个完全独立的游戏实例
## 相关文档
- [持久实体](/guide/persistent-entity/) - 了解如何在场景切换时保持实体
SceneManager 为大多数游戏提供了简单而强大的场景管理能力。通过 Core 的静态方法,你可以轻松管理场景切换。
@@ -305,11 +305,11 @@ const tree = BehaviorTreeBuilder.create('Timeout')
### Cocos Creator集成 ### Cocos Creator集成
参见[Cocos Creator集成指南](./cocos-integration/) 参见[Cocos Creator集成指南](/modules/behavior-tree/cocos-integration/)
### LayaAir集成 ### LayaAir集成
参见[LayaAir集成指南](./laya-integration/) 参见[LayaAir集成指南](/modules/behavior-tree/laya-integration/)
## 最佳实践 ## 最佳实践
@@ -389,6 +389,6 @@ const tree = BehaviorTreeBuilder.create('AI')
## 下一步 ## 下一步
- 查看[自定义节点执行器](./custom-actions/)学习如何创建自定义节点 - 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何创建自定义节点
- 阅读[最佳实践](./best-practices/)了解行为树设计技巧 - 阅读[最佳实践](/modules/behavior-tree/best-practices/)了解行为树设计技巧
- 参考[编辑器使用指南](./editor-guide/)学习可视化编辑 - 参考[编辑器使用指南](/modules/behavior-tree/editor-guide/)学习可视化编辑
@@ -503,6 +503,6 @@ console.log(json);
## 下一步 ## 下一步
- 学习[Cocos Creator 集成](./cocos-integration/)了解如何在游戏引擎中加载资源 - 学习[Cocos Creator 集成](/modules/behavior-tree/cocos-integration/)了解如何在游戏引擎中加载资源
- 查看[自定义节点执行器](./custom-actions/)创建自定义行为 - 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)创建自定义行为
- 阅读[最佳实践](./best-practices/)优化你的行为树设计 - 阅读[最佳实践](/modules/behavior-tree/best-practices/)优化你的行为树设计
@@ -26,7 +26,7 @@ Root Selector
### 2. 单一职责原则 ### 2. 单一职责原则
每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](./custom-actions/)。 每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](/modules/behavior-tree/custom-actions/)。
```typescript ```typescript
// 好的设计 - 使用内置节点 // 好的设计 - 使用内置节点
@@ -465,6 +465,6 @@ export class SmartUpdate implements INodeExecutor {
## 下一步 ## 下一步
- 学习[自定义节点执行器](./custom-actions/)扩展行为树功能 - 学习[自定义节点执行器](/modules/behavior-tree/custom-actions/)扩展行为树功能
- 探索[高级用法](./advanced-usage/)了解更多技巧 - 探索[高级用法](/modules/behavior-tree/advanced-usage/)了解更多技巧
- 参考[核心概念](./core-concepts/)深入理解原理 - 参考[核心概念](/modules/behavior-tree/core-concepts/)深入理解原理
@@ -8,7 +8,7 @@ title: "Cocos Creator 集成"
- Cocos Creator 3.x 或更高版本 - Cocos Creator 3.x 或更高版本
- 基本的 TypeScript 知识 - 基本的 TypeScript 知识
- 已完成[快速开始](./getting-started/)教程 - 已完成[快速开始](/modules/behavior-tree/getting-started/)教程
## 安装 ## 安装
@@ -679,7 +679,7 @@ const updateInterval = sys.isNative ? 0.016 : 0.05;
## 下一步 ## 下一步
- 查看[资产管理](./asset-management/)了解如何加载和管理行为树资产、使用子树 - 查看[资产管理](/modules/behavior-tree/asset-management/)了解如何加载和管理行为树资产、使用子树
- 学习[高级用法](./advanced-usage/)了解性能优化和调试技巧 - 学习[高级用法](/modules/behavior-tree/advanced-usage/)了解性能优化和调试技巧
- 阅读[最佳实践](./best-practices/)优化你的 AI - 阅读[最佳实践](/modules/behavior-tree/best-practices/)优化你的 AI
- 学习[自定义节点执行器](./custom-actions/)创建自定义行为 - 学习[自定义节点执行器](/modules/behavior-tree/custom-actions/)创建自定义行为
@@ -192,7 +192,7 @@ const tree = BehaviorTreeBuilder.create('Actions')
.build(); .build();
``` ```
要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](./custom-actions/)。 要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](/modules/behavior-tree/custom-actions/)。
#### Condition(条件) #### Condition(条件)
@@ -487,7 +487,7 @@ NodeRuntimeState
现在你已经理解了行为树的核心概念,接下来可以: 现在你已经理解了行为树的核心概念,接下来可以:
- 查看[快速开始](./getting-started/)创建第一个行为树 - 查看[快速开始](/modules/behavior-tree/getting-started/)创建第一个行为树
- 学习[自定义节点执行器](./custom-actions/)创建自定义节点 - 学习[自定义节点执行器](/modules/behavior-tree/custom-actions/)创建自定义节点
- 探索[高级用法](./advanced-usage/)了解更多功能 - 探索[高级用法](/modules/behavior-tree/advanced-usage/)了解更多功能
- 阅读[最佳实践](./best-practices/)学习设计模式 - 阅读[最佳实践](/modules/behavior-tree/best-practices/)学习设计模式
@@ -1123,6 +1123,6 @@ execute(context: NodeExecutionContext): TaskStatus {
## 下一步 ## 下一步
- 学习[编辑器工作流](./editor-workflow/)了解如何在编辑器中使用自定义节点 - 学习[编辑器工作流](/modules/behavior-tree/editor-workflow/)了解如何在编辑器中使用自定义节点
- 阅读[最佳实践](./best-practices/)学习行为树设计模式 - 阅读[最佳实践](/modules/behavior-tree/best-practices/)学习行为树设计模式
- 查看[高级用法](./advanced-usage/)了解更多功能 - 查看[高级用法](/modules/behavior-tree/advanced-usage/)了解更多功能
@@ -117,5 +117,5 @@ BehaviorTreeStarter.start(entity, tree);
## 下一步 ## 下一步
- 查看[编辑器工作流](./editor-workflow/)了解完整的开发流程 - 查看[编辑器工作流](/modules/behavior-tree/editor-workflow/)了解完整的开发流程
- 查看[自定义节点执行器](./custom-actions/)学习如何扩展节点 - 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何扩展节点
@@ -112,7 +112,7 @@ setInterval(() => {
## 实现自定义执行器 ## 实现自定义执行器
要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](./custom-actions/)): 要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](/modules/behavior-tree/custom-actions/)):
```typescript ```typescript
import { import {
@@ -250,6 +250,6 @@ setInterval(() => {
## 下一步 ## 下一步
- 查看[自定义节点执行器](./custom-actions/)学习如何创建自定义节点 - 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何创建自定义节点
- 查看[高级用法](./advanced-usage/)了解性能优化等高级特性 - 查看[高级用法](/modules/behavior-tree/advanced-usage/)了解性能优化等高级特性
- 查看[最佳实践](./best-practices/)优化你的AI设计 - 查看[最佳实践](/modules/behavior-tree/best-practices/)优化你的AI设计
@@ -333,11 +333,11 @@ BehaviorTreeStarter.restart(entity);
现在你已经创建了第一个行为树,接下来可以: 现在你已经创建了第一个行为树,接下来可以:
1. 学习[核心概念](./core-concepts/)深入理解行为树原理 1. 学习[核心概念](/modules/behavior-tree/core-concepts/)深入理解行为树原理
2. 学习[资产管理](./asset-management/)了解如何加载和复用行为树、使用子树 2. 学习[资产管理](/modules/behavior-tree/asset-management/)了解如何加载和复用行为树、使用子树
3. 查看[自定义节点执行器](./custom-actions/)学习如何创建自定义节点 3. 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何创建自定义节点
4. 根据你的场景查看集成教程:[Cocos Creator](./cocos-integration/) 或 [Node.js](./nodejs-usage.md) 4. 根据你的场景查看集成教程:[Cocos Creator](/modules/behavior-tree/cocos-integration/) 或 [Node.js](/modules/behavior-tree/nodejs-usage/)
5. 查看[高级用法](./advanced-usage/)了解更多功能 5. 查看[高级用法](/modules/behavior-tree/advanced-usage/)了解更多功能
## 常见问题 ## 常见问题
@@ -384,4 +384,4 @@ console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
内置的`executeAction``executeCondition`节点只是占位符。要实现真正的自定义逻辑,你需要创建自定义执行器: 内置的`executeAction``executeCondition`节点只是占位符。要实现真正的自定义逻辑,你需要创建自定义执行器:
参见[自定义节点执行器](./custom-actions/)学习如何创建。 参见[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何创建。
@@ -8,7 +8,7 @@ title: "Laya 引擎集成"
- LayaAir 3.x 或更高版本 - LayaAir 3.x 或更高版本
- 基本的 TypeScript 知识 - 基本的 TypeScript 知识
- 已完成[快速开始](./getting-started/)教程 - 已完成[快速开始](/modules/behavior-tree/getting-started/)教程
## 安装 ## 安装
@@ -311,5 +311,5 @@ class AIManager {
## 下一步 ## 下一步
- 查看[高级用法](./advanced-usage/) - 查看[高级用法](/modules/behavior-tree/advanced-usage/)
- 学习[最佳实践](./best-practices/) - 学习[最佳实践](/modules/behavior-tree/best-practices/)
@@ -577,6 +577,6 @@ function loadAIState(entity: Entity, savedState: any) {
## 下一步 ## 下一步
- 查看[资产管理](./asset-management/)了解资源加载和子树 - 查看[资产管理](/modules/behavior-tree/asset-management/)了解资源加载和子树
- 学习[自定义节点执行器](./custom-actions/)创建自定义行为 - 学习[自定义节点执行器](/modules/behavior-tree/custom-actions/)创建自定义行为
- 阅读[最佳实践](./best-practices/)优化你的服务端AI - 阅读[最佳实践](/modules/behavior-tree/best-practices/)优化你的服务端AI
@@ -0,0 +1,383 @@
---
title: "Cocos Creator 蓝图编辑器"
description: "在 Cocos Creator 中使用蓝图可视化脚本系统"
---
本文档介绍如何在 Cocos Creator 项目中安装和使用蓝图可视化脚本编辑器扩展。
## 安装扩展
### 1. 复制扩展到项目
`cocos-node-editor` 扩展复制到你的 Cocos Creator 项目的 `extensions` 目录:
```
your-project/
├── assets/
├── extensions/
│ └── cocos-node-editor/ # 蓝图编辑器扩展
└── ...
```
### 2. 安装依赖
在扩展目录中安装依赖:
```bash
cd extensions/cocos-node-editor
npm install
```
### 3. 启用扩展
1. 打开 Cocos Creator
2. 进入 **扩展 → 扩展管理器**
3. 找到 `cocos-node-editor` 并启用
## 打开蓝图编辑器
通过菜单 **面板 → Node Editor** 打开蓝图编辑器面板。
## 编辑器界面
### 工具栏
| 按钮 | 快捷键 | 功能 |
|------|--------|------|
| 新建 | - | 创建空白蓝图 |
| 加载 | - | 从文件加载蓝图 |
| 保存 | `Ctrl+S` | 保存蓝图到文件 |
| 撤销 | `Ctrl+Z` | 撤销上一步操作 |
| 重做 | `Ctrl+Shift+Z` | 重做操作 |
| 剪切 | `Ctrl+X` | 剪切选中节点 |
| 复制 | `Ctrl+C` | 复制选中节点 |
| 粘贴 | `Ctrl+V` | 粘贴节点 |
| 删除 | `Delete` | 删除选中项 |
| 重新扫描 | - | 重新扫描项目中的蓝图节点 |
### 画布操作
- **右键单击画布**:打开节点添加菜单
- **拖拽节点**:移动节点位置
- **点击节点**:选中节点
- **Ctrl+点击**:多选节点
- **拖拽引脚到引脚**:创建连接
- **滚轮**:缩放画布
- **中键拖拽**:平移画布
### 节点菜单
右键单击画布后会显示节点菜单:
- 顶部搜索框可以快速搜索节点
- 节点按类别分组显示
- 按 `Enter` 快速添加第一个搜索结果
- 按 `Esc` 关闭菜单
## 蓝图文件格式
蓝图保存为 `.blueprint.json` 文件,格式与运行时完全兼容:
```json
{
"version": 1,
"type": "blueprint",
"metadata": {
"name": "My Blueprint",
"createdAt": 1704307200000,
"modifiedAt": 1704307200000
},
"variables": [],
"nodes": [
{
"id": "node-1",
"type": "PrintString",
"position": { "x": 100, "y": 200 },
"data": {}
}
],
"connections": [
{
"id": "conn-1",
"fromNodeId": "node-1",
"fromPin": "exec",
"toNodeId": "node-2",
"toPin": "exec"
}
]
}
```
## 在游戏中运行蓝图
使用 ECS 系统方式管理和执行蓝图。
### 1. 定义蓝图组件
```typescript
import { Component, ECSComponent, Property, Serialize } from '@esengine/ecs-framework';
import type { BlueprintAsset } from '@esengine/blueprint';
@ECSComponent('Blueprint')
export class BlueprintComponent extends Component {
@Serialize()
@Property({ type: 'asset', label: 'Blueprint Asset' })
blueprintPath: string = '';
@Serialize()
@Property({ type: 'boolean', label: 'Auto Start' })
autoStart: boolean = true;
// 运行时数据(不序列化)
blueprintAsset: BlueprintAsset | null = null;
vm: BlueprintVM | null = null;
isStarted: boolean = false;
}
```
### 2. 创建蓝图执行系统
```typescript
import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
import {
BlueprintVM,
validateBlueprintAsset
} from '@esengine/blueprint';
import { BlueprintComponent } from './BlueprintComponent';
export class BlueprintExecutionSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(BlueprintComponent));
}
protected override process(entities: readonly Entity[]): void {
const dt = Time.deltaTime;
for (const entity of entities) {
const bp = entity.getComponent(BlueprintComponent)!;
// 跳过没有蓝图资产的实体
if (!bp.blueprintAsset) continue;
// 初始化 VM
if (!bp.vm) {
bp.vm = new BlueprintVM(bp.blueprintAsset, entity, this.scene!);
}
// 自动启动
if (bp.autoStart && !bp.isStarted) {
bp.vm.start();
bp.isStarted = true;
}
// 更新蓝图
if (bp.isStarted) {
bp.vm.tick(dt);
}
}
}
protected override onRemoved(entity: Entity): void {
const bp = entity.getComponent(BlueprintComponent);
if (bp?.vm && bp.isStarted) {
bp.vm.stop();
bp.vm = null;
bp.isStarted = false;
}
}
}
```
### 3. 加载蓝图并添加到实体
```typescript
import { resources, JsonAsset } from 'cc';
import { validateBlueprintAsset } from '@esengine/blueprint';
// 加载蓝图资产
async function loadBlueprint(path: string): Promise<BlueprintAsset | null> {
return new Promise((resolve) => {
resources.load(path, JsonAsset, (err, asset) => {
if (err || !asset) {
console.error('Failed to load blueprint:', err);
resolve(null);
return;
}
const data = asset.json;
if (validateBlueprintAsset(data)) {
resolve(data as BlueprintAsset);
} else {
console.error('Invalid blueprint format');
resolve(null);
}
});
});
}
// 创建带蓝图的实体
async function createBlueprintEntity(scene: IScene, blueprintPath: string): Promise<Entity> {
const entity = scene.createEntity('BlueprintEntity');
const bpComponent = entity.addComponent(BlueprintComponent);
bpComponent.blueprintPath = blueprintPath;
bpComponent.blueprintAsset = await loadBlueprint(blueprintPath);
return entity;
}
```
### 4. 注册系统到场景
```typescript
// 在场景初始化时
scene.addSystem(new BlueprintExecutionSystem());
```
## 创建自定义节点
### 使用装饰器标记组件
推荐使用装饰器让组件自动生成蓝图节点:
```typescript
import { Component, ECSComponent } from '@esengine/ecs-framework';
import { BlueprintExpose, BlueprintProperty, BlueprintMethod } from '@esengine/blueprint';
@ECSComponent('Health')
@BlueprintExpose({ displayName: '生命值组件' })
export class HealthComponent extends Component {
@BlueprintProperty({ displayName: '当前生命值', category: 'number' })
current: number = 100;
@BlueprintProperty({ displayName: '最大生命值', category: 'number' })
max: number = 100;
@BlueprintMethod({ displayName: '治疗', isExec: true })
heal(amount: number): void {
this.current = Math.min(this.current + amount, this.max);
}
@BlueprintMethod({ displayName: '受伤', isExec: true })
takeDamage(amount: number): void {
this.current = Math.max(this.current - amount, 0);
}
@BlueprintMethod({ displayName: '是否死亡' })
isDead(): boolean {
return this.current <= 0;
}
}
```
### 注册组件节点
```typescript
import { registerAllComponentNodes } from '@esengine/blueprint';
// 在应用启动时注册所有标记的组件
registerAllComponentNodes();
```
### 手动定义节点(高级)
如需完全自定义节点逻辑:
```typescript
import {
BlueprintNodeTemplate,
INodeExecutor,
RegisterNode,
ExecutionContext,
ExecutionResult
} from '@esengine/blueprint';
const MyNodeTemplate: BlueprintNodeTemplate = {
type: 'MyCustomNode',
title: '我的自定义节点',
category: 'custom',
description: '自定义节点示例',
inputs: [
{ name: 'exec', type: 'exec', direction: 'input', isExec: true },
{ name: 'value', type: 'number', direction: 'input', defaultValue: 0 }
],
outputs: [
{ name: 'exec', type: 'exec', direction: 'output', isExec: true },
{ name: 'result', type: 'number', direction: 'output' }
]
};
@RegisterNode(MyNodeTemplate)
class MyNodeExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const value = context.getInput<number>(node.id, 'value');
return {
outputs: { result: value * 2 },
nextExec: 'exec'
};
}
}
```
## 节点类别
| 类别 | 说明 | 颜色 |
|------|------|------|
| `event` | 事件节点 | 红色 |
| `flow` | 流程控制 | 灰色 |
| `entity` | 实体操作 | 蓝色 |
| `component` | 组件访问 | 青色 |
| `math` | 数学运算 | 绿色 |
| `logic` | 逻辑运算 | 红色 |
| `variable` | 变量访问 | 紫色 |
| `time` | 时间工具 | 青色 |
| `debug` | 调试工具 | 灰色 |
| `custom` | 自定义节点 | 蓝灰色 |
## 最佳实践
1. **文件组织**
- 将蓝图文件放在 `assets/blueprints/` 目录下
- 使用有意义的文件名,如 `player-controller.blueprint.json`
2. **组件设计**
- 使用 `@BlueprintExpose` 标记需要暴露给蓝图的组件
- 为属性和方法提供清晰的 `displayName`
- 将执行方法标记为 `isExec: true`
3. **性能考虑**
- 避免在 Tick 事件中执行重计算
- 使用变量缓存中间结果
- 纯函数节点会自动缓存输出
4. **调试技巧**
- 使用 Print 节点输出中间值
- 启用 `vm.debug = true` 查看执行日志
## 常见问题
### Q: 节点菜单是空的?
A: 点击 **重新扫描** 按钮扫描项目中的蓝图节点类。确保已调用 `registerAllComponentNodes()`
### Q: 蓝图不执行?
A: 检查:
1. 实体是否添加了 `BlueprintComponent`
2. `BlueprintExecutionSystem` 是否注册到场景
3. `blueprintAsset` 是否正确加载
4. `autoStart` 是否为 `true`
### Q: 如何触发自定义事件?
A: 通过 VM 触发:
```typescript
const bp = entity.getComponent(BlueprintComponent);
bp.vm?.triggerCustomEvent('OnPickup', { item: itemEntity });
```
## 相关文档
- [蓝图运行时 API](/modules/blueprint/) - BlueprintVM 和核心 API
- [自定义节点](/modules/blueprint/custom-nodes) - 详细的节点创建指南
- [内置节点](/modules/blueprint/nodes) - 内置节点参考
@@ -870,7 +870,7 @@ your-project/
## 下一步 ## 下一步
- [ECS 节点参考](./nodes) - 完整节点列表 - [ECS 节点参考](/modules/blueprint/nodes) - 完整节点列表
- [自定义节点](./custom-nodes) - 创建自定义节点 - [自定义节点](/modules/blueprint/custom-nodes) - 创建自定义节点
- [运行时集成](./vm) - 蓝图虚拟机 API - [运行时集成](/modules/blueprint/vm) - 蓝图虚拟机 API
- [实际示例](./examples) - 更多游戏逻辑示例 - [实际示例](/modules/blueprint/examples) - 更多游戏逻辑示例
@@ -535,6 +535,6 @@ description: "蓝图内置 ECS 操作节点完整参考"
## 相关文档 ## 相关文档
- [蓝图编辑器指南](./editor-guide) - 学习如何使用编辑器 - [蓝图编辑器指南](/modules/blueprint/editor-guide) - 学习如何使用编辑器
- [自定义节点](./custom-nodes) - 创建自定义节点 - [自定义节点](/modules/blueprint/custom-nodes) - 创建自定义节点
- [蓝图虚拟机](./vm) - 运行时 API - [蓝图虚拟机](/modules/blueprint/vm) - 运行时 API
@@ -0,0 +1,326 @@
---
title: "定点数"
description: "用于帧同步的确定性定点数数学库"
---
`@esengine/ecs-framework-math` 提供确定性定点数计算,专为**帧同步 (Lockstep)** 设计。定点数在所有平台上保证产生完全相同的计算结果。
## 为什么需要定点数?
浮点数在不同平台上可能产生不同的舍入结果:
```typescript
// 浮点数:不同平台可能得到不同结果
const a = 0.1 + 0.2; // 0.30000000000000004 (某些平台)
// 0.3 (其他平台)
// 定点数:所有平台结果一致
const x = Fixed32.from(0.1);
const y = Fixed32.from(0.2);
const z = x.add(y); // raw = 19661 (所有平台)
```
| 特性 | 浮点数 | 定点数 |
|------|--------|--------|
| 跨平台一致性 | ❌ 可能不同 | ✅ 完全一致 |
| 网络同步模式 | 状态同步 | 帧同步 (Lockstep) |
| 适用游戏类型 | FPS、RPG | RTS、MOBA、格斗 |
## 安装
```bash
npm install @esengine/ecs-framework-math
```
## Fixed32 定点数
Q16.16 格式:16 位整数 + 16 位小数,范围 ±32767.99998。
### 创建定点数
```typescript
import { Fixed32 } from '@esengine/ecs-framework-math';
// 从浮点数创建
const speed = Fixed32.from(5.5);
// 从整数创建(无精度损失)
const count = Fixed32.fromInt(10);
// 从原始值创建(网络接收后使用)
const received = Fixed32.fromRaw(360448); // 等于 5.5
// 预定义常量
Fixed32.ZERO // 0
Fixed32.ONE // 1
Fixed32.HALF // 0.5
Fixed32.PI // π
Fixed32.TWO_PI // 2π
Fixed32.HALF_PI // π/2
```
### 基本运算
```typescript
const a = Fixed32.from(10);
const b = Fixed32.from(3);
const sum = a.add(b); // 13
const diff = a.sub(b); // 7
const prod = a.mul(b); // 30
const quot = a.div(b); // 3.333...
const mod = a.mod(b); // 1
const neg = a.neg(); // -10
const abs = neg.abs(); // 10
```
### 比较运算
```typescript
const x = Fixed32.from(5);
const y = Fixed32.from(3);
x.eq(y) // false - 等于
x.ne(y) // true - 不等于
x.lt(y) // false - 小于
x.le(y) // false - 小于等于
x.gt(y) // true - 大于
x.ge(y) // true - 大于等于
x.isZero() // false
x.isPositive() // true
x.isNegative() // false
```
### 数学函数
```typescript
// 平方根(牛顿迭代法,确定性)
const sqrt = Fixed32.sqrt(Fixed32.from(16)); // 4
// 取整
Fixed32.floor(Fixed32.from(3.7)) // 3
Fixed32.ceil(Fixed32.from(3.2)) // 4
Fixed32.round(Fixed32.from(3.5)) // 4
// 范围限制
Fixed32.clamp(value, min, max)
// 线性插值
Fixed32.lerp(from, to, t)
// 最大/最小值
Fixed32.min(a, b)
Fixed32.max(a, b)
```
### 类型转换
```typescript
const value = Fixed32.from(3.14159);
// 转为浮点数(用于渲染)
const float = value.toNumber(); // 3.14159
// 获取原始值(用于网络传输)
const raw = value.toRaw(); // 205887
// 转为整数(向下取整)
const int = value.toInt(); // 3
```
## FixedVector2 定点数向量
不可变的 2D 向量类,所有运算返回新实例。
### 创建向量
```typescript
import { FixedVector2, Fixed32 } from '@esengine/ecs-framework-math';
// 从浮点数创建
const pos = FixedVector2.from(100, 200);
// 从原始值创建(网络接收后使用)
const received = FixedVector2.fromRaw(6553600, 13107200);
// 从 Fixed32 创建
const vec = new FixedVector2(Fixed32.from(10), Fixed32.from(20));
// 预定义常量
FixedVector2.ZERO // (0, 0)
FixedVector2.ONE // (1, 1)
FixedVector2.RIGHT // (1, 0)
FixedVector2.LEFT // (-1, 0)
FixedVector2.UP // (0, 1)
FixedVector2.DOWN // (0, -1)
```
### 向量运算
```typescript
const a = FixedVector2.from(3, 4);
const b = FixedVector2.from(1, 2);
// 基本运算
const sum = a.add(b); // (4, 6)
const diff = a.sub(b); // (2, 2)
const scaled = a.mul(Fixed32.from(2)); // (6, 8)
const divided = a.div(Fixed32.from(2)); // (1.5, 2)
// 向量积
const dot = a.dot(b); // 3*1 + 4*2 = 11
const cross = a.cross(b); // 3*2 - 4*1 = 2
// 长度
const lenSq = a.lengthSquared(); // 25
const len = a.length(); // 5
// 归一化
const norm = a.normalize(); // (0.6, 0.8)
// 距离
const dist = a.distanceTo(b); // sqrt((3-1)² + (4-2)²)
```
### 旋转和角度
```typescript
import { FixedMath } from '@esengine/ecs-framework-math';
const vec = FixedVector2.from(1, 0);
const angle = Fixed32.from(Math.PI / 2); // 90度
// 旋转向量
const rotated = vec.rotate(angle); // (0, 1)
// 围绕点旋转
const center = FixedVector2.from(5, 5);
const around = vec.rotateAround(center, angle);
// 获取向量角度
const vecAngle = vec.angle();
// 两向量夹角
const between = vec.angleTo(other);
// 从角度创建单位向量
const dir = FixedVector2.fromAngle(angle);
// 从极坐标创建
const polar = FixedVector2.fromPolar(length, angle);
```
### 类型转换
```typescript
const pos = FixedVector2.from(100.5, 200.5);
// 转为浮点对象(用于渲染)
const obj = pos.toObject(); // { x: 100.5, y: 200.5 }
// 转为数组
const arr = pos.toArray(); // [100.5, 200.5]
// 获取原始值(用于网络传输)
const raw = pos.toRawObject(); // { x: 6586368, y: 13140992 }
```
## FixedMath 三角函数
使用查找表实现确定性三角函数。
```typescript
import { FixedMath, Fixed32 } from '@esengine/ecs-framework-math';
const angle = Fixed32.from(Math.PI / 6); // 30度
// 三角函数
const sin = FixedMath.sin(angle); // 0.5
const cos = FixedMath.cos(angle); // 0.866
const tan = FixedMath.tan(angle); // 0.577
// 反三角函数
const atan = FixedMath.atan2(y, x);
const asin = FixedMath.asin(value);
const acos = FixedMath.acos(value);
// 角度规范化到 [-π, π]
const normalized = FixedMath.normalizeAngle(angle);
// 角度差(最短路径)
const delta = FixedMath.angleDelta(from, to);
// 角度插值(处理 360° 环绕)
const lerped = FixedMath.lerpAngle(from, to, t);
// 弧度/角度转换
const deg = FixedMath.radToDeg(rad);
const rad = FixedMath.degToRad(deg);
```
## 最佳实践
### 1. 全程使用定点数计算
```typescript
// ✅ 正确:所有游戏逻辑使用定点数
function calculateDamage(baseDamage: Fixed32, multiplier: Fixed32): Fixed32 {
return baseDamage.mul(multiplier);
}
// ❌ 错误:混用浮点数
function calculateDamage(baseDamage: number, multiplier: number): number {
return baseDamage * multiplier; // 可能不一致
}
```
### 2. 只在渲染时转换为浮点数
```typescript
// 游戏逻辑层
const position: FixedVector2 = calculatePosition(input);
// 渲染层
const { x, y } = position.toObject();
sprite.position.set(x, y);
```
### 3. 使用原始值进行网络传输
```typescript
// ✅ 正确:传输整数原始值
const raw = position.toRawObject();
send(JSON.stringify(raw));
// ❌ 错误:传输浮点数
const float = position.toObject();
send(JSON.stringify(float)); // 可能丢失精度
```
### 4. 使用 FixedMath 进行三角运算
```typescript
// ✅ 正确:使用查找表
const direction = FixedVector2.fromAngle(FixedMath.atan2(dy, dx));
// ❌ 错误:使用 Math 库
const angle = Math.atan2(dy.toNumber(), dx.toNumber()); // 不确定
```
## API 导出
```typescript
import {
Fixed32,
FixedVector2,
FixedMath,
type IFixed32,
type IFixedVector2
} from '@esengine/ecs-framework-math';
```
## 相关文档
- [状态同步](/modules/network/sync) - 定点数快照缓冲区
- [客户端预测](/modules/network/prediction) - 定点数客户端预测
@@ -252,3 +252,145 @@ if (predictionSystem) {
console.log('Current sequence:', predictionSystem.inputSequence); console.log('Current sequence:', predictionSystem.inputSequence);
} }
``` ```
---
## 定点数客户端预测(帧同步)
用于**帧同步 (Lockstep)** 的确定性客户端预测。
> 定点数基础知识请参考 [定点数文档](/modules/network/fixed-point)
### 基本用法
```typescript
import {
FixedClientPrediction,
createFixedClientPrediction,
type IFixedPredictor,
type IFixedStatePositionExtractor
} from '@esengine/network';
import { Fixed32, FixedVector2 } from '@esengine/ecs-framework-math';
// 定义游戏状态
interface GameState {
position: FixedVector2;
velocity: FixedVector2;
}
// 实现预测器(必须使用定点数运算)
const predictor: IFixedPredictor<GameState, PlayerInput> = {
predict(state: GameState, input: PlayerInput, deltaTime: Fixed32): GameState {
const speed = Fixed32.from(100);
const inputVec = FixedVector2.from(input.dx, input.dy);
const velocity = inputVec.normalize().mul(speed);
const displacement = velocity.mul(deltaTime);
return {
position: state.position.add(displacement),
velocity
};
}
};
// 创建预测器
const prediction = createFixedClientPrediction(predictor, {
maxUnacknowledgedInputs: 60,
fixedDeltaTime: Fixed32.from(1 / 60),
reconciliationThreshold: Fixed32.from(0.001),
enableSmoothReconciliation: false // 帧同步通常关闭
});
```
### 记录输入
```typescript
function onUpdate(input: PlayerInput, currentState: GameState) {
// 记录输入并获得预测状态
const predicted = prediction.recordInput(input, currentState);
// 渲染预测状态
const pos = predicted.position.toObject();
sprite.position.set(pos.x, pos.y);
// 发送输入
socket.send(JSON.stringify({
frame: prediction.currentFrame,
input
}));
}
```
### 服务器校正
```typescript
// 位置提取器
const posExtractor: IFixedStatePositionExtractor<GameState> = {
getPosition(state: GameState): FixedVector2 {
return state.position;
}
};
// 收到服务器状态
function onServerState(serverState: GameState, serverFrame: number) {
const reconciled = prediction.reconcile(
serverState,
serverFrame,
posExtractor
);
}
```
### 回滚重播
```typescript
// 发现不同步时回滚
const correctedState = prediction.rollbackAndResimulate(
serverFrame,
authoritativeState
);
// 查看历史状态
const historicalState = prediction.getStateAtFrame(100);
```
### 预设移动预测器
```typescript
import {
createFixedMovementPredictor,
createFixedMovementPositionExtractor,
type IFixedMovementInput,
type IFixedMovementState
} from '@esengine/network';
// 创建移动预测器(速度 100 单位/秒)
const movePredictor = createFixedMovementPredictor(Fixed32.from(100));
const posExtractor = createFixedMovementPositionExtractor();
const prediction = createFixedClientPrediction<IFixedMovementState, IFixedMovementInput>(
movePredictor,
{ fixedDeltaTime: Fixed32.from(1 / 60) }
);
// 输入格式
const input: IFixedMovementInput = { dx: 1, dy: 0 };
```
### API 导出
```typescript
import {
FixedClientPrediction,
createFixedClientPrediction,
createFixedMovementPredictor,
createFixedMovementPositionExtractor,
type IFixedInputSnapshot,
type IFixedPredictedState,
type IFixedPredictor,
type IFixedStatePositionExtractor,
type FixedClientPredictionConfig,
type IFixedMovementInput,
type IFixedMovementState
} from '@esengine/network';
```
@@ -340,3 +340,139 @@ if (!identity.bIsLocalPlayer) {
3. **校正阈值**:根据游戏精度需求设置合适的阈值 3. **校正阈值**:根据游戏精度需求设置合适的阈值
4. **快照数量**:保持足够的快照以应对网络抖动 4. **快照数量**:保持足够的快照以应对网络抖动
---
## 定点数同步(帧同步)
以下内容用于**帧同步 (Lockstep)** 架构,使用定点数确保跨平台确定性。
> 定点数基础知识请参考 [定点数文档](/modules/network/fixed-point)
### FixedTransformState
定点数变换状态,用于网络传输:
```typescript
import {
FixedTransformState,
FixedTransformStateWithVelocity,
type IFixedTransformStateRaw
} from '@esengine/network';
// 创建状态
const state = FixedTransformState.from(100, 200, Math.PI / 4);
// 序列化(发送方)
const raw: IFixedTransformStateRaw = state.toRaw();
socket.send(JSON.stringify({ type: 'sync', state: raw }));
// 反序列化(接收方)
const received = FixedTransformState.fromRaw(message.state);
// 用于渲染
const { x, y, rotation } = received.toFloat();
sprite.position.set(x, y);
```
带速度的状态(用于外推):
```typescript
const state = FixedTransformStateWithVelocity.from(
100, 200, // 位置
0, // 旋转
5, 3, // 速度
0.1 // 角速度
);
```
### 定点数插值器
```typescript
import {
createFixedTransformInterpolator,
createFixedHermiteTransformInterpolator
} from '@esengine/network';
import { Fixed32 } from '@esengine/ecs-framework-math';
// 线性插值器
const interpolator = createFixedTransformInterpolator();
const from = FixedTransformState.from(0, 0, 0);
const to = FixedTransformState.from(100, 50, Math.PI);
const t = Fixed32.from(0.5);
const result = interpolator.interpolate(from, to, t);
// Hermite 插值器(更平滑)
const hermite = createFixedHermiteTransformInterpolator(100);
```
### 定点数快照缓冲区
管理定点数状态历史,用于帧同步回放:
```typescript
import {
FixedSnapshotBuffer,
createFixedSnapshotBuffer
} from '@esengine/network';
// 创建缓冲区(最多 30 快照,2 帧延迟)
const buffer = createFixedSnapshotBuffer<FixedTransformState>(30, 2);
// 添加快照
buffer.push({
frame: 100,
state: FixedTransformState.from(100, 200, 0)
});
// 获取插值快照
const result = buffer.getInterpolationSnapshots(103);
if (result) {
const { from, to, t } = result;
const interpolated = interpolator.interpolate(from.state, to.state, t);
}
// 获取最新/指定帧快照
const latest = buffer.getLatest();
const atFrame = buffer.getAtFrame(100);
// 回滚重播
const snapshotsToReplay = buffer.getSnapshotsAfter(98);
// 清理旧快照
buffer.removeSnapshotsBefore(95);
```
子帧插值:
```typescript
// 使用 Fixed32 帧时间(支持小数帧)
const frameTime = Fixed32.from(102.5);
const result = buffer.getInterpolationSnapshotsFixed(frameTime);
```
### API 导出
```typescript
import {
// 状态类
FixedTransformState,
FixedTransformStateWithVelocity,
type IFixedTransformStateRaw,
type IFixedTransformStateWithVelocityRaw,
// 插值器
FixedTransformInterpolator,
FixedHermiteTransformInterpolator,
createFixedTransformInterpolator,
createFixedHermiteTransformInterpolator,
// 快照缓冲区
FixedSnapshotBuffer,
createFixedSnapshotBuffer,
type IFixedStateSnapshot,
type IFixedInterpolationResult
} from '@esengine/network';
```
+450
View File
@@ -0,0 +1,450 @@
/**
* @zh Q16.16
* @en Q16.16 fixed-point number for deterministic calculations (lockstep)
*
* @zh 使 16 + 16 ±32767.99998
* @en Uses 16-bit integer + 16-bit fraction, range ±32767.99998
*
* @example
* ```typescript
* const a = Fixed32.from(3.14);
* const b = Fixed32.from(2);
* const c = a.mul(b); // 6.28
* console.log(c.toNumber());
* ```
*/
export class Fixed32 {
/**
* @zh 32
* @en Internal raw value (32-bit integer)
*/
readonly raw: number;
/**
* @zh
* @en Fraction bits
*/
static readonly FRACTION_BITS = 16;
/**
* @zh (2^16 = 65536)
* @en Scale factor (2^16 = 65536)
*/
static readonly SCALE = 65536;
/**
* @zh ( 32767.99998)
* @en Maximum value (approximately 32767.99998)
*/
static readonly MAX_VALUE = 0x7FFFFFFF;
/**
* @zh ( -32768)
* @en Minimum value (approximately -32768)
*/
static readonly MIN_VALUE = -0x80000000;
/**
* @zh (1/65536 0.0000153)
* @en Precision (1/65536 0.0000153)
*/
static readonly EPSILON = 1;
// ==================== 常量 ====================
/** @zh 零 @en Zero */
static readonly ZERO = new Fixed32(0);
/** @zh 一 @en One */
static readonly ONE = new Fixed32(Fixed32.SCALE);
/** @zh 负一 @en Negative one */
static readonly NEG_ONE = new Fixed32(-Fixed32.SCALE);
/** @zh 二分之一 @en One half */
static readonly HALF = new Fixed32(Fixed32.SCALE >> 1);
/** @zh 圆周率 π @en Pi */
static readonly PI = new Fixed32(205887); // π * 65536
/** @zh 2π @en Two Pi */
static readonly TWO_PI = new Fixed32(411775); // 2π * 65536
/** @zh π/2 @en Pi divided by 2 */
static readonly HALF_PI = new Fixed32(102944); // π/2 * 65536
/** @zh 弧度转角度系数 (180/π) @en Radians to degrees factor */
static readonly RAD_TO_DEG = new Fixed32(3754936); // (180/π) * 65536
/** @zh 角度转弧度系数 (π/180) @en Degrees to radians factor */
static readonly DEG_TO_RAD = new Fixed32(1144); // (π/180) * 65536
// ==================== 构造 ====================
/**
* @zh 使
* @en Private constructor, use static methods to create instances
*/
private constructor(raw: number) {
// 确保是 32 位有符号整数
this.raw = raw | 0;
}
/**
* @zh
* @en Create fixed-point from floating-point number
* @param n - @zh @en Floating-point value
*/
static from(n: number): Fixed32 {
return new Fixed32(Math.round(n * Fixed32.SCALE));
}
/**
* @zh
* @en Create fixed-point from raw integer value
* @param raw - @zh @en Raw value
*/
static fromRaw(raw: number): Fixed32 {
return new Fixed32(raw);
}
/**
* @zh
* @en Create fixed-point from integer (no precision loss)
* @param n - @zh @en Integer value
*/
static fromInt(n: number): Fixed32 {
return new Fixed32((n | 0) << Fixed32.FRACTION_BITS);
}
// ==================== 转换 ====================
/**
* @zh
* @en Convert to floating-point number
*/
toNumber(): number {
return this.raw / Fixed32.SCALE;
}
/**
* @zh
* @en Get raw integer value
*/
toRaw(): number {
return this.raw;
}
/**
* @zh
* @en Convert to integer (floor)
*/
toInt(): number {
return this.raw >> Fixed32.FRACTION_BITS;
}
/**
* @zh
* @en Convert to string
*/
toString(): string {
return `Fixed32(${this.toNumber().toFixed(5)})`;
}
// ==================== 基础运算 ====================
/**
* @zh
* @en Addition
*/
add(other: Fixed32): Fixed32 {
return new Fixed32(this.raw + other.raw);
}
/**
* @zh
* @en Subtraction
*/
sub(other: Fixed32): Fixed32 {
return new Fixed32(this.raw - other.raw);
}
/**
* @zh 使 64
* @en Multiplication (uses 64-bit intermediate to prevent overflow)
*/
mul(other: Fixed32): Fixed32 {
// 拆分为高低 16 位进行乘法,避免溢出
const a = this.raw;
const b = other.raw;
// 使用 BigInt 确保精度(JS 数字在大数时会丢失精度)
// 或者使用拆分法
const aLow = a & 0xFFFF;
const aHigh = a >> 16;
const bLow = b & 0xFFFF;
const bHigh = b >> 16;
// (aHigh * 2^16 + aLow) * (bHigh * 2^16 + bLow) / 2^16
// = aHigh * bHigh * 2^16 + aHigh * bLow + aLow * bHigh + aLow * bLow / 2^16
const lowLow = (aLow * bLow) >>> 16;
const lowHigh = aLow * bHigh;
const highLow = aHigh * bLow;
const highHigh = aHigh * bHigh;
const result = highHigh * Fixed32.SCALE + lowHigh + highLow + lowLow;
return new Fixed32(result | 0);
}
/**
* @zh
* @en Division
* @throws @zh @en Throws when dividing by zero
*/
div(other: Fixed32): Fixed32 {
if (other.raw === 0) {
throw new Error('Fixed32: Division by zero');
}
// 先左移再除,保持精度
const result = ((this.raw * Fixed32.SCALE) / other.raw) | 0;
return new Fixed32(result);
}
/**
* @zh
* @en Modulo operation
*/
mod(other: Fixed32): Fixed32 {
return new Fixed32(this.raw % other.raw);
}
/**
* @zh
* @en Negation
*/
neg(): Fixed32 {
return new Fixed32(-this.raw);
}
/**
* @zh
* @en Absolute value
*/
abs(): Fixed32 {
return this.raw >= 0 ? this : new Fixed32(-this.raw);
}
// ==================== 比较运算 ====================
/**
* @zh
* @en Equal to
*/
eq(other: Fixed32): boolean {
return this.raw === other.raw;
}
/**
* @zh
* @en Not equal to
*/
ne(other: Fixed32): boolean {
return this.raw !== other.raw;
}
/**
* @zh
* @en Less than
*/
lt(other: Fixed32): boolean {
return this.raw < other.raw;
}
/**
* @zh
* @en Less than or equal to
*/
le(other: Fixed32): boolean {
return this.raw <= other.raw;
}
/**
* @zh
* @en Greater than
*/
gt(other: Fixed32): boolean {
return this.raw > other.raw;
}
/**
* @zh
* @en Greater than or equal to
*/
ge(other: Fixed32): boolean {
return this.raw >= other.raw;
}
/**
* @zh
* @en Check if zero
*/
isZero(): boolean {
return this.raw === 0;
}
/**
* @zh
* @en Check if positive
*/
isPositive(): boolean {
return this.raw > 0;
}
/**
* @zh
* @en Check if negative
*/
isNegative(): boolean {
return this.raw < 0;
}
// ==================== 数学函数 ====================
/**
* @zh
* @en Square root (Newton's method, deterministic)
*/
static sqrt(x: Fixed32): Fixed32 {
if (x.raw <= 0) return Fixed32.ZERO;
// 牛顿迭代法
let guess = x.raw;
let prev = 0;
// 固定迭代次数确保确定性
for (let i = 0; i < 16; i++) {
prev = guess;
guess = ((guess + ((x.raw * Fixed32.SCALE) / guess) | 0) >> 1) | 0;
if (guess === prev) break;
}
return new Fixed32(guess);
}
/**
* @zh
* @en Floor
*/
static floor(x: Fixed32): Fixed32 {
return new Fixed32(x.raw & ~(Fixed32.SCALE - 1));
}
/**
* @zh
* @en Ceiling
*/
static ceil(x: Fixed32): Fixed32 {
const frac = x.raw & (Fixed32.SCALE - 1);
if (frac === 0) return x;
return new Fixed32((x.raw & ~(Fixed32.SCALE - 1)) + Fixed32.SCALE);
}
/**
* @zh
* @en Round
*/
static round(x: Fixed32): Fixed32 {
return new Fixed32((x.raw + (Fixed32.SCALE >> 1)) & ~(Fixed32.SCALE - 1));
}
/**
* @zh
* @en Minimum
*/
static min(a: Fixed32, b: Fixed32): Fixed32 {
return a.raw < b.raw ? a : b;
}
/**
* @zh
* @en Maximum
*/
static max(a: Fixed32, b: Fixed32): Fixed32 {
return a.raw > b.raw ? a : b;
}
/**
* @zh
* @en Clamp to range
*/
static clamp(x: Fixed32, min: Fixed32, max: Fixed32): Fixed32 {
if (x.raw < min.raw) return min;
if (x.raw > max.raw) return max;
return x;
}
/**
* @zh 线
* @en Linear interpolation
* @param a - @zh @en Start value
* @param b - @zh @en End value
* @param t - @zh (0-1) @en Interpolation parameter (0-1)
*/
static lerp(a: Fixed32, b: Fixed32, t: Fixed32): Fixed32 {
// a + (b - a) * t
return a.add(b.sub(a).mul(t));
}
/**
* @zh
* @en Sign function
* @returns @zh -1, 0, 1 @en -1, 0, or 1
*/
static sign(x: Fixed32): Fixed32 {
if (x.raw > 0) return Fixed32.ONE;
if (x.raw < 0) return Fixed32.NEG_ONE;
return Fixed32.ZERO;
}
// ==================== 静态运算(便捷方法) ====================
/**
* @zh
* @en Addition (static)
*/
static add(a: Fixed32, b: Fixed32): Fixed32 {
return a.add(b);
}
/**
* @zh
* @en Subtraction (static)
*/
static sub(a: Fixed32, b: Fixed32): Fixed32 {
return a.sub(b);
}
/**
* @zh
* @en Multiplication (static)
*/
static mul(a: Fixed32, b: Fixed32): Fixed32 {
return a.mul(b);
}
/**
* @zh
* @en Division (static)
*/
static div(a: Fixed32, b: Fixed32): Fixed32 {
return a.div(b);
}
}
/**
* @zh Fixed32
* @en Fixed32 data interface for serialization
*/
export interface IFixed32 {
raw: number;
}
+298
View File
@@ -0,0 +1,298 @@
import { Fixed32 } from './Fixed32';
/**
* @zh 使
* @en Fixed-point math functions using lookup tables for determinism
*
* @zh 使
* @en All trigonometric functions use precomputed lookup tables to ensure consistent results across all platforms
*/
export class FixedMath {
/**
* @zh 90
* @en Sine table size (samples per 90 degrees)
*/
private static readonly SIN_TABLE_SIZE = 1024;
/**
* @zh 0 90
* @en Sine lookup table (0 to 90 degrees)
*/
private static readonly SIN_TABLE: Int32Array = FixedMath.generateSinTable();
/**
* @zh
* @en Generate sine lookup table
*/
private static generateSinTable(): Int32Array {
const table = new Int32Array(FixedMath.SIN_TABLE_SIZE + 1);
for (let i = 0; i <= FixedMath.SIN_TABLE_SIZE; i++) {
const angle = (i * Math.PI) / (2 * FixedMath.SIN_TABLE_SIZE);
table[i] = Math.round(Math.sin(angle) * Fixed32.SCALE);
}
return table;
}
/**
* @zh
* @en Sine function (deterministic)
* @param angle - @zh @en Angle in radians (fixed-point)
*/
static sin(angle: Fixed32): Fixed32 {
// 将角度规范化到 [0, 2π)
let raw = angle.raw % Fixed32.TWO_PI.raw;
if (raw < 0) raw += Fixed32.TWO_PI.raw;
const halfPi = Fixed32.HALF_PI.raw;
const pi = Fixed32.PI.raw;
const threeHalfPi = halfPi * 3;
let tableAngle: number;
let negative = false;
if (raw <= halfPi) {
// 第一象限: [0, π/2]
tableAngle = raw;
} else if (raw <= pi) {
// 第二象限: (π/2, π]
tableAngle = pi - raw;
} else if (raw <= threeHalfPi) {
// 第三象限: (π, 3π/2]
tableAngle = raw - pi;
negative = true;
} else {
// 第四象限: (3π/2, 2π)
tableAngle = Fixed32.TWO_PI.raw - raw;
negative = true;
}
// 计算表索引 (tableAngle 范围是 [0, π/2])
const tableIndex = Math.min(
((tableAngle * FixedMath.SIN_TABLE_SIZE) / halfPi) | 0,
FixedMath.SIN_TABLE_SIZE
);
const result = FixedMath.SIN_TABLE[tableIndex];
return Fixed32.fromRaw(negative ? -result : result);
}
/**
* @zh
* @en Cosine function (deterministic)
* @param angle - @zh @en Angle in radians (fixed-point)
*/
static cos(angle: Fixed32): Fixed32 {
// cos(x) = sin(x + π/2)
return FixedMath.sin(angle.add(Fixed32.HALF_PI));
}
/**
* @zh
* @en Tangent function (deterministic)
* @param angle - @zh @en Angle in radians (fixed-point)
*/
static tan(angle: Fixed32): Fixed32 {
const cosVal = FixedMath.cos(angle);
if (cosVal.isZero()) {
// 返回最大值表示无穷大
return Fixed32.fromRaw(Fixed32.MAX_VALUE);
}
return FixedMath.sin(angle).div(cosVal);
}
/**
* @zh atan2
* @en Arc tangent of y/x (deterministic)
* @param y - @zh Y @en Y coordinate
* @param x - @zh X @en X coordinate
* @returns @zh -π π@en Angle in radians (range -π to π)
*/
static atan2(y: Fixed32, x: Fixed32): Fixed32 {
const yRaw = y.raw;
const xRaw = x.raw;
if (xRaw === 0 && yRaw === 0) {
return Fixed32.ZERO;
}
// 使用 CORDIC 算法的简化版本
const absY = Math.abs(yRaw);
const absX = Math.abs(xRaw);
let angle: number;
if (absX >= absY) {
// |y/x| <= 1,使用泰勒展开近似
angle = FixedMath.atanApprox(absY, absX);
} else {
// |y/x| > 1,使用恒等式 atan(y/x) = π/2 - atan(x/y)
angle = Fixed32.HALF_PI.raw - FixedMath.atanApprox(absX, absY);
}
// 根据象限调整
if (xRaw < 0) {
angle = Fixed32.PI.raw - angle;
}
if (yRaw < 0) {
angle = -angle;
}
return Fixed32.fromRaw(angle);
}
/**
* @zh atan 使
* @en Approximate atan calculation (internal use)
*/
private static atanApprox(num: number, den: number): number {
if (den === 0) return Fixed32.HALF_PI.raw;
// 使用多项式近似: atan(x) ≈ x - x³/3 + x⁵/5
// 对于 |x| <= 1 精度足够
const ratio = ((num * Fixed32.SCALE) / den) | 0;
// 简化的多项式: atan(x) ≈ x * (1 - x²/3)
// 更精确的版本: atan(x) ≈ x / (1 + 0.28125 * x²)
const x2 = ((ratio * ratio) / Fixed32.SCALE) | 0;
const factor = Fixed32.SCALE + ((x2 * 18432) / Fixed32.SCALE | 0); // 0.28125 * 65536 ≈ 18432
const result = ((ratio * Fixed32.SCALE) / factor) | 0;
return result;
}
/**
* @zh
* @en Arc sine function (deterministic)
* @param x - @zh -1 1@en Value (range -1 to 1)
*/
static asin(x: Fixed32): Fixed32 {
// asin(x) = atan2(x, sqrt(1 - x²))
const one = Fixed32.ONE;
const x2 = x.mul(x);
const sqrt = Fixed32.sqrt(one.sub(x2));
return FixedMath.atan2(x, sqrt);
}
/**
* @zh
* @en Arc cosine function (deterministic)
* @param x - @zh -1 1@en Value (range -1 to 1)
*/
static acos(x: Fixed32): Fixed32 {
// acos(x) = π/2 - asin(x)
return Fixed32.HALF_PI.sub(FixedMath.asin(x));
}
/**
* @zh [-π, π]
* @en Normalize angle to [-π, π]
*/
static normalizeAngle(angle: Fixed32): Fixed32 {
let raw = angle.raw % Fixed32.TWO_PI.raw;
if (raw > Fixed32.PI.raw) {
raw -= Fixed32.TWO_PI.raw;
} else if (raw < -Fixed32.PI.raw) {
raw += Fixed32.TWO_PI.raw;
}
return Fixed32.fromRaw(raw);
}
/**
* @zh
* @en Angle difference (shortest path)
*/
static angleDelta(from: Fixed32, to: Fixed32): Fixed32 {
return FixedMath.normalizeAngle(to.sub(from));
}
/**
* @zh 线
* @en Angle linear interpolation (shortest path)
*/
static lerpAngle(from: Fixed32, to: Fixed32, t: Fixed32): Fixed32 {
const delta = FixedMath.angleDelta(from, to);
return from.add(delta.mul(t));
}
/**
* @zh
* @en Radians to degrees
*/
static radToDeg(rad: Fixed32): Fixed32 {
return rad.mul(Fixed32.RAD_TO_DEG);
}
/**
* @zh
* @en Degrees to radians
*/
static degToRad(deg: Fixed32): Fixed32 {
return deg.mul(Fixed32.DEG_TO_RAD);
}
/**
* @zh
* @en Power function (integer exponent)
*/
static pow(base: Fixed32, exp: number): Fixed32 {
if (exp === 0) return Fixed32.ONE;
if (exp < 0) {
base = Fixed32.ONE.div(base);
exp = -exp;
}
let result = Fixed32.ONE;
while (exp > 0) {
if (exp & 1) {
result = result.mul(base);
}
base = base.mul(base);
exp >>= 1;
}
return result;
}
/**
* @zh e^x
* @en Exponential function approximation (e^x)
*/
static exp(x: Fixed32): Fixed32 {
// 使用泰勒展开: e^x ≈ 1 + x + x²/2 + x³/6 + x⁴/24
const one = Fixed32.ONE;
const x2 = x.mul(x);
const x3 = x2.mul(x);
const x4 = x3.mul(x);
return one
.add(x)
.add(x2.div(Fixed32.from(2)))
.add(x3.div(Fixed32.from(6)))
.add(x4.div(Fixed32.from(24)));
}
/**
* @zh
* @en Natural logarithm approximation
*/
static ln(x: Fixed32): Fixed32 {
if (x.raw <= 0) {
throw new Error('FixedMath.ln: argument must be positive');
}
// 使用牛顿迭代法: y_{n+1} = y_n + 2 * (x - exp(y_n)) / (x + exp(y_n))
let y = Fixed32.ZERO;
const two = Fixed32.from(2);
for (let i = 0; i < 10; i++) {
const expY = FixedMath.exp(y);
const diff = x.sub(expY);
const sum = x.add(expY);
y = y.add(two.mul(diff).div(sum));
}
return y;
}
}
+504
View File
@@ -0,0 +1,504 @@
import { Fixed32, type IFixed32 } from './Fixed32';
import { FixedMath } from './FixedMath';
/**
* @zh 2D
* @en Fixed-point 2D vector data interface
*/
export interface IFixedVector2 {
x: IFixed32;
y: IFixed32;
}
/**
* @zh 2D
* @en Fixed-point 2D vector class for deterministic calculations (lockstep)
*
* @zh
* @en All operations return new instances, ensuring immutability
*
* @example
* ```typescript
* const a = FixedVector2.from(3, 4);
* const b = FixedVector2.from(1, 2);
* const c = a.add(b); // (4, 6)
* const len = a.length(); // 5
* ```
*/
export class FixedVector2 {
/** @zh X 分量 @en X component */
readonly x: Fixed32;
/** @zh Y 分量 @en Y component */
readonly y: Fixed32;
// ==================== 常量 ====================
/** @zh 零向量 (0, 0) @en Zero vector */
static readonly ZERO = new FixedVector2(Fixed32.ZERO, Fixed32.ZERO);
/** @zh 单位向量 (1, 1) @en One vector */
static readonly ONE = new FixedVector2(Fixed32.ONE, Fixed32.ONE);
/** @zh 右方向 (1, 0) @en Right direction */
static readonly RIGHT = new FixedVector2(Fixed32.ONE, Fixed32.ZERO);
/** @zh 左方向 (-1, 0) @en Left direction */
static readonly LEFT = new FixedVector2(Fixed32.NEG_ONE, Fixed32.ZERO);
/** @zh 上方向 (0, 1) @en Up direction */
static readonly UP = new FixedVector2(Fixed32.ZERO, Fixed32.ONE);
/** @zh 下方向 (0, -1) @en Down direction */
static readonly DOWN = new FixedVector2(Fixed32.ZERO, Fixed32.NEG_ONE);
// ==================== 构造 ====================
/**
* @zh
* @en Create fixed-point vector
*/
constructor(x: Fixed32, y: Fixed32) {
this.x = x;
this.y = y;
}
/**
* @zh
* @en Create vector from floating-point numbers
*/
static from(x: number, y: number): FixedVector2 {
return new FixedVector2(Fixed32.from(x), Fixed32.from(y));
}
/**
* @zh
* @en Create vector from raw integer values
*/
static fromRaw(rawX: number, rawY: number): FixedVector2 {
return new FixedVector2(Fixed32.fromRaw(rawX), Fixed32.fromRaw(rawY));
}
/**
* @zh
* @en Create vector from integers
*/
static fromInt(x: number, y: number): FixedVector2 {
return new FixedVector2(Fixed32.fromInt(x), Fixed32.fromInt(y));
}
/**
* @zh
* @en Create from plain vector interface
*/
static fromObject(obj: { x: number; y: number }): FixedVector2 {
return FixedVector2.from(obj.x, obj.y);
}
// ==================== 转换 ====================
/**
* @zh
* @en Convert to floating-point object (for rendering)
*/
toObject(): { x: number; y: number } {
return {
x: this.x.toNumber(),
y: this.y.toNumber()
};
}
/**
* @zh
* @en Convert to array
*/
toArray(): [number, number] {
return [this.x.toNumber(), this.y.toNumber()];
}
/**
* @zh
* @en Get raw values object (for network transmission)
*/
toRawObject(): { x: number; y: number } {
return {
x: this.x.toRaw(),
y: this.y.toRaw()
};
}
/**
* @zh
* @en Convert to string
*/
toString(): string {
return `FixedVector2(${this.x.toNumber().toFixed(3)}, ${this.y.toNumber().toFixed(3)})`;
}
/**
* @zh
* @en Clone vector
*/
clone(): FixedVector2 {
return new FixedVector2(this.x, this.y);
}
// ==================== 基础运算 ====================
/**
* @zh
* @en Vector addition
*/
add(other: FixedVector2): FixedVector2 {
return new FixedVector2(this.x.add(other.x), this.y.add(other.y));
}
/**
* @zh
* @en Vector subtraction
*/
sub(other: FixedVector2): FixedVector2 {
return new FixedVector2(this.x.sub(other.x), this.y.sub(other.y));
}
/**
* @zh
* @en Scalar multiplication
*/
mul(scalar: Fixed32): FixedVector2 {
return new FixedVector2(this.x.mul(scalar), this.y.mul(scalar));
}
/**
* @zh
* @en Scalar division
*/
div(scalar: Fixed32): FixedVector2 {
return new FixedVector2(this.x.div(scalar), this.y.div(scalar));
}
/**
* @zh
* @en Component-wise multiplication
*/
mulComponents(other: FixedVector2): FixedVector2 {
return new FixedVector2(this.x.mul(other.x), this.y.mul(other.y));
}
/**
* @zh
* @en Component-wise division
*/
divComponents(other: FixedVector2): FixedVector2 {
return new FixedVector2(this.x.div(other.x), this.y.div(other.y));
}
/**
* @zh
* @en Negate
*/
neg(): FixedVector2 {
return new FixedVector2(this.x.neg(), this.y.neg());
}
// ==================== 向量运算 ====================
/**
* @zh
* @en Dot product
*/
dot(other: FixedVector2): Fixed32 {
return this.x.mul(other.x).add(this.y.mul(other.y));
}
/**
* @zh 2D
* @en Cross product (returns scalar in 2D)
*/
cross(other: FixedVector2): Fixed32 {
return this.x.mul(other.y).sub(this.y.mul(other.x));
}
/**
* @zh
* @en Length squared
*/
lengthSquared(): Fixed32 {
return this.dot(this);
}
/**
* @zh
* @en Length (magnitude)
*/
length(): Fixed32 {
return Fixed32.sqrt(this.lengthSquared());
}
/**
* @zh
* @en Normalize (convert to unit vector)
*/
normalize(): FixedVector2 {
const len = this.length();
if (len.isZero()) {
return FixedVector2.ZERO;
}
return this.div(len);
}
/**
* @zh
* @en Distance squared to another vector
*/
distanceSquaredTo(other: FixedVector2): Fixed32 {
const dx = this.x.sub(other.x);
const dy = this.y.sub(other.y);
return dx.mul(dx).add(dy.mul(dy));
}
/**
* @zh
* @en Distance to another vector
*/
distanceTo(other: FixedVector2): Fixed32 {
return Fixed32.sqrt(this.distanceSquaredTo(other));
}
/**
* @zh 90
* @en Get perpendicular vector (clockwise 90 degrees)
*/
perpendicular(): FixedVector2 {
return new FixedVector2(this.y, this.x.neg());
}
/**
* @zh 90
* @en Get perpendicular vector (counter-clockwise 90 degrees)
*/
perpendicularCCW(): FixedVector2 {
return new FixedVector2(this.y.neg(), this.x);
}
/**
* @zh
* @en Project onto another vector
*/
projectOnto(onto: FixedVector2): FixedVector2 {
const dot = this.dot(onto);
const lenSq = onto.lengthSquared();
if (lenSq.isZero()) {
return FixedVector2.ZERO;
}
return onto.mul(dot.div(lenSq));
}
/**
* @zh 线
* @en Reflect vector (about normal)
*/
reflect(normal: FixedVector2): FixedVector2 {
const dot = this.dot(normal);
const two = Fixed32.from(2);
return this.sub(normal.mul(two.mul(dot)));
}
// ==================== 旋转和角度 ====================
/**
* @zh
* @en Rotate vector (clockwise positive, left-hand coordinate system)
* @param angle - @zh @en Rotation angle in radians
*/
rotate(angle: Fixed32): FixedVector2 {
const cos = FixedMath.cos(angle);
const sin = FixedMath.sin(angle);
// 顺时针旋转: x' = x*cos + y*sin, y' = -x*sin + y*cos
return new FixedVector2(
this.x.mul(cos).add(this.y.mul(sin)),
this.x.neg().mul(sin).add(this.y.mul(cos))
);
}
/**
* @zh
* @en Rotate around a point
*/
rotateAround(center: FixedVector2, angle: Fixed32): FixedVector2 {
return this.sub(center).rotate(angle).add(center);
}
/**
* @zh
* @en Get vector angle in radians
*/
angle(): Fixed32 {
return FixedMath.atan2(this.y, this.x);
}
/**
* @zh
* @en Get angle between this and another vector
*/
angleTo(other: FixedVector2): Fixed32 {
const cross = this.cross(other);
const dot = this.dot(other);
return FixedMath.atan2(cross, dot);
}
/**
* @zh
* @en Create vector from polar coordinates
*/
static fromPolar(length: Fixed32, angle: Fixed32): FixedVector2 {
return new FixedVector2(
length.mul(FixedMath.cos(angle)),
length.mul(FixedMath.sin(angle))
);
}
/**
* @zh
* @en Create unit vector from angle
*/
static fromAngle(angle: Fixed32): FixedVector2 {
return new FixedVector2(FixedMath.cos(angle), FixedMath.sin(angle));
}
// ==================== 比较运算 ====================
/**
* @zh
* @en Check equality
*/
equals(other: FixedVector2): boolean {
return this.x.eq(other.x) && this.y.eq(other.y);
}
/**
* @zh
* @en Check if zero vector
*/
isZero(): boolean {
return this.x.isZero() && this.y.isZero();
}
// ==================== 限制和插值 ====================
/**
* @zh
* @en Clamp length
*/
clampLength(maxLength: Fixed32): FixedVector2 {
const lenSq = this.lengthSquared();
const maxLenSq = maxLength.mul(maxLength);
if (lenSq.gt(maxLenSq)) {
return this.normalize().mul(maxLength);
}
return this;
}
/**
* @zh
* @en Clamp components
*/
clamp(min: FixedVector2, max: FixedVector2): FixedVector2 {
return new FixedVector2(
Fixed32.clamp(this.x, min.x, max.x),
Fixed32.clamp(this.y, min.y, max.y)
);
}
/**
* @zh 线
* @en Linear interpolation
*/
lerp(target: FixedVector2, t: Fixed32): FixedVector2 {
return new FixedVector2(
Fixed32.lerp(this.x, target.x, t),
Fixed32.lerp(this.y, target.y, t)
);
}
/**
* @zh
* @en Move towards target by fixed distance
*/
moveTowards(target: FixedVector2, maxDistance: Fixed32): FixedVector2 {
const diff = target.sub(this);
const dist = diff.length();
if (dist.isZero() || dist.le(maxDistance)) {
return target;
}
return this.add(diff.div(dist).mul(maxDistance));
}
// ==================== 静态方法 ====================
/**
* @zh
* @en Vector addition (static)
*/
static add(a: FixedVector2, b: FixedVector2): FixedVector2 {
return a.add(b);
}
/**
* @zh
* @en Vector subtraction (static)
*/
static sub(a: FixedVector2, b: FixedVector2): FixedVector2 {
return a.sub(b);
}
/**
* @zh
* @en Dot product (static)
*/
static dot(a: FixedVector2, b: FixedVector2): Fixed32 {
return a.dot(b);
}
/**
* @zh
* @en Cross product (static)
*/
static cross(a: FixedVector2, b: FixedVector2): Fixed32 {
return a.cross(b);
}
/**
* @zh
* @en Distance (static)
*/
static distance(a: FixedVector2, b: FixedVector2): Fixed32 {
return a.distanceTo(b);
}
/**
* @zh 线
* @en Linear interpolation (static)
*/
static lerp(a: FixedVector2, b: FixedVector2, t: Fixed32): FixedVector2 {
return a.lerp(b, t);
}
/**
* @zh
* @en Get minimum components of two vectors
*/
static min(a: FixedVector2, b: FixedVector2): FixedVector2 {
return new FixedVector2(Fixed32.min(a.x, b.x), Fixed32.min(a.y, b.y));
}
/**
* @zh
* @en Get maximum components of two vectors
*/
static max(a: FixedVector2, b: FixedVector2): FixedVector2 {
return new FixedVector2(Fixed32.max(a.x, b.x), Fixed32.max(a.y, b.y));
}
}
+6
View File
@@ -3,6 +3,7 @@
* *
* 2D数学库 * 2D数学库
* - * -
* -
* - * -
* - * -
* - * -
@@ -16,6 +17,11 @@ export { Matrix3 } from './Matrix3';
export { Rectangle } from './Rectangle'; export { Rectangle } from './Rectangle';
export { Circle } from './Circle'; export { Circle } from './Circle';
// 定点数数学(帧同步确定性计算)
export { Fixed32, type IFixed32 } from './Fixed32';
export { FixedVector2, type IFixedVector2 } from './FixedVector2';
export { FixedMath } from './FixedMath';
// 数学工具 // 数学工具
export { MathUtils } from './MathUtils'; export { MathUtils } from './MathUtils';
@@ -0,0 +1,225 @@
import { Fixed32 } from '../src/Fixed32';
import { FixedMath } from '../src/FixedMath';
describe('Fixed32', () => {
describe('创建和转换', () => {
test('from 应正确从浮点数创建', () => {
const a = Fixed32.from(3.5);
expect(a.toNumber()).toBeCloseTo(3.5, 4);
});
test('fromInt 应正确从整数创建', () => {
const a = Fixed32.fromInt(42);
expect(a.toInt()).toBe(42);
expect(a.toNumber()).toBe(42);
});
test('fromRaw 应正确从原始值创建', () => {
const raw = 65536 * 2; // 2.0
const a = Fixed32.fromRaw(raw);
expect(a.toNumber()).toBe(2);
});
test('常量应正确', () => {
expect(Fixed32.ZERO.toNumber()).toBe(0);
expect(Fixed32.ONE.toNumber()).toBe(1);
expect(Fixed32.HALF.toNumber()).toBe(0.5);
expect(Fixed32.PI.toNumber()).toBeCloseTo(Math.PI, 3);
});
});
describe('基础运算', () => {
test('add 应正确计算', () => {
const a = Fixed32.from(2.5);
const b = Fixed32.from(1.5);
expect(a.add(b).toNumber()).toBeCloseTo(4, 4);
});
test('sub 应正确计算', () => {
const a = Fixed32.from(5);
const b = Fixed32.from(3);
expect(a.sub(b).toNumber()).toBeCloseTo(2, 4);
});
test('mul 应正确计算', () => {
const a = Fixed32.from(3);
const b = Fixed32.from(4);
expect(a.mul(b).toNumber()).toBeCloseTo(12, 4);
});
test('mul 应正确处理小数', () => {
const a = Fixed32.from(2.5);
const b = Fixed32.from(1.5);
expect(a.mul(b).toNumber()).toBeCloseTo(3.75, 4);
});
test('div 应正确计算', () => {
const a = Fixed32.from(10);
const b = Fixed32.from(4);
expect(a.div(b).toNumber()).toBeCloseTo(2.5, 4);
});
test('div 应抛出除零错误', () => {
const a = Fixed32.from(10);
expect(() => a.div(Fixed32.ZERO)).toThrow('Division by zero');
});
test('neg 应正确取反', () => {
const a = Fixed32.from(5);
expect(a.neg().toNumber()).toBeCloseTo(-5, 4);
});
test('abs 应正确取绝对值', () => {
const a = Fixed32.from(-5);
expect(a.abs().toNumber()).toBeCloseTo(5, 4);
});
});
describe('比较运算', () => {
test('eq 应正确比较', () => {
const a = Fixed32.from(5);
const b = Fixed32.from(5);
const c = Fixed32.from(6);
expect(a.eq(b)).toBe(true);
expect(a.eq(c)).toBe(false);
});
test('lt/le/gt/ge 应正确比较', () => {
const a = Fixed32.from(3);
const b = Fixed32.from(5);
expect(a.lt(b)).toBe(true);
expect(a.le(b)).toBe(true);
expect(b.gt(a)).toBe(true);
expect(b.ge(a)).toBe(true);
});
});
describe('数学函数', () => {
test('sqrt 应正确计算', () => {
const a = Fixed32.from(16);
expect(Fixed32.sqrt(a).toNumber()).toBeCloseTo(4, 3);
const b = Fixed32.from(2);
expect(Fixed32.sqrt(b).toNumber()).toBeCloseTo(Math.sqrt(2), 3);
});
test('floor/ceil/round 应正确计算', () => {
const a = Fixed32.from(3.7);
expect(Fixed32.floor(a).toNumber()).toBeCloseTo(3, 4);
expect(Fixed32.ceil(a).toNumber()).toBeCloseTo(4, 4);
expect(Fixed32.round(a).toNumber()).toBeCloseTo(4, 4);
const b = Fixed32.from(3.2);
expect(Fixed32.round(b).toNumber()).toBeCloseTo(3, 4);
});
test('min/max/clamp 应正确计算', () => {
const a = Fixed32.from(3);
const b = Fixed32.from(5);
expect(Fixed32.min(a, b).toNumber()).toBe(3);
expect(Fixed32.max(a, b).toNumber()).toBe(5);
const x = Fixed32.from(7);
expect(Fixed32.clamp(x, a, b).toNumber()).toBe(5);
});
test('lerp 应正确插值', () => {
const a = Fixed32.from(0);
const b = Fixed32.from(10);
const t = Fixed32.from(0.5);
expect(Fixed32.lerp(a, b, t).toNumber()).toBeCloseTo(5, 4);
});
});
describe('确定性', () => {
test('相同输入应产生相同输出', () => {
const results: number[] = [];
for (let i = 0; i < 100; i++) {
const a = Fixed32.from(3.14159);
const b = Fixed32.from(2.71828);
const result = a.mul(b).add(Fixed32.sqrt(a)).toRaw();
results.push(result);
}
// 所有结果应该完全相同
expect(new Set(results).size).toBe(1);
});
});
});
describe('FixedMath', () => {
describe('三角函数', () => {
test('sin 应正确计算', () => {
expect(FixedMath.sin(Fixed32.ZERO).toNumber()).toBeCloseTo(0, 3);
expect(FixedMath.sin(Fixed32.HALF_PI).toNumber()).toBeCloseTo(1, 3);
expect(FixedMath.sin(Fixed32.PI).toNumber()).toBeCloseTo(0, 2);
});
test('cos 应正确计算', () => {
expect(FixedMath.cos(Fixed32.ZERO).toNumber()).toBeCloseTo(1, 3);
expect(FixedMath.cos(Fixed32.HALF_PI).toNumber()).toBeCloseTo(0, 2);
expect(FixedMath.cos(Fixed32.PI).toNumber()).toBeCloseTo(-1, 3);
});
test('sin²x + cos²x = 1', () => {
const angles = [0, 0.5, 1, 1.5, 2, 2.5, 3];
for (const a of angles) {
const angle = Fixed32.from(a);
const sin = FixedMath.sin(angle);
const cos = FixedMath.cos(angle);
const sum = sin.mul(sin).add(cos.mul(cos));
expect(sum.toNumber()).toBeCloseTo(1, 2);
}
});
test('atan2 应正确计算', () => {
// atan2(0, 1) = 0
expect(FixedMath.atan2(Fixed32.ZERO, Fixed32.ONE).toNumber()).toBeCloseTo(0, 3);
// atan2(1, 0) = π/2
expect(FixedMath.atan2(Fixed32.ONE, Fixed32.ZERO).toNumber()).toBeCloseTo(Math.PI / 2, 2);
// atan2(1, 1) = π/4
expect(FixedMath.atan2(Fixed32.ONE, Fixed32.ONE).toNumber()).toBeCloseTo(Math.PI / 4, 2);
});
});
describe('角度函数', () => {
test('radToDeg/degToRad 应正确转换', () => {
const rad = Fixed32.PI;
const deg = FixedMath.radToDeg(rad);
expect(deg.toNumber()).toBeCloseTo(180, 1);
const deg90 = Fixed32.from(90);
const rad90 = FixedMath.degToRad(deg90);
expect(rad90.toNumber()).toBeCloseTo(Math.PI / 2, 2);
});
test('normalizeAngle 应正确规范化', () => {
const angle1 = Fixed32.from(Math.PI * 3); // 3π -> π
expect(Math.abs(FixedMath.normalizeAngle(angle1).toNumber())).toBeLessThanOrEqual(Math.PI + 0.1);
const angle2 = Fixed32.from(-Math.PI * 3); // -3π -> -π
expect(Math.abs(FixedMath.normalizeAngle(angle2).toNumber())).toBeLessThanOrEqual(Math.PI + 0.1);
});
test('lerpAngle 应走最短路径', () => {
const from = Fixed32.from(0.1);
const to = Fixed32.from(-0.1);
const t = Fixed32.HALF;
const result = FixedMath.lerpAngle(from, to, t);
expect(result.toNumber()).toBeCloseTo(0, 2);
});
});
describe('确定性', () => {
test('三角函数应产生确定性结果', () => {
const results: number[] = [];
for (let i = 0; i < 100; i++) {
const angle = Fixed32.from(1.234);
const result = FixedMath.sin(angle).toRaw();
results.push(result);
}
expect(new Set(results).size).toBe(1);
});
});
});
@@ -0,0 +1,242 @@
import { Fixed32 } from '../src/Fixed32';
import { FixedVector2 } from '../src/FixedVector2';
describe('FixedVector2', () => {
describe('创建和转换', () => {
test('from 应正确从浮点数创建', () => {
const v = FixedVector2.from(3, 4);
const obj = v.toObject();
expect(obj.x).toBeCloseTo(3, 4);
expect(obj.y).toBeCloseTo(4, 4);
});
test('fromInt 应正确从整数创建', () => {
const v = FixedVector2.fromInt(5, 6);
expect(v.x.toInt()).toBe(5);
expect(v.y.toInt()).toBe(6);
});
test('常量应正确', () => {
expect(FixedVector2.ZERO.isZero()).toBe(true);
expect(FixedVector2.ONE.x.toNumber()).toBe(1);
expect(FixedVector2.ONE.y.toNumber()).toBe(1);
expect(FixedVector2.RIGHT.x.toNumber()).toBe(1);
expect(FixedVector2.RIGHT.y.toNumber()).toBe(0);
});
test('toRawObject 应返回原始值', () => {
const v = FixedVector2.from(1, 2);
const raw = v.toRawObject();
expect(raw.x).toBe(Fixed32.from(1).toRaw());
expect(raw.y).toBe(Fixed32.from(2).toRaw());
});
});
describe('基础运算', () => {
test('add 应正确计算', () => {
const a = FixedVector2.from(1, 2);
const b = FixedVector2.from(3, 4);
const result = a.add(b).toObject();
expect(result.x).toBeCloseTo(4, 4);
expect(result.y).toBeCloseTo(6, 4);
});
test('sub 应正确计算', () => {
const a = FixedVector2.from(5, 7);
const b = FixedVector2.from(2, 3);
const result = a.sub(b).toObject();
expect(result.x).toBeCloseTo(3, 4);
expect(result.y).toBeCloseTo(4, 4);
});
test('mul 应正确计算标量乘法', () => {
const v = FixedVector2.from(3, 4);
const result = v.mul(Fixed32.from(2)).toObject();
expect(result.x).toBeCloseTo(6, 4);
expect(result.y).toBeCloseTo(8, 4);
});
test('div 应正确计算标量除法', () => {
const v = FixedVector2.from(6, 8);
const result = v.div(Fixed32.from(2)).toObject();
expect(result.x).toBeCloseTo(3, 4);
expect(result.y).toBeCloseTo(4, 4);
});
test('neg 应正确取反', () => {
const v = FixedVector2.from(3, -4);
const result = v.neg().toObject();
expect(result.x).toBeCloseTo(-3, 4);
expect(result.y).toBeCloseTo(4, 4);
});
});
describe('向量运算', () => {
test('dot 应正确计算点积', () => {
const a = FixedVector2.from(1, 2);
const b = FixedVector2.from(3, 4);
// 1*3 + 2*4 = 11
expect(a.dot(b).toNumber()).toBeCloseTo(11, 4);
});
test('cross 应正确计算叉积', () => {
const a = FixedVector2.from(1, 0);
const b = FixedVector2.from(0, 1);
// 1*1 - 0*0 = 1
expect(a.cross(b).toNumber()).toBeCloseTo(1, 4);
});
test('length 应正确计算', () => {
const v = FixedVector2.from(3, 4);
expect(v.length().toNumber()).toBeCloseTo(5, 3);
});
test('lengthSquared 应正确计算', () => {
const v = FixedVector2.from(3, 4);
expect(v.lengthSquared().toNumber()).toBeCloseTo(25, 4);
});
test('normalize 应正确归一化', () => {
const v = FixedVector2.from(3, 4);
const n = v.normalize();
expect(n.length().toNumber()).toBeCloseTo(1, 2);
expect(n.x.toNumber()).toBeCloseTo(0.6, 2);
expect(n.y.toNumber()).toBeCloseTo(0.8, 2);
});
test('normalize 零向量应返回零向量', () => {
const v = FixedVector2.ZERO;
const n = v.normalize();
expect(n.isZero()).toBe(true);
});
test('distanceTo 应正确计算', () => {
const a = FixedVector2.from(0, 0);
const b = FixedVector2.from(3, 4);
expect(a.distanceTo(b).toNumber()).toBeCloseTo(5, 3);
});
test('perpendicular 应正确计算', () => {
const v = FixedVector2.from(1, 0);
const perp = v.perpendicular();
// 顺时针 90 度: (1, 0) -> (0, -1)
expect(perp.x.toNumber()).toBeCloseTo(0, 4);
expect(perp.y.toNumber()).toBeCloseTo(-1, 4);
});
});
describe('旋转和角度', () => {
test('rotate 应正确旋转', () => {
const v = FixedVector2.from(1, 0);
const angle = Fixed32.HALF_PI; // 90 度
const rotated = v.rotate(angle);
// 顺时针旋转 90 度: (1, 0) -> (0, -1)
expect(rotated.x.toNumber()).toBeCloseTo(0, 2);
expect(rotated.y.toNumber()).toBeCloseTo(-1, 2);
});
test('angle 应正确计算', () => {
const v = FixedVector2.from(1, 0);
expect(v.angle().toNumber()).toBeCloseTo(0, 3);
const v2 = FixedVector2.from(0, 1);
expect(v2.angle().toNumber()).toBeCloseTo(Math.PI / 2, 2);
});
test('fromAngle 应正确创建', () => {
const v = FixedVector2.fromAngle(Fixed32.ZERO);
expect(v.x.toNumber()).toBeCloseTo(1, 3);
expect(v.y.toNumber()).toBeCloseTo(0, 3);
});
test('fromPolar 应正确创建', () => {
const v = FixedVector2.fromPolar(Fixed32.from(5), Fixed32.ZERO);
expect(v.x.toNumber()).toBeCloseTo(5, 3);
expect(v.y.toNumber()).toBeCloseTo(0, 3);
});
});
describe('插值和限制', () => {
test('lerp 应正确插值', () => {
const a = FixedVector2.from(0, 0);
const b = FixedVector2.from(10, 20);
const result = a.lerp(b, Fixed32.HALF).toObject();
expect(result.x).toBeCloseTo(5, 4);
expect(result.y).toBeCloseTo(10, 4);
});
test('clampLength 应正确限制长度', () => {
const v = FixedVector2.from(6, 8); // 长度 10
const clamped = v.clampLength(Fixed32.from(5));
expect(clamped.length().toNumber()).toBeCloseTo(5, 2);
});
test('moveTowards 应正确移动', () => {
const a = FixedVector2.from(0, 0);
const b = FixedVector2.from(10, 0);
const result = a.moveTowards(b, Fixed32.from(3));
expect(result.x.toNumber()).toBeCloseTo(3, 3);
expect(result.y.toNumber()).toBeCloseTo(0, 3);
});
});
describe('比较运算', () => {
test('equals 应正确比较', () => {
const a = FixedVector2.from(3, 4);
const b = FixedVector2.from(3, 4);
const c = FixedVector2.from(3, 5);
expect(a.equals(b)).toBe(true);
expect(a.equals(c)).toBe(false);
});
test('isZero 应正确判断', () => {
expect(FixedVector2.ZERO.isZero()).toBe(true);
expect(FixedVector2.ONE.isZero()).toBe(false);
});
});
describe('确定性', () => {
test('向量运算应产生确定性结果', () => {
const results: string[] = [];
for (let i = 0; i < 100; i++) {
const a = FixedVector2.from(3.14159, 2.71828);
const b = FixedVector2.from(1.41421, 1.73205);
const result = a.add(b).mul(Fixed32.from(0.5)).normalize();
results.push(`${result.x.toRaw()},${result.y.toRaw()}`);
}
expect(new Set(results).size).toBe(1);
});
test('旋转应产生确定性结果', () => {
const results: string[] = [];
for (let i = 0; i < 100; i++) {
const v = FixedVector2.from(1, 0);
const angle = Fixed32.from(0.7853981634); // π/4
const rotated = v.rotate(angle);
results.push(`${rotated.x.toRaw()},${rotated.y.toRaw()}`);
}
expect(new Set(results).size).toBe(1);
});
});
describe('静态方法', () => {
test('distance 应正确计算', () => {
const a = FixedVector2.from(0, 0);
const b = FixedVector2.from(3, 4);
expect(FixedVector2.distance(a, b).toNumber()).toBeCloseTo(5, 3);
});
test('min/max 应正确计算', () => {
const a = FixedVector2.from(1, 5);
const b = FixedVector2.from(3, 2);
const min = FixedVector2.min(a, b);
expect(min.x.toNumber()).toBeCloseTo(1, 4);
expect(min.y.toNumber()).toBeCloseTo(2, 4);
const max = FixedVector2.max(a, b);
expect(max.x.toNumber()).toBeCloseTo(3, 4);
expect(max.y.toNumber()).toBeCloseTo(5, 4);
});
});
});
+2
View File
@@ -32,6 +32,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@esengine/ecs-framework": "workspace:*", "@esengine/ecs-framework": "workspace:*",
"@esengine/ecs-framework-math": "workspace:*",
"@esengine/blueprint": "workspace:*" "@esengine/blueprint": "workspace:*"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
@@ -42,6 +43,7 @@
"devDependencies": { "devDependencies": {
"@esengine/blueprint": "workspace:*", "@esengine/blueprint": "workspace:*",
"@esengine/ecs-framework": "workspace:*", "@esengine/ecs-framework": "workspace:*",
"@esengine/ecs-framework-math": "workspace:*",
"@esengine/build-config": "workspace:*", "@esengine/build-config": "workspace:*",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"tsup": "^8.0.0", "tsup": "^8.0.0",
+14
View File
@@ -138,6 +138,11 @@ export type {
ComponentSyncEvent, ComponentSyncEvent,
ComponentSyncEventListener, ComponentSyncEventListener,
ComponentSyncConfig, ComponentSyncConfig,
// Fixed-point sync types
IFixedTransformStateRaw,
IFixedTransformStateWithVelocityRaw,
IFixedInterpolator,
IFixedExtrapolator,
} from './sync' } from './sync'
export { export {
@@ -158,6 +163,15 @@ export {
// Component sync // Component sync
ComponentSyncSystem, ComponentSyncSystem,
createComponentSyncSystem, createComponentSyncSystem,
// Fixed-point sync (Deterministic Lockstep)
FixedTransformState,
FixedTransformStateWithVelocity,
createZeroFixedTransformState,
createZeroFixedTransformStateWithVelocity,
FixedTransformInterpolator,
FixedHermiteTransformInterpolator,
createFixedTransformInterpolator,
createFixedHermiteTransformInterpolator,
} from './sync' } from './sync'
// ============================================================================ // ============================================================================
@@ -0,0 +1,485 @@
/**
* @zh
* @en Fixed-point Client Prediction
*
* @zh
* @en Deterministic client prediction and rollback for lockstep
*/
import { Fixed32, FixedVector2 } from '@esengine/ecs-framework-math';
// =============================================================================
// 定点数输入快照接口 | Fixed Input Snapshot Interface
// =============================================================================
/**
* @zh
* @en Fixed-point input snapshot
*/
export interface IFixedInputSnapshot<TInput> {
/**
* @zh
* @en Input frame number
*/
readonly frame: number;
/**
* @zh
* @en Input data
*/
readonly input: TInput;
}
/**
* @zh
* @en Fixed-point predicted state
*/
export interface IFixedPredictedState<TState> {
/**
* @zh
* @en State data
*/
readonly state: TState;
/**
* @zh
* @en Corresponding frame number
*/
readonly frame: number;
}
// =============================================================================
// 定点数预测器接口 | Fixed Predictor Interface
// =============================================================================
/**
* @zh
* @en Fixed-point state predictor interface
*
* @zh 使
* @en Must use fixed-point arithmetic to ensure determinism
*/
export interface IFixedPredictor<TState, TInput> {
/**
* @zh
* @en Predict next state based on current state and input
*
* @param state - @zh @en Current state
* @param input - @zh @en Input
* @param deltaTime - @zh @en Fixed delta time (fixed-point)
* @returns @zh @en Predicted state
*/
predict(state: TState, input: TInput, deltaTime: Fixed32): TState;
}
/**
* @zh
* @en State position extractor interface
*/
export interface IFixedStatePositionExtractor<TState> {
/**
* @zh
* @en Extract position from state
*/
getPosition(state: TState): FixedVector2;
}
// =============================================================================
// 定点数客户端预测配置 | Fixed Client Prediction Config
// =============================================================================
/**
* @zh
* @en Fixed-point client prediction configuration
*/
export interface FixedClientPredictionConfig {
/**
* @zh
* @en Maximum unacknowledged inputs
*/
maxUnacknowledgedInputs: number;
/**
* @zh
* @en Fixed delta time (fixed-point)
*/
fixedDeltaTime: Fixed32;
/**
* @zh
* @en Reconciliation threshold (fixed-point, correction only above this value)
*/
reconciliationThreshold: Fixed32;
/**
* @zh
* @en Enable smooth reconciliation (usually disabled for lockstep)
*/
enableSmoothReconciliation: boolean;
/**
* @zh
* @en Smooth reconciliation speed (fixed-point)
*/
reconciliationSpeed: Fixed32;
}
// =============================================================================
// 定点数客户端预测管理器 | Fixed Client Prediction Manager
// =============================================================================
/**
* @zh
* @en Fixed-point client prediction manager
*
* @zh
* @en Provides deterministic client prediction and server state rollback reconciliation
*/
export class FixedClientPrediction<TState, TInput> {
private readonly _predictor: IFixedPredictor<TState, TInput>;
private readonly _config: FixedClientPredictionConfig;
private readonly _pendingInputs: IFixedInputSnapshot<TInput>[] = [];
private _lastAcknowledgedFrame: number = 0;
private _currentFrame: number = 0;
private _lastServerState: TState | null = null;
private _predictedState: TState | null = null;
private _correctionOffset: FixedVector2 = FixedVector2.ZERO;
private _stateHistory: Map<number, TState> = new Map();
private readonly _maxHistorySize: number = 120;
constructor(
predictor: IFixedPredictor<TState, TInput>,
config?: Partial<FixedClientPredictionConfig>
) {
this._predictor = predictor;
this._config = {
maxUnacknowledgedInputs: 60,
fixedDeltaTime: Fixed32.from(1 / 60),
reconciliationThreshold: Fixed32.from(0.001),
enableSmoothReconciliation: false,
reconciliationSpeed: Fixed32.from(10),
...config
};
}
/**
* @zh
* @en Get current predicted state
*/
get predictedState(): TState | null {
return this._predictedState;
}
/**
* @zh
* @en Get correction offset (for render smoothing)
*/
get correctionOffset(): FixedVector2 {
return this._correctionOffset;
}
/**
* @zh
* @en Get pending input count
*/
get pendingInputCount(): number {
return this._pendingInputs.length;
}
/**
* @zh
* @en Get current frame number
*/
get currentFrame(): number {
return this._currentFrame;
}
/**
* @zh
* @en Get last acknowledged frame
*/
get lastAcknowledgedFrame(): number {
return this._lastAcknowledgedFrame;
}
/**
* @zh
* @en Record and predict input
*
* @param input - @zh @en Input data
* @param currentState - @zh @en Current state
* @returns @zh @en Predicted state
*/
recordInput(input: TInput, currentState: TState): TState {
this._currentFrame++;
const inputSnapshot: IFixedInputSnapshot<TInput> = {
frame: this._currentFrame,
input
};
this._pendingInputs.push(inputSnapshot);
while (this._pendingInputs.length > this._config.maxUnacknowledgedInputs) {
this._pendingInputs.shift();
}
this._predictedState = this._predictor.predict(
currentState,
input,
this._config.fixedDeltaTime
);
this._stateHistory.set(this._currentFrame, this._predictedState);
this._cleanupHistory();
return this._predictedState;
}
/**
* @zh
* @en Get input at specific frame
*/
getInputAtFrame(frame: number): IFixedInputSnapshot<TInput> | null {
return this._pendingInputs.find(i => i.frame === frame) ?? null;
}
/**
* @zh
* @en Get all pending inputs
*/
getPendingInputs(): readonly IFixedInputSnapshot<TInput>[] {
return this._pendingInputs;
}
/**
* @zh
* @en Process server state and perform rollback reconciliation
*
* @param serverState - @zh @en Server authoritative state
* @param serverFrame - @zh @en Server state frame number
* @param positionExtractor - @zh @en State position extractor
* @returns @zh @en Reconciled state
*/
reconcile(
serverState: TState,
serverFrame: number,
positionExtractor: IFixedStatePositionExtractor<TState>
): TState {
this._lastServerState = serverState;
this._lastAcknowledgedFrame = serverFrame;
while (this._pendingInputs.length > 0 && this._pendingInputs[0].frame <= serverFrame) {
this._pendingInputs.shift();
}
const localStateAtServerFrame = this._stateHistory.get(serverFrame);
if (localStateAtServerFrame) {
const serverPos = positionExtractor.getPosition(serverState);
const localPos = positionExtractor.getPosition(localStateAtServerFrame);
const error = serverPos.sub(localPos);
const errorMagnitude = error.length();
if (errorMagnitude.gt(this._config.reconciliationThreshold)) {
if (this._config.enableSmoothReconciliation) {
const t = Fixed32.min(
Fixed32.ONE,
this._config.reconciliationSpeed.mul(this._config.fixedDeltaTime)
);
this._correctionOffset = this._correctionOffset.add(error.mul(t));
const decayRate = Fixed32.from(0.9);
this._correctionOffset = this._correctionOffset.mul(decayRate);
} else {
this._correctionOffset = FixedVector2.ZERO;
}
let state = serverState;
for (const inputSnapshot of this._pendingInputs) {
state = this._predictor.predict(
state,
inputSnapshot.input,
this._config.fixedDeltaTime
);
this._stateHistory.set(inputSnapshot.frame, state);
}
this._predictedState = state;
return state;
}
}
let state = serverState;
for (const inputSnapshot of this._pendingInputs) {
state = this._predictor.predict(
state,
inputSnapshot.input,
this._config.fixedDeltaTime
);
}
this._predictedState = state;
return state;
}
/**
* @zh
* @en Rollback to specific frame and re-simulate
*
* @param targetFrame - @zh @en Target frame number
* @param authoritativeState - @zh @en Authoritative state
* @returns @zh @en Re-simulated current state
*/
rollbackAndResimulate(targetFrame: number, authoritativeState: TState): TState {
this._stateHistory.set(targetFrame, authoritativeState);
let state = authoritativeState;
const inputsToResimulate = this._pendingInputs.filter(i => i.frame > targetFrame);
for (const inputSnapshot of inputsToResimulate) {
state = this._predictor.predict(
state,
inputSnapshot.input,
this._config.fixedDeltaTime
);
this._stateHistory.set(inputSnapshot.frame, state);
}
this._predictedState = state;
return state;
}
/**
* @zh
* @en Get historical state
*/
getStateAtFrame(frame: number): TState | null {
return this._stateHistory.get(frame) ?? null;
}
/**
* @zh
* @en Clear prediction state
*/
clear(): void {
this._pendingInputs.length = 0;
this._lastAcknowledgedFrame = 0;
this._currentFrame = 0;
this._lastServerState = null;
this._predictedState = null;
this._correctionOffset = FixedVector2.ZERO;
this._stateHistory.clear();
}
private _cleanupHistory(): void {
if (this._stateHistory.size > this._maxHistorySize) {
const sortedFrames = Array.from(this._stateHistory.keys()).sort((a, b) => a - b);
const framesToRemove = sortedFrames.slice(
0,
this._stateHistory.size - this._maxHistorySize
);
for (const frame of framesToRemove) {
this._stateHistory.delete(frame);
}
}
}
}
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh
* @en Create fixed-point client prediction manager
*/
export function createFixedClientPrediction<TState, TInput>(
predictor: IFixedPredictor<TState, TInput>,
config?: Partial<FixedClientPredictionConfig>
): FixedClientPrediction<TState, TInput> {
return new FixedClientPrediction(predictor, config);
}
// =============================================================================
// 预设预测器 | Preset Predictors
// =============================================================================
/**
* @zh
* @en Movement input type
*/
export interface IFixedMovementInput {
/**
* @zh X方向输入 (-1, 0, 1)
* @en X direction input (-1, 0, 1)
*/
readonly dx: number;
/**
* @zh Y方向输入 (-1, 0, 1)
* @en Y direction input (-1, 0, 1)
*/
readonly dy: number;
}
/**
* @zh
* @en Movement state type
*/
export interface IFixedMovementState {
/**
* @zh
* @en Position
*/
readonly position: FixedVector2;
/**
* @zh
* @en Velocity
*/
readonly velocity: FixedVector2;
}
/**
* @zh
* @en Create simple movement predictor
*
* @param speed - @zh @en Movement speed (fixed-point)
*/
export function createFixedMovementPredictor(
speed: Fixed32
): IFixedPredictor<IFixedMovementState, IFixedMovementInput> {
return {
predict(
state: IFixedMovementState,
input: IFixedMovementInput,
deltaTime: Fixed32
): IFixedMovementState {
const inputVec = FixedVector2.from(input.dx, input.dy);
const normalizedInput =
inputVec.lengthSquared().gt(Fixed32.ZERO) ? inputVec.normalize() : inputVec;
const velocity = normalizedInput.mul(speed);
const displacement = velocity.mul(deltaTime);
const newPosition = state.position.add(displacement);
return {
position: newPosition,
velocity
};
}
};
}
/**
* @zh
* @en Create movement state position extractor
*/
export function createFixedMovementPositionExtractor(): IFixedStatePositionExtractor<IFixedMovementState> {
return {
getPosition(state: IFixedMovementState): FixedVector2 {
return state.position;
}
};
}
@@ -0,0 +1,304 @@
/**
* @zh
* @en Fixed-point Snapshot Buffer
*
* @zh
* @en Snapshot buffer for deterministic lockstep calculations
*/
import { Fixed32 } from '@esengine/ecs-framework-math';
// =============================================================================
// 定点数快照接口 | Fixed Snapshot Interfaces
// =============================================================================
/**
* @zh
* @en Fixed-point state snapshot
*/
export interface IFixedStateSnapshot<T> {
/**
* @zh
* @en Frame number (fixed-point timestamp)
*/
readonly frame: number;
/**
* @zh
* @en State data
*/
readonly state: T;
}
/**
* @zh
* @en Fixed-point snapshot buffer configuration
*/
export interface IFixedSnapshotBufferConfig {
/**
* @zh
* @en Maximum snapshot count
*/
maxSize: number;
/**
* @zh
* @en Interpolation delay in frames
*/
interpolationDelayFrames: number;
}
/**
* @zh
* @en Interpolation result
*/
export interface IFixedInterpolationResult<T> {
/**
* @zh
* @en Previous snapshot
*/
readonly from: IFixedStateSnapshot<T>;
/**
* @zh
* @en Next snapshot
*/
readonly to: IFixedStateSnapshot<T>;
/**
* @zh (0-1)
* @en Interpolation factor (0-1)
*/
readonly t: Fixed32;
}
// =============================================================================
// 定点数快照缓冲区实现 | Fixed Snapshot Buffer Implementation
// =============================================================================
/**
* @zh
* @en Fixed-point snapshot buffer
*
* @zh 使
* @en Uses frame numbers instead of millisecond timestamps for cross-platform determinism
*/
export class FixedSnapshotBuffer<T> {
private readonly _buffer: IFixedStateSnapshot<T>[] = [];
private readonly _maxSize: number;
private readonly _interpolationDelayFrames: number;
constructor(config: IFixedSnapshotBufferConfig) {
this._maxSize = config.maxSize;
this._interpolationDelayFrames = config.interpolationDelayFrames;
}
/**
* @zh
* @en Get buffer size
*/
get size(): number {
return this._buffer.length;
}
/**
* @zh
* @en Get interpolation delay in frames
*/
get interpolationDelayFrames(): number {
return this._interpolationDelayFrames;
}
/**
* @zh
* @en Add snapshot
*
* @param snapshot - @zh @en State snapshot
*/
push(snapshot: IFixedStateSnapshot<T>): void {
let insertIndex = this._buffer.length;
for (let i = this._buffer.length - 1; i >= 0; i--) {
if (this._buffer[i].frame <= snapshot.frame) {
insertIndex = i + 1;
break;
}
if (i === 0) {
insertIndex = 0;
}
}
this._buffer.splice(insertIndex, 0, snapshot);
while (this._buffer.length > this._maxSize) {
this._buffer.shift();
}
}
/**
* @zh
* @en Get interpolation snapshots by frame number
*
* @param currentFrame - @zh @en Current frame number
* @returns @zh null @en Interpolation result with fixed-point factor or null
*/
getInterpolationSnapshots(currentFrame: number): IFixedInterpolationResult<T> | null {
if (this._buffer.length < 2) {
return null;
}
const targetFrame = currentFrame - this._interpolationDelayFrames;
for (let i = 0; i < this._buffer.length - 1; i++) {
const prev = this._buffer[i];
const next = this._buffer[i + 1];
if (prev.frame <= targetFrame && next.frame >= targetFrame) {
const duration = next.frame - prev.frame;
let t: Fixed32;
if (duration > 0) {
const elapsed = targetFrame - prev.frame;
t = Fixed32.from(elapsed).div(Fixed32.from(duration));
t = Fixed32.clamp(t, Fixed32.ZERO, Fixed32.ONE);
} else {
t = Fixed32.ZERO;
}
return { from: prev, to: next, t };
}
}
if (targetFrame > this._buffer[this._buffer.length - 1].frame) {
const prev = this._buffer[this._buffer.length - 2];
const next = this._buffer[this._buffer.length - 1];
const duration = next.frame - prev.frame;
let t: Fixed32;
if (duration > 0) {
const elapsed = targetFrame - prev.frame;
t = Fixed32.from(elapsed).div(Fixed32.from(duration));
t = Fixed32.min(t, Fixed32.from(2));
} else {
t = Fixed32.ONE;
}
return { from: prev, to: next, t };
}
return null;
}
/**
* @zh
* @en Get interpolation snapshots by precise frame time (supports sub-frame interpolation)
*
* @param frameTime - @zh @en Precise frame time (fixed-point)
* @returns @zh null @en Interpolation result or null
*/
getInterpolationSnapshotsFixed(frameTime: Fixed32): IFixedInterpolationResult<T> | null {
if (this._buffer.length < 2) {
return null;
}
const targetFrame = frameTime.sub(Fixed32.from(this._interpolationDelayFrames));
for (let i = 0; i < this._buffer.length - 1; i++) {
const prev = this._buffer[i];
const next = this._buffer[i + 1];
const prevFrame = Fixed32.from(prev.frame);
const nextFrame = Fixed32.from(next.frame);
if (prevFrame.le(targetFrame) && nextFrame.ge(targetFrame)) {
const duration = nextFrame.sub(prevFrame);
let t: Fixed32;
if (duration.gt(Fixed32.ZERO)) {
t = targetFrame.sub(prevFrame).div(duration);
t = Fixed32.clamp(t, Fixed32.ZERO, Fixed32.ONE);
} else {
t = Fixed32.ZERO;
}
return { from: prev, to: next, t };
}
}
const lastFrame = Fixed32.from(this._buffer[this._buffer.length - 1].frame);
if (targetFrame.gt(lastFrame)) {
const prev = this._buffer[this._buffer.length - 2];
const next = this._buffer[this._buffer.length - 1];
const prevFrame = Fixed32.from(prev.frame);
const nextFrame = Fixed32.from(next.frame);
const duration = nextFrame.sub(prevFrame);
let t: Fixed32;
if (duration.gt(Fixed32.ZERO)) {
t = targetFrame.sub(prevFrame).div(duration);
t = Fixed32.min(t, Fixed32.from(2));
} else {
t = Fixed32.ONE;
}
return { from: prev, to: next, t };
}
return null;
}
/**
* @zh
* @en Get latest snapshot
*/
getLatest(): IFixedStateSnapshot<T> | null {
return this._buffer.length > 0 ? this._buffer[this._buffer.length - 1] : null;
}
/**
* @zh
* @en Get snapshot at specific frame
*/
getAtFrame(frame: number): IFixedStateSnapshot<T> | null {
for (const snapshot of this._buffer) {
if (snapshot.frame === frame) {
return snapshot;
}
}
return null;
}
/**
* @zh
* @en Get all snapshots after specific frame
*/
getSnapshotsAfter(frame: number): IFixedStateSnapshot<T>[] {
return this._buffer.filter(s => s.frame > frame);
}
/**
* @zh
* @en Remove all snapshots before specific frame
*/
removeSnapshotsBefore(frame: number): void {
while (this._buffer.length > 0 && this._buffer[0].frame < frame) {
this._buffer.shift();
}
}
/**
* @zh
* @en Clear buffer
*/
clear(): void {
this._buffer.length = 0;
}
}
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh
* @en Create fixed-point snapshot buffer
*
* @param maxSize - @zh 30@en Maximum snapshot count (default 30)
* @param interpolationDelayFrames - @zh 2@en Interpolation delay frames (default 2)
*/
export function createFixedSnapshotBuffer<T>(
maxSize: number = 30,
interpolationDelayFrames: number = 2
): FixedSnapshotBuffer<T> {
return new FixedSnapshotBuffer<T>({ maxSize, interpolationDelayFrames });
}
@@ -0,0 +1,229 @@
/**
* @zh
* @en Fixed-point Transform Interpolator
*
* @zh
* @en Interpolator for deterministic lockstep calculations
*/
import { Fixed32, FixedVector2, FixedMath } from '@esengine/ecs-framework-math';
import {
FixedTransformState,
FixedTransformStateWithVelocity,
type IFixedTransformStateRaw,
type IFixedTransformStateWithVelocityRaw
} from './FixedTransformState';
// =============================================================================
// 插值器接口 | Interpolator Interface
// =============================================================================
/**
* @zh
* @en Fixed-point interpolator interface
*/
export interface IFixedInterpolator<T> {
/**
* @zh
* @en Interpolate between two states
* @param from - @zh @en Start state
* @param to - @zh @en End state
* @param t - @zh (0-1) @en Interpolation factor (0-1)
*/
interpolate(from: T, to: T, t: Fixed32): T;
}
/**
* @zh
* @en Fixed-point extrapolator interface
*/
export interface IFixedExtrapolator<T> {
/**
* @zh
* @en Extrapolate state based on velocity
* @param state - @zh @en Current state
* @param deltaTime - @zh @en Time delta
*/
extrapolate(state: T, deltaTime: Fixed32): T;
}
// =============================================================================
// 定点数变换插值器 | Fixed Transform Interpolator
// =============================================================================
/**
* @zh
* @en Fixed-point transform state interpolator
*/
export class FixedTransformInterpolator
implements IFixedInterpolator<FixedTransformState>, IFixedExtrapolator<FixedTransformStateWithVelocity> {
/**
* @zh
* @en Interpolate between two transform states
*/
interpolate(from: FixedTransformState, to: FixedTransformState, t: Fixed32): FixedTransformState {
return new FixedTransformState(
from.position.lerp(to.position, t),
FixedMath.lerpAngle(from.rotation, to.rotation, t)
);
}
/**
* @zh
* @en Extrapolate transform state based on velocity
*/
extrapolate(
state: FixedTransformStateWithVelocity,
deltaTime: Fixed32
): FixedTransformStateWithVelocity {
return new FixedTransformStateWithVelocity(
state.position.add(state.velocity.mul(deltaTime)),
state.rotation.add(state.angularVelocity.mul(deltaTime)),
state.velocity,
state.angularVelocity
);
}
/**
* @zh 使
* @en Interpolate using raw values
*/
interpolateRaw(
from: IFixedTransformStateRaw,
to: IFixedTransformStateRaw,
t: number
): IFixedTransformStateRaw {
const fromState = FixedTransformState.fromRaw(from);
const toState = FixedTransformState.fromRaw(to);
const tFixed = Fixed32.from(t);
return this.interpolate(fromState, toState, tFixed).toRaw();
}
/**
* @zh 使
* @en Extrapolate using raw values
*/
extrapolateRaw(
state: IFixedTransformStateWithVelocityRaw,
deltaTimeMs: number
): IFixedTransformStateWithVelocityRaw {
const fixedState = FixedTransformStateWithVelocity.fromRaw(state);
const deltaTime = Fixed32.from(deltaTimeMs / 1000); // ms to seconds
return this.extrapolate(fixedState, deltaTime).toRaw();
}
}
// =============================================================================
// 赫尔米特插值器 | Hermite Interpolator
// =============================================================================
/**
* @zh 线
* @en Fixed-point Hermite transform interpolator (smoother curves)
*/
export class FixedHermiteTransformInterpolator
implements IFixedInterpolator<FixedTransformStateWithVelocity> {
/**
* @zh
* @en Snapshot interval in seconds
*/
private readonly snapshotInterval: Fixed32;
constructor(snapshotIntervalMs: number = 100) {
this.snapshotInterval = Fixed32.from(snapshotIntervalMs / 1000);
}
/**
* @zh 使
* @en Use Hermite interpolation
*/
interpolate(
from: FixedTransformStateWithVelocity,
to: FixedTransformStateWithVelocity,
t: Fixed32
): FixedTransformStateWithVelocity {
const t2 = t.mul(t);
const t3 = t2.mul(t);
const two = Fixed32.from(2);
const three = Fixed32.from(3);
const six = Fixed32.from(6);
const four = Fixed32.from(4);
// Hermite basis functions
// h00 = 2t³ - 3t² + 1
const h00 = two.mul(t3).sub(three.mul(t2)).add(Fixed32.ONE);
// h10 = t³ - 2t² + t
const h10 = t3.sub(two.mul(t2)).add(t);
// h01 = -2t³ + 3t²
const h01 = two.neg().mul(t3).add(three.mul(t2));
// h11 = t³ - t²
const h11 = t3.sub(t2);
const dt = this.snapshotInterval;
// Position interpolation
const x = h00.mul(from.position.x)
.add(h10.mul(from.velocity.x).mul(dt))
.add(h01.mul(to.position.x))
.add(h11.mul(to.velocity.x).mul(dt));
const y = h00.mul(from.position.y)
.add(h10.mul(from.velocity.y).mul(dt))
.add(h01.mul(to.position.y))
.add(h11.mul(to.velocity.y).mul(dt));
// Velocity derivatives
// dh00 = 6t² - 6t
const dh00 = six.mul(t2).sub(six.mul(t));
// dh10 = 3t² - 4t + 1
const dh10 = three.mul(t2).sub(four.mul(t)).add(Fixed32.ONE);
// dh01 = -6t² + 6t
const dh01 = six.neg().mul(t2).add(six.mul(t));
// dh11 = 3t² - 2t
const dh11 = three.mul(t2).sub(two.mul(t));
const velocityX = dh00.mul(from.position.x)
.add(dh10.mul(from.velocity.x).mul(dt))
.add(dh01.mul(to.position.x))
.add(dh11.mul(to.velocity.x).mul(dt))
.div(dt);
const velocityY = dh00.mul(from.position.y)
.add(dh10.mul(from.velocity.y).mul(dt))
.add(dh01.mul(to.position.y))
.add(dh11.mul(to.velocity.y).mul(dt))
.div(dt);
return new FixedTransformStateWithVelocity(
new FixedVector2(x, y),
FixedMath.lerpAngle(from.rotation, to.rotation, t),
new FixedVector2(velocityX, velocityY),
Fixed32.lerp(from.angularVelocity, to.angularVelocity, t)
);
}
}
// =============================================================================
// 工厂函数 | Factory Functions
// =============================================================================
/**
* @zh
* @en Create fixed-point transform interpolator
*/
export function createFixedTransformInterpolator(): FixedTransformInterpolator {
return new FixedTransformInterpolator();
}
/**
* @zh
* @en Create fixed-point Hermite transform interpolator
*/
export function createFixedHermiteTransformInterpolator(
snapshotIntervalMs?: number
): FixedHermiteTransformInterpolator {
return new FixedHermiteTransformInterpolator(snapshotIntervalMs);
}
@@ -0,0 +1,265 @@
/**
* @zh
* @en Fixed-point Transform State
*
* @zh
* @en Transform state for deterministic lockstep calculations
*/
import { Fixed32, FixedVector2 } from '@esengine/ecs-framework-math';
// =============================================================================
// 定点数变换状态接口 | Fixed Transform State Interface
// =============================================================================
/**
* @zh
* @en Fixed-point transform state (raw values)
*
* @zh
* @en Raw integer format for network transmission, ensures cross-platform consistency
*/
export interface IFixedTransformStateRaw {
/**
* @zh X
* @en X coordinate raw value
*/
x: number;
/**
* @zh Y
* @en Y coordinate raw value
*/
y: number;
/**
* @zh * 65536
* @en Rotation raw value (radians * 65536)
*/
rotation: number;
}
/**
* @zh
* @en Fixed-point transform state with velocity (raw values)
*/
export interface IFixedTransformStateWithVelocityRaw extends IFixedTransformStateRaw {
/**
* @zh X
* @en X velocity raw value
*/
velocityX: number;
/**
* @zh Y
* @en Y velocity raw value
*/
velocityY: number;
/**
* @zh
* @en Angular velocity raw value
*/
angularVelocity: number;
}
// =============================================================================
// 定点数变换状态类 | Fixed Transform State Class
// =============================================================================
/**
* @zh
* @en Fixed-point transform state
*/
export class FixedTransformState {
readonly position: FixedVector2;
readonly rotation: Fixed32;
constructor(position: FixedVector2, rotation: Fixed32) {
this.position = position;
this.rotation = rotation;
}
/**
* @zh
* @en Create from raw values
*/
static fromRaw(raw: IFixedTransformStateRaw): FixedTransformState {
return new FixedTransformState(
FixedVector2.fromRaw(raw.x, raw.y),
Fixed32.fromRaw(raw.rotation)
);
}
/**
* @zh
* @en Create from floating-point numbers
*/
static from(x: number, y: number, rotation: number): FixedTransformState {
return new FixedTransformState(
FixedVector2.from(x, y),
Fixed32.from(rotation)
);
}
/**
* @zh
* @en Convert to raw values (for network transmission)
*/
toRaw(): IFixedTransformStateRaw {
return {
x: this.position.x.toRaw(),
y: this.position.y.toRaw(),
rotation: this.rotation.toRaw()
};
}
/**
* @zh
* @en Convert to floating-point object (for rendering)
*/
toFloat(): { x: number; y: number; rotation: number } {
return {
x: this.position.x.toNumber(),
y: this.position.y.toNumber(),
rotation: this.rotation.toNumber()
};
}
/**
* @zh
* @en Check equality
*/
equals(other: FixedTransformState): boolean {
return this.position.equals(other.position) && this.rotation.eq(other.rotation);
}
}
/**
* @zh
* @en Fixed-point transform state with velocity
*/
export class FixedTransformStateWithVelocity {
readonly position: FixedVector2;
readonly rotation: Fixed32;
readonly velocity: FixedVector2;
readonly angularVelocity: Fixed32;
constructor(
position: FixedVector2,
rotation: Fixed32,
velocity: FixedVector2,
angularVelocity: Fixed32
) {
this.position = position;
this.rotation = rotation;
this.velocity = velocity;
this.angularVelocity = angularVelocity;
}
/**
* @zh
* @en Create from raw values
*/
static fromRaw(raw: IFixedTransformStateWithVelocityRaw): FixedTransformStateWithVelocity {
return new FixedTransformStateWithVelocity(
FixedVector2.fromRaw(raw.x, raw.y),
Fixed32.fromRaw(raw.rotation),
FixedVector2.fromRaw(raw.velocityX, raw.velocityY),
Fixed32.fromRaw(raw.angularVelocity)
);
}
/**
* @zh
* @en Create from floating-point numbers
*/
static from(
x: number,
y: number,
rotation: number,
velocityX: number,
velocityY: number,
angularVelocity: number
): FixedTransformStateWithVelocity {
return new FixedTransformStateWithVelocity(
FixedVector2.from(x, y),
Fixed32.from(rotation),
FixedVector2.from(velocityX, velocityY),
Fixed32.from(angularVelocity)
);
}
/**
* @zh
* @en Convert to raw values
*/
toRaw(): IFixedTransformStateWithVelocityRaw {
return {
x: this.position.x.toRaw(),
y: this.position.y.toRaw(),
rotation: this.rotation.toRaw(),
velocityX: this.velocity.x.toRaw(),
velocityY: this.velocity.y.toRaw(),
angularVelocity: this.angularVelocity.toRaw()
};
}
/**
* @zh
* @en Convert to floating-point object
*/
toFloat(): {
x: number;
y: number;
rotation: number;
velocityX: number;
velocityY: number;
angularVelocity: number;
} {
return {
x: this.position.x.toNumber(),
y: this.position.y.toNumber(),
rotation: this.rotation.toNumber(),
velocityX: this.velocity.x.toNumber(),
velocityY: this.velocity.y.toNumber(),
angularVelocity: this.angularVelocity.toNumber()
};
}
/**
* @zh
* @en Check equality
*/
equals(other: FixedTransformStateWithVelocity): boolean {
return this.position.equals(other.position) &&
this.rotation.eq(other.rotation) &&
this.velocity.equals(other.velocity) &&
this.angularVelocity.eq(other.angularVelocity);
}
}
// =============================================================================
// 工具函数 | Utility Functions
// =============================================================================
/**
* @zh
* @en Create zero state
*/
export function createZeroFixedTransformState(): FixedTransformState {
return new FixedTransformState(FixedVector2.ZERO, Fixed32.ZERO);
}
/**
* @zh
* @en Create zero state with velocity
*/
export function createZeroFixedTransformStateWithVelocity(): FixedTransformStateWithVelocity {
return new FixedTransformStateWithVelocity(
FixedVector2.ZERO,
Fixed32.ZERO,
FixedVector2.ZERO,
Fixed32.ZERO
);
}
@@ -0,0 +1,63 @@
/**
* @zh
* @en Fixed-point network sync module
*
* @zh
* @en Network sync types and utilities for deterministic lockstep calculations
*/
// =============================================================================
// 变换状态 | Transform State
// =============================================================================
export {
FixedTransformState,
FixedTransformStateWithVelocity,
createZeroFixedTransformState,
createZeroFixedTransformStateWithVelocity,
type IFixedTransformStateRaw,
type IFixedTransformStateWithVelocityRaw,
} from './FixedTransformState';
// =============================================================================
// 插值器 | Interpolators
// =============================================================================
export {
FixedTransformInterpolator,
FixedHermiteTransformInterpolator,
createFixedTransformInterpolator,
createFixedHermiteTransformInterpolator,
type IFixedInterpolator,
type IFixedExtrapolator,
} from './FixedTransformInterpolator';
// =============================================================================
// 快照缓冲区 | Snapshot Buffer
// =============================================================================
export {
FixedSnapshotBuffer,
createFixedSnapshotBuffer,
type IFixedStateSnapshot,
type IFixedSnapshotBufferConfig,
type IFixedInterpolationResult,
} from './FixedSnapshotBuffer';
// =============================================================================
// 客户端预测 | Client Prediction
// =============================================================================
export {
FixedClientPrediction,
createFixedClientPrediction,
createFixedMovementPredictor,
createFixedMovementPositionExtractor,
type IFixedInputSnapshot,
type IFixedPredictedState,
type IFixedPredictor,
type IFixedStatePositionExtractor,
type FixedClientPredictionConfig,
type IFixedMovementInput,
type IFixedMovementState,
} from './FixedClientPrediction';
@@ -78,3 +78,49 @@ export {
ComponentSyncSystem, ComponentSyncSystem,
createComponentSyncSystem createComponentSyncSystem
} from './ComponentSync'; } from './ComponentSync';
// =============================================================================
// 定点数同步 | Fixed-point Sync (Deterministic Lockstep)
// =============================================================================
export {
// Transform State
FixedTransformState,
FixedTransformStateWithVelocity,
createZeroFixedTransformState,
createZeroFixedTransformStateWithVelocity,
// Interpolators
FixedTransformInterpolator,
FixedHermiteTransformInterpolator,
createFixedTransformInterpolator,
createFixedHermiteTransformInterpolator,
// Snapshot Buffer
FixedSnapshotBuffer,
createFixedSnapshotBuffer,
// Client Prediction
FixedClientPrediction,
createFixedClientPrediction,
createFixedMovementPredictor,
createFixedMovementPositionExtractor,
} from './fixed';
export type {
// Transform State Types
IFixedTransformStateRaw,
IFixedTransformStateWithVelocityRaw,
// Interpolator Types
IFixedInterpolator,
IFixedExtrapolator,
// Snapshot Buffer Types
IFixedStateSnapshot,
IFixedSnapshotBufferConfig,
IFixedInterpolationResult,
// Client Prediction Types
IFixedInputSnapshot,
IFixedPredictedState,
IFixedPredictor,
IFixedStatePositionExtractor,
FixedClientPredictionConfig,
IFixedMovementInput,
IFixedMovementState,
} from './fixed';
+65 -28
View File
@@ -1628,6 +1628,9 @@ importers:
'@esengine/ecs-framework': '@esengine/ecs-framework':
specifier: workspace:* specifier: workspace:*
version: link:../core/dist version: link:../core/dist
'@esengine/ecs-framework-math':
specifier: workspace:*
version: link:../math
rimraf: rimraf:
specifier: ^5.0.5 specifier: ^5.0.5
version: 5.0.10 version: 5.0.10
@@ -1652,10 +1655,10 @@ importers:
version: 5.0.10 version: 5.0.10
tsrpc-cli: tsrpc-cli:
specifier: ^2.4.5 specifier: ^2.4.5
version: 2.4.5(@swc/core@1.15.7(@swc/helpers@0.5.18))(@swc/wasm@1.15.7)(@types/node@20.19.27) version: 2.4.5(@swc/core@1.15.7(@swc/helpers@0.5.18))(@swc/wasm@1.15.7)(@types/node@22.19.3)
tsup: tsup:
specifier: ^8.0.0 specifier: ^8.0.0
version: 8.5.1(@microsoft/api-extractor@7.55.2(@types/node@20.19.27))(@swc/core@1.15.7(@swc/helpers@0.5.18))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) version: 8.5.1(@microsoft/api-extractor@7.55.2(@types/node@22.19.3))(@swc/core@1.15.7(@swc/helpers@0.5.18))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
typescript: typescript:
specifier: ^5.3.3 specifier: ^5.3.3
version: 5.9.3 version: 5.9.3
@@ -13266,6 +13269,13 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/node': 20.19.27 '@types/node': 20.19.27
'@inquirer/external-editor@1.0.3(@types/node@22.19.3)':
dependencies:
chardet: 2.1.1
iconv-lite: 0.7.1
optionalDependencies:
'@types/node': 22.19.3
'@ioredis/commands@1.4.0': {} '@ioredis/commands@1.4.0': {}
'@isaacs/balanced-match@4.0.1': {} '@isaacs/balanced-match@4.0.1': {}
@@ -15063,7 +15073,6 @@ snapshots:
'@types/node@22.19.3': '@types/node@22.19.3':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
optional: true
'@types/normalize-package-data@2.4.4': {} '@types/normalize-package-data@2.4.4': {}
@@ -15294,6 +15303,14 @@ snapshots:
optionalDependencies: optionalDependencies:
vite: 5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(terser@5.44.1) vite: 5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(terser@5.44.1)
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.3)(lightningcss@1.30.2)(terser@5.44.1))':
dependencies:
'@vitest/spy': 2.1.9
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 5.4.21(@types/node@22.19.3)(lightningcss@1.30.2)(terser@5.44.1)
'@vitest/pretty-format@2.1.9': '@vitest/pretty-format@2.1.9':
dependencies: dependencies:
tinyrainbow: 1.2.0 tinyrainbow: 1.2.0
@@ -17859,6 +17876,26 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
inquirer@8.2.7(@types/node@22.19.3):
dependencies:
'@inquirer/external-editor': 1.0.3(@types/node@22.19.3)
ansi-escapes: 4.3.2
chalk: 4.1.2
cli-cursor: 3.1.0
cli-width: 3.0.0
figures: 3.2.0
lodash: 4.17.21
mute-stream: 0.0.8
ora: 5.4.1
run-async: 2.4.1
rxjs: 7.8.2
string-width: 4.2.3
strip-ansi: 6.0.1
through: 2.3.8
wrap-ansi: 6.2.0
transitivePeerDependencies:
- '@types/node'
into-stream@7.0.0: into-stream@7.0.0:
dependencies: dependencies:
from2: 2.3.0 from2: 2.3.0
@@ -21457,27 +21494,6 @@ snapshots:
'@ts-morph/common': 0.22.0 '@ts-morph/common': 0.22.0
code-block-writer: 12.0.0 code-block-writer: 12.0.0
ts-node@10.9.2(@swc/core@1.15.7(@swc/helpers@0.5.18))(@swc/wasm@1.15.7)(@types/node@20.19.27)(typescript@4.9.5):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.12
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 20.19.27
acorn: 8.15.0
acorn-walk: 8.3.4
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 4.9.5
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
optionalDependencies:
'@swc/core': 1.15.7(@swc/helpers@0.5.18)
'@swc/wasm': 1.15.7
ts-node@10.9.2(@swc/core@1.15.7(@swc/helpers@0.5.18))(@swc/wasm@1.15.7)(@types/node@20.19.27)(typescript@5.9.3): ts-node@10.9.2(@swc/core@1.15.7(@swc/helpers@0.5.18))(@swc/wasm@1.15.7)(@types/node@20.19.27)(typescript@5.9.3):
dependencies: dependencies:
'@cspotcode/source-map-support': 0.8.1 '@cspotcode/source-map-support': 0.8.1
@@ -21500,6 +21516,27 @@ snapshots:
'@swc/wasm': 1.15.7 '@swc/wasm': 1.15.7
optional: true optional: true
ts-node@10.9.2(@swc/core@1.15.7(@swc/helpers@0.5.18))(@swc/wasm@1.15.7)(@types/node@22.19.3)(typescript@4.9.5):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.12
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 22.19.3
acorn: 8.15.0
acorn-walk: 8.3.4
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 4.9.5
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
optionalDependencies:
'@swc/core': 1.15.7(@swc/helpers@0.5.18)
'@swc/wasm': 1.15.7
tsbuffer-proto-generator@1.7.2: tsbuffer-proto-generator@1.7.2:
dependencies: dependencies:
k8w-crypto: 0.2.0 k8w-crypto: 0.2.0
@@ -21536,18 +21573,18 @@ snapshots:
tslib@2.8.1: {} tslib@2.8.1: {}
tsrpc-cli@2.4.5(@swc/core@1.15.7(@swc/helpers@0.5.18))(@swc/wasm@1.15.7)(@types/node@20.19.27): tsrpc-cli@2.4.5(@swc/core@1.15.7(@swc/helpers@0.5.18))(@swc/wasm@1.15.7)(@types/node@22.19.3):
dependencies: dependencies:
chalk: 4.1.2 chalk: 4.1.2
chokidar: 3.6.0 chokidar: 3.6.0
fs-extra: 10.1.0 fs-extra: 10.1.0
glob: 7.2.3 glob: 7.2.3
inquirer: 8.2.7(@types/node@20.19.27) inquirer: 8.2.7(@types/node@22.19.3)
k8w-extend-native: 1.4.6 k8w-extend-native: 1.4.6
minimist: 1.2.8 minimist: 1.2.8
ora: 5.4.1 ora: 5.4.1
os-locale: 5.0.0 os-locale: 5.0.0
ts-node: 10.9.2(@swc/core@1.15.7(@swc/helpers@0.5.18))(@swc/wasm@1.15.7)(@types/node@20.19.27)(typescript@4.9.5) ts-node: 10.9.2(@swc/core@1.15.7(@swc/helpers@0.5.18))(@swc/wasm@1.15.7)(@types/node@22.19.3)(typescript@4.9.5)
tsbuffer: 2.2.23 tsbuffer: 2.2.23
tsbuffer-proto-generator: 1.7.2 tsbuffer-proto-generator: 1.7.2
tsbuffer-schema: 2.2.0 tsbuffer-schema: 2.2.0
@@ -22249,7 +22286,7 @@ snapshots:
vitest@2.1.9(@types/node@22.19.3)(jsdom@20.0.3)(lightningcss@1.30.2)(terser@5.44.1): vitest@2.1.9(@types/node@22.19.3)(jsdom@20.0.3)(lightningcss@1.30.2)(terser@5.44.1):
dependencies: dependencies:
'@vitest/expect': 2.1.9 '@vitest/expect': 2.1.9
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(terser@5.44.1)) '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.3)(lightningcss@1.30.2)(terser@5.44.1))
'@vitest/pretty-format': 2.1.9 '@vitest/pretty-format': 2.1.9
'@vitest/runner': 2.1.9 '@vitest/runner': 2.1.9
'@vitest/snapshot': 2.1.9 '@vitest/snapshot': 2.1.9