Firebase Javascript + P5.js:异步函数妨碍重绘 canvas

Firebase Javascript + P5.js: Asynchronous function getting in the way of redrawing the canvas

我正在尝试创建一个应用程序,通过从存储圆圈 x 和 y 坐标的 Firebase 数据库中读取信息,在 canvas 上绘制圆圈。然而,执行下面的代码,什么也没有产生,没有任何圆圈的符号,因为函数 drawCricles 是异步运行的,因此命令 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(); 
}

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

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
  }

问题不在于 drawCircles() 是异步的——问题在于 draw()frameRate() 处被调用,并且 background() 以纯色清除屏幕绘制循环时的每一分之一秒:参见 draw reference and background。如果从 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 缓冲区上积累你的圈子。每一帧都将圆圈缓冲区重绘到 canvas 上,然后在顶部绘制临时元素(如鼠标指示器等)。

这里有一个例子:canvas的每一帧都用background()清除,然后pg缓冲区被绘制到canvas上,然后一个白色的圆圈是在鼠标上绘制。因为背景会清除屏幕,所以白色圆圈不会在帧与帧之间留下痕迹 -- 但红色圆圈会绘制到未清除的图形缓冲区,因此它们会持续存在。

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>

您的问题似乎只是每秒 60 帧,这导致了竞跑情况。 Firebase .once 将在完成抓取后执行异步操作,而 P5 不会等待它抓取,因为它会坚持其帧率计时。

在这个具体案例中,我有多个建议,希望能让您非常接近您想要的结果。

1 - 重组代码

您当前的代码结构存在两个问题。

  • 案例 1:您当前的代码会让我认为您的圈子在数据库中实时更新,您需要保持最新状态,所以您不断获取他们的最新位置。如果是这种情况,您应该使用 .on("value") 而不是 .once("value") 并让 firebase 在圆圈发生变化时向您发送更新,而不是每秒询问 60 次以节省往返请求时间。如果是这种情况:请参阅下面我的 解决方案 1

  • 情况 2:如果你的圈子在数据库中没有实时更新,而你只想要整个圈子列表,那么你每秒获取列表 60 次原因。您应该改为在设置时使用 .once 获取列表,然后在 draw() 中遍历该列表。请参阅下面的解决方案 2

2 - 重构你的数据库

在任何一种情况下,您当前的数据库模型都要求您在循环中不断获取数据。这意味着您提出的请求与 circleCount 一样多。这对您的使用不利,仅仅是因为每个请求都需要额外的行程时间,而我们正在努力减少所花费的时间,以便更接近实时。 (或匹配帧率)

目前您的圈子似乎在 root 中保存为 circles1 circles2 等,因为您正在使用 .ref("circles" + j) 检索它们.这样你就可以像这样保存你的圈子:.ref("circles/" + j) 这意味着每个 circle 现在都保存在 circles。像 circles/circle1 circles/circle2 等等

这样做的好处是,现在您不需要向 firebase 获取所有圈子的额外请求。 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 的另一位粉丝。