基于磁贴的游戏加载Firebase的速度太慢

时间:2016-01-02 04:38:51

标签: swift firebase

我正在使用swift和Firebase为iOS构建一个基于图块的2D游戏。因为世界很大,我设计了它,所以我只订阅屏幕上的瓷砖。也就是说,我不是为所有10,000x10,000个磁贴添加监听器,而是将它们添加到屏幕上的磁贴上。当玩家移动时,我取消注册旧听众并注册新听众。我在屏幕边缘添加了一点缓冲区,希望在屏幕上移动时所有内容都能充分加载。不幸的是,Firebase通常存在足够的延迟,因为这种策略根本不起作用。在次优的互联网连接上,可以继续走进“卸载世界”,#34;有时需要几秒钟来装载丢失的瓷砖。

但事情是这样的:同一连接和同一设备上的其他MMO iOS游戏工作正常。这不是一个糟糕的联系。这让我怀疑我的实现,或Firebase本身是错误的。

从根本上说,我等待#34;加载一次"每次迈出一步,大约20个瓷砖的事件。一步需要大约1/4秒,所以每一秒我都要从Firebase请求大约100个项目。不过,我无法想到更好的方法。 Firebase文档表明这应该不是问题,因为它是所有一个套接字连接。我可以"桶"将对象分成10x10块,这意味着我订阅了更少的对象,但这在总数据传输方面也会更浪费。如果套接字连接真正优化,总数据传输应该是唯一的瓶颈,这意味着这种策略是错误的。

修改

这是一段展示其运作方式的视频。缓冲区大小已减少到-1,因此您可以轻松查看屏幕边缘和瓷砖加载和卸载。在视频结束时,滞后袭击,我徘徊在空虚中。我打开了另一个游戏,它几乎立即加载。 http://forevermaze.inzania.com/videos/FirebaseLag.mov nb,我在再次加载屏幕之前结束了录制。它永远不会加载,所以它不像代码无法正常工作。它的纯滞后

以下是我用来加载图块的代码。它为每个瓷砖调用一次。正如我所说的,这意味着每个步骤并行调用此代码大约20次。所有其他应用程序都以良好的速度运行,没有延迟。我在东京使用LTE连接的MiFi,因此它是一个可靠的连接。

  /**
   * Given a path to a firebase object, get the snapshot with a timeout.
   */
  static func loadSnapshot(firebasePath: String!) -> Promise<FDataSnapshot?> {
    let (promise, fulfill, _) = Promise<FDataSnapshot?>.pendingPromise()
    let connection = Firebase(url: Config.firebaseUrl + firebasePath)
    connection.observeSingleEventOfType(.Value, withBlock: { snapshot in
      if !promise.resolved {
        fulfill(snapshot)
      }
    })
    after(Config.timeout).then { () -> Void in
      if !promise.resolved {
        DDLogWarn("[TIMEOUT] [FIREBASE-READ] \(firebasePath)")
        fulfill(nil)
        //reject(Errors.network)
      }
    }
    return promise
  }

瓷砖位于[ROOT]/tiles/[X]x[Y]。大多数图块包含非常少的数据,但是如果该图块上存在对象(即,其他玩家)则存储这些对象。这是来自Firebase的屏幕截图: enter image description here

EDIT2

根据请求,我已经非常简单地重新创建了这个问题。这是一个100行XCTestCase类:http://forevermaze.com/code/LagTests.swift

用法:

  1. 将文件放入Swift项目(它应该是独立的,只需要Firebase)
  2. firebaseUrl的值更改为您的根网址(即https://MyProject.firebaseio.com
  3. 运行testSetupDatabase()功能测试一次以设置数据库的初始状态
  4. 运行testWalking()功能以测试延迟。这是主要的考验。如果任何瓷砖加载时间超过2秒,它将失败。
  5. 我已经在几个不同的连接上尝试过这个测试。一流的办公室连接没有问题,但即使是高端的LTE或MiFi连接也会失败。 2 seconds已经是非常长的超时,因为它意味着我需要一个10 tile缓冲区(0.2秒* 10个tile = 2秒)。当我连接到LTE连接时,这是一些输出,显示加载磁贴花了将近10秒钟(!!): error: -[ForeverMazeTests.LagTests testWalking] : XCTAssertTrue failed - Tile 2x20 took 9.50058007240295

1 个答案:

答案 0 :(得分:1)

我进行了一些测试,当我通过3G连接进行测试时,加载在15-20秒内完成。在我的常规连接中,它需要1-2秒,因此差异可能完全基于带宽。

我将您的测试用例重写为JavaScript版本,因为我很难弄清楚发生了什么。在这里找到我的:http://jsbin.com/dijiba/edit?js,console

var ref = new Firebase(URL);
var tilesPerStep = 20;
var stepsToTake = 100;

function testWalking() {
  var startTime = Date.now();
  var promises = [];
  for (var x=0; x < stepsToTake; x++) {
    promises.push(testStep(x));
  }
  Promise.all(promises).then(function() {
    console.log('All '+promises.length+' steps done in '+(Date.now()-startTime)+'ms');
  });
}

function testStep(x) {
  var result = new Promise(function(resolve, reject){
    var tiles = ref.child("/tiles_test");
    var loading = 0;
    var startTime = Date.now();
    console.log('Start loading step '+x);

    for (var y=0; y < tilesPerStep; y++) {
      loading ++;
      tiles.child(x+'x'+y).once('value', function(snapshot) {
        var time = Date.now() - startTime;
        loading--;
        if (loading === 0) {
          console.log('Step '+x+' took '+(Date.now()-startTime)+'ms');
          resolve(Date.now() - startTime);
        }
      });
    }
  });
  return result;
}

testWalking();

最大的区别在于我没有延迟开始任何加载,而且我没有因特定磁贴而失败。我认为最后一点是你的测试失败的原因。

Firebase的所有加载都是异步发生的,但所有请求都是通过相同的连接进行的。当您开始加载时,您正在排队很多请求。这个时间因先前尚未履行的请求而被扭曲了。

这是一个只有10个步骤的测试运行示例:

"Start loading step 0"
"Start loading step 1"
"Start loading step 2"
"Start loading step 3"
"Start loading step 4"
"Start loading step 5"
"Start loading step 6"
"Start loading step 7"
"Start loading step 8"
"Start loading step 9"
"Step 0 took 7930ms"
"Step 1 took 7929ms"
"Step 2 took 7948ms"
"Step 3 took 8594ms"
"Step 4 took 8669ms"
"Step 5 took 9141ms"
"Step 6 took 9851ms"
"Step 7 took 10365ms"
"Step 8 took 10425ms"
"Step 9 took 11520ms"
"All 10 steps done in 11579ms"

您可能会注意到,每个步骤所花费的时间并不会累计所有步骤合并所需的时间。基本上,当管道中仍有请求时,您正在启动每个请求。这是加载这些项目的最有效方式,但它确实意味着您需要以不同方式衡量效果。

基本上所有步骤几乎同时开始。然后,您等待第一个响应(在上述情况下包括建立从客户端到正确的Firebase服务器的WebSocket连接),之后响应以合理的间隔进行(假设每个步骤有20个请求) )。

所有这些都非常有趣,但它当然不会解决您的问题。我建议您将数据建模为屏幕大小的存储桶。因此,不要将每个瓷砖分开,而是将每个10x10瓷砖存储在&#34;桶中#34;。您将减少每个单独请求的开销,并且每10个步骤最多只需要一个桶。

更新

我非常确定我们只是调试基准方法的多个工件。如果我将代码更新为:

func testWalking() {
    let expectation = expectationWithDescription("Load tiles")
    let maxTime = self.timeLimit + self.stepTime * Double(stepsToTake)

    let startTime = NSDate().timeIntervalSince1970

    for (var x=0; x<stepsToTake; x++) {
        let delay = Double(x) * stepTime
        let data = ["x":x, "ex": expectation]
        stepsRemaining++
        NSTimer.scheduledTimerWithTimeInterval(0, target: self, selector: Selector("testStep:"), userInfo: data, repeats: false)
    }
    waitForExpectationsWithTimeout(maxTime) { error in
        let time = NSDate().timeIntervalSince1970 - startTime
        print("Completed loading after \(time)")
        if error != nil {
            print("Error: \(error!.localizedDescription)")
        }
    }
}

/**
* Helper function to test a single step (executes `tilesPerStep` number of tile loads)
*/
func testStep(timer : NSTimer) {
    let tiles = Firebase(url: firebaseUrl).childByAppendingPath("/tiles_test")
    let data = timer.userInfo as! Dictionary<String, AnyObject>
    let x = data["x"] as! Int
    let expectation = data["ex"] as! XCTestExpectation
    var loading = 0
    print("Start loading \(x)")

    for (var y=0; y<tilesPerStep; y++) {
        loading++
        tiles.childByAppendingPath("\(x)x\(y)").observeSingleEventOfType(.Value, withBlock: { snapshot in
            loading--
            if loading == 0 {
                print("Done loading \(x)")
                self.stepsRemaining--
                if self.stepsRemaining == 0 {
                    expectation.fulfill()
                }
            }
        })
    }
}

它可以在不到2秒的时间内通过高速网络完成整个加载,而在3G上则需要15到25秒。

但我建议的建模水平超过每个单一的图块仍然存在。