Ожидание/пауза JS в сгенерированных функциях

Что я делаю

Я занимаюсь созданием графического приложения для черепах с помощью Blockly. Пользователь может создавать код из блоков, затем движок Blockly генерирует JS-код, который рисует на холсте.

В чем моя проблема

Движок Blockly генерирует JS-код, но возвращает его в виде строки, которую мне нужно eval() нарисовать на холсте.

Я могу изменить код блоков, чтобы генерировать разные выходные данные, но важно сделать его как можно более простым, потому что пользователи могут прочитать фактический код, стоящий за вводом блока. Поэтому я хотел бы не испортить это.

Что я хотел бы сделать

У меня есть полный контроль над атомарными операциями (go, turn и т. д.), поэтому я хотел бы вставить в начало функций небольшой фрагмент кода, который задерживает выполнение остальных тел функций. Что-то вроде:

function go(dir, dist) {
  // wait here a little

  // do the drawing
}

Я думаю, что это должно быть что-то синхронное, сохраняющее задержку в потоке выполнения. Я пытался использовать setTimeout (асинхронно, сбой), promise (сбой), проверку временных меток в цикле (сбой).

Возможно ли это вообще в JS?


person Nekomajin42    schedule 21.08.2016    source источник
comment
Зачем вам такая задержка? Например, показать какую-нибудь анимацию?   -  person Tamas Hegedus    schedule 22.08.2016
comment
Что значит ждать? Если вы имеете в виду блокировку механизма выполнения js, единственным способом является цикл.   -  person zhang    schedule 22.08.2016
comment
Вы не можете отложить синхронное выполнение. Вам придется генерировать асинхронный код, но, как я вижу, для блочного пока нет такого генератора кода. В этом мало смысла, асинхронный вариант гораздо труднее читать. Но есть интерпретатор js, с помощью которого вы можете запускать код построчно асинхронно (и безопасно).   -  person Tamas Hegedus    schedule 22.08.2016
comment
если вы используете среду, поддерживающую async/await, вы можете написать код, который кажется синхронным, однако в конце концов он должен быть асинхронным.   -  person zzzzBov    schedule 22.08.2016
comment
@TamasHegedus Это образовательное приложение. Хорошо видеть шаг за шагом, как создается форма.   -  person Nekomajin42    schedule 22.08.2016


Ответы (3)


Вы не должны заставлять код ждать синхронно. Единственное, что вы получите, это зависшее окно браузера.

Вам нужно использовать интерпретатор js вместо оценить Таким образом, вы можете приостановить выполнение, воспроизвести анимацию, выделить выполняющиеся в данный момент блоки и т. д. В руководстве есть много примеров, которые помогут вам начать работу. Вот рабочий код, основанный на примере интерпретатора JS:

var workspace = Blockly.inject("editor-div", {
  toolbox: document.getElementById('toolbox')
});

Blockly.JavaScript.STATEMENT_PREFIX = 'highlightBlock(%1);\n';
Blockly.JavaScript.addReservedWords('highlightBlock');

Blockly.JavaScript['text_print'] = function(block) {
  var argument0 = Blockly.JavaScript.valueToCode(
    block, 'TEXT',
    Blockly.JavaScript.ORDER_FUNCTION_CALL
  ) || '\'\'';
  return "print(" + argument0 + ');\n';
};

function run() {
  var code = Blockly.JavaScript.workspaceToCode(workspace);
  var running = false;

  workspace.traceOn(true);
  workspace.highlightBlock(null);

  var lastBlockToHighlight = null;
  var myInterpreter = new Interpreter(code, (interpreter, scope) => {
    interpreter.setProperty(
      scope, 'highlightBlock',
      interpreter.createNativeFunction(id => {
        id = id ? id.toString() : '';
        running = false;
        workspace.highlightBlock(lastBlockToHighlight);
        lastBlockToHighlight = id;
      })
    );
    interpreter.setProperty(
      scope, 'print',
      interpreter.createNativeFunction(val => {
        val = val ? val.toString() : '';
        console.log(val);
      })
    );
  });

  var intervalId = setInterval(() => {
    running = true;
    while (running) {
      if (!myInterpreter.step()) {
        workspace.highlightBlock(lastBlockToHighlight);
        clearInterval(intervalId);
        return;
      }
    }
  }, 500);
}
#editor-div {
  width: 500px;
  height: 150px;
}
<script src="https://rawgit.com/google/blockly/master/blockly_compressed.js"></script>
<script src="https://rawgit.com/google/blockly/master/blocks_compressed.js"></script>
<script src="https://rawgit.com/google/blockly/master/javascript_compressed.js"></script>
<script src="https://rawgit.com/google/blockly/master/msg/js/en.js"></script>
<script src="https://rawgit.com/NeilFraser/JS-Interpreter/master/acorn_interpreter.js"></script>

<xml id="toolbox" style="display: none">
  <block type="text"></block>
  <block type="text_print"></block>
  <block type="controls_repeat_ext"></block>
 <block type="math_number"></block>
</xml>

<div>
  <button id="run-code" onclick="run()">run</button>
</div>
<div id="editor-div"></div>

ИЗМЕНИТЬ

Добавлена ​​переменная running для управления интерпретатором. Теперь он переходит до тех пор, пока для переменной running не будет установлено значение false, поэтому оператор running = false внутри функции highlightBlock по сути работает как точка останова.

ИЗМЕНИТЬ

Введена переменная lastBlockToHighlight для задержки выделения, поэтому выделяется последний оператор запуска, а не следующий. К сожалению, у генератора кода JavaScript нет конфигурации STATEMENT_SUFFIX, аналогичной STATEMENT_PREFIX.

person Tamas Hegedus    schedule 21.08.2016
comment
Я посмотрю. Спасибо! - person Nekomajin42; 22.08.2016
comment
Могу ли я передать объект интерпретатору со всеми его членами? Все мои атомарные функции являются членами объекта turtle. Было бы проще передать все сразу, а не два десятка функций по одной, если это возможно. - person Nekomajin42; 22.08.2016
comment
Хорошо, я заставил это работать. Однако выполнение кода очень медленное. Кажется, что задержка между двумя шагами примерно в 30 раз медленнее, чем должна. Если я установлю задержку 1000 мс для setInterval, задержка между двумя шагами составит около 30 с. Есть идеи? - person Nekomajin42; 24.08.2016
comment
Интерпретатор выполняет много шагов при вычислении выражений. В одном из примеров они использовали глобальную переменную для прерывания после определенных выражений, давайте посмотрим, смогу ли я воспроизвести такое же поведение. - person Tamas Hegedus; 24.08.2016
comment
@Nekomajin42: добавлены некоторые улучшения. - person Tamas Hegedus; 24.08.2016
comment
ОК, наконец-то это работает. У меня есть последняя проблема. Подсветка блока и выполнение кода не синхронизированы. Код №1 выполняется, когда выделен блок №2, и так далее. Кажется, это тоже проблема с вашим фрагментом. - person Nekomajin42; 24.08.2016
comment
@Nekomajin42 Я сделал это нарочно. Для меня выделение следующего оператора, который должен быть выполнен, казалось разумным. Я собираюсь отредактировать фрагмент. - person Tamas Hegedus; 24.08.2016
comment
@Nekomajin42 Ну вот - person Tamas Hegedus; 24.08.2016

Недавно я опубликовал библиотеку, которая позволяет асинхронно взаимодействовать с blockly, я разработал эту библиотеку для таких игр. На самом деле в документации вы можете найти демо-версию игры, которая является ремейком игры лабиринта. Библиотека называется blockly-gamepad ????, надеюсь, это то, что вы искали.


Вот gif демо.

Демо


Как это устроено

Это другой и упрощенный подход по сравнению с обычным использованием blockly.

Сначала вам нужно определить блоки (см., как их определить в документация).
Вам не нужно определять any code generator, все, что касается генерации кода, выполняется библиотекой.

введите здесь описание изображения


Каждый блок генерирует запрос.

// the request
{ method: 'TURN', args: ['RIGHT'] }


Когда блок выполняется, соответствующий запрос передается в вашу игру.

class Game{
    manageRequests(request){
        // requests are passed here
        if(request.method == 'TURN')
            // animate your sprite
            turn(request.args)
    }
}


Вы можете использовать обещания для управления асинхронными анимациями, как в вашем случае.

class Game{
    async manageRequests(request){
        if(request.method == 'TURN')
            await turn(request.args)
    }
}


Связью между блоками и вашей игрой управляет геймпад.

let gamepad = new Blockly.Gamepad(),
    game = new Game()

// requests will be passed here
gamepad.setGame(game, game.manageRequest)


Геймпад предоставляет несколько методов для управления выполнением блоков и, следовательно, генерацией запросов.

// load the code from the blocks in the workspace
gamepad.load()
// reset the code loaded previously
gamepad.reset()

// the blocks are executed one after the other
gamepad.play() 
// play in reverse
gamepad.play(true)
// the blocks execution is paused
gamepad.pause()
// toggle play
gamepad.togglePlay()

// load the next request 
gamepad.forward()
// load the prior request
gamepad.backward()

// use a block as a breakpoint and play until it is reached
gamepad.debug(id)

Полную документацию можно прочитать здесь.


EDIT: я обновил название библиотеки, теперь она называется blockly-gamepad.

person Paolo Longo    schedule 12.07.2019
comment
Привет, Паоло. Пожалуйста, объясните, как ваша библиотека решает проблему, о которой спрашивает ОП. - person Mike Poole; 12.07.2019
comment
Вы можете найти объяснение того, как это работает здесь. @МайкПул - person Paolo Longo; 13.07.2019
comment
Привет, Паоло. Смысл моего комментария в том, что вы должны объяснить, как это работает в SO, а не ссылаться на другой сайт, где ссылка может умереть. - person Mike Poole; 15.07.2019
comment
Привет, Майк, ссылка перенаправляет на документацию github, которая не должна умереть. Теперь я изменяю ответ, чтобы все было понятнее, спасибо за совет. - person Paolo Longo; 15.07.2019

Если я тебя понял!

Вы можете создать новый класс для обработки выполнения функций go(dir, dist) и переопределить функцию go для создания нового go в исполнителе.

function GoExecutor(){

    var executeArray = [];     // Store go methods that waiting for execute
    var isRunning = false;     // Handle looper function

    // start runner function
    var run = function(){
        if(isRunning)
            return;
        isRunning = true;
        runner();
    }

    // looper for executeArray
    var runner = function(){
        if(executeArray.length == 0){
            isRunning = false;
            return;
        }

        // pop the first inserted params 
        var currentExec = executeArray.shift(0);

        // wait delay miliseconds
        setTimeout(function(){
            // execute the original go function
            originalGoFunction(currentExec.dir, currentExec.dist);

            // after finish drawing loop on the next execute method
            runner();
        }, currentExec.delay);

    }
    this.push = function(dir, dist){
        executeArray.push([dir,dist]);
        run();
    }
}

// GoExecutor instance
var goExec = new GoExecutor();

// Override go function
var originalGoFunction = go;
var go = function (dir, dist, delay){
    goExec.push({"dir":dir, "dist":dist, "delay":delay});
}

Редактировать 1:

Теперь вам нужно вызвать callWithDelay с вашей функцией и параметрами, исполнитель обработает этот вызов, применив параметры к указанной функции.

function GoExecutor(){

    var executeArray = [];     // Store go methods that waiting for execute
    var isRunning = false;     // Handle looper function

    // start runner function
    var run = function(){
        if(isRunning)
            return;
        isRunning = true;
        runner();
    }

    // looper for executeArray
    var runner = function(){
        if(executeArray.length == 0){
            isRunning = false;
            return;
        }

        // pop the first inserted params 
        var currentExec = executeArray.shift(0);

        // wait delay miliseconds
        setTimeout(function(){
            // execute the original go function
            currentExec.funcNam.apply(currentExec.funcNam, currentExec.arrayParams);

            // after finish drawing loop on the next execute method
            runner();
        }, currentExec.delay);

    }
    this.push = function(dir, dist){
        executeArray.push([dir,dist]);
        run();
    }
}

// GoExecutor instance
var goExec = new GoExecutor();

var callWithDelay = function (func, arrayParams, delay){
    goExec.push({"func": func, "arrayParams":arrayParams, "delay":delay});
}
person David Antoon    schedule 21.08.2016
comment
Но у меня есть несколько функций с разными именами и списками параметров. - person Nekomajin42; 22.08.2016
comment
@Nekomajin42 см. Редактировать 1. - person David Antoon; 22.08.2016
comment
А также их порядок случайный, потому что пользователи создают код, который генерирует Blockly. - person Nekomajin42; 22.08.2016
comment
Затем вам следует работать с обратными вызовами, вы упомянули, что я пытался использовать setTimeout (асинхронно, сбой), обещание (сбой), проверку временных меток в цикле (сбой). Как вы пытались это реализовать? - person David Antoon; 22.08.2016
comment
Я буду придерживаться решения от @Tamas, но голосую за вас, потому что оно может быть полезно для других. Благодарю вас! - person Nekomajin42; 24.08.2016