开发者问题收集

将 P5.js 绘制函数放置在自定义循环函数中

2017-03-01
3097

P5.js 有自己的循环,每秒调用 draw() 函数 FPS 次。但是,有一个函数 noLoop() 可以放在 setup() 函数中,它禁用 P5.js 自己的循环。

所以我制作了自己的循环(这对我的项目来说是必要的),如下所示:

customLoop = function(){
  while(somethingIsTrue){
    //custom code here
    draw();
  }
}

因此,由于没有设置 FPS,我希望看到画布更新得相当快。但是,只有在 customLoop 完成后,画布才会再次重新绘制。

简而言之:自定义循环仍在运行时,画布不会更新,它只会在自定义循环完成后显示更新。

如何在自己的循环中连续更新画布?


人们要求提供更多背景信息,所以我会尽力解释。我已经制作了 自己的 JS 库 ,可以轻松创建神经网络的遗传算法。

我的库中的遗传算法设置如下:

var evol = new Evolution({
  size: 50,
  mutationRate: 0.05,
  networkSize: [4,10,1],
  mutationMethod: [Mutate.MODIFY_RANDOM_BIAS, Mutate.MODIFY_RANDOM_WEIGHT],
  crossOverMethod: [Crossover.UNIFORM, Crossover.AVERAGE],
  selectionMethod: [Selection.FITNESS_PROPORTIONATE],
  elitism: 5,
  fitnessFunction: function(network){
    //something here that computes for every network, AT THE SAME TIME showing it LIVE for the user to watch
  }
});

注意 fitnessFunction 中的注释。然后我运行遗传算法循环如下:

 var notFinished = true;
  while(notFinished){
    evol.evaluate();
    if(evol.getAverage() > 2000){
      notFinished = false;
      console.log('done');
    }
    evol.select();
    evol.crossOver();
    evol.mutate();
    evol.replace();
  };

所以如您所见,我的代码连续运行遗传算法。但整个问题在于适应度函数。这种特定的遗传算法用于训练神经网络如何玩 贪吃蛇 。因此,在适应度函数中,我实际上在运行神经网络,直到蛇死亡。但是,对于每条测试过的蛇,我希望用户 看到 神经网络的表现。因此,我的适应度函数如下所示:

fitnessFunction = function(network){
  while(snakeIsNotDead){
    computeNeuralNetworkOutput();
    giveSnakeNewMovementDirectionBasedOnOutput();

    // most importantly
    redrawTheSnakeWithNewPosition(); // which is redraw() with P5.js
}

因此,对于每一代测试过的蛇,我希望看到所有 50 条蛇的表现。但是,在网络完成后,我看到的只是蛇在最后一次蛇评估中的最后位置。

2个回答

请尝试将您的代码发布为我们可以实际运行的 MCVE 。下面是一个显示您的问题的小示例:

function setup(){
  createCanvas(500, 500);
  for(var i = 0; i < 600; i ++){
    redraw();
  }
}

function draw(){
  ellipse(random(width), random(height), 20, 20);
}

这将显示一个画布,上面已经绘制了一堆随机圆圈。我猜您想要的是看到圆圈累积起来,就像您正在“快进”草图一样。

使用您自己的循环有点可疑。Processing 已经有了自己的循环机制,它包含对画布进行双缓冲的逻辑。这就是为什么在绘制完成之前您看不到任何东西的原因。您可以只依赖 Processing 的内置计时机制。下面是一个例子:

function setup(){
  createCanvas(500, 500);
  frameRate(120);
}

function draw(){
  if(frameCount == 600){
    frameRate(60);
  }
  ellipse(random(width), random(height), 20, 20);

}

请注意,如果您运行的设备限制了帧速率,这可能不起作用。但关键是您不需要创建自己的循环。只需使用 Processing 的内置循环逻辑即可。

编辑: 就像我说的,您看不到任何帧的原因是因为 Processing 是双缓冲的。这意味着 Processing 将等待,直到它处理完所有事件并将所有内容绘制到屏幕上。这过于简单,但您可以将上述循环视为向事件队列添加 600 个重绘事件。Processing 会处理它们,但直到完成才会真正显示结果。

您可以通过在此事件队列清空后调用每个重绘来解决这个问题。一种略显 hack 的方法是使用 setTimeout() 函数:

function setup(){
  createCanvas(500, 500);
  for(var i = 0; i < 600; i ++){
    setTimeout(redraw, 0);
  }
}

function draw(){
  ellipse(random(width), random(height), 20, 20);
}

现在,代码不再连续调用 redraw() 函数 600 次,而是向事件队列添加 600 个超时事件。它们中的每一个都将单独处理,因此您会看到间歇性帧。

我要说的是,这仍然似乎是一种实现目标的黑客方法。如果我是你,我仍然会尝试将 draw() 函数与实际逻辑分开。让您的模拟设置变量,并让 draw() 函数使用这些变量。这是解决这个问题的“正确”方法。使用 setTimeout() 函数目前可以工作,但我猜它会在以后给您带来问题。

Kevin Workman
2017-03-01

根据文档,您需要避免直接调用“draw()”。请尝试以下方法:

customLoop = function(){
    while(somethingIsTrue){
        //custom code here
        redraw();  // Causes the code inside "draw()" to execute once
    }
}

https://p5js.org/reference/#/p5/draw

具体来说: draw() 是自动调用的,不应显式调用。 它应始终使用 noLoop()、redraw() 和 loop() 进行控制。

此外,由于 JavaScript 是单线程的,只要 while 循环在运行,其他任何操作都无法执行。以下是相关文章:

while 循环中的异步函数

以及一个详尽的解释:

http://blog.carbonfive.com/2013/10/27/the-javascript-event-loop-explained/

您可以尝试类似以下操作:

var timerId = 0;

timerId = setTimeout(myCustomRedraw(), 10); // Every 10 milliseconds.
myCustomRedraw = function () {
    // custom code here
    if (<stop loop condition>) {
        clearTimeout(timer); // Will stop the timeout loop.
    }
    redraw()
}
Courtney G
2017-03-01