swift – 基于磁贴的游戏加载Firebase太慢了

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

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

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

编辑

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

这是我用来加载磁贴的代码.每个瓷砖都会调用一次.正如我所说的,这意味着每个步骤并行调用此代码大约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的截图:
《swift – 基于磁贴的游戏加载Firebase太慢了》

EDIT2

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

用法:

>将文件放入Swift项目(它应该是独立的,只需要Firebase)
>将firebaseUrl的值更改为根URL(即https://MyProject.firebaseio.com)
>运行testSetupDatabase()函数测试一次以设置数据库的初始状态
>运行testWalking()函数来测试滞后.这是主要的考验.如果任何瓷砖加载时间超过2秒,它将失败.

我在几个不同的连接上尝试过这个测试.一流的办公室连接没有问题,但即使是高端的LTE或MiFi连接也会失败. 2秒已经是一个非常长的超时,因为它意味着我需要一个10瓦片缓冲区(0.2秒* 10瓦片= 2秒).当我连接到LTE连接时,这是一些输出,显示加载磁贴花了将近10秒钟(!!):
错误: – [ForeverMazeTests.LagTests testWalking]:XCTAssertTrue失败 – Tile 2×20花了9.50058007240295

最佳答案 我运行了一些测试,当我通过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个请求).

所有这些都非常有趣,但它当然不能解决您的问题.我建议您将数据建模为屏幕大小的存储桶.因此,不是将每个瓷砖分开,而是将每个10×10瓷砖存储在“桶”中.您将减少每个单独请求的开销,并且每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秒.

但我建议的建模水平超过每个单一的瓷砖仍然存在.

点赞