fix(spatial): 修复 GridAOI 可见性更新问题
- 修复 addObserver 时现有观察者无法检测到新实体的问题 - 修复实体远距离移动时观察者可见性未正确更新的问题 - 重构 demos 抽取公共测试工具
This commit is contained in:
9
.changeset/cli-module-commands.md
Normal file
9
.changeset/cli-module-commands.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
"@esengine/cli": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(cli): 添加模块管理命令
|
||||||
|
|
||||||
|
- 新增 `list` 命令:按分类显示可用模块
|
||||||
|
- 新增 `add [modules...]` 命令:添加模块到项目,支持交互式选择
|
||||||
|
- 新增 `remove [modules...]` 命令:从项目移除模块,支持确认提示
|
||||||
8
.changeset/spatial-aoi-fix.md
Normal file
8
.changeset/spatial-aoi-fix.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
"@esengine/spatial": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(spatial): 修复 GridAOI 可见性更新问题
|
||||||
|
|
||||||
|
- 修复 `addObserver` 时现有观察者无法检测到新实体的问题
|
||||||
|
- 修复实体远距离移动时观察者可见性未正确更新的问题
|
||||||
@@ -107,8 +107,13 @@ export class GridAOI<T> implements IAOIManager<T> {
|
|||||||
this._observers.set(entity, data);
|
this._observers.set(entity, data);
|
||||||
this._addToCell(cellKey, data);
|
this._addToCell(cellKey, data);
|
||||||
|
|
||||||
// Initial visibility check
|
// Initial visibility check for this observer
|
||||||
this._updateVisibility(data);
|
this._updateVisibility(data);
|
||||||
|
|
||||||
|
// Notify other observers about this new entity
|
||||||
|
if (data.observable) {
|
||||||
|
this._updateObserversOfEntity(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -398,16 +403,10 @@ export class GridAOI<T> implements IAOIManager<T> {
|
|||||||
* @en Update other observers' visibility of an entity
|
* @en Update other observers' visibility of an entity
|
||||||
*/
|
*/
|
||||||
private _updateObserversOfEntity(movedData: AOIObserverData<T>): void {
|
private _updateObserversOfEntity(movedData: AOIObserverData<T>): void {
|
||||||
const cellRadius = Math.ceil(this._getMaxViewRange() / this._cellSize) + 1;
|
// Check all observers for visibility changes
|
||||||
const centerCell = this._getCellCoords(movedData.position);
|
// This handles both: observers who can now see the entity (enter)
|
||||||
|
// and observers who could see it before but can't anymore (exit)
|
||||||
for (let dx = -cellRadius; dx <= cellRadius; dx++) {
|
for (const [, otherData] of this._observers) {
|
||||||
for (let dy = -cellRadius; dy <= cellRadius; dy++) {
|
|
||||||
const cellKey = `${centerCell.x + dx},${centerCell.y + dy}`;
|
|
||||||
const cell = this._cells.get(cellKey);
|
|
||||||
if (!cell) continue;
|
|
||||||
|
|
||||||
for (const otherData of cell) {
|
|
||||||
if (otherData === movedData) continue;
|
if (otherData === movedData) continue;
|
||||||
|
|
||||||
const distSq = distanceSquared(otherData.position, movedData.position);
|
const distSq = distanceSquared(otherData.position, movedData.position);
|
||||||
@@ -433,8 +432,6 @@ export class GridAOI<T> implements IAOIManager<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 获取最大视野范围(用于优化搜索)
|
* @zh 获取最大视野范围(用于优化搜索)
|
||||||
|
|||||||
@@ -3,22 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createStateMachine } from '@esengine/fsm';
|
import { createStateMachine } from '@esengine/fsm';
|
||||||
|
import { assert, section, demoHeader, demoFooter } from './utils.js';
|
||||||
function assert(condition: boolean, message: string): void {
|
|
||||||
if (!condition) throw new Error(`FAILED: ${message}`);
|
|
||||||
console.log(` ✓ ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function section(name: string): void {
|
|
||||||
console.log(`\n▶ ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlayerState = 'idle' | 'walk' | 'run' | 'jump';
|
type PlayerState = 'idle' | 'walk' | 'run' | 'jump';
|
||||||
|
|
||||||
export async function runFSMDemo(): Promise<void> {
|
export async function runFSMDemo(): Promise<void> {
|
||||||
console.log('═══════════════════════════════════════');
|
demoHeader('FSM Module Demo');
|
||||||
console.log(' FSM Module Demo');
|
|
||||||
console.log('═══════════════════════════════════════');
|
|
||||||
|
|
||||||
// 1. Basic Creation
|
// 1. Basic Creation
|
||||||
section('1. createStateMachine()');
|
section('1. createStateMachine()');
|
||||||
@@ -167,9 +157,7 @@ export async function runFSMDemo(): Promise<void> {
|
|||||||
const history = fsmHist.getHistory();
|
const history = fsmHist.getHistory();
|
||||||
assert(history.length >= 2, 'History recorded');
|
assert(history.length >= 2, 'History recorded');
|
||||||
|
|
||||||
console.log('\n═══════════════════════════════════════');
|
demoFooter('FSM Demo');
|
||||||
console.log(' FSM Demo: ALL TESTS PASSED ✓');
|
|
||||||
console.log('═══════════════════════════════════════\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runFSMDemo().catch(console.error);
|
runFSMDemo().catch(console.error);
|
||||||
|
|||||||
@@ -10,20 +10,10 @@ import {
|
|||||||
manhattanDistance,
|
manhattanDistance,
|
||||||
octileDistance
|
octileDistance
|
||||||
} from '@esengine/pathfinding';
|
} from '@esengine/pathfinding';
|
||||||
|
import { assert, section, demoHeader, demoFooter } from './utils.js';
|
||||||
function assert(condition: boolean, message: string): void {
|
|
||||||
if (!condition) throw new Error(`FAILED: ${message}`);
|
|
||||||
console.log(` ✓ ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function section(name: string): void {
|
|
||||||
console.log(`\n▶ ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runPathfindingDemo(): Promise<void> {
|
export async function runPathfindingDemo(): Promise<void> {
|
||||||
console.log('═══════════════════════════════════════');
|
demoHeader('Pathfinding Module Demo');
|
||||||
console.log(' Pathfinding Module Demo');
|
|
||||||
console.log('═══════════════════════════════════════');
|
|
||||||
|
|
||||||
// 1. Create Grid Map
|
// 1. Create Grid Map
|
||||||
section('1. createGridMap()');
|
section('1. createGridMap()');
|
||||||
@@ -156,9 +146,7 @@ export async function runPathfindingDemo(): Promise<void> {
|
|||||||
grid.reset();
|
grid.reset();
|
||||||
assert(grid.isWalkable(5, 5), 'Grid reset - all walkable');
|
assert(grid.isWalkable(5, 5), 'Grid reset - all walkable');
|
||||||
|
|
||||||
console.log('\n═══════════════════════════════════════');
|
demoFooter('Pathfinding Demo');
|
||||||
console.log(' Pathfinding Demo: ALL TESTS PASSED ✓');
|
|
||||||
console.log('═══════════════════════════════════════\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runPathfindingDemo().catch(console.error);
|
runPathfindingDemo().catch(console.error);
|
||||||
|
|||||||
@@ -17,20 +17,10 @@ import {
|
|||||||
weightedPick,
|
weightedPick,
|
||||||
weightedPickFromMap
|
weightedPickFromMap
|
||||||
} from '@esengine/procgen';
|
} from '@esengine/procgen';
|
||||||
|
import { assert, section, demoHeader, demoFooter } from './utils.js';
|
||||||
function assert(condition: boolean, message: string): void {
|
|
||||||
if (!condition) throw new Error(`FAILED: ${message}`);
|
|
||||||
console.log(` ✓ ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function section(name: string): void {
|
|
||||||
console.log(`\n▶ ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runProcgenDemo(): Promise<void> {
|
export async function runProcgenDemo(): Promise<void> {
|
||||||
console.log('═══════════════════════════════════════');
|
demoHeader('Procgen Module Demo');
|
||||||
console.log(' Procgen Module Demo');
|
|
||||||
console.log('═══════════════════════════════════════');
|
|
||||||
|
|
||||||
// 1. Perlin Noise
|
// 1. Perlin Noise
|
||||||
section('1. createPerlinNoise()');
|
section('1. createPerlinNoise()');
|
||||||
@@ -200,9 +190,7 @@ export async function runProcgenDemo(): Promise<void> {
|
|||||||
const afterReset = rngReset.next();
|
const afterReset = rngReset.next();
|
||||||
assert(first === afterReset, 'Reset restores initial state');
|
assert(first === afterReset, 'Reset restores initial state');
|
||||||
|
|
||||||
console.log('\n═══════════════════════════════════════');
|
demoFooter('Procgen Demo');
|
||||||
console.log(' Procgen Demo: ALL TESTS PASSED ✓');
|
|
||||||
console.log('═══════════════════════════════════════\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runProcgenDemo().catch(console.error);
|
runProcgenDemo().catch(console.error);
|
||||||
|
|||||||
@@ -13,15 +13,7 @@ import {
|
|||||||
distance,
|
distance,
|
||||||
distanceSquared
|
distanceSquared
|
||||||
} from '@esengine/spatial';
|
} from '@esengine/spatial';
|
||||||
|
import { assert, section, demoHeader, demoFooter } from './utils.js';
|
||||||
function assert(condition: boolean, message: string): void {
|
|
||||||
if (!condition) throw new Error(`FAILED: ${message}`);
|
|
||||||
console.log(` ✓ ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function section(name: string): void {
|
|
||||||
console.log(`\n▶ ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Entity {
|
interface Entity {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -29,9 +21,7 @@ interface Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runSpatialDemo(): Promise<void> {
|
export async function runSpatialDemo(): Promise<void> {
|
||||||
console.log('═══════════════════════════════════════');
|
demoHeader('Spatial Module Demo');
|
||||||
console.log(' Spatial Module Demo');
|
|
||||||
console.log('═══════════════════════════════════════');
|
|
||||||
|
|
||||||
// 1. Create Spatial Index
|
// 1. Create Spatial Index
|
||||||
section('1. createGridSpatialIndex()');
|
section('1. createGridSpatialIndex()');
|
||||||
@@ -150,10 +140,6 @@ export async function runSpatialDemo(): Promise<void> {
|
|||||||
aoi.addObserver(p1, { x: 100, y: 100 }, { viewRange: 200 });
|
aoi.addObserver(p1, { x: 100, y: 100 }, { viewRange: 200 });
|
||||||
aoi.addObserver(p2, { x: 150, y: 150 }, { viewRange: 200 });
|
aoi.addObserver(p2, { x: 150, y: 150 }, { viewRange: 200 });
|
||||||
|
|
||||||
// Note: After adding p2, p2 can see p1, but p1's visibility isn't auto-updated
|
|
||||||
// We need to trigger an update for p1 to detect p2
|
|
||||||
aoi.updatePosition(p1, { x: 100, y: 100 });
|
|
||||||
|
|
||||||
// 15. getEntitiesInView
|
// 15. getEntitiesInView
|
||||||
section('15. getEntitiesInView()');
|
section('15. getEntitiesInView()');
|
||||||
const visible = aoi.getEntitiesInView(p1);
|
const visible = aoi.getEntitiesInView(p1);
|
||||||
@@ -166,14 +152,11 @@ export async function runSpatialDemo(): Promise<void> {
|
|||||||
// 17. updatePosition
|
// 17. updatePosition
|
||||||
section('17. updatePosition()');
|
section('17. updatePosition()');
|
||||||
aoi.updatePosition(p2, { x: 1000, y: 1000 });
|
aoi.updatePosition(p2, { x: 1000, y: 1000 });
|
||||||
// Refresh p1's visibility (implementation requires explicit update for distant moves)
|
|
||||||
aoi.updatePosition(p1, { x: 100, y: 100 });
|
|
||||||
assert(!aoi.canSee(p1, p2), 'p1 cannot see p2 after move');
|
assert(!aoi.canSee(p1, p2), 'p1 cannot see p2 after move');
|
||||||
|
|
||||||
// 18. getObserversOf
|
// 18. getObserversOf
|
||||||
section('18. getObserversOf()');
|
section('18. getObserversOf()');
|
||||||
aoi.updatePosition(p2, { x: 120, y: 120 });
|
aoi.updatePosition(p2, { x: 120, y: 120 });
|
||||||
aoi.updatePosition(p1, { x: 100, y: 100 }); // Refresh p1's visibility
|
|
||||||
const observers = aoi.getObserversOf(p2);
|
const observers = aoi.getObserversOf(p2);
|
||||||
assert(observers.includes(p1), 'p1 observes p2');
|
assert(observers.includes(p1), 'p1 observes p2');
|
||||||
|
|
||||||
@@ -184,16 +167,13 @@ export async function runSpatialDemo(): Promise<void> {
|
|||||||
eventCount++;
|
eventCount++;
|
||||||
});
|
});
|
||||||
aoi.updatePosition(p2, { x: 2000, y: 2000 }); // Should trigger exit
|
aoi.updatePosition(p2, { x: 2000, y: 2000 }); // Should trigger exit
|
||||||
aoi.updatePosition(p1, { x: 100, y: 100 }); // Refresh p1
|
|
||||||
aoi.updatePosition(p2, { x: 130, y: 130 }); // Should trigger enter
|
aoi.updatePosition(p2, { x: 130, y: 130 }); // Should trigger enter
|
||||||
aoi.updatePosition(p1, { x: 100, y: 100 }); // Refresh p1
|
|
||||||
assert(eventCount >= 1, `Events triggered: ${eventCount}`);
|
assert(eventCount >= 1, `Events triggered: ${eventCount}`);
|
||||||
|
|
||||||
// 20. updateViewRange
|
// 20. updateViewRange
|
||||||
section('20. updateViewRange()');
|
section('20. updateViewRange()');
|
||||||
aoi.updateViewRange(p1, 50);
|
aoi.updateViewRange(p1, 50);
|
||||||
aoi.updatePosition(p2, { x: 200, y: 200 });
|
aoi.updatePosition(p2, { x: 200, y: 200 });
|
||||||
aoi.updatePosition(p1, { x: 100, y: 100 }); // Refresh p1's visibility with new range
|
|
||||||
assert(!aoi.canSee(p1, p2), 'Cannot see after view range reduced');
|
assert(!aoi.canSee(p1, p2), 'Cannot see after view range reduced');
|
||||||
|
|
||||||
// 21. removeObserver
|
// 21. removeObserver
|
||||||
@@ -238,9 +218,7 @@ export async function runSpatialDemo(): Promise<void> {
|
|||||||
const dsq = distanceSquared({ x: 0, y: 0 }, { x: 3, y: 4 });
|
const dsq = distanceSquared({ x: 0, y: 0 }, { x: 3, y: 4 });
|
||||||
assert(dsq === 25, `Distance squared: ${dsq}`);
|
assert(dsq === 25, `Distance squared: ${dsq}`);
|
||||||
|
|
||||||
console.log('\n═══════════════════════════════════════');
|
demoFooter('Spatial Demo');
|
||||||
console.log(' Spatial Demo: ALL TESTS PASSED ✓');
|
|
||||||
console.log('═══════════════════════════════════════\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runSpatialDemo().catch(console.error);
|
runSpatialDemo().catch(console.error);
|
||||||
|
|||||||
@@ -3,20 +3,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createTimerService } from '@esengine/timer';
|
import { createTimerService } from '@esengine/timer';
|
||||||
|
import { assert, section, demoHeader, demoFooter } from './utils.js';
|
||||||
function assert(condition: boolean, message: string): void {
|
|
||||||
if (!condition) throw new Error(`FAILED: ${message}`);
|
|
||||||
console.log(` ✓ ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function section(name: string): void {
|
|
||||||
console.log(`\n▶ ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runTimerDemo(): Promise<void> {
|
export async function runTimerDemo(): Promise<void> {
|
||||||
console.log('═══════════════════════════════════════');
|
demoHeader('Timer Module Demo');
|
||||||
console.log(' Timer Module Demo');
|
|
||||||
console.log('═══════════════════════════════════════');
|
|
||||||
|
|
||||||
// 1. Basic Creation
|
// 1. Basic Creation
|
||||||
section('1. createTimerService()');
|
section('1. createTimerService()');
|
||||||
@@ -111,9 +101,7 @@ export async function runTimerDemo(): Promise<void> {
|
|||||||
const limited = createTimerService({ maxTimers: 2, maxCooldowns: 1 });
|
const limited = createTimerService({ maxTimers: 2, maxCooldowns: 1 });
|
||||||
assert(limited !== null, 'Created with config');
|
assert(limited !== null, 'Created with config');
|
||||||
|
|
||||||
console.log('\n═══════════════════════════════════════');
|
demoFooter('Timer Demo');
|
||||||
console.log(' Timer Demo: ALL TESTS PASSED ✓');
|
|
||||||
console.log('═══════════════════════════════════════\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runTimerDemo().catch(console.error);
|
runTimerDemo().catch(console.error);
|
||||||
|
|||||||
41
packages/tools/demos/src/utils.ts
Normal file
41
packages/tools/demos/src/utils.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* @zh Demo 测试工具函数
|
||||||
|
* @en Demo test utility functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 断言条件为真,否则抛出错误
|
||||||
|
* @en Assert condition is true, otherwise throw error
|
||||||
|
*/
|
||||||
|
export function assert(condition: boolean, message: string): void {
|
||||||
|
if (!condition) throw new Error(`FAILED: ${message}`);
|
||||||
|
console.log(` ✓ ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 打印测试章节标题
|
||||||
|
* @en Print test section header
|
||||||
|
*/
|
||||||
|
export function section(name: string): void {
|
||||||
|
console.log(`\n▶ ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 打印 Demo 开始标题
|
||||||
|
* @en Print demo start header
|
||||||
|
*/
|
||||||
|
export function demoHeader(name: string): void {
|
||||||
|
console.log('═══════════════════════════════════════');
|
||||||
|
console.log(` ${name}`);
|
||||||
|
console.log('═══════════════════════════════════════');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 打印 Demo 结束标题
|
||||||
|
* @en Print demo end header
|
||||||
|
*/
|
||||||
|
export function demoFooter(name: string): void {
|
||||||
|
console.log('\n═══════════════════════════════════════');
|
||||||
|
console.log(` ${name}: ALL TESTS PASSED ✓`);
|
||||||
|
console.log('═══════════════════════════════════════\n');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user