I'm building a turn based game with SpriteKit. It's a human player versus 1+ CPU opponents.
During the CPUs' turns I'm using an SKAction.sequence in order to show what the CPU is doing. This usually consists of a wait action followed by a custom SKAction.run block.
Now part of the problem I have is that the CPU could perform multiple attack actions per turn, but after each attack task I recalculate if there should be anymore attacks done, as conquering a country might open up new avenues of attack. The trouble I've got here is that I then have no way of knowing how many attacks should take place upfront, and as the initial attack itself is in a block, it doesn't know if it will generate more attacks. Thus I don't know how to add additional attack steps to my sequence.
Does anyone have any ideas how I could go about this? One thought was that I actually perform all the attacks upfront without displaying them and subsequently then just replay them for the benefit of the player. But I'm worried it will seem like the game is hanging in this instance.
Is there anyway I could append more SKActions midway through a running sequence?
let sequence:[SKAction] = []
let attackTasks = actions.getAttackTasks()
if attackTasks.count > 0 {
sequence.append(SKAction.wait(forDuration:0.5))
sequence.append(SKAction.run {
//PerformAttackTask Here calls another recursive function which calculates and generates more attacks
self.performAttackTask(attackTask: attackTasks.first!)
self.combatFinished = false
})
}
let sequenceAction = SKAction.sequence(sequence)
node.run(sequenceAction)
For those that are interested I ended up changing my approach for this moving the animations down a level to the performAttackTask method and recursively calling itself.
I then make use of a callback that only fires when all combat is complete to exit this loop.
This also makes use of SKAction.run with a completion handler to then move onto the next set of actions
Related
I have a UIAnimation that pulls out a message log, but I need to populate the message log using a for loop as well. I have the animation taking place before the for loop begins, however, the animation doesn't actually begin until after the for loop finishes executing (even though the loop is located after the UIAnimation).
I've tried putting the contents of the for loop within an autoreleasepool (did not work)
I've tried running the for loop on a background thread, however, most of the code within the loop needs to be executed on the main thread, and I haven't had success with that approach.
The for loop is very large, and most of the code is irrelevant to the question, so you can set this up using any UIAnimation, and any relatively time consuming for loop...
for example:
let someView = UIView(frame:CGRect(x:self.view.frame.size.width,y:0,width:self.view.frame.size.width,height:self.view.frame.size.height))
someView.backgroundColor = .red
UIView.animate(withDuration:0.3,animations:{
self.someView.frame.origin.x = 0
})
//You can wrap this with something like:
//DispatchQueue.global(qos: .background).async(execute: {
//But this^ doesn't work on the majority of the code within the loop
for i in 0...1000{
print("hopefully this loop takes a few seconds to finish executing")
print("mainthread tasks being executed")
print("populating UITable in my particular case, and rendering Images/Video/text")
}
The closest that I can get to making it better is to add a "completion" to my UIAnimation, (which will allow the UIView to pull out completely/immediately) but then there is still a second of wait time before the first messages appear...
Also, I am populating a UITableView within the for loop. Not sure if that has any extra relevance to the primary issue I'm having or not...
I'm pretty familiar with DispatchQueue and the differences between async/sync. If you have any potential solutions, or ideas, please let me know.
The animation does not happen immediately, when you call UIView.animate. In stead all the animations are collected and run at some appropriate time. This is why the animation only starts after your for loop is done.
So the way to fix it is to get the animation to run before your for loop runs. Now, accepting what you say, that the for must run on the main thread, you have a number of options. You could put the for loop inside a DispatchQueue.main.async{} or somehow throttle it down so that the animation gets going first. Your idea of using the completion is a good one but you mention some practical issues you picked up with it. So either solve those practical issues, try to run the for inside a DispatchQueue.main.async{} or even, based on practical considerations, start the for loop on the main thread but somewhere else, say in something like didLayoutSubviews (fixed) or as now together with the animation but after a few millis delay to allow the animation to get going.
It's an interesting question. This made me think!
Yes, you mentioned that you understand differences between async/sync but didn't mention if you tried animating in the main thread.
Did you try putting the
UIView.animate(...) in DispatchQueue.main.async{}?
I'm simulating a security control process, and i can't do that each passenger pickup their baggage. I have tried with Match, Combine, Pickup, but I still can't execute the commands correctly.
I've created the follow flowchart, and the problem is in the wReclaimPax, pickup and wReclaimBags blocks (you can see them in the picture).
https://ibb.co/v3V57Tm
I saw this link Anylogic - Combined multiple items back to original owner to understand something, but I still need help.
I've created 3 functions:
isMatch:
if(equipaje.pasajeroLink.equals(pasajero.equipajeLink)){
return true;
}
return false;
paxBags:
for(int i=0;i<wait.size();i++){
Pasajero p=(Pasajero)wait.get(i);
if(isMatch(p,bag))
return p;
}
return null;
bagsPax:
for(int i=0;i<wait.size();i++){
Equipaje e=(Equipaje)wait.get(i);
if(isMatch(pasajero,e))
return e;
}
return null;
Assumed context
You haven't really explained how your code is related to your process but I'm assuming the following:
Because this is luggage-retrieval, you want to ensure that a passenger
agent (Pasajero) only enters the Pickup block (representing taking bag from
carousel) when his bag (Equipaje agent by the look of it) has
arrived into the wReclaimBag Wait, and been released from it to
queue4 Queue.
For this you need triggers (to remove agents from Wait blocks) when
either a passenger (Pasajero) arrives in wReclaimPax Wait, or a bag (Equipaje) arrives
in the wReclaimBag Wait (because you don't know whether the passenger or their bag will get to their respective Wait blocks first).
So your paxBags function is called in on-entry action of the wReclaimBag Wait, and your bagsPax function in the on-entry action of the wReclaimPax Wait.
Possible problems with current approach
Without knowing more of your model it's hard to say but problems I can think of based on what you've supplied are:
Your functions return the Pasajero or Equipaje if there is one that matches. Your match check relies seemingly on bidirectional connections (links) between Pasajero and Equipaje. Obviously if they're not setup properly the model won't work and, if you're using bidirectional connections you shouldn't need to check both ends.
Your functions need calling so that, if they return non null, they then free the matching agent from the other Wait block, and free themselves. Are you doing that? Without checking, there may be issues with calling free for yourself as you enter a Wait block (since this kind of depends on AnyLogic internals as to whether you count as being 'in' the block at this stage and can be freed). If this seems to be the problem you could create a timeout 0 dynamic event instance to do the free so that you're not doing it within the scope of the on-enter action.
Your pickup block (since it's been setup so that the entering agent will always want to pickup the first agent (Equipaje) in queue4) just needs to be set as waiting for quantity 1 (though see below).
If you've done all this the most likely problem is that the underlying events ordering of AnyLogic is affecting things. When you free agents I'm fairly sure the freeing actually happens in a timeout 0 event scheduled under-the-covers. So it may be that the passenger arrives at the Pickup before their Equipment arrives in queue4 though, if you set the Pickup to be "Exact quantity (wait for)", with quantity of 1, it should handle that.
The animation of the process (numbers in/out/within each block and details when clicking on blocks) should also help you debug what is going wrong; e.g., are bags being left in the Wait when they should have been released, etc.
P.S. With this kind of thing you should always create a minimal example model to make testing the issue/solution easier (and for sharing in help forums such as this where the rest of the complexity of your model is irrelevant). Often you find the problem 'naturally' in the process of trying to construct such a model that reproduces your problem in a minimal way.
I am curious what to use for my game. A timer:
let goodFPS = SKAction.wait(forDuration: 0.01666)
let mainGameRunner = SKAction.run {
//my code is here
}
let toRepeat = SKAction.repeatForever(SKAction.sequence([goodFPS,mainGameRunner]))
inGameHighScore.run(toRepeat,withKey:"mainGame")
or the update function:
override func update(_ currentTime: TimeInterval){
//my code is here
}
Which provides faster more consistent updates?
note: my frame rate is in the range of 45 to 58
First I think you are taking the FPS problem the wrong way around. You cannot "force" a faster frame rate than the device can give you. If you are basing the movements in your game on the assumption that every frame will be consistent, you are doing it wrong. It's actually how they did in the early days because CPUs were so slow and the difference from one generation to the new one wasn't too bad at first. But running an old DOS game on younger hardware will be tricky because the framerate is so high that the whole game mechanic becomes unstable or simply too fast to be playable.
The concept here is to think "over time" and to scale down any action in relation with the time elapsed between two frames.
The update() method gives you that opportunity by providing the current system clock state every frame. By keeping track of the time on the last frame, you can calculate the time difference with the current frame and use that difference to scale down what you are doing.
Using a timer to get the update on a consistent frame rate is not recommended nor practical. You may be calling the update closure at a given time interval, but the code inside that closure is taking time to execute on its own, and depending on your game logic, it might even have different execution times. So maybe the pause timing is consistent, but the code running before and after that pause might not be consistent. Then what happens if you run your game on a slower CPU? The code speed will change even more, making your timing inaccurate.
Another point against using an SKAction for your game loop is simply what they are. An action is an object in memory, meany to be reused by multiple objects. If you are making a "jump" action, for example, it is recommended to store that action somewhere and to reuse the same object every time you need something that "jumps", no matter what node it is. Your game loop is meant to be executed every frame, but not by different objects. Actions are also disposable. Meaning that you can kill an action even while it's running. If you put your game loop in an action, it will probably be run by the SKScene. If you use another action on your scene it becomes a puzzle right away because there are only two ways of removing an action besides letting it come to term: removing all actions or creating the action with an identifier key and use that key to remove any action with that key. If you don't see it already, it then forces you to put identifiers on every action that will be run by the scene and remove them one by one. And them again it leave a door open for a mistake that will get rid of your game loop because, keep it in mind, actions are DISPOSABLE! Then there is also no guarantee that your action will get executed first every single frame.
Why use the update() method? Simply because it is built IN your scene. No matter what, every frame, update() gets called first. THEN, the actions get executed. You cannot flush the update() method accidentally like you can with an action. You don't have to be careful about strong/weak relationships causing memory leaks because you are referring to objects from inside a closure like you do with an action.
Suggested reads:
SKAction API reference
SKScene API reference : read about the frame processing in SpriteKit. It will help you understand how they put everything together at every frame.
I hope it makes things clearer.
I'm pretty sure that SKAction's timing facilities are based on the same game loop that is calling update.
The advantage of SKAction is that it's fire and forget for you, while using update would get awkward with setting and checking a bunch of timer variables.
I don't have a ton of experience with SpriteKit but I do have some experience making games and animations in iOS.
There is a class called CADisplayLink that fires a call to a delegate every time the screen is refreshed, this is a great way to update the screen, either in a game or in an animation, because you can know it will be called every frame and no more.
I'm not sure if SpriteKit uses that class to fire the update method, but I'm sure it uses something similar. This is usually called the run loop.
SKActions run on top of this run loop.
By creating your own run loop using a wait action, not only you're not gaining any benefits, you could be introducing inconsistencies in the way your logic is run.
Imagine that you have your wait action set to 0.01 seconds (I rounded it down for simplicity). If the screen is refreshing faster than you expect, and the action is updated every 0.009 seconds, the first time it's checked, it won't fire because there's a remaining 0.001 second on the wait command, so another 0.009 seconds will pass, and your code will be executed after 0.018 seconds, instead of your desired 0.01. Not only that, but two frames will have passed between the execution.
If you use the update function, you can know that your logic will be executed once every screen refresh, and no more.
I spent some time looking into various approaches for queuing and even read this http://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1 which was helpful, but I feel as the more I read into GCD, the more confused and unsure I become of myself about the right approach. Could someone give me a recommendation?
My goal is this: I'm making a game where a player can lose health due to some game event. The player has a health bar (hearts) at the top, and each time they get hit, I animate a heart to fly upwards over a .1 time interval. The reason i'm exploring a queue of some sort is the player can lose health so rapidly in some cases that the events fire faster than the animation. As a result, it sometimes "skips" hearts because it can't keep up with the health loss. This is only on some levels where the damage taken is higher than usual. Even without the animation it still happens occasionally although I think a tad bit less.
So I was thinking what I could do is queue up each health loss event, this way it will force the events to go in order, but I wasn't successful (it still went super fast and skipped hearts, removing the 10th heart, the 8th heart, and the 5th, then it said game over. I want to slow it down and remove the hearts in order so you see them vanishing).
This was the code I attempted:
dispatch_async(dispatch_get_main_queue(), {
heartToRemove!.runAction(
SKAction.sequence([SKAction.moveByX(0, y:50, duration:0.1),
SKAction.runBlock
{
heartToRemove!.removeFromParent()
self.hearts.removeLast()
}]))
})
So the idea is to run 3 actions on a queue:
first, move the heart up. next, remove visually from the parent (so it vanishes off the game not just positioned above), and finally remove it from the array of SKNodes (the hearts) so that the next time a heart is removed, it goes to the next one in line not the same one if that makes sense.
I also tried dispatch_sync but that just froze my game so I assume that is wrong. Any other recommendations?
Thank you so much for your time!
You could use the number of items in your hearths array to offset the animation times
something like :
let wait = SKAction.waitForDuration(0.1 * NSTimeInterval(hearts.count-1))
let move = SKAction.moveByX(0, y:50, duration:0.1)
let remove = SKAction.runBlock()
{
heartToRemove!.removeFromParent()
self.hearts.removeLast()
}
heartToRemove!.runAction(SKAction.sequence([ wait, move, remove ]))
Then you won't have to worry about queueing the actions.
What is the best way to handle this situation on an iPhone device: My program ramps the pitch of a sound between two values. A button pressed calls a method that has a while loop that does the ramping in small increments. It will take some time to finish. In the meantime the user has pressed another button calling the same method. Now I want the loop in the first call to stop and the second to start from the current state. Here is the something like what the method should look like:
-(void)changePitchSample: (float) newPitch{
float oldPitch=channel.pitch;
if (oldPitch>newPitch) {
while (channel.pitch>newPitch) {
channel.pitch = channel.pitch-0.001;
}
}
else if (oldPitch<newPitch) {
while (channel.pitch<newPitch) {
channel.pitch = channel.pitch+0.001;
}
}
}
Now how to best handle the situation where the method is called again? Do I need some kind of mulitthreading? I do not need two processes going at the same time, so it seems there must be some easier solution that I cannot find (being new to this language).
Any help greatly appreciated!
You cannot do this like that. While your loop is running no events will be processed. So if the user pushes the button again nothing will happen before your loop is finished. Also like this you can’t control the speed of your ramp. I’d suggest using a NSTimer. In your changePitchSample: method you store the new pitch somewhere (don’t overwrite the old one) and start a timer that fires once. When the timer fires you increment your pitch and if it is less than the new pitch you restart the timer.
Have a look at NSOperation and the Concurrency Programming Guide. You can first start you operation the increase the pitch and also store the operation object. On the second call you can call [operation cancel] to stop the last operation. Start a second operation to i.e. decrease the pitch and also store the new object.
Btw: What you are doing right now is very bad since you "block the main thread". Calculations that take some time should not be directly executed. You should probably also have a look at NSTimer to make your code independent of the processor speed.
Don't use a while loop; it blocks everything else. Use a timer and a state machine. The timer can call the state machine at the rate at which you want things to change. The state machine can look at the last ramp value and the time of the last button hit (or even an array of UI event times) and decide whether and how much to ramp the volume during the next time step (logic is often just a pile of if and select/case statements if the control algorithm isn't amenable to a nice table). Then the state machine can call the object or routine that handles the actual sound level.