@eva/plugin-state-machine 2.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/StateMachine.ts +136 -0
- package/lib/StateMachineSystem.ts +12 -0
- package/lib/index.ts +3 -0
- package/lib/types.ts +29 -0
- package/package.json +22 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Component, decorators } from '@eva/eva.js';
|
|
2
|
+
import { getSignalBus, SignalHandle } from '@eva/plugin-signal-bus';
|
|
3
|
+
import type { StateMachineParams, StateConfig } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* StateMachine 组件 — 简易有限状态机。
|
|
7
|
+
*
|
|
8
|
+
* DSL 用法:
|
|
9
|
+
* ```json
|
|
10
|
+
* {
|
|
11
|
+
* "type": "StateMachine",
|
|
12
|
+
* "props": {
|
|
13
|
+
* "initial": "moving",
|
|
14
|
+
* "states": {
|
|
15
|
+
* "moving": { "onEnter": "monster:state-moving",
|
|
16
|
+
* "transitions": [
|
|
17
|
+
* { "on": "monster:hit", "to": "knockback" },
|
|
18
|
+
* { "after": 3000, "to": "resting" }
|
|
19
|
+
* ] },
|
|
20
|
+
* "resting": { "transitions": [{ "after": 1000, "to": "moving" }] },
|
|
21
|
+
* "knockback":{ "transitions": [{ "after": 800, "to": "resting" }] }
|
|
22
|
+
* },
|
|
23
|
+
* "signalChange": "monster:state-change"
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* 行为:
|
|
29
|
+
* - 进入状态:emit `onEnter` + `signalChange` { from, to }
|
|
30
|
+
* - `transitions[i].on`: 监听信号,信号触发即迁移
|
|
31
|
+
* - `transitions[i].after`: 进入状态 N ms 后自动迁移
|
|
32
|
+
* - `transitions[i].guard`: JS 表达式,以 `ctx` 为变量,假则跳过该规则
|
|
33
|
+
*
|
|
34
|
+
* 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
|
|
35
|
+
*/
|
|
36
|
+
@decorators.componentObserver({})
|
|
37
|
+
export class StateMachine extends Component<StateMachineParams> {
|
|
38
|
+
static componentName = 'StateMachine';
|
|
39
|
+
|
|
40
|
+
private states: Record<string, StateConfig> = {};
|
|
41
|
+
private current: string = '';
|
|
42
|
+
private signalChange?: string;
|
|
43
|
+
private elapsedInState = 0;
|
|
44
|
+
private subs: SignalHandle[] = [];
|
|
45
|
+
|
|
46
|
+
/** 上下文,guard 可读 */
|
|
47
|
+
ctx: Record<string, any> = {};
|
|
48
|
+
|
|
49
|
+
init(params?: StateMachineParams) {
|
|
50
|
+
if (!params) return;
|
|
51
|
+
this.states = params.states ?? {};
|
|
52
|
+
this.signalChange = params.signalChange;
|
|
53
|
+
this.ctx = params.context ?? {};
|
|
54
|
+
if (params.initial && this.states[params.initial]) {
|
|
55
|
+
this.enter(params.initial, '__init__');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 主动迁移(代码侧也能调) */
|
|
60
|
+
goto(to: string, reason: string = 'manual') {
|
|
61
|
+
if (!this.states[to]) {
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
console.warn(`[plugin-state-machine] no such state: ${to}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (this.current === to) return;
|
|
67
|
+
const from = this.current;
|
|
68
|
+
if (from && this.states[from]?.onExit) {
|
|
69
|
+
getSignalBus().emit(this.states[from].onExit!, { from, to, reason });
|
|
70
|
+
}
|
|
71
|
+
this.cleanupSubs();
|
|
72
|
+
this.enter(to, reason);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get state(): string {
|
|
76
|
+
return this.current;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private enter(to: string, reason: string) {
|
|
80
|
+
const from = this.current;
|
|
81
|
+
this.current = to;
|
|
82
|
+
this.elapsedInState = 0;
|
|
83
|
+
const cfg = this.states[to];
|
|
84
|
+
if (!cfg) return;
|
|
85
|
+
if (cfg.onEnter) getSignalBus().emit(cfg.onEnter, { from, to, reason });
|
|
86
|
+
if (this.signalChange) getSignalBus().emit(this.signalChange, { from, to, reason });
|
|
87
|
+
// 订阅本 state 的 on 信号
|
|
88
|
+
const bus = getSignalBus();
|
|
89
|
+
for (const t of cfg.transitions ?? []) {
|
|
90
|
+
if (!t.on) continue;
|
|
91
|
+
const target = t.to;
|
|
92
|
+
const guard = t.guard;
|
|
93
|
+
const h = bus.on(t.on, () => {
|
|
94
|
+
if (this.current !== to) return; // 已经离开
|
|
95
|
+
if (guard && !this.evalGuard(guard)) return;
|
|
96
|
+
this.goto(target, t.on!);
|
|
97
|
+
});
|
|
98
|
+
this.subs.push(h);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private cleanupSubs() {
|
|
103
|
+
for (const h of this.subs) h.dispose();
|
|
104
|
+
this.subs = [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private evalGuard(guard: string): boolean {
|
|
108
|
+
try {
|
|
109
|
+
// 简单 expression eval,只暴露 ctx
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
|
|
111
|
+
const fn = new Function('ctx', `return (${guard});`);
|
|
112
|
+
return Boolean(fn(this.ctx));
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// eslint-disable-next-line no-console
|
|
115
|
+
console.warn(`[plugin-state-machine] bad guard "${guard}":`, err);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
update(e: { deltaTime: number }) {
|
|
121
|
+
this.elapsedInState += e.deltaTime;
|
|
122
|
+
const cfg = this.states[this.current];
|
|
123
|
+
if (!cfg?.transitions) return;
|
|
124
|
+
for (const t of cfg.transitions) {
|
|
125
|
+
if (t.after == null) continue;
|
|
126
|
+
if (this.elapsedInState < t.after) continue;
|
|
127
|
+
if (t.guard && !this.evalGuard(t.guard)) continue;
|
|
128
|
+
this.goto(t.to, `after:${t.after}`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
onDestroy() {
|
|
134
|
+
this.cleanupSubs();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { System } from '@eva/eva.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
|
|
5
|
+
*
|
|
6
|
+
* 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
|
|
7
|
+
* 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
|
|
8
|
+
*/
|
|
9
|
+
export class StateMachineSystem extends System {
|
|
10
|
+
static systemName = 'StateMachine';
|
|
11
|
+
readonly name = 'StateMachine';
|
|
12
|
+
}
|
package/lib/index.ts
ADDED
package/lib/types.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** 单条迁移规则 */
|
|
2
|
+
export interface TransitionRule {
|
|
3
|
+
/** 触发该迁移的信号名 */
|
|
4
|
+
on?: string;
|
|
5
|
+
/** 进入该状态后等待 N ms 自动迁移(可与 on 二选一) */
|
|
6
|
+
after?: number;
|
|
7
|
+
/** 目标状态名 */
|
|
8
|
+
to: string;
|
|
9
|
+
/** 可选:JS 表达式字符串,以 ctx 为上下文 */
|
|
10
|
+
guard?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StateConfig {
|
|
14
|
+
/** 进入时 emit 的信号 */
|
|
15
|
+
onEnter?: string;
|
|
16
|
+
/** 退出时 emit 的信号 */
|
|
17
|
+
onExit?: string;
|
|
18
|
+
/** 该状态下的迁移规则 */
|
|
19
|
+
transitions?: TransitionRule[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StateMachineParams {
|
|
23
|
+
initial: string;
|
|
24
|
+
states: Record<string, StateConfig>;
|
|
25
|
+
/** 状态切换 emit 信号 */
|
|
26
|
+
signalChange?: string;
|
|
27
|
+
/** 上下文变量,可在 guard 中读 */
|
|
28
|
+
context?: Record<string, any>;
|
|
29
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eva/plugin-state-machine",
|
|
3
|
+
"version": "2.1.0-beta.1",
|
|
4
|
+
"description": "Finite state machine — DSL 声明 states/transitions/timeouts/guards/actions,emit state-change 信号。",
|
|
5
|
+
"main": "lib/index.ts",
|
|
6
|
+
"module": "lib/index.ts",
|
|
7
|
+
"types": "lib/index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"lib"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"eva.js",
|
|
13
|
+
"plugin",
|
|
14
|
+
"fsm",
|
|
15
|
+
"state-machine"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@eva/eva.js": "2.1.0-beta.1",
|
|
20
|
+
"@eva/plugin-signal-bus": "2.1.0-beta.1"
|
|
21
|
+
}
|
|
22
|
+
}
|