Firebase JavaScript + P5.js:异步函数阻碍了画布的重绘

6
我正在尝试创建一个应用程序,通过读取 Firebase 数据库中存储的圆的 x 和 y 坐标来在画布上绘制圆。然而,执行下面的代码只会简单地产生没有任何圆形的结果,因为函数 drawCircles 是异步运行的,因此命令 background(40) 在圆形被绘制之前清除了所有内容。
以下是我的代码:
function setup() {
    createCanvas(windowWidth, windowHeight); 
    background(40); 
    stroke(80); 
    smooth();
    frameRate(60);
}

function drawCircles() {
    firebase.database().ref("circles").once("value", function(snapshot) {
        var snapshotVal = snapshot.val();
        var circleCount = snapshotVal.numCircles;

        for (var j = 0; j < circleCount; j++) {
            firebase.database().ref("circles" + j).once("value", function(snapshot) {
                var snapshotValue = snapshot.val();
                fill(143, 2, 2);
                ellipse(snapshotValue.xPos, 50, 50);
            });
        }
    });
}

function draw() {
    stroke(80);
    background(40);

    stroke(0);
    drawCircles(); 
}

1
请尝试将您的问题分解成较小的部分,并一次只处理一个部分。例如,您能否编写一个简单的程序从Firebase检索数据?在继续之前完美地解决这个问题。除此之外,您可以创建一个显示一些硬编码点的程序吗?在将它们合并为一个程序之前,完美地解决这个问题。如果您在特定步骤上遇到困难,请发布一个[mcve]仅涉及该步骤。祝你好运。 - Kevin Workman
1
@KevinWorkman,我已经缩小了问题范围,发现是因为background(40)清除了屏幕上绘制的所有圆形,因为drawCircles()是异步运行的。 - Adam Lee
3个回答

2
看起来您的问题只是每秒60帧,这导致了一个竞争条件。Firebase .once 在完成获取后异步执行,而P5不会等待它获取,因为它会遵循其帧速率定时。
在这种特定情况下,我有多个建议,希望能让您接近所需的结果。 1- 重新构造您的代码 当前代码结构存在两个问题。
Case 1:根据您的当前代码,我认为您的圆圈在数据库中实时更新,并且您需要保持最新状态,因此您一直在获取它们的最新位置。如果是这种情况,则应使用.on("value")而不是.once("value"),并让Firebase在圆圈更改时向您发送更新,而不是请求60次/秒以节省往返请求时间。如果是这种情况:请参见下面的解决方案1
Case 2:如果您的圆圈在数据库中不是实时更新的,而您只想要整个圆圈列表,则每秒60次获取列表没有任何意义。相反,您应该在设置时使用.once获取列表,稍后在draw()中遍历该列表。请参见下面的解决方案22- 重新构造您的数据库 在任何情况下,您当前的数据库模型都要求您保持循环获取。这意味着您正在进行与circleCount相同的请求。这对于您的使用情况来说是不好的,因为每个请求需要额外的往返时间,我们试图减少它所需的时间,使其更接近实时。 (或匹配帧速率) 目前,您的圆圈似乎保存为根目录下的circles1 circles2等,因为您使用.ref("circles" + j)检索它们。将其设置为这样: .ref("circles/" + j),这将意味着每个circle现在都保存在circles中。例如: circles/circle1 circles/circle2等。
这样做的好处是现在您不需要额外的请求来获取所有圆圈。Firebase具有非常方便的功能,如forEach,可以通过单个请求遍历所有子项。 3- 在Firebase回调中清除背景 目前,您以特定帧率的方式清除背景。这意味着,如果每个firebase调用所需的时间超过1/60秒(16毫秒),则您将清除背景并继续进行。即使我们结构化数据库,您实现此速度的机会也非常低。因此,我建议首先使用30fps,这也将减少您对firebase的调用次数为每秒30次。 解决方案1 如果您的圆圈在数据库中更新(例如由其他游戏玩家或其他人更新),并且您希望您的代码始终显示最新的xPos。
var latestCirclePositionsSnapshot;

function setup() {
  createCanvas(windowWidth, windowHeight); 
  background(40); 
  stroke(80); 
  smooth();
  frameRate(60);

  firebase.database().ref("circles").on("value", function(snapshot) {
    // got a new value from database, so let's save this in a global variable. 
    latestCirclePositionsSnapshot = snapshot;
    // we will keep drawing this update until we get a new one from the database.
  });
}

function draw() {
  drawCircles(); 
}

function clearBackground () {
  stroke(80);
  background(40);
}

function drawCircles() {
  clearBackground();
  stroke(0);  
  latestCirclePositionsSnapshot.forEach(function(circleSnapshot) {  
    // circleData will be the actual contents of each circle
    var circleData = circleSnapshot.val();
    fill(143, 2, 2);
    ellipse(circleData.xPos, 50, 50);
  });
}

基本上,这将保持绘制我们从firebase获取到的最后一个圆形位置,直到我们得到一个新的位置。(因此,P5将以60fps刷新,但是您的firebase更新将根据firebase运行和从firebase获取等实时情况进行。)

解决方案2

如果您的数据库中没有实时更新,并且您只想通过一次从Firebase获取数据来绘制圆圈(例如,基于某些数据绘制一些点),

var circlePositions;
var gotPositions = false;

function setup() {
  createCanvas(windowWidth, windowHeight); 
  background(40); 
  stroke(80); 
  smooth();
  frameRate(60);

  firebase.database().ref("circles").once("value", function(snapshot) {
    // got the circle values from the database
    // let's store them and we'll keep drawing them forever. 
    circlePositions = snapshot;
    gotPositions = true;
  });
}

function draw() {
  drawCircles(); 
}

function clearBackground () {
  stroke(80);
  background(40);
}

function drawCircles() {
  clearBackground();
  stroke(0); 

  if (gotPositions) {
    circlePositions.forEach(function(circleSnapshot) {  
      // circleData will be the actual contents of each circle
      var circleData = circleSnapshot.val();
      fill(143, 2, 2);
      ellipse(circleData.xPos, 50, 50);
    });
  } else {
    // Display some text here like "LOADING DATA FROM SERVERS..." 
  }
}

希望这些可以帮到您 :) 很高兴看到另一个Processing和Firebase的粉丝。

0
问题不在于drawCircles()是异步的,而在于draw()frameRate()调用,并且background()每秒钟的一小部分清除屏幕上的纯色,当循环绘制时:请参见draw referencebackground。如果您从draw()中删除background(40)这行,则它将不会在每帧清除屏幕,绘制的圆将累积。这比每帧重新绘制所有Firebase数据要简单得多。
以下草图演示了这个概念:background()仅在setup()期间调用,而不是在draw()期间调用,因此屏幕区域被着色一次,然后逐渐被覆盖以累积圆形。

function setup() {
  createCanvas(400, 200);
  frameRate(5)
  background(40);
}
function drawCircles() {
  fill(143, 2, 2);
  ellipse(random(width), 50, 50);
}
function draw() {
  // background(40);
  drawCircles();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.16/p5.js"></script>
<html>
  <head>
  </head>
  <body>
  </body>
 </html>

如果您想在每帧逐渐累加一些元素并清除其他元素,则需要将这些圆圈累加到 createGraphics 缓冲区上。 然后,在每帧重新绘制该圆圈缓冲区,然后在其上绘制短暂的元素(如鼠标指示器等)。
以下是一个例子:每帧使用 background() 清除画布,然后绘制 pg 缓冲区,并在鼠标位置绘制一个白色圆形。由于 background 清除屏幕,因此白色圆形不会在帧之间留下轨迹 - 但是红色圆圈被绘制到未清除的图形缓冲区中,因此它们会保留下来。

var pg;
function setup() {
  createCanvas(400, 200);
  pg = createGraphics(400, 200);
  background(40);
}
function drawCircles() {
  pg.fill(143, 2, 2);
  pg.ellipse(random(pg.width), 50, 50);
}
function draw() {
  background(40);
  drawCircles();
  image(pg,0,0);
  fill(255);
  ellipse(mouseX,mouseY,50,50);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.16/p5.js"></script>
<html>
  <head>
  </head>
  <body>
  </body>
</html>


0

我看了一下文档,这里有一个他们建议如何处理获取到的数据的示例。在你的情况下,尝试将获取和绘制分开,使用一些全局变量缓存你的数据:

var circles = [];

function fetchData() {
    firebase.database().ref("circles").once("value",
    function(snapshot) {
        var snapshotVal = snapshot.val();
        var circleCount = snapshotVal.numCircles;
        
        circles = [];

        for (var j = 0; j < circleCount; j++) {
            firebase.database().ref("circles" + j).once("value",                 function(snapshot) {
                circles.push(snapshot.val());
            });
        }
    });
}

function setup() {
    createCanvas(windowWidth, windowHeight); 
    background(40); 
    stroke(80); 
    smooth();
    frameRate(60);
    fetchData();
}

function drawCircles() {
    circles.forEach(function (snapshotValue) {
        var snapshotValue = snapshot.val();
        fill(143, 2, 2);
        ellipse(snapshotValue.xPos, 50, 50);
    });
}

function draw() {
    stroke(80);
    background(40);

    stroke(0);
    drawCircles(); 
}

如果您需要始终显示相关数据,请尝试使用setInterval调用fetchData函数,例如:

function setup() {
      createCanvas(windowWidth, windowHeight); 
      background(40); 
      stroke(80); 
      smooth();
      frameRate(60);
      setInterval(fetchData, 5000); //will call fetchData every 5000 ms
  }


我尝试了你的代码,但不幸的是圆圈仍然不可见。 - Adam Lee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接