@eva/plugin-state-machine 2.1.0-beta.2 → 2.1.0-beta.4
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/dist/plugin-state-machine.cjs.js +193 -0
- package/dist/plugin-state-machine.cjs.prod.js +1 -0
- package/dist/plugin-state-machine.d.ts +98 -0
- package/dist/plugin-state-machine.esm.js +189 -0
- package/index.js +7 -0
- package/package.json +8 -7
- package/lib/StateMachine.ts +0 -136
- package/lib/StateMachineSystem.ts +0 -12
- package/lib/index.ts +0 -3
- package/lib/types.ts +0 -29
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var eva_js = require('@eva/eva.js');
|
|
6
|
+
var pluginSignalBus = require('@eva/plugin-signal-bus');
|
|
7
|
+
|
|
8
|
+
/******************************************************************************
|
|
9
|
+
Copyright (c) Microsoft Corporation.
|
|
10
|
+
|
|
11
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
12
|
+
purpose with or without fee is hereby granted.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
15
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
16
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
17
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
18
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
19
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
20
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
21
|
+
***************************************************************************** */
|
|
22
|
+
|
|
23
|
+
function __decorate(decorators, target, key, desc) {
|
|
24
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
25
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
26
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
27
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
31
|
+
var e = new Error(message);
|
|
32
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* StateMachine 组件 — 简易有限状态机。
|
|
37
|
+
*
|
|
38
|
+
* DSL 用法:
|
|
39
|
+
* ```json
|
|
40
|
+
* {
|
|
41
|
+
* "type": "StateMachine",
|
|
42
|
+
* "props": {
|
|
43
|
+
* "initial": "moving",
|
|
44
|
+
* "states": {
|
|
45
|
+
* "moving": { "onEnter": "monster:state-moving",
|
|
46
|
+
* "transitions": [
|
|
47
|
+
* { "on": "monster:hit", "to": "knockback" },
|
|
48
|
+
* { "after": 3000, "to": "resting" }
|
|
49
|
+
* ] },
|
|
50
|
+
* "resting": { "transitions": [{ "after": 1000, "to": "moving" }] },
|
|
51
|
+
* "knockback":{ "transitions": [{ "after": 800, "to": "resting" }] }
|
|
52
|
+
* },
|
|
53
|
+
* "signalChange": "monster:state-change"
|
|
54
|
+
* }
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* 行为:
|
|
59
|
+
* - 进入状态:emit `onEnter` + `signalChange` { from, to }
|
|
60
|
+
* - `transitions[i].on`: 监听信号,信号触发即迁移
|
|
61
|
+
* - `transitions[i].after`: 进入状态 N ms 后自动迁移
|
|
62
|
+
* - `transitions[i].guard`: JS 表达式,以 `ctx` 为变量,假则跳过该规则
|
|
63
|
+
*
|
|
64
|
+
* 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
|
|
65
|
+
*/
|
|
66
|
+
exports.StateMachine = class StateMachine extends eva_js.Component {
|
|
67
|
+
constructor() {
|
|
68
|
+
super(...arguments);
|
|
69
|
+
this.states = {};
|
|
70
|
+
this.current = '';
|
|
71
|
+
this.elapsedInState = 0;
|
|
72
|
+
this.subs = [];
|
|
73
|
+
/** 上下文,guard 可读 */
|
|
74
|
+
this.ctx = {};
|
|
75
|
+
}
|
|
76
|
+
init(params) {
|
|
77
|
+
var _a, _b;
|
|
78
|
+
if (!params)
|
|
79
|
+
return;
|
|
80
|
+
this.states = (_a = params.states) !== null && _a !== void 0 ? _a : {};
|
|
81
|
+
this.signalChange = params.signalChange;
|
|
82
|
+
this.ctx = (_b = params.context) !== null && _b !== void 0 ? _b : {};
|
|
83
|
+
if (params.initial && this.states[params.initial]) {
|
|
84
|
+
this.enter(params.initial, '__init__');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** 主动迁移(代码侧也能调) */
|
|
88
|
+
goto(to, reason = 'manual') {
|
|
89
|
+
var _a;
|
|
90
|
+
if (!this.states[to]) {
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.warn(`[plugin-state-machine] no such state: ${to}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (this.current === to)
|
|
96
|
+
return;
|
|
97
|
+
const from = this.current;
|
|
98
|
+
if (from && ((_a = this.states[from]) === null || _a === void 0 ? void 0 : _a.onExit)) {
|
|
99
|
+
pluginSignalBus.getSignalBus().emit(this.states[from].onExit, { from, to, reason });
|
|
100
|
+
}
|
|
101
|
+
this.cleanupSubs();
|
|
102
|
+
this.enter(to, reason);
|
|
103
|
+
}
|
|
104
|
+
get state() {
|
|
105
|
+
return this.current;
|
|
106
|
+
}
|
|
107
|
+
enter(to, reason) {
|
|
108
|
+
var _a;
|
|
109
|
+
const from = this.current;
|
|
110
|
+
this.current = to;
|
|
111
|
+
this.elapsedInState = 0;
|
|
112
|
+
const cfg = this.states[to];
|
|
113
|
+
if (!cfg)
|
|
114
|
+
return;
|
|
115
|
+
if (cfg.onEnter)
|
|
116
|
+
pluginSignalBus.getSignalBus().emit(cfg.onEnter, { from, to, reason });
|
|
117
|
+
if (this.signalChange)
|
|
118
|
+
pluginSignalBus.getSignalBus().emit(this.signalChange, { from, to, reason });
|
|
119
|
+
// 订阅本 state 的 on 信号
|
|
120
|
+
const bus = pluginSignalBus.getSignalBus();
|
|
121
|
+
for (const t of (_a = cfg.transitions) !== null && _a !== void 0 ? _a : []) {
|
|
122
|
+
if (!t.on)
|
|
123
|
+
continue;
|
|
124
|
+
const target = t.to;
|
|
125
|
+
const guard = t.guard;
|
|
126
|
+
const h = bus.on(t.on, () => {
|
|
127
|
+
if (this.current !== to)
|
|
128
|
+
return; // 已经离开
|
|
129
|
+
if (guard && !this.evalGuard(guard))
|
|
130
|
+
return;
|
|
131
|
+
this.goto(target, t.on);
|
|
132
|
+
});
|
|
133
|
+
this.subs.push(h);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
cleanupSubs() {
|
|
137
|
+
for (const h of this.subs)
|
|
138
|
+
h.dispose();
|
|
139
|
+
this.subs = [];
|
|
140
|
+
}
|
|
141
|
+
evalGuard(guard) {
|
|
142
|
+
try {
|
|
143
|
+
// 简单 expression eval,只暴露 ctx
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
|
|
145
|
+
const fn = new Function('ctx', `return (${guard});`);
|
|
146
|
+
return Boolean(fn(this.ctx));
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
// eslint-disable-next-line no-console
|
|
150
|
+
console.warn(`[plugin-state-machine] bad guard "${guard}":`, err);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
update(e) {
|
|
155
|
+
this.elapsedInState += e.deltaTime;
|
|
156
|
+
const cfg = this.states[this.current];
|
|
157
|
+
if (!(cfg === null || cfg === void 0 ? void 0 : cfg.transitions))
|
|
158
|
+
return;
|
|
159
|
+
for (const t of cfg.transitions) {
|
|
160
|
+
if (t.after == null)
|
|
161
|
+
continue;
|
|
162
|
+
if (this.elapsedInState < t.after)
|
|
163
|
+
continue;
|
|
164
|
+
if (t.guard && !this.evalGuard(t.guard))
|
|
165
|
+
continue;
|
|
166
|
+
this.goto(t.to, `after:${t.after}`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
onDestroy() {
|
|
171
|
+
this.cleanupSubs();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
exports.StateMachine.componentName = 'StateMachine';
|
|
175
|
+
exports.StateMachine = __decorate([
|
|
176
|
+
eva_js.decorators.componentObserver({})
|
|
177
|
+
], exports.StateMachine);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
|
|
181
|
+
*
|
|
182
|
+
* 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
|
|
183
|
+
* 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
|
|
184
|
+
*/
|
|
185
|
+
class StateMachineSystem extends eva_js.System {
|
|
186
|
+
constructor() {
|
|
187
|
+
super(...arguments);
|
|
188
|
+
this.name = 'StateMachine';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
StateMachineSystem.systemName = 'StateMachine';
|
|
192
|
+
|
|
193
|
+
exports.StateMachineSystem = StateMachineSystem;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t=require("@eva/eva.js"),e=require("@eva/plugin-signal-bus");"function"==typeof SuppressedError&&SuppressedError,exports.StateMachine=class extends t.Component{constructor(){super(...arguments),this.states={},this.current="",this.elapsedInState=0,this.subs=[],this.ctx={}}init(t){var e,s;t&&(this.states=null!==(e=t.states)&&void 0!==e?e:{},this.signalChange=t.signalChange,this.ctx=null!==(s=t.context)&&void 0!==s?s:{},t.initial&&this.states[t.initial]&&this.enter(t.initial,"__init__"))}goto(t,s="manual"){var n;if(!this.states[t])return void console.warn(`[plugin-state-machine] no such state: ${t}`);if(this.current===t)return;const i=this.current;i&&(null===(n=this.states[i])||void 0===n?void 0:n.onExit)&&e.getSignalBus().emit(this.states[i].onExit,{from:i,to:t,reason:s}),this.cleanupSubs(),this.enter(t,s)}get state(){return this.current}enter(t,s){var n;const i=this.current;this.current=t,this.elapsedInState=0;const r=this.states[t];if(!r)return;r.onEnter&&e.getSignalBus().emit(r.onEnter,{from:i,to:t,reason:s}),this.signalChange&&e.getSignalBus().emit(this.signalChange,{from:i,to:t,reason:s});const a=e.getSignalBus();for(const e of null!==(n=r.transitions)&&void 0!==n?n:[]){if(!e.on)continue;const s=e.to,n=e.guard,i=a.on(e.on,()=>{this.current===t&&(n&&!this.evalGuard(n)||this.goto(s,e.on))});this.subs.push(i)}}cleanupSubs(){for(const t of this.subs)t.dispose();this.subs=[]}evalGuard(t){try{const e=new Function("ctx",`return (${t});`);return Boolean(e(this.ctx))}catch(e){return console.warn(`[plugin-state-machine] bad guard "${t}":`,e),!1}}update(t){this.elapsedInState+=t.deltaTime;const e=this.states[this.current];if(null==e?void 0:e.transitions)for(const t of e.transitions)if(null!=t.after&&!(this.elapsedInState<t.after)&&(!t.guard||this.evalGuard(t.guard)))return void this.goto(t.to,`after:${t.after}`)}onDestroy(){this.cleanupSubs()}},exports.StateMachine.componentName="StateMachine",exports.StateMachine=function(t,e,s,n){var i,r=arguments.length,a=r<3?e:null===n?n=Object.getOwnPropertyDescriptor(e,s):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(t,e,s,n);else for(var o=t.length-1;o>=0;o--)(i=t[o])&&(a=(r<3?i(a):r>3?i(e,s,a):i(e,s))||a);return r>3&&a&&Object.defineProperty(e,s,a),a}([t.decorators.componentObserver({})],exports.StateMachine);class s extends t.System{constructor(){super(...arguments),this.name="StateMachine"}}s.systemName="StateMachine",exports.StateMachineSystem=s;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Component } from '@eva/eva.js';
|
|
2
|
+
import { System } from '@eva/eva.js';
|
|
3
|
+
|
|
4
|
+
export declare interface StateConfig {
|
|
5
|
+
/** 进入时 emit 的信号 */
|
|
6
|
+
onEnter?: string;
|
|
7
|
+
/** 退出时 emit 的信号 */
|
|
8
|
+
onExit?: string;
|
|
9
|
+
/** 该状态下的迁移规则 */
|
|
10
|
+
transitions?: TransitionRule[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* StateMachine 组件 — 简易有限状态机。
|
|
15
|
+
*
|
|
16
|
+
* DSL 用法:
|
|
17
|
+
* ```json
|
|
18
|
+
* {
|
|
19
|
+
* "type": "StateMachine",
|
|
20
|
+
* "props": {
|
|
21
|
+
* "initial": "moving",
|
|
22
|
+
* "states": {
|
|
23
|
+
* "moving": { "onEnter": "monster:state-moving",
|
|
24
|
+
* "transitions": [
|
|
25
|
+
* { "on": "monster:hit", "to": "knockback" },
|
|
26
|
+
* { "after": 3000, "to": "resting" }
|
|
27
|
+
* ] },
|
|
28
|
+
* "resting": { "transitions": [{ "after": 1000, "to": "moving" }] },
|
|
29
|
+
* "knockback":{ "transitions": [{ "after": 800, "to": "resting" }] }
|
|
30
|
+
* },
|
|
31
|
+
* "signalChange": "monster:state-change"
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* 行为:
|
|
37
|
+
* - 进入状态:emit `onEnter` + `signalChange` { from, to }
|
|
38
|
+
* - `transitions[i].on`: 监听信号,信号触发即迁移
|
|
39
|
+
* - `transitions[i].after`: 进入状态 N ms 后自动迁移
|
|
40
|
+
* - `transitions[i].guard`: JS 表达式,以 `ctx` 为变量,假则跳过该规则
|
|
41
|
+
*
|
|
42
|
+
* 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
|
|
43
|
+
*/
|
|
44
|
+
export declare class StateMachine extends Component<StateMachineParams> {
|
|
45
|
+
static componentName: string;
|
|
46
|
+
private states;
|
|
47
|
+
private current;
|
|
48
|
+
private signalChange?;
|
|
49
|
+
private elapsedInState;
|
|
50
|
+
private subs;
|
|
51
|
+
/** 上下文,guard 可读 */
|
|
52
|
+
ctx: Record<string, any>;
|
|
53
|
+
init(params?: StateMachineParams): void;
|
|
54
|
+
/** 主动迁移(代码侧也能调) */
|
|
55
|
+
goto(to: string, reason?: string): void;
|
|
56
|
+
get state(): string;
|
|
57
|
+
private enter;
|
|
58
|
+
private cleanupSubs;
|
|
59
|
+
private evalGuard;
|
|
60
|
+
update(e: {
|
|
61
|
+
deltaTime: number;
|
|
62
|
+
}): void;
|
|
63
|
+
onDestroy(): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export declare interface StateMachineParams {
|
|
67
|
+
initial: string;
|
|
68
|
+
states: Record<string, StateConfig>;
|
|
69
|
+
/** 状态切换 emit 信号 */
|
|
70
|
+
signalChange?: string;
|
|
71
|
+
/** 上下文变量,可在 guard 中读 */
|
|
72
|
+
context?: Record<string, any>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
|
|
77
|
+
*
|
|
78
|
+
* 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
|
|
79
|
+
* 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
|
|
80
|
+
*/
|
|
81
|
+
export declare class StateMachineSystem extends System {
|
|
82
|
+
static systemName: string;
|
|
83
|
+
readonly name = "StateMachine";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 单条迁移规则 */
|
|
87
|
+
export declare interface TransitionRule {
|
|
88
|
+
/** 触发该迁移的信号名 */
|
|
89
|
+
on?: string;
|
|
90
|
+
/** 进入该状态后等待 N ms 自动迁移(可与 on 二选一) */
|
|
91
|
+
after?: number;
|
|
92
|
+
/** 目标状态名 */
|
|
93
|
+
to: string;
|
|
94
|
+
/** 可选:JS 表达式字符串,以 ctx 为上下文 */
|
|
95
|
+
guard?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { }
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Component, decorators, System } from '@eva/eva.js';
|
|
2
|
+
import { getSignalBus } from '@eva/plugin-signal-bus';
|
|
3
|
+
|
|
4
|
+
/******************************************************************************
|
|
5
|
+
Copyright (c) Microsoft Corporation.
|
|
6
|
+
|
|
7
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
8
|
+
purpose with or without fee is hereby granted.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
11
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
12
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
13
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
14
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
15
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
16
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
17
|
+
***************************************************************************** */
|
|
18
|
+
|
|
19
|
+
function __decorate(decorators, target, key, desc) {
|
|
20
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
21
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
22
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
23
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
27
|
+
var e = new Error(message);
|
|
28
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* StateMachine 组件 — 简易有限状态机。
|
|
33
|
+
*
|
|
34
|
+
* DSL 用法:
|
|
35
|
+
* ```json
|
|
36
|
+
* {
|
|
37
|
+
* "type": "StateMachine",
|
|
38
|
+
* "props": {
|
|
39
|
+
* "initial": "moving",
|
|
40
|
+
* "states": {
|
|
41
|
+
* "moving": { "onEnter": "monster:state-moving",
|
|
42
|
+
* "transitions": [
|
|
43
|
+
* { "on": "monster:hit", "to": "knockback" },
|
|
44
|
+
* { "after": 3000, "to": "resting" }
|
|
45
|
+
* ] },
|
|
46
|
+
* "resting": { "transitions": [{ "after": 1000, "to": "moving" }] },
|
|
47
|
+
* "knockback":{ "transitions": [{ "after": 800, "to": "resting" }] }
|
|
48
|
+
* },
|
|
49
|
+
* "signalChange": "monster:state-change"
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* 行为:
|
|
55
|
+
* - 进入状态:emit `onEnter` + `signalChange` { from, to }
|
|
56
|
+
* - `transitions[i].on`: 监听信号,信号触发即迁移
|
|
57
|
+
* - `transitions[i].after`: 进入状态 N ms 后自动迁移
|
|
58
|
+
* - `transitions[i].guard`: JS 表达式,以 `ctx` 为变量,假则跳过该规则
|
|
59
|
+
*
|
|
60
|
+
* 不主动 emit 任何 lifecycle 信号超出上述列表;复杂语义请组合多个规则。
|
|
61
|
+
*/
|
|
62
|
+
let StateMachine = class StateMachine extends Component {
|
|
63
|
+
constructor() {
|
|
64
|
+
super(...arguments);
|
|
65
|
+
this.states = {};
|
|
66
|
+
this.current = '';
|
|
67
|
+
this.elapsedInState = 0;
|
|
68
|
+
this.subs = [];
|
|
69
|
+
/** 上下文,guard 可读 */
|
|
70
|
+
this.ctx = {};
|
|
71
|
+
}
|
|
72
|
+
init(params) {
|
|
73
|
+
var _a, _b;
|
|
74
|
+
if (!params)
|
|
75
|
+
return;
|
|
76
|
+
this.states = (_a = params.states) !== null && _a !== void 0 ? _a : {};
|
|
77
|
+
this.signalChange = params.signalChange;
|
|
78
|
+
this.ctx = (_b = params.context) !== null && _b !== void 0 ? _b : {};
|
|
79
|
+
if (params.initial && this.states[params.initial]) {
|
|
80
|
+
this.enter(params.initial, '__init__');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** 主动迁移(代码侧也能调) */
|
|
84
|
+
goto(to, reason = 'manual') {
|
|
85
|
+
var _a;
|
|
86
|
+
if (!this.states[to]) {
|
|
87
|
+
// eslint-disable-next-line no-console
|
|
88
|
+
console.warn(`[plugin-state-machine] no such state: ${to}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (this.current === to)
|
|
92
|
+
return;
|
|
93
|
+
const from = this.current;
|
|
94
|
+
if (from && ((_a = this.states[from]) === null || _a === void 0 ? void 0 : _a.onExit)) {
|
|
95
|
+
getSignalBus().emit(this.states[from].onExit, { from, to, reason });
|
|
96
|
+
}
|
|
97
|
+
this.cleanupSubs();
|
|
98
|
+
this.enter(to, reason);
|
|
99
|
+
}
|
|
100
|
+
get state() {
|
|
101
|
+
return this.current;
|
|
102
|
+
}
|
|
103
|
+
enter(to, reason) {
|
|
104
|
+
var _a;
|
|
105
|
+
const from = this.current;
|
|
106
|
+
this.current = to;
|
|
107
|
+
this.elapsedInState = 0;
|
|
108
|
+
const cfg = this.states[to];
|
|
109
|
+
if (!cfg)
|
|
110
|
+
return;
|
|
111
|
+
if (cfg.onEnter)
|
|
112
|
+
getSignalBus().emit(cfg.onEnter, { from, to, reason });
|
|
113
|
+
if (this.signalChange)
|
|
114
|
+
getSignalBus().emit(this.signalChange, { from, to, reason });
|
|
115
|
+
// 订阅本 state 的 on 信号
|
|
116
|
+
const bus = getSignalBus();
|
|
117
|
+
for (const t of (_a = cfg.transitions) !== null && _a !== void 0 ? _a : []) {
|
|
118
|
+
if (!t.on)
|
|
119
|
+
continue;
|
|
120
|
+
const target = t.to;
|
|
121
|
+
const guard = t.guard;
|
|
122
|
+
const h = bus.on(t.on, () => {
|
|
123
|
+
if (this.current !== to)
|
|
124
|
+
return; // 已经离开
|
|
125
|
+
if (guard && !this.evalGuard(guard))
|
|
126
|
+
return;
|
|
127
|
+
this.goto(target, t.on);
|
|
128
|
+
});
|
|
129
|
+
this.subs.push(h);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
cleanupSubs() {
|
|
133
|
+
for (const h of this.subs)
|
|
134
|
+
h.dispose();
|
|
135
|
+
this.subs = [];
|
|
136
|
+
}
|
|
137
|
+
evalGuard(guard) {
|
|
138
|
+
try {
|
|
139
|
+
// 简单 expression eval,只暴露 ctx
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
|
|
141
|
+
const fn = new Function('ctx', `return (${guard});`);
|
|
142
|
+
return Boolean(fn(this.ctx));
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
// eslint-disable-next-line no-console
|
|
146
|
+
console.warn(`[plugin-state-machine] bad guard "${guard}":`, err);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
update(e) {
|
|
151
|
+
this.elapsedInState += e.deltaTime;
|
|
152
|
+
const cfg = this.states[this.current];
|
|
153
|
+
if (!(cfg === null || cfg === void 0 ? void 0 : cfg.transitions))
|
|
154
|
+
return;
|
|
155
|
+
for (const t of cfg.transitions) {
|
|
156
|
+
if (t.after == null)
|
|
157
|
+
continue;
|
|
158
|
+
if (this.elapsedInState < t.after)
|
|
159
|
+
continue;
|
|
160
|
+
if (t.guard && !this.evalGuard(t.guard))
|
|
161
|
+
continue;
|
|
162
|
+
this.goto(t.to, `after:${t.after}`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
onDestroy() {
|
|
167
|
+
this.cleanupSubs();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
StateMachine.componentName = 'StateMachine';
|
|
171
|
+
StateMachine = __decorate([
|
|
172
|
+
decorators.componentObserver({})
|
|
173
|
+
], StateMachine);
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* StateMachineSystem — 仅用于注册;StateMachine 自身在 Component.update 中驱动。
|
|
177
|
+
*
|
|
178
|
+
* 之所以保留这个空 System,是因为 Eva.js 的注册习惯是 component + system 成对出现,
|
|
179
|
+
* 而且未来如果要加全局调度(例如 group: physics 在 HitArea 之后才能切状态)可以扩展这里。
|
|
180
|
+
*/
|
|
181
|
+
class StateMachineSystem extends System {
|
|
182
|
+
constructor() {
|
|
183
|
+
super(...arguments);
|
|
184
|
+
this.name = 'StateMachine';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
StateMachineSystem.systemName = 'StateMachine';
|
|
188
|
+
|
|
189
|
+
export { StateMachine, StateMachineSystem };
|
package/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eva/plugin-state-machine",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.4",
|
|
4
4
|
"description": "Finite state machine — DSL 声明 states/transitions/timeouts/guards/actions,emit state-change 信号。",
|
|
5
|
-
"main": "
|
|
6
|
-
"module": "
|
|
7
|
-
"types": "
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "dist/plugin-state-machine.esm.js",
|
|
7
|
+
"types": "dist/plugin-state-machine.d.ts",
|
|
8
8
|
"files": [
|
|
9
|
-
"
|
|
9
|
+
"index.js",
|
|
10
|
+
"dist"
|
|
10
11
|
],
|
|
11
12
|
"keywords": [
|
|
12
13
|
"eva.js",
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
],
|
|
17
18
|
"license": "MIT",
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"@eva/eva.js": "2.1.0-beta.
|
|
20
|
-
"@eva/plugin-signal-bus": "2.1.0-beta.
|
|
20
|
+
"@eva/eva.js": "2.1.0-beta.4",
|
|
21
|
+
"@eva/plugin-signal-bus": "2.1.0-beta.4"
|
|
21
22
|
}
|
|
22
23
|
}
|
package/lib/StateMachine.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
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
DELETED
package/lib/types.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
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
|
-
}
|