iMac freezes after while loop - swift

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.

Related

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!

How to suspend a work item on the main queue

I want to know if it is possible to suspend and then resume a work item on the main queue whilst maintaining the '.asyncAfter' time. If not, is there a workaround to achieve this?
At a certain point, I queue up the following DispatchWorkItem:
dispatchWorkItem = DispatchWorkItem(qos: .userInteractive, block: {
self.view.backgroundColor = UIColor.workoutBackgroundColor
self.runTimer()
self.timerButton.animateableTrackLayer.removeAnimation(forKey: "strokeEndAnimation")
self.isRestState = false
})
I queue this up using:
DispatchQueue.main.asyncAfter(deadline: delayTime, execute: self.dispatchWorkItem))
(delayTime being a parameter to the function)
Now, the problem I am running into is how can I suspend this work item if the user performs a 'pause' action in my app.
I have tried using the DispatchQueue.main.suspend() method but the work item continues to execute after the specified delay time. From what I have read, this method should suspend the queue and this queued work item since it is not being executed. (Please correct me if I am wrong there!)
What I need to achieve is the work item is 'paused' until the user performs the 'resume' action in the app which will resume the work item from where the delay time left off.
This works on background queues that I have created when I do not need to make UI updates; however, on the main queue is appears to falter.
One workaround I have considered is when the user performs the pause action, storing the time left until the work item was going to be executed and re-adding the work item to the queue with that time on the resume action. This seems like a poor quality approach and I feel there is a more appropriate method to this.
On that, is it possible to create a background queue that on execution, executes a work item on the main queue?
Thanks in advance!
On that, is it possible to create a background queue that on execution, executes a work item on the main queue?
You are suggesting something like this:
var q = DispatchQueue(label: "myqueue")
func configAndStart(seconds:TimeInterval, handler:#escaping ()->Void) {
self.q.asyncAfter(deadline: .now() + seconds, execute: {
DispatchQueue.main.async(execute: handler())
})
}
func pause() {
self.q.suspend()
}
func resume() {
self.q.resume()
}
But my actual tests seem to show that that won't work as you desire; the countdown doesn't resume from where it was suspended.
One workaround I have considered is when the user performs the pause action, storing the time left until the work item was going to be executed and re-adding the work item to the queue with that time on the resume action. This seems like a poor quality approach and I feel there is a more appropriate method to this.
It isn't poor quality. There is no built-in mechanism for pausing a dispatch timer countdown, or for introspecting the timer, so if you want to do the whole thing on the main queue your only recourse is just what you said: maintain your own timer and the necessary state variables. Here is a rather silly mockup I hobbled together:
class PausableTimer {
var t : DispatchSourceTimer!
var d : Date!
var orig : TimeInterval = 0
var diff : TimeInterval = 0
var f : (()->Void)!
func configAndStart(seconds:TimeInterval, handler:#escaping ()->Void) {
orig = seconds
f = handler
t = DispatchSource.makeTimerSource()
t.schedule(deadline: DispatchTime.now()+orig, repeating: .never)
t.setEventHandler(handler: f)
d = Date()
t.resume()
}
func pause() {
t.cancel()
diff = Date().timeIntervalSince(d)
}
func resume() {
orig = orig-diff
t = DispatchSource.makeTimerSource()
t.schedule(deadline: DispatchTime.now()+orig, repeating: .never)
t.setEventHandler(handler: f)
t.resume()
}
}
That worked in my crude testing, and seems to be interruptible (pausable) as desired, but don't quote me; I didn't spend much time on it. The details are left as an exercise for the reader!

SKAction Sequencing and Grouping Animations

I'm doing some death animations for a game, and wanted to ask for some help. I want my monster to disappear in a puff of smoke, but not before it animates a slash effect going across his body.
I have 3 animations that I want to use:
weaponSlash - a line that draws across the monster. Looks like you slashed him with a sword.
smoke - a puff of smoke that slowly expands out
monsterFalling - the monster falls back, startled
What I want to do is play it in this order:
Simultaneously, the slash appears & the monster starts to fall back
About 0.25s into the above animation, I want the cloud to start to appear
When the cloud is about to end (so maybe after 1s) I want the monster to disappear
Remove the smoke, the monster, the sword, etc, and drop some coins on the ground
I started like this, as a test that works somewhat: (ignore the above times for now)
//Cancel any current actions, like a monster attacking
monster.removeAllActions()
//since you can't play 3 animations on one node at the same time, you have to create 3 separate nodes for everything
let slash = SKSpriteNode()
let cloud = SKSpriteNode()
cloud.size = monster.size
slash.size = monster.size
monster.addChild(cloud)
monster.addChild(slash)
//Start the slash animation
slash.run(self.player.currentlyEquippedWeapon.attackAnimation())
//Start the cloud animation (how I get it is elsewhere and not relevant)
cloud.run(cloudAnimation)
//Run the monster death animation, followed by the cleanup/coin dropping
monster.run(SKAction.sequence([monster.deathAnimation(), SKAction.wait(forDuration: 1), postDeathActions]))
The variable PostDeathActions above simply removes the monster node and animates some coins falling.
WHERE I NEED SOME HELP
So the above code doesn't work so great in that the animations all run independently of each other. Based on this, you can see how regardless of whether the slash/cloud finish, the monster will run two actions: him falling back, followed by cleanup, which just removes the monster and spawns the coins. As you can see I tried to delay this by adding a 1s delay but this is all somewhat of a hack since I may have different monsters or attacks, etc, that are faster/slower. I'd rather guarantee that everything finishes before I despawn the monster.
I tried to group this into an SKAction.Run like so:
let preDeath = SKAction.run {
[unowned self] in
monster.run(monster.deathAnimation()
slash.run(self.player.currentlyEquippedWeapon.attackAnimation())
cloud.run(cloudAnimation)
}
but this runs everything at the same time again.
What I want to do is sequence it like this (pseudo code):
let preDeathAnimations = SKAction.Group([slash, cloud, monsterDeathAnimation])
])
SKAction.sequence([preDeathAnimations, postDeathActions])
So this way it'll run all 3 before running cleanup.
Is there a way to do something like this? I know Sequnce/Group need to be run against an SKNode, but I don't have 3 separate ones.
Thanks for your time reading this and any advice you can offer!
This is one idea that I had, but you could use threading + state + onCompletion blocks to take the math out of it. I didn't test it out fully but this general concept should work:
let slash = SKAction.fadeIn(withDuration: 0.5)
let fall = SKAction.fadeOut(withDuration: 0.25)
let puff = SKAction.fadeIn(withDuration: 0.1)
// Put in ALL of the actions from ALL parties that you want to happen prior to puff:
func findLongestTime(from actions: [SKAction]) -> TimeInterval {
var longestTime = TimeInterval(0)
for action in actions {
if action.duration > longestTime { longestTime = action.duration }
}
// Note, if you put a sequence into this function I don't know if it will work right..
// Might need another func like `findDurationOfSequence(_ sequence: SKAction) -> TimeInterval
return longestTime
}
// Note, if you have the monster doing more than falling prior to puff, then you will
// need to subtract those as well:
let monsterActionsPriorToPuff = [fall]
// Add the duration of all monster animations prior to puff:
var MAPTP_duration = TimeInterval(0)
for action in monsterActionsPriorToPuff {
MAPTP_duration += action.duration
}
// Calculate our final wait time, with no negative numbers:
var waitTime = findLongestTime(from: [slash, fall]) - MAPTP_duration
if waitTime < 0 { waitTime = 0 }
let wait = SKAction.wait(forDuration: waitTime)
// Our monster sequence (I forgot to add the disappear, just add after puff)
let monsterSequence = SKAction.sequence([fall, wait, puff])
// Player slashes:
SKSpriteNode().run(slash)
// Monster will wait 0.25 seconds after falling,
// for slash to finish before puffing:
SKSpriteNode().run(monsterSequence)
et me know if this idea isn't working I can try updating it.

Trying to program a timer to do something when it reaches 5 seconds

I can't seem to figure out what to do here. I'm creating a learn to type game where if the user doesn't get the word correct in 5 seconds, they lose a life.
Either this could be implemented by counting down to 0 from 5, or counting up to 5.
I tried using the stopwatch for c# system diagnostics, which works for everything except I can't figure out how to check when it hits 5 seconds. It is a time object you cant compare it to an int.
you have two options here.
You can check the time in the Update function with Time.time or Time.deltaTime, or you can use a Coroutine with the new WaitForSeconds(5) object.
It could be as simple as:
float time;
void Update()
{
time += Time.deltaTime;
if(time > 5)
{
LoseALife();
}
}
If you need to restart the timer each time you complete a word you might have an aditional method like:
public void RestartTimer()
{
time = 0;
}
This would make the timer go back to 0 any moment you need it.

Firebase too slow to load for a tile-based game

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.