waitable blockly scripts. new waitForSeconds block
This commit is contained in:
parent
830c2bdde6
commit
3bdde094f8
|
|
@ -18,5 +18,14 @@ export default defineConfig([
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
|
rules: {
|
||||||
|
"padding-line-between-statements": [
|
||||||
|
"warn",
|
||||||
|
{ blankLine: 'always', prev: '*', next: 'block' },
|
||||||
|
{ blankLine: 'always', prev: 'block', next: '*' },
|
||||||
|
{ blankLine: 'always', prev: '*', next: 'block-like' },
|
||||||
|
{ blankLine: 'always', prev: 'block-like', next: '*' },
|
||||||
|
]
|
||||||
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Blockly.Blocks['console_log_action'] = {
|
||||||
init(this: Blockly.Block) {
|
init(this: Blockly.Block) {
|
||||||
this.appendValueInput('VALUE')
|
this.appendValueInput('VALUE')
|
||||||
.setAlign(Blockly.inputs.Align.RIGHT)
|
.setAlign(Blockly.inputs.Align.RIGHT)
|
||||||
.setCheck('ObjectType')
|
// .setCheck('String')
|
||||||
.appendField('print');
|
.appendField('print');
|
||||||
this.setInputsInline(false)
|
this.setInputsInline(false)
|
||||||
this.setPreviousStatement(true, null);
|
this.setPreviousStatement(true, null);
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './consoleLog';
|
export * from './consoleLog';
|
||||||
export * from './physics';
|
export * from './physics';
|
||||||
|
export * from './waitSeconds';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import * as Blockly from "blockly";
|
||||||
|
import { javascriptGenerator } from "blockly/javascript";
|
||||||
|
|
||||||
|
Blockly.Blocks['wait_seconds_action'] = {
|
||||||
|
init(this: Blockly.Block) {
|
||||||
|
this.appendDummyInput()
|
||||||
|
.appendField('Wait')
|
||||||
|
.appendField(new Blockly.FieldNumber(1, 0, 3600, 0.1), 'SECONDS')
|
||||||
|
.appendField('seconds');
|
||||||
|
this.setPreviousStatement(true, null);
|
||||||
|
this.setNextStatement(true, null);
|
||||||
|
this.setTooltip('Pause execution for N seconds');
|
||||||
|
this.setColour(120);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
javascriptGenerator.forBlock['wait_seconds_action'] = function (block) {
|
||||||
|
const seconds = block.getFieldValue('SECONDS');
|
||||||
|
return `yield wait(${seconds});\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { };
|
||||||
|
|
@ -12,7 +12,7 @@ export function generateCode(workspace: Blockly.WorkspaceSvg): string {
|
||||||
body += javascriptGenerator.blockToCode(next, true);
|
body += javascriptGenerator.blockToCode(next, true);
|
||||||
next = next.getNextBlock();
|
next = next.getNextBlock();
|
||||||
}
|
}
|
||||||
body = javascriptGenerator.prefixLines(body, javascriptGenerator.INDENT);
|
body = javascriptGenerator.prefixLines(body, javascriptGenerator.INDENT.repeat(2));
|
||||||
|
|
||||||
// const args = Array.from(block.getFields())
|
// const args = Array.from(block.getFields())
|
||||||
// .filter(field => field.name !== undefined)
|
// .filter(field => field.name !== undefined)
|
||||||
|
|
@ -23,12 +23,10 @@ export function generateCode(workspace: Blockly.WorkspaceSvg): string {
|
||||||
|
|
||||||
result += `onGameEvent(
|
result += `onGameEvent(
|
||||||
'${block.type.replace(/_event$/, '')}',
|
'${block.type.replace(/_event$/, '')}',
|
||||||
function(context) {
|
function*(context) {
|
||||||
// console.dir({context, api, object, objectType});
|
|
||||||
if (context.object && (context.object.id !== object.id))
|
if (context.object && (context.object.id !== object.id))
|
||||||
return;
|
return;
|
||||||
${body}
|
${body} },
|
||||||
}
|
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,8 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
|
||||||
colour: '20',
|
colour: '20',
|
||||||
contents: [
|
contents: [
|
||||||
{ kind: 'block', type: 'console_log_action' },
|
{ kind: 'block', type: 'console_log_action' },
|
||||||
{ kind: 'block', type: 'text_print' },
|
|
||||||
{ kind: 'block', type: 'physics_apply_impulse_action' },
|
{ kind: 'block', type: 'physics_apply_impulse_action' },
|
||||||
|
{ kind: 'block', type: 'wait_seconds_action' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
export type YieldCommand =
|
||||||
|
| { type: 'waitSeconds'; seconds: number }
|
||||||
|
| { type: 'waitFrames'; frames: number }
|
||||||
|
| { type: 'waitUntil'; condition: () => boolean };
|
||||||
|
|
||||||
|
type Coroutine =
|
||||||
|
& { gen: Generator<YieldCommand, void, void> }
|
||||||
|
& (| { wakeType: 'time'; remaining: number }
|
||||||
|
| { wakeType: 'frames'; remaining: number }
|
||||||
|
| { wakeType: 'condition'; test: () => boolean });
|
||||||
|
|
||||||
|
export class GameScheduler {
|
||||||
|
private coroutines: Coroutine[] = [];
|
||||||
|
|
||||||
|
tick(deltaTime: number): void {
|
||||||
|
const next: Coroutine[] = [];
|
||||||
|
for (const co of this.coroutines) {
|
||||||
|
if (this.isReady(co)) this.step(co, next);
|
||||||
|
else next.push(this.countdown(co, deltaTime));
|
||||||
|
}
|
||||||
|
this.coroutines = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
startCoroutine(gen: Generator<YieldCommand, void, void>): void {
|
||||||
|
const result = gen.next();
|
||||||
|
if (!result.done)
|
||||||
|
this.coroutines.push(this.make(gen, result.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void { this.coroutines = []; }
|
||||||
|
|
||||||
|
private isReady(co: Coroutine): boolean {
|
||||||
|
switch (co.wakeType) {
|
||||||
|
case 'time': return co.remaining <= 0;
|
||||||
|
case 'frames': return co.remaining <= 0;
|
||||||
|
case 'condition': return co.test();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private countdown(co: Coroutine, deltaTime: number): Coroutine {
|
||||||
|
switch (co.wakeType) {
|
||||||
|
case 'time': return { ...co, remaining: co.remaining - deltaTime };
|
||||||
|
case 'frames': return { ...co, remaining: co.remaining - 1 };
|
||||||
|
default: return co;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private step(co: Coroutine, list: Coroutine[]): void {
|
||||||
|
const result = co.gen.next();
|
||||||
|
if (!result.done) list.push(this.make(co.gen, result.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private make(gen: Generator<YieldCommand, void, void>, cmd: YieldCommand): Coroutine {
|
||||||
|
switch (cmd.type) {
|
||||||
|
case 'waitSeconds':
|
||||||
|
return { gen, wakeType: 'time', remaining: cmd.seconds };
|
||||||
|
case 'waitFrames':
|
||||||
|
return { gen, wakeType: 'frames', remaining: cmd.frames };
|
||||||
|
case 'waitUntil':
|
||||||
|
return { gen, wakeType: 'condition', test: cmd.condition };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import type { Game, RuntimeGameScene, RuntimeScene, World } from "../types";
|
import type { Game, RuntimeGameScene, RuntimeScene, World } from "../types";
|
||||||
import type { GameEventHandler, GameEventContext, InternalGameEventBus } from "../types/runtime/gameBus";
|
import type { GameEventContext, GameEventHandler, InternalGameEventBus } from "../types/runtime/gameBus";
|
||||||
import { clone } from "../utils";
|
import { clone } from "../utils";
|
||||||
import { populateRuntimeScene } from "../utils/runtime";
|
import { populateRuntimeScene } from "../utils/runtime";
|
||||||
import { ObjectApi } from "../utils/runtime/objectApi";
|
import { ObjectApi } from "../utils/runtime/objectApi";
|
||||||
|
import type { GameScheduler, YieldCommand } from "./GameScheduler";
|
||||||
|
|
||||||
export class GameEventBus {
|
export class GameEventBus {
|
||||||
private handlers = new Map<string, GameEventHandler[]>();
|
private handlers = new Map<string, GameEventHandler[]>();
|
||||||
|
|
@ -66,25 +67,41 @@ export class GameFactory {
|
||||||
world: World,
|
world: World,
|
||||||
scene: RuntimeGameScene,
|
scene: RuntimeGameScene,
|
||||||
eventBus: GameEventBus,
|
eventBus: GameEventBus,
|
||||||
|
scheduler: GameScheduler,
|
||||||
): void {
|
): void {
|
||||||
|
|
||||||
|
const handlerWrappers = new WeakMap<Function, (ctx: GameEventContext) => void>();
|
||||||
|
|
||||||
|
const wait = (seconds: number): YieldCommand => ({ type: 'waitSeconds', seconds });
|
||||||
|
const waitFrames = (frames: number): YieldCommand => ({ type: 'waitFrames', frames });
|
||||||
|
const waitUntil = (condition: () => boolean): YieldCommand => ({ type: 'waitUntil', condition });
|
||||||
|
|
||||||
const internalBus: InternalGameEventBus = {
|
const internalBus: InternalGameEventBus = {
|
||||||
onGameEvent: eventBus.on.bind(eventBus),
|
onGameEvent(event, genFn) {
|
||||||
offGameEvent: eventBus.off.bind(eventBus),
|
const wrapper: GameEventHandler = (ctx) => scheduler.startCoroutine(genFn(ctx) as Generator<YieldCommand, void, void>);
|
||||||
|
handlerWrappers.set(genFn, wrapper);
|
||||||
|
eventBus.on(event, wrapper);
|
||||||
|
},
|
||||||
|
offGameEvent(event, genFn) {
|
||||||
|
const wrapper = handlerWrappers.get(genFn);
|
||||||
|
if (wrapper) {
|
||||||
|
eventBus.off(event, wrapper);
|
||||||
|
handlerWrappers.delete(genFn);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Object.values(world.objectTypes)
|
Object.values(world.objectTypes)
|
||||||
.filter((ot) => ot.javascript)
|
.filter((ot) => ot.javascript)
|
||||||
.forEach((ot) => {
|
.forEach((ot) => {
|
||||||
const gameScript = eval(`(function gameScript({onGameEvent, offGameEvent }, {api, object, objectType}) {${ot.javascript}})`);
|
const gameScript = eval(`(function gameScript({onGameEvent, offGameEvent, wait, waitFrames, waitUntil}, {api, object, objectType}) {${ot.javascript}})`);
|
||||||
|
|
||||||
Object.values(scene.objects)
|
Object.values(scene.objects)
|
||||||
.filter((o) => o.typeId == ot.id)
|
.filter((o) => o.typeId == ot.id)
|
||||||
.forEach((o) => {
|
.forEach((o) => {
|
||||||
const api = new ObjectApi(o, ot, world, scene);
|
const api = new ObjectApi(o, ot, world, scene);
|
||||||
gameScript(internalBus, { api, object: o, objectType: ot });
|
gameScript({ ...internalBus, wait, waitFrames, waitUntil }, { api, object: o, objectType: ot });
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { WorldState } from "./worldState";
|
||||||
import type { Game, Pos3, RuntimeGameScene } from "../types";
|
import type { Game, Pos3, RuntimeGameScene } from "../types";
|
||||||
import type { CameraProps } from "@react-three/fiber";
|
import type { CameraProps } from "@react-three/fiber";
|
||||||
import { GameEventBus, GameFactory } from "../model/gameFactory";
|
import { GameEventBus, GameFactory } from "../model/gameFactory";
|
||||||
|
import { GameScheduler } from "../model/GameScheduler";
|
||||||
import type { GameEventContext } from "../types/runtime/gameBus";
|
import type { GameEventContext } from "../types/runtime/gameBus";
|
||||||
|
|
||||||
export class GameState {
|
export class GameState {
|
||||||
|
|
@ -14,6 +15,7 @@ export class GameState {
|
||||||
public scene: RuntimeGameScene;
|
public scene: RuntimeGameScene;
|
||||||
|
|
||||||
private eventBus: GameEventBus;
|
private eventBus: GameEventBus;
|
||||||
|
private scheduler: GameScheduler;
|
||||||
|
|
||||||
private startAutoSave() {
|
private startAutoSave() {
|
||||||
return reaction(
|
return reaction(
|
||||||
|
|
@ -46,11 +48,12 @@ export class GameState {
|
||||||
|
|
||||||
const eventBus = new GameEventBus();
|
const eventBus = new GameEventBus();
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
|
this.scheduler = new GameScheduler();
|
||||||
|
|
||||||
this._stopAutoSave = this.startAutoSave();
|
this._stopAutoSave = this.startAutoSave();
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
|
||||||
GameFactory.evalGameScripts(rawWorld, this.scene, eventBus);
|
GameFactory.evalGameScripts(rawWorld, this.scene, eventBus, this.scheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public emitEvent(event: string, context: Omit<GameEventContext, 'world' | 'scene'> = {}) {
|
public emitEvent(event: string, context: Omit<GameEventContext, 'world' | 'scene'> = {}) {
|
||||||
|
|
@ -126,6 +129,7 @@ export class GameState {
|
||||||
if (this.isPaused)
|
if (this.isPaused)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
this.scheduler.tick(deltaTime);
|
||||||
this.time += deltaTime;
|
this.time += deltaTime;
|
||||||
|
|
||||||
if (this.isFirstTick) {
|
if (this.isFirstTick) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ export type GameEventContext = {
|
||||||
|
|
||||||
export type GameEventHandler = (context: GameEventContext) => void;
|
export type GameEventHandler = (context: GameEventContext) => void;
|
||||||
|
|
||||||
|
export type GeneratorEventHandler = (context: GameEventContext) => Generator<unknown, void, void>;
|
||||||
|
|
||||||
export type InternalGameEventBus = {
|
export type InternalGameEventBus = {
|
||||||
onGameEvent: (event: string, handler: GameEventHandler) => void;
|
onGameEvent: (event: string, handler: GeneratorEventHandler) => void;
|
||||||
offGameEvent: (event: string, handler: GameEventHandler) => void;
|
offGameEvent: (event: string, handler: GeneratorEventHandler) => void;
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue