Firebase too slow to load for a tile-based game - swift

I'm building a 2d tile-based game for iOS with swift and Firebase. Because the world is large, I've designed it so that I only subscribe to the tiles that are on screen. That is, instead of adding listeners for all 10,000x10,000 tiles, I add them to just the tiles on screen. As the player moves, I de-register the old listeners and register the new ones. I've added a bit of a buffer zone around the edge of the screen, in the hopes that everything will be sufficiently loaded by the time it moves on screen. Unfortunately, there is often significant enough lag from Firebase that this strategy simply isn't working. On sub-optimal internet connections, it's possible to keep walking into the "unloaded world," taking several seconds at times to load the missing tiles.
Here's the thing, though: other MMO iOS games on the same connection and same device work fine. It's not a terrible connection. Which makes me suspect my implementation, or Firebase itself is at fault.
Fundamentally I'm waiting on the "load once" event for about 20 tiles each time I take a step. A step takes about 1/4 of a second, so each second I'm requesting about 100 items from Firebase. I can't think of a better way, though. Firebase documentation suggests that this should not be a problem, since it's all one socket connection. I could "bucket" the objects into, say, 10x10 blocks which would mean that I'd subscribe to fewer objects, but this would also be more wasteful in terms of total data transfer. If the socket connection is truly optimized, total data transfer should be the only bottleneck, implying this strategy would be wrong.
edit
Here's a video showing how it works. The buffer-size has been reduced to -1, so that you can easily see the edges of the screen and the tiles loading and unloading. Near the end of the video, lag strikes and I wander into the emptiness. I opened up another game and it loaded almost instantly. http://forevermaze.inzania.com/videos/FirebaseLag.mov (n.b., I ended the recording before the screen loaded again. It never fails to load, so it's not as if the code is failing to work. It's pure lag.)
Here is the code I'm using to load the tiles. It's called once for each tile. As I said, this means that this code is called about 20 times per step, in parallel. All other apps are running at a fine speed with no lag. I'm on a MiFi with LTE connectivity in Tokyo, so it's a solid connection.
/**
* 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
}
The tiles reside at [ROOT]/tiles/[X]x[Y]. Most tiles contain very little data, but if there are objects on that tile (i.e., other players) those are stored. Here's a screenshot from Firebase:
edit2
Per request, I've recreated this issue very simply. Here is a 100-line XCTestCase class: http://forevermaze.com/code/LagTests.swift
Usage:
Drop the file into your Swift project (it should be stand-alone, requiring only Firebase)
Change the value of firebaseUrl to your root URL (i.e., https://MyProject.firebaseio.com)
Run the testSetupDatabase() function test once to setup the initial state of the database
Run the testWalking() function to test the lag. This is the main test. It will fail if any tile takes longer than 2 seconds to load.
I've tried this test on several different connections. A top-notch office connection passes with no problem, but even a high-end LTE or MiFi connection fails. 2 seconds is already a very long timeout, since it implies that I need to have a 10 tile buffer zone (0.2 seconds * 10 tiles = 2 seconds). Here's some output when I'm connected to a LTE connection, showing that it took nearly 10 seconds (!!) to load a tile:
error: -[ForeverMazeTests.LagTests testWalking] : XCTAssertTrue failed - Tile 2x20 took 9.50058007240295

I ran a few tests and the loading completes in 15-20 seconds when I test over a 3G connection. Over my regular connection it takes 1-2 seconds, so the difference is likely purely based on bandwidth.
I rewrote your test case into a JavaScript version, because I had a hard time figuring out what's going on. Find mine here: 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();
The biggest difference is that I don't delay starting any of the loading and I don't fail for a specific tile. I think that last bit is why your tests are failing.
All loading from Firebase happens asynchronously, but all requests are going through the same connection. When you start loading, you are queueing up a lot of requests. This timing is skewed by "preceding requests that haven't been fulfilled yet".
This is a sample of a test run with just 10 steps:
"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"
You'll probably note that the time taken for each step does not add up to the time taken for all steps combined. Essentially you are starting each request while there are still requests in the pipeline. This is the most efficient way of loading these items, but it does mean that you'll need to measure performance differently.
Essentially all steps start at almost the same time. Then you're waiting for the first response (which is in the above case includes establishing a WebSocket connection from the client to the correct Firebase server) and after that the responses come in reasonable intervals (given that there are 20 requests per step).
All of this is very interesting, but it doesn't solve your problem of course. I would recommend that you model your data into screen-sized buckets. So instead of having each tile separate, store every 10x10 tiles in a "bucket". You'll reduce the overhead of each separate request and only need to request at most one bucket for every 10 steps.
Update
I'm pretty sure we're just debugging multiple artifacts of your benchmark approach. If I update the code to this:
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()
}
}
})
}
}
It completes the entire loading in less than 2 seconds over a high-speed network, over 3G it takes between 15 and 25 seconds.
But my recommendation of modeling at a level of more than each single tile remains.

Related

GKLeaderboard's localPlayerScore delay after saving a GKScore

I'm trying to get a user's new leaderboard rank after scoring a high score and I found it takes about 5 seconds for the GKLeaderboard scores to update. I've tested the code against a Release build (from Xcode) and the delay is still there.
let score = GKScore(leaderboardIdentifier: leaderboardId)
score.value = Int64(highScore)
GKScore.report([score]) { _ in
// Adding a 5 second delay here solves the problem.
let leaderboard = GKLeaderboard()
leaderboard.identifier = leaderboardId
leaderboard.loadScores { _, _ in
// leaderboard.localPlayerScore shows data from before saving the new score.
}
}
Is there a way around this? The 5-second delay seems flaky. Maybe once the app is in the App Store this delay is no longer there?
I could probably create a workaround by storing the scores before the user plays the game and locally calculate the new rank but the shared code should avoid the need for that, right?

How to use the loop if the track was not started from the beginning (with buffering type = .always in AKPlayer )

I run several AKPlayer with different files and the same length - the first one starts from the beginning, the others start by the button relative to the time of the first. Files are seamless, worth buffering type = .always, loop = true. If AKPlayer did not start from the beginning, then only that part of the track from which the track began to play to the end, and when the loop occurs, starts StartTime is not a zero value. It is necessary that when a loop is played all the tracks from the beginning.
With AKWaveTable, everything works adequately, but there are no pan and pitch settings in the player.
Here is a sample code. In the original, I use an array of players and in a loop I upload all the files to my players. I’m doing something like drummachine - several audio files are playing, and I can turn on / off other files in parallel with respect to the time the player was first started. Each file is the same in duration. When I do the same in AKWaveTable, then everything works correctly, but in the future it does not suit me.
In this example, I run the playPlayer1 method first and after a while I run playPlayer2. When Player1 starts the loop from the beginning, then Player2 starts the loop from the previous currentTime (for example, from the middle of the file) and its length becomes equal forever (endTime - currentTime)
var player1: AKPlayer!
var player2: AKPlayer!
var playersMixer: AKMixer!
init() {
do {
let file1 = try AKAudioFile(readFileName: "Audio/file1.m4a")
let file2 = try AKAudioFile(readFileName: "Audio/file2.m4a")
player1.load(audioFile: file1)
player2.load(audioFile: file2)
} catch {
print(error.localizedDescription)
}
player1.buffering = .always
player1.isLooping = true
player2.buffering = .always
player2.isLooping = true
playersMixer = AKMixer (player1,player2)
AudioKit.output = playersMixer
do {
try AudioKit.start()
} catch {
print(error.localizedDescription)
}
}
func playPlayer1() {
player1.play()
}
func playPlayer2() {
player2.play(from: currentTime)
}
var currentTime: Double {
get {
if player1.isPlaying {
return player1.currentTime
}
return 0
}
}
I think that's a missing feature in AudioKit or a bug. Meanwhile, the current version does use the startTime for the loop points as we can see in the source code (https://github.com/AudioKit/AudioKit/blob/master/AudioKit/Common/Nodes/Playback/Players/Player/AKPlayer.swift)
Meanwhile, there's a solution I found and hope it's useful for other readers:
player.isLooping = false
player.buffering = .dynamic
player.completionHandler = {
player.isLooping = true
player.buffering = .always
player.play(from: 0, to: player.duration)
}
player.play(from: masterPlayer.currentTime)
What I'm doing is to create a player and set .isLooping and .buffering falsely and call .play to run it as a one-time, or a single shot play! A .completionHandler callback is called on complete, where I set .isLooping and .buffering positively. Finally, call .play setting the desired loop points using the properties from and to from self. The masterPlayer refers to a separate player that is used as a reference to get the current play position, I know it's easy to understand but just to avoid assumptions.
EDIT:
After going throw tests, I found an issue that I've reported and deleted before (Is synchronised looping supported for AKPlayers that are multiples in their duration?) and had to reopen, so hopefully, I get an answer and will help understand if it's supposed to work or not.
Also forced player to detach and created a new one:
player.completionHandler = {
player.detach()
let player = self.createPlayer(audioFile: audioFile)
self.connectPlayerToMixer(player)
player.isLooping = true
player.buffering = .always
player.startTime = 0
player.endTime = audioFile.duration
player.play(at: AVAudioTime.now())
}
I've now tested AKWaveTable and hopefully, I did it correctly but it also fails just after the recording that is twice the size of previews recording, for example:
Loop1 > 1 2 3 4
Loop2 > 1 2 3 4
Loop3 > 1 2 3 4 5 6 7 8
Loop4 > 1 2 3 4
Loop5 > 5 6 7 8
(recorded as 1 2 3 4 5 6 7 8, but plays back as 5 6 7 8)
I'll have to go back and test the original AKNodeRecorder to see if the behaviour is similar, but for the moment I'll try another tests with AKWaveTable.
After some time testing * *
Here's the audio recording describing the issue above:
https://drive.google.com/open?id=1zxIJgFFvTwGsve11RFpc-_Z94gEEzql7
After looking at the problem for some time, and all the research I found what seems to be a solution. By shifting the procedure that is used! Instead of setting the player to start along the current time, should instead schedule it to start from the start time. This because the start time is what is used for the loop start and endpoints! To learn a bit more follow the link to another StackOverflow post I've posted before (Solution is presented here (Is synchronised looping supported for AKPlayers that are multiples in their duration?), thanks!

Occasional stuttering with SceneKit when touching the screen

When I interact with the screen the objects in my game start to stutter. My FPS is at 60 and doesn't drop but the stuttering is still prevalent. I believe my problem is how I'm animating the objects on screen(code below).If anybody could help I would appreciate it.
I have an x amount of nodes inside an array called _activePool. In the Update function I am moving the nodes x position inside _activePool, adding new nodes when the last node in _activePool position is <= 25 and removing the first node in _activePool if it's position is <= -25.
if _cycleIsActive{
for obj in _activePool{
//move the obj in _activePool
obj.position.x += Float(dt * self.speedConstant);
}
let lastObj = _activePool.last;
if (lastObj?.position.x)! + getWidthOfNode(node: lastObj!) + Float(random(min: 15, max: 20)) <= 25{
// get new obj(pattern) and add to _activePool
self.getPatternData(sequencePassedIn: selectedSeq, level: self._currentLevel, randomPattern: randomPattern());
}
let firstObj = _activePool.first;
if (firstObj?.position.x)! + getWidthOfNode(node: firstObj!) <= -25{
// remove object and return to specific pool
firstObj?.removeFromParentNode();
returnItems(item: firstObj!);
_activePool.removeFirst();
}
}
I create several arrays and add them to a dictionary
func activatePools(){
temp1Pool = ObjectPool(tag: 1, data: []);
dictPool[(temp1Pool?.tag)!] = temp1Pool;
temp2Pool = ObjectPool(tag: 2, data: []);
dictPool[(temp2Pool?.tag)!] = temp2Pool;
for i in 0... dictPool.count {
obstacleCreationFactory(factorySwitch: i);
}
}
Creating my obstacles(enemies)
func obstacleCreationFactory(factorySwitch: Int){
Enemies = Enemy();
switch factorySwitch {
case 0:
for _ in 0...100{
let blueEnemy = Enemies?.makeCopy() as! Enemy
blueEnemy.geometry = (Enemies?.geometry?.copy() as! SCNGeometry);
blueEnemy.geometry?.firstMaterial?.diffuse.contents = UIColor.blue;
blueEnemy.tag = 1;
temp1Pool?.addItemToPool(item: blueEnemy);
}
case 1:
for _ in 0...100{
let redEnemy = Enemies?.makeCopy() as! Enemy
redEnemy.geometry = (Enemies?.geometry?.copy() as! SCNGeometry);
redEnemy.geometry?.firstMaterial?.diffuse.contents = UIColor.red;
redEnemy.tag = 2;
temp2Pool?.addItemToPool(item: redEnemy);
}
default:
print("factory error");
}
}
Without being able to look at the rest of your code base it’s really difficult to guess what would be causing your issue.
If somewhere you are creating a ton of temporary objects in a loop somewhere, you might consider creating a local autorelease pool to prevent memory spikes. Here is a good article that describes why in some situations it’s a good idea.
You could also be calling some particularly expensive functions on a timer or something. It’s difficult to say.
In short, you should consider using Xcode’s Profiling tools (called Instruments). Specifically I would recommend using Time Profiler to examine what functions are taking the most time and causing those spikes.
Here is a great WWDC session video that shows how you can use the time profiler, I’d recommend regularly profiling your app, especially when you have an issue like this.

AudioKit tap skips over time intervals

I am building an app that uses microphone input to detect sounds and trigger events. I based my code on AKAmplitudeTap, but I when I ran it, I found that I was only obtaining sample data for intervals with missing sections.
The tap code looks like this (with the guts ripped out and simply keeping track of how many samples would have been processed):
open class MyTap {
// internal let bufferSize: UInt32 = 1_024 // 8-9 kSamples/sec
internal let bufferSize: UInt32 = 4096 // 39.6 kSamples/sec
// internal let bufferSize: UInt32 = 16536 // 43.3 kSamples/sec
public init(_ input: AKNode?) {
input?.avAudioNode.installTap(onBus: 0, bufferSize: bufferSize, format: nil ) { buffer, _ in
sampleCount += self.bufferSize
}
}
I initialize the tap with:
func afterLoad() {
assert(!loaded)
AKSettings.audioInputEnabled = true
do {
try AKSettings.setSession(category: .playAndRecord, with: .allowBluetoothA2DP)
} catch {
print("Could not set session category.")
}
mic = AKMicrophone()
myTap = MyTap(mic) // seriously, can it be that easy?
loaded = true
}
The original tap code was capturing samples to a buffer, but I saw that big chunks of time were missing with a buffer size of 1024. I suspected that the processing time for the sample buffer might be excessive, so...
I simplified the code to simply keep track of how many samples were being passed to the tap. In another part of the code, I simply print out sampleCount/elapsedTime and, as noted in the comments after 'bufferSize' I get different amounts of samples per second.
The sample rate converges on 43.1 KSamples/sec with a 16K buffer, and only collects about 20% of the samples with a 1K buffer. I would prefer to use the small buffer size to obtain near real-time response to detected sounds. As I've been writing this, the 4K buffer version has been running and has stabilized at 39678 samples/sec.
Am I missing something? Can a tap with a small buffer size actually capture 44.1 Khz sample data?
Problem resolved... the tap requires this line of code
buffer.frameLength = self.bufferSize
... and suddenly all the samples appear. I obviously stripped out a bit too much code from the code I obviously didn't understand.

iMac freezes after while loop

I'm creating a game in xcode. The winner will have a negative score or 0. Everything went well but now I want those negative points to be added to his/hers opponent. I used this code:
while (activePlayer.score < 0) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.activePlayer.score += 1
self.activePlayer.scoreLabel.text = String (self.activePlayer.score)
self.notActivePlayer.score += 1
self.notActivePlayer.scoreLabel.text = String (self.notActivePlayer.score)
}
}
When I now run the Simulator it freezes when it comes to this part. The worst thing is my whole iMac freezes. It's becoming so extremely slow I have to wait like 10 minutes to close the simulator and getting some speed back.
My simple conclusion is this code is wrong. But why?
I want to player to see the score change that's why the label text will be updated after every point added to the score.
Your while loop is a very bad idea there. What do you expect it to do? On first iteration you schedule an asnyc task, then the iteration is complete and the next task is scheduled, etc.
You will have a couple of thousand async task scheduled in the first split second the loop is running.
If you want to animate the change you should do it by scheduling the next task after the first finished. The following is a general way of doing that, I have not run it in Playground:
func schedule() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.activePlayer.score += 1
self.activePlayer.scoreLabel.text = String (self.activePlayer.score)
self.notActivePlayer.score += 1
self.notActivePlayer.scoreLabel.text = String (self.notActivePlayer.score)
if (activePlayer.score < 0) {
self.schedule()
}
}
}
As Whirlwind correctly pointed out using dispatch in sprite-kit makes you break out of the present game-loop. You can do the same thing via SKActions and repeatedly create new actions after one completed.