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:
11
.changeset/fixed-point-math.md
Normal file
11
.changeset/fixed-point-math.md
Normal 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
.changeset/fixed-point-network.md
Normal file
10
.changeset/fixed-point-network.md
Normal 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
|
||||
@@ -100,6 +100,8 @@ export default defineConfig({
|
||||
{ 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: '序列化',
|
||||
translations: { en: 'Serialization' },
|
||||
@@ -237,6 +239,7 @@ export default defineConfig({
|
||||
items: [
|
||||
{ label: '概述', slug: 'modules/blueprint', translations: { en: 'Overview' } },
|
||||
{ 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: '自定义节点', slug: 'modules/blueprint/custom-nodes', translations: { en: 'Custom 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: '认证系统', slug: 'modules/network/auth', translations: { en: 'Authentication' } },
|
||||
{ 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/fixed-point', translations: { en: 'Fixed-Point Sync' } },
|
||||
{ label: '客户端预测', slug: 'modules/network/prediction', translations: { en: 'Prediction' } },
|
||||
{ label: 'AOI 兴趣区域', slug: 'modules/network/aoi', translations: { en: 'AOI' } },
|
||||
{ label: '增量压缩', slug: 'modules/network/delta', translations: { en: 'Delta Compression' } },
|
||||
|
||||
@@ -359,6 +359,5 @@ class GoodScene extends Scene {
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Scene](./scene) - Learn the basics of scenes
|
||||
- [SceneManager](./scene-manager) - Learn about scene transitions
|
||||
- [WorldManager](./world-manager) - Learn about multi-world management
|
||||
- [Scene](/en/guide/scene/) - Learn the basics of scenes
|
||||
- [SceneManager](/en/guide/scene-manager/) - Learn about scene transitions
|
||||
|
||||
@@ -16,7 +16,7 @@ The ECS framework provides a platform adapter interface that allows users to imp
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ SceneManager is suitable for:
|
||||
- Automatic ECS fluent API management
|
||||
- Automatic scene lifecycle handling
|
||||
- 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
|
||||
|
||||
@@ -434,7 +434,6 @@ Core (Global Services)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Persistent Entity](./persistent-entity) - Learn how to keep entities across scene transitions
|
||||
- [WorldManager](./world-manager) - Learn about advanced multi-world isolation features
|
||||
- [Persistent Entity](/en/guide/persistent-entity/) - Learn how to keep entities across 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
|
||||
|
||||
- [Core Concepts](./core-concepts/) - Understand nodes and execution
|
||||
- [Custom Actions](./custom-actions/) - Create your own nodes
|
||||
- [Editor Guide](./editor-guide/) - Visual tree creation
|
||||
- [Core Concepts](/en/modules/behavior-tree/core-concepts/) - Understand nodes and execution
|
||||
- [Custom Actions](/en/modules/behavior-tree/custom-actions/) - Create your own nodes
|
||||
- [Editor Guide](/en/modules/behavior-tree/editor-guide/) - Visual tree creation
|
||||
|
||||
383
docs/src/content/docs/en/modules/blueprint/cocos-editor.md
Normal file
383
docs/src/content/docs/en/modules/blueprint/cocos-editor.md
Normal file
@@ -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
|
||||
|
||||
- [ECS Node Reference](./nodes) - Complete node list
|
||||
- [Custom Nodes](./custom-nodes) - Create custom nodes
|
||||
- [Runtime Integration](./vm) - Blueprint VM API
|
||||
- [Examples](./examples) - More game logic examples
|
||||
- [ECS Node Reference](/en/modules/blueprint/nodes) - Complete node list
|
||||
- [Custom Nodes](/en/modules/blueprint/custom-nodes) - Create custom nodes
|
||||
- [Runtime Integration](/en/modules/blueprint/vm) - Blueprint VM API
|
||||
- [Examples](/en/modules/blueprint/examples) - More game logic examples
|
||||
|
||||
@@ -604,6 +604,6 @@ Blueprint-defined variables automatically generate Get and Set nodes:
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Blueprint Editor Guide](./editor-guide) - Learn how to use the editor
|
||||
- [Custom Nodes](./custom-nodes) - Create custom nodes
|
||||
- [Blueprint VM](./vm) - Runtime API
|
||||
- [Blueprint Editor Guide](/en/modules/blueprint/editor-guide) - Learn how to use the editor
|
||||
- [Custom Nodes](/en/modules/blueprint/custom-nodes) - Create custom nodes
|
||||
- [Blueprint VM](/en/modules/blueprint/vm) - Runtime API
|
||||
|
||||
326
docs/src/content/docs/en/modules/network/fixed-point.md
Normal file
326
docs/src/content/docs/en/modules/network/fixed-point.md
Normal file
@@ -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);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
2. **Prediction**: Use only for local player, interpolate remote players
|
||||
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';
|
||||
```
|
||||
|
||||
@@ -434,6 +434,6 @@ const found = hierarchySystem.findChild(parent, "Child");
|
||||
|
||||
## 下一步
|
||||
|
||||
- 了解 [实体类](./entity/) 的其他功能
|
||||
- 了解 [场景管理](./scene/) 如何组织实体和系统
|
||||
- 了解 [组件系统](./component/) 如何定义和使用组件
|
||||
- 了解 [实体类](/guide/entity/) 的其他功能
|
||||
- 了解 [场景管理](/guide/scene/) 如何组织实体和系统
|
||||
- 了解 [组件系统](/guide/component/) 如何定义和使用组件
|
||||
|
||||
363
docs/src/content/docs/guide/persistent-entity.md
Normal file
363
docs/src/content/docs/guide/persistent-entity.md
Normal file
@@ -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 等。
|
||||
|
||||
@@ -30,7 +30,7 @@ ECS框架提供了平台适配器接口,允许用户为不同的运行环境
|
||||
|
||||
---
|
||||
|
||||
### 📱 [微信小游戏适配器](./platform-adapter/wechat-minigame/)
|
||||
### 📱 [微信小游戏适配器](/guide/platform-adapter/wechat-minigame/)
|
||||
|
||||
专为微信小游戏环境设计,处理微信小游戏的特殊限制和API。
|
||||
|
||||
@@ -44,7 +44,7 @@ ECS框架提供了平台适配器接口,允许用户为不同的运行环境
|
||||
|
||||
---
|
||||
|
||||
### 🖥️ [Node.js适配器](./platform-adapter/nodejs/)
|
||||
### 🖥️ [Node.js适配器](/guide/platform-adapter/nodejs/)
|
||||
|
||||
为 Node.js 服务器环境提供支持,适用于游戏服务器和计算服务器。
|
||||
|
||||
|
||||
439
docs/src/content/docs/guide/scene-manager.md
Normal file
439
docs/src/content/docs/guide/scene-manager.md
Normal file
@@ -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-integration/)
|
||||
参见[Cocos Creator集成指南](/modules/behavior-tree/cocos-integration/)
|
||||
|
||||
### LayaAir集成
|
||||
|
||||
参见[LayaAir集成指南](./laya-integration/)
|
||||
参见[LayaAir集成指南](/modules/behavior-tree/laya-integration/)
|
||||
|
||||
## 最佳实践
|
||||
|
||||
@@ -389,6 +389,6 @@ const tree = BehaviorTreeBuilder.create('AI')
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[自定义节点执行器](./custom-actions/)学习如何创建自定义节点
|
||||
- 阅读[最佳实践](./best-practices/)了解行为树设计技巧
|
||||
- 参考[编辑器使用指南](./editor-guide/)学习可视化编辑
|
||||
- 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何创建自定义节点
|
||||
- 阅读[最佳实践](/modules/behavior-tree/best-practices/)了解行为树设计技巧
|
||||
- 参考[编辑器使用指南](/modules/behavior-tree/editor-guide/)学习可视化编辑
|
||||
|
||||
@@ -503,6 +503,6 @@ console.log(json);
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[Cocos Creator 集成](./cocos-integration/)了解如何在游戏引擎中加载资源
|
||||
- 查看[自定义节点执行器](./custom-actions/)创建自定义行为
|
||||
- 阅读[最佳实践](./best-practices/)优化你的行为树设计
|
||||
- 学习[Cocos Creator 集成](/modules/behavior-tree/cocos-integration/)了解如何在游戏引擎中加载资源
|
||||
- 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)创建自定义行为
|
||||
- 阅读[最佳实践](/modules/behavior-tree/best-practices/)优化你的行为树设计
|
||||
|
||||
@@ -26,7 +26,7 @@ Root Selector
|
||||
|
||||
### 2. 单一职责原则
|
||||
|
||||
每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](./custom-actions/)。
|
||||
每个节点应该只做一件事。要实现复杂动作,创建自定义执行器,参见[自定义节点执行器](/modules/behavior-tree/custom-actions/)。
|
||||
|
||||
```typescript
|
||||
// 好的设计 - 使用内置节点
|
||||
@@ -465,6 +465,6 @@ export class SmartUpdate implements INodeExecutor {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[自定义节点执行器](./custom-actions/)扩展行为树功能
|
||||
- 探索[高级用法](./advanced-usage/)了解更多技巧
|
||||
- 参考[核心概念](./core-concepts/)深入理解原理
|
||||
- 学习[自定义节点执行器](/modules/behavior-tree/custom-actions/)扩展行为树功能
|
||||
- 探索[高级用法](/modules/behavior-tree/advanced-usage/)了解更多技巧
|
||||
- 参考[核心概念](/modules/behavior-tree/core-concepts/)深入理解原理
|
||||
|
||||
@@ -8,7 +8,7 @@ title: "Cocos Creator 集成"
|
||||
|
||||
- Cocos Creator 3.x 或更高版本
|
||||
- 基本的 TypeScript 知识
|
||||
- 已完成[快速开始](./getting-started/)教程
|
||||
- 已完成[快速开始](/modules/behavior-tree/getting-started/)教程
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -679,7 +679,7 @@ const updateInterval = sys.isNative ? 0.016 : 0.05;
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[资产管理](./asset-management/)了解如何加载和管理行为树资产、使用子树
|
||||
- 学习[高级用法](./advanced-usage/)了解性能优化和调试技巧
|
||||
- 阅读[最佳实践](./best-practices/)优化你的 AI
|
||||
- 学习[自定义节点执行器](./custom-actions/)创建自定义行为
|
||||
- 查看[资产管理](/modules/behavior-tree/asset-management/)了解如何加载和管理行为树资产、使用子树
|
||||
- 学习[高级用法](/modules/behavior-tree/advanced-usage/)了解性能优化和调试技巧
|
||||
- 阅读[最佳实践](/modules/behavior-tree/best-practices/)优化你的 AI
|
||||
- 学习[自定义节点执行器](/modules/behavior-tree/custom-actions/)创建自定义行为
|
||||
|
||||
@@ -192,7 +192,7 @@ const tree = BehaviorTreeBuilder.create('Actions')
|
||||
.build();
|
||||
```
|
||||
|
||||
要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](./custom-actions/)。
|
||||
要实现自定义动作,需要创建自定义执行器,参见[自定义节点执行器](/modules/behavior-tree/custom-actions/)。
|
||||
|
||||
#### Condition(条件)
|
||||
|
||||
@@ -487,7 +487,7 @@ NodeRuntimeState
|
||||
|
||||
现在你已经理解了行为树的核心概念,接下来可以:
|
||||
|
||||
- 查看[快速开始](./getting-started/)创建第一个行为树
|
||||
- 学习[自定义节点执行器](./custom-actions/)创建自定义节点
|
||||
- 探索[高级用法](./advanced-usage/)了解更多功能
|
||||
- 阅读[最佳实践](./best-practices/)学习设计模式
|
||||
- 查看[快速开始](/modules/behavior-tree/getting-started/)创建第一个行为树
|
||||
- 学习[自定义节点执行器](/modules/behavior-tree/custom-actions/)创建自定义节点
|
||||
- 探索[高级用法](/modules/behavior-tree/advanced-usage/)了解更多功能
|
||||
- 阅读[最佳实践](/modules/behavior-tree/best-practices/)学习设计模式
|
||||
|
||||
@@ -1123,6 +1123,6 @@ execute(context: NodeExecutionContext): TaskStatus {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 学习[编辑器工作流](./editor-workflow/)了解如何在编辑器中使用自定义节点
|
||||
- 阅读[最佳实践](./best-practices/)学习行为树设计模式
|
||||
- 查看[高级用法](./advanced-usage/)了解更多功能
|
||||
- 学习[编辑器工作流](/modules/behavior-tree/editor-workflow/)了解如何在编辑器中使用自定义节点
|
||||
- 阅读[最佳实践](/modules/behavior-tree/best-practices/)学习行为树设计模式
|
||||
- 查看[高级用法](/modules/behavior-tree/advanced-usage/)了解更多功能
|
||||
|
||||
@@ -117,5 +117,5 @@ BehaviorTreeStarter.start(entity, tree);
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[编辑器工作流](./editor-workflow/)了解完整的开发流程
|
||||
- 查看[自定义节点执行器](./custom-actions/)学习如何扩展节点
|
||||
- 查看[编辑器工作流](/modules/behavior-tree/editor-workflow/)了解完整的开发流程
|
||||
- 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何扩展节点
|
||||
|
||||
@@ -112,7 +112,7 @@ setInterval(() => {
|
||||
|
||||
## 实现自定义执行器
|
||||
|
||||
要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](./custom-actions/)):
|
||||
要扩展行为树的功能,需要创建自定义执行器(详见[自定义节点执行器](/modules/behavior-tree/custom-actions/)):
|
||||
|
||||
```typescript
|
||||
import {
|
||||
@@ -250,6 +250,6 @@ setInterval(() => {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[自定义节点执行器](./custom-actions/)学习如何创建自定义节点
|
||||
- 查看[高级用法](./advanced-usage/)了解性能优化等高级特性
|
||||
- 查看[最佳实践](./best-practices/)优化你的AI设计
|
||||
- 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何创建自定义节点
|
||||
- 查看[高级用法](/modules/behavior-tree/advanced-usage/)了解性能优化等高级特性
|
||||
- 查看[最佳实践](/modules/behavior-tree/best-practices/)优化你的AI设计
|
||||
|
||||
@@ -333,11 +333,11 @@ BehaviorTreeStarter.restart(entity);
|
||||
|
||||
现在你已经创建了第一个行为树,接下来可以:
|
||||
|
||||
1. 学习[核心概念](./core-concepts/)深入理解行为树原理
|
||||
2. 学习[资产管理](./asset-management/)了解如何加载和复用行为树、使用子树
|
||||
3. 查看[自定义节点执行器](./custom-actions/)学习如何创建自定义节点
|
||||
4. 根据你的场景查看集成教程:[Cocos Creator](./cocos-integration/) 或 [Node.js](./nodejs-usage.md)
|
||||
5. 查看[高级用法](./advanced-usage/)了解更多功能
|
||||
1. 学习[核心概念](/modules/behavior-tree/core-concepts/)深入理解行为树原理
|
||||
2. 学习[资产管理](/modules/behavior-tree/asset-management/)了解如何加载和复用行为树、使用子树
|
||||
3. 查看[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何创建自定义节点
|
||||
4. 根据你的场景查看集成教程:[Cocos Creator](/modules/behavior-tree/cocos-integration/) 或 [Node.js](/modules/behavior-tree/nodejs-usage/)
|
||||
5. 查看[高级用法](/modules/behavior-tree/advanced-usage/)了解更多功能
|
||||
|
||||
## 常见问题
|
||||
|
||||
@@ -384,4 +384,4 @@ console.log('活动节点:', Array.from(runtime?.activeNodeIds || []));
|
||||
|
||||
内置的`executeAction`和`executeCondition`节点只是占位符。要实现真正的自定义逻辑,你需要创建自定义执行器:
|
||||
|
||||
参见[自定义节点执行器](./custom-actions/)学习如何创建。
|
||||
参见[自定义节点执行器](/modules/behavior-tree/custom-actions/)学习如何创建。
|
||||
|
||||
@@ -8,7 +8,7 @@ title: "Laya 引擎集成"
|
||||
|
||||
- LayaAir 3.x 或更高版本
|
||||
- 基本的 TypeScript 知识
|
||||
- 已完成[快速开始](./getting-started/)教程
|
||||
- 已完成[快速开始](/modules/behavior-tree/getting-started/)教程
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -311,5 +311,5 @@ class AIManager {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[高级用法](./advanced-usage/)
|
||||
- 学习[最佳实践](./best-practices/)
|
||||
- 查看[高级用法](/modules/behavior-tree/advanced-usage/)
|
||||
- 学习[最佳实践](/modules/behavior-tree/best-practices/)
|
||||
|
||||
@@ -577,6 +577,6 @@ function loadAIState(entity: Entity, savedState: any) {
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看[资产管理](./asset-management/)了解资源加载和子树
|
||||
- 学习[自定义节点执行器](./custom-actions/)创建自定义行为
|
||||
- 阅读[最佳实践](./best-practices/)优化你的服务端AI
|
||||
- 查看[资产管理](/modules/behavior-tree/asset-management/)了解资源加载和子树
|
||||
- 学习[自定义节点执行器](/modules/behavior-tree/custom-actions/)创建自定义行为
|
||||
- 阅读[最佳实践](/modules/behavior-tree/best-practices/)优化你的服务端AI
|
||||
|
||||
383
docs/src/content/docs/modules/blueprint/cocos-editor.md
Normal file
383
docs/src/content/docs/modules/blueprint/cocos-editor.md
Normal file
@@ -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) - 完整节点列表
|
||||
- [自定义节点](./custom-nodes) - 创建自定义节点
|
||||
- [运行时集成](./vm) - 蓝图虚拟机 API
|
||||
- [实际示例](./examples) - 更多游戏逻辑示例
|
||||
- [ECS 节点参考](/modules/blueprint/nodes) - 完整节点列表
|
||||
- [自定义节点](/modules/blueprint/custom-nodes) - 创建自定义节点
|
||||
- [运行时集成](/modules/blueprint/vm) - 蓝图虚拟机 API
|
||||
- [实际示例](/modules/blueprint/examples) - 更多游戏逻辑示例
|
||||
|
||||
@@ -535,6 +535,6 @@ description: "蓝图内置 ECS 操作节点完整参考"
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [蓝图编辑器指南](./editor-guide) - 学习如何使用编辑器
|
||||
- [自定义节点](./custom-nodes) - 创建自定义节点
|
||||
- [蓝图虚拟机](./vm) - 运行时 API
|
||||
- [蓝图编辑器指南](/modules/blueprint/editor-guide) - 学习如何使用编辑器
|
||||
- [自定义节点](/modules/blueprint/custom-nodes) - 创建自定义节点
|
||||
- [蓝图虚拟机](/modules/blueprint/vm) - 运行时 API
|
||||
|
||||
326
docs/src/content/docs/modules/network/fixed-point.md
Normal file
326
docs/src/content/docs/modules/network/fixed-point.md
Normal file
@@ -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);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 定点数客户端预测(帧同步)
|
||||
|
||||
用于**帧同步 (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. **校正阈值**:根据游戏精度需求设置合适的阈值
|
||||
|
||||
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
packages/framework/math/src/Fixed32.ts
Normal file
450
packages/framework/math/src/Fixed32.ts
Normal 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
packages/framework/math/src/FixedMath.ts
Normal file
298
packages/framework/math/src/FixedMath.ts
Normal 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
packages/framework/math/src/FixedVector2.ts
Normal file
504
packages/framework/math/src/FixedVector2.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* 2D数学库,为游戏开发提供完整的数学工具
|
||||
* - 基础数学类(向量、矩阵、几何形状)
|
||||
* - 定点数数学(用于帧同步确定性计算)
|
||||
* - 碰撞检测算法
|
||||
* - 动画插值和缓动函数
|
||||
* - 数学工具函数
|
||||
@@ -16,6 +17,11 @@ export { Matrix3 } from './Matrix3';
|
||||
export { Rectangle } from './Rectangle';
|
||||
export { Circle } from './Circle';
|
||||
|
||||
// 定点数数学(帧同步确定性计算)
|
||||
export { Fixed32, type IFixed32 } from './Fixed32';
|
||||
export { FixedVector2, type IFixedVector2 } from './FixedVector2';
|
||||
export { FixedMath } from './FixedMath';
|
||||
|
||||
// 数学工具
|
||||
export { MathUtils } from './MathUtils';
|
||||
|
||||
|
||||
225
packages/framework/math/tests/Fixed32.test.ts
Normal file
225
packages/framework/math/tests/Fixed32.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
242
packages/framework/math/tests/FixedVector2.test.ts
Normal file
242
packages/framework/math/tests/FixedVector2.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/ecs-framework-math": "workspace:*",
|
||||
"@esengine/blueprint": "workspace:*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -42,6 +43,7 @@
|
||||
"devDependencies": {
|
||||
"@esengine/blueprint": "workspace:*",
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/ecs-framework-math": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.0",
|
||||
|
||||
@@ -138,6 +138,11 @@ export type {
|
||||
ComponentSyncEvent,
|
||||
ComponentSyncEventListener,
|
||||
ComponentSyncConfig,
|
||||
// Fixed-point sync types
|
||||
IFixedTransformStateRaw,
|
||||
IFixedTransformStateWithVelocityRaw,
|
||||
IFixedInterpolator,
|
||||
IFixedExtrapolator,
|
||||
} from './sync'
|
||||
|
||||
export {
|
||||
@@ -158,6 +163,15 @@ export {
|
||||
// Component sync
|
||||
ComponentSyncSystem,
|
||||
createComponentSyncSystem,
|
||||
// Fixed-point sync (Deterministic Lockstep)
|
||||
FixedTransformState,
|
||||
FixedTransformStateWithVelocity,
|
||||
createZeroFixedTransformState,
|
||||
createZeroFixedTransformStateWithVelocity,
|
||||
FixedTransformInterpolator,
|
||||
FixedHermiteTransformInterpolator,
|
||||
createFixedTransformInterpolator,
|
||||
createFixedHermiteTransformInterpolator,
|
||||
} 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
304
packages/framework/network/src/sync/fixed/FixedSnapshotBuffer.ts
Normal file
304
packages/framework/network/src/sync/fixed/FixedSnapshotBuffer.ts
Normal file
@@ -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);
|
||||
}
|
||||
265
packages/framework/network/src/sync/fixed/FixedTransformState.ts
Normal file
265
packages/framework/network/src/sync/fixed/FixedTransformState.ts
Normal file
@@ -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
|
||||
);
|
||||
}
|
||||
63
packages/framework/network/src/sync/fixed/index.ts
Normal file
63
packages/framework/network/src/sync/fixed/index.ts
Normal file
@@ -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,
|
||||
createComponentSyncSystem
|
||||
} 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';
|
||||
|
||||
93
pnpm-lock.yaml
generated
93
pnpm-lock.yaml
generated
@@ -1628,6 +1628,9 @@ importers:
|
||||
'@esengine/ecs-framework':
|
||||
specifier: workspace:*
|
||||
version: link:../core/dist
|
||||
'@esengine/ecs-framework-math':
|
||||
specifier: workspace:*
|
||||
version: link:../math
|
||||
rimraf:
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.10
|
||||
@@ -1652,10 +1655,10 @@ importers:
|
||||
version: 5.0.10
|
||||
tsrpc-cli:
|
||||
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:
|
||||
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:
|
||||
specifier: ^5.3.3
|
||||
version: 5.9.3
|
||||
@@ -13266,6 +13269,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@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': {}
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
@@ -15063,7 +15073,6 @@ snapshots:
|
||||
'@types/node@22.19.3':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
optional: true
|
||||
|
||||
'@types/normalize-package-data@2.4.4': {}
|
||||
|
||||
@@ -15294,6 +15303,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
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':
|
||||
dependencies:
|
||||
tinyrainbow: 1.2.0
|
||||
@@ -17859,6 +17876,26 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@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:
|
||||
dependencies:
|
||||
from2: 2.3.0
|
||||
@@ -21457,27 +21494,6 @@ snapshots:
|
||||
'@ts-morph/common': 0.22.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):
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
@@ -21500,6 +21516,27 @@ snapshots:
|
||||
'@swc/wasm': 1.15.7
|
||||
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:
|
||||
dependencies:
|
||||
k8w-crypto: 0.2.0
|
||||
@@ -21536,18 +21573,18 @@ snapshots:
|
||||
|
||||
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:
|
||||
chalk: 4.1.2
|
||||
chokidar: 3.6.0
|
||||
fs-extra: 10.1.0
|
||||
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
|
||||
minimist: 1.2.8
|
||||
ora: 5.4.1
|
||||
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-proto-generator: 1.7.2
|
||||
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):
|
||||
dependencies:
|
||||
'@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/runner': 2.1.9
|
||||
'@vitest/snapshot': 2.1.9
|
||||
|
||||
Reference in New Issue
Block a user