I'm building my first game in Swift and I wanted to know how to go about handling multiple on screen sprites at once. My game pushes sprites on to screen with addChild continuously, so there are many active at once. I realized that I didn't have a proper way of simultaneously affecting all of them- like if I wanted to affect a physics property of all enemy sprites at once. So far I created an empty array var enemySprites = [enemyType1]() at the begining of GameScene and have been appending the sprite instances to it instead of using addChild to draw them directly to the scene. However, I'm not able to simply loop through and draw them to screen with:
for enemy in enemySprites{
addChild(enemy)
}
this bit of code is in the override func update(currentTime: CFTimeInterval) function, so maybe I'm just misplacing it? Any help on how to go about this would be great!
Sam,
Here's some sample code to update enemies when your lives reach 0:
First, we set a property observer on the lives property so we can call a function when you lose all lives:
var lives = 3 {
didSet {
if lives == 0 {
updateEnemies()
}
}
And then a function to enumerate over all the enemies and change each one's velocity to (0, 0):
func update enemies() {
enumerateChildNodesWithName("type1") {
node, stop in
let enemy = node as! SKSpriteNode
enemy.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
}
}
Instead of use update method, you could use a timer. From sources:
public class func scheduledTimerWithTimeInterval(ti: NSTimeInterval, target aTarget: AnyObject, selector aSelector: Selector, userInfo: AnyObject?, repeats yesOrNo: Bool) -> NSTimer
So if you follow Apple guide, it will be for example:
NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("spawnAlien:"), userInfo: myParameter, repeats: true)
func spawnAlien(timer : NSTimer) {
if let myUserInfo = timer.userInfo {
print(myUserInfo) // a parameters passed to help you to the alien creation
}
timer.invalidate()
}
BUT according to Whirlwind I agree with him and with LearnCocos2d work, sprite-kit don't work well with timers (as explained in the link by LearnCocos2d) and the better way, especially as you say you develop your first game, it's to use SKAction, a combination of actions to achieve the similar behavior obtained by NSTimer.
I've think about a function or an extension, let me know if it's work as expected:
extension SKAction {
class func scheduledTimerWithTimeInterval(time:NSTimeInterval, selector: Selector, repeats:Bool)->SKAction {
let call = SKAction.customActionWithDuration(0.0) { node, _ in
node.performSelector(selector)
}
let wait = SKAction.waitForDuration(time)
let seq = SKAction.sequence([wait,call])
let callSelector = repeats ? SKAction.repeatActionForever(seq) : seq
return callSelector
}
}
Usage:
let spawn = SKAction.scheduledTimerWithTimeInterval(time, selector: #selector(GenericArea.spawnAlien), repeats: true)
self.runAction(spawn,withKey: "spawnAlien")
Related
I want to implement an action that when I press and hold begins to repeatedly do an action (similar to a scroll button on a Desktop UI). Is there first class support for this in the UIGestureRecognizer/events framework, or do I just roll my own?
E.g.
var timer:Timer?
func killDing() {
self.timer?.invalidate()
self.timer = nil
}
func startDing() {
self.killTimer()
self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) {
self.ding() // this is where the repeated code happens
}
}
override func beginTracking(_ touch:UITouch, with event:UIEvent?) -> Bool {
self.startDing()
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)
self.killDing()
}
I can of course do this with a LongPressGestureRecognizer as well. My question is whether I need to roll my own ding loop as shown above, or if there's something more first class in UIKit that I'm currently not aware of and should be taking advantage of.
I think you are on the right way. You can use use timers to repeat some actions, but you should add the created timer into a run loop with a common mode, without this mode, the run loop will not call the timer's action while a user is touching the screen
let timer = Timer(timerInterval: interval, repeats: true, block: block)
RunLoop.current.add(timer, forMode: . common)
Also you can use CADisplayLink, to call your action. You can find example of using CADisplayLink in my library, witch can help to you implement animation based on CADisplayLink:
let link = CADisplayLink(target: self, selector: #selector(updateAction(sender:)));
link.add(to: RunLoop.main, forMode: .commonModes);
I'm new to Swift's #selector paradigm. Ultimately I'm trying to move SKSpriteNode of a spaceShip over an CGMutablePath and add exhaust fumes as SKEmitterNodes on the path that the spaceShip has travelled.
However I'm stuck on #selector paradigm. I'm trying to send position of a the SpaceShipSprite via a timer to a addFumeToPosition(_ point: CGPoint) function so later I can add the SKEmitternode to that position.
For some reason that I cannot comprehend the #selector thing sends out something totally different than what I'm putting into the variable. Please see prints below. What's going on here?
let spaceShipSprite = SKSpriteNode(texture: SKTexture(image: #imageLiteral(resourceName: "spaceShip")))
spaceShipSprite.position = CGPoint(x: 142.0, y:160.0)
spaceShipSprite.name = "spaceShip"
self.addChild(spaceShipSprite)
print("spaceShipSprite.position before timer point x:\(spaceShipSprite.position.x) y:\(spaceShipSprite.position.y)")
let gameTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector:#selector(addFumeToPosition(_: )), userInfo: spaceShipSprite.position, repeats: true)
print("spaceShipSprite.position before timer point x:\(spaceShipSprite.position.x) y:\(spaceShipSprite.position.y)")
...
func addFumeToPosition(_ point: CGPoint){
print("spaceShipSprite.position during point x:\(point.x) y:\(point.y)")
}
spaceShipSprite.position before timer point x:142.0 y:160.0
spaceShipSprite.position after timer point x:142.0 y:160.0
spaceShipSprite.position during timer point x:196092657881833.0 y:1000000000.0
You cannot do that.
The two supported forms of target/action are
Without parameter
func addFumeToPosition()
With one parameter representing the affected class (in this case the Timer instance)
func addFumeToPosition(_ timer: Timer)
If a parameter is passed it must be the Timer instance.
All custom parameters have to be passed and handled via the userInfo parameter.
func addFumeToPosition(_ timer: Timer){
let point = timer.userInfo as! CGPoint
print("spaceShipSprite.position during point x:\(point.x) y:\(point.y)")
}
Note: In Swift 4 you might have to add the #objc attribute to the function.
I am trying to print a value from a slider at regular intervals. But only print the value if it is different to that last printed. I also do not want to miss any of the output values from the slider.
To do this I have created an array and added an element to the start of that array if it is different to the one already at the start. I have then used a repeating NSTimer to regularly call a function that prints the last element in the array before removing it from the array.
What happens when I run the app is the NSTimer stops anything being printed for it's set time, but then all of the elements print at once and more than one of each print. I've tried messing about with lots of different things - this is the closest I have got to making it work.
If you need to know any more info let me know.
I really appreciate any help given, thanks very much.
var sliderArray: [Float] = []
var timer: NSTimer!
let step: Float = 1
#IBAction func sliderValueChanged(sender: AnyObject)
{
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
if sliderArray.first != slider.value
{
sliderArray.insert(slider.value, atIndex: 0)
}
NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(sendSliderPosition), userInfo: nil, repeats: true)
}
func sendSliderPosition()
{
if sliderArray.count > 0
{
print(self.sliderArray.last)
sliderArray.removeLast()
}
}
I would suggest using CADisplayLink. A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display. Which is perfect for your slider case.
This will also not trigger unnecessary call when the slider or the UI is at rest.
class C: UIViewController {
var displayLinkTimer: CADisplayLink?
#IBOutlet weak var slider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
displayLinkTimer = CADisplayLink(target: self, selector: #selector(printSliderValue))
let runLoop = NSRunLoop.mainRunLoop()
displayLinkTimer?.addToRunLoop(runLoop, forMode: runLoop.currentMode ?? NSDefaultRunLoopMode )
displayLinkTimer?.paused = true
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
displayLinkTimer?.paused = true
}
deinit {
displayLinkTimer?.invalidate()
}
func printSliderValue()
{
let step: Float = 1
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
print(roundedValue)
}
}
The basic idea is this:
--> Every time the screen needs to redraw (this will happen at max around 60 frames per second taking into consideration this is fps rate), we get a chance to perform function.
--> to do so, we add the displayLink to the Run Loop. (Run lopp processes input/ refreshes UI and time slices)
--> NOTE This method wont be called if there is no redraw needed on the screen. This is not a timer per say that fires periodically. It fires when redraw is needed. In Sliders case, we want this to fire when we move slightest of the slider too.
For more info on how it actually works try it out and see the apple documentation. Make sure to invalidate before deinitializing the ViewController.
Figured out the answer, thank to everyone for the help and suggestions:
override func viewDidLoad()
{
super.viewDidLoad()
NSTimer.scheduledTimerWithTimeInterval(0.02, target: self, selector: #selector(sendSliderPosition), userInfo: nil, repeats: true)
}
#IBAction func sliderValueChanged(sender: AnyObject)
{
let step: Float = 1
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
if sliderArray.first != slider.value
{
sliderArray.insert(slider.value, atIndex: 0)
}
}
func sendSliderPosition()
{
if sliderArray.count > 1
{
let end1 = sliderArray.count-2
print(sliderArray[end1])
sliderArray.removeLast()
}
}
Explanation:
If the new slider value is different to the one already in the array then add it to the array at the start. Use an NSTimer to repeatedly call the sendSliderPosition function from viewDidLoad. The function will only be performed if there is more than one element in the array. If there is, print the element before the last one and remove the last. This always ensures that there is one element in the array so the function does not always run and that the element printed is the most recent one that hasn't already been printed.
In my game, there's a class for a "wall" that's moving to the left. I want to change the speed of it based on count i that I added to touchesBegan method:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent {
count++
}
func startMoving() {
let moveLeft = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: 1 )
let move = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: 0.5)
if(count <= 10)
{
runAction(SKAction.repeatActionForever(moveLeft))
}
else
{
runAction(SKAction.repeatActionForever(move))
}
}
but it's not working. Can you help?
As I said there are a lot of changes which have to be done:
First let's change MLWall class and add a duration property which will be used in startMoving method:
var duration:NSTimeInterval = 0.5
Then still inside MLWall class change the init method:
init(duration: NSTimeInterval) {
self.duration = duration
//...
}
And change startMoving method to use this passed parameter:
func startMoving() {
let moveLeft = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: self.duration)
runAction(SKAction.repeatActionForever(moveLeft))
}
Those are changes inside Wall class. Now let's make some changes in WallGenerator class:
First WallGenerator class should be aware of how fast walls should go. So we are adding property to store that info:
var currentDuration: NSTimeInterval = 1 // I named it duration, because SKAction takes duration as a parameter, but this actually affects on speed of a wall.
After that the first method which has to be changed is startGeneratingWallsEvery(second:) into startGeneratingWallsEvery(second: duration:
//duration parameter added
func startGeneratingWallsEvery(seconds: NSTimeInterval, duration : NSTimeInterval) {
self.currentDuration = duration
generationTimer = NSTimer.scheduledTimerWithTimeInterval(seconds, target: self, selector: "generateWall", userInfo: nil, repeats: true)
}
Here, we are making a WallGenerator aware of desired wall's speed.
And the next method which has to be changed in order to use that speed is:
//duration parameter added
func generateWall() {
//...
//Duration parameter added
let wall = MLWall(duration: self.currentDuration)
//...
}
And there is a GameScene left. There, I've added a tapCounter property:
let debugLabel = SKLabelNode(fontNamed: "Arial") //I've also added a debug label to track taps count visually
var tapCounter = 0
Here is how you can initialize label if you want to see number of tap counts:
//Setup debug label
debugLabel.text = "Tap counter : \(tapCounter)"
debugLabel.position = CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMaxY(frame)-50.0)
debugLabel.fontColor = SKColor.purpleColor()
self.addChild(debugLabel)
First I've changed the start method:
func start() {
//...
// adding duration parameter in method call
wallGenerator.startGeneratingWallsEvery(1,duration: 1)
}
The important part is : wallGenerator.startGeneratingWallsEvery(1,duration: 1) which says start generating walls every second with one second duration(which affects on node's speed).
Next, I've modified touchesBegan of the scene into this:
if isGameOver {
restart()
} else if !isStarted {
start()
} else {
tapCounter++
debugLabel.text = "Tap counter : \(tapCounter)"
if(tapCounter > 10){
wallGenerator.stopGenerating()
wallGenerator.startGeneratingWallsEvery(0.5, duration:0.5)
}
hero.flip()
}
Then, changed restart() method in order to restart the counter when game ends:
func restart() {
tapCounter = 0
//...
}
And that's pretty much it. I guess I haven't forgot something, but at my side it works as it should. Also, note that using NSTimer like from this GitHub project is not what you want in SpriteKit. That is because NSTimer don't respect scene's , view's or node's paused state. That means it will continue with spawning walls even if you think that game is paused. SKAction would be a preferred replacement for this situation.
Hope this helps, and if you have further questions, feel free to ask, but I guess that you can understand what's happening from the code above. Basically what is done is that WallGenerator has become aware of how fast their wall nodes should go, and Wall node has become aware of how fast it should go...
EDIT:
There is another way of changing walls speed by running an moving action with key. Then, at the moment of spawning, based on tapCounter value, you can access an moving action by the key, and directly change actions's speed property...This is probably a shorter way, but still requires some changes (passing a duration parameter inside Wall class and implementing tapCounter inside scene).
Try doing it like so :
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent {
count++
startMoving()
}
func startMoving() {
removeAllActions()
let moveLeft = SKAction.moveByX(-kDefaultXToMovePerSecond, y: 0, duration: count <= 10 ? 1.0 : 0.5)
runAction(SKAction.repeatActionForever(moveLeft))
}
As of I read comments below your question, you made a really unclear question, but nevertheless you have idealogical problems in your code.
Your touchesBegan method is implemented in the wall if I understood everything right, so it has no effect on newly generated walls. You have to move that logic to the scene and then spawn new walls with speed as a parameter, or at least make count a class var, so every wall can access that, but you still has to handle your touches in the scene, because now touch is handled when user taps directly in the wall.
I'm trying to animate some bezier paths in swift, and I need to get some of them to launch after a set delay. To do this, I have to write both of these sets of code in the viewDidLoad function.
Here is some sample code with the same idea:
override func viewDidLoad() {
super.viewDidLoad
func testFunc() {
println("Hello")
}
var frontOfBaseTimer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector("testFunc"), userInfo: nil, repeats: false)
}
This would crash the app 3 seconds after the view loaded. The error message leads me to believe that the only problem in this case is the target property.
What should I change here to get this to work?
If you're bound and determined to use your own delay loop, consider using GCD and the dispatch_after method. That method takes a closure and invokes the closure after a specified delay, which is pretty much exactly what you want. You would pass nil for the queue parameter so your closure would be run on the main queue.
I created a global function delay that lets me invoke dispatch_async painlessly without having to figure out it's confusing parameters:
func delay(delay: Double, block:()->())
{
let nSecDispatchTime = dispatch_time(DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC)));
let queue = dispatch_get_main_queue()
dispatch_after(nSecDispatchTime, queue, block)
}
You call it like this:
delay(2.0)
{
//code to fire after a delay
}
try this:-
override func viewDidLoad() {
super.viewDidLoad()
var frontOfBaseTimer =
NSTimer.scheduledTimerWithTimeInterval(3,
target: self,
selector:"testFunc",
userInfo: nil,
repeats: false)
}
func testFunc(){
println("hello its me")
}
Do like this way, Your app get crash because, timer call the function which has been added in viewdidload method.
override func viewDidLoad() {
super.viewDidLoad()
var frontOfBaseTimer =
NSTimer.scheduledTimerWithTimeInterval(3,
target: self,
selector: Selector("testFunc"),
userInfo: nil,
repeats: false)
}
func testFunc() {
println("Hello")
}
I just tested it and confirmed that you can't use a nested function as the selector for an NSTimer. The method the timer calls needs to be defined at the global scope of the target.
If you're trying to do animation, why not use UIView animation? That family of methods includes a delay parameter.
You could also use Core Animation. You can animate a path very easily by installing the CGPath from a bezierPath into a shape layer, then changing the shape layer's path as part of a CABasicAnimation.
Using Core Animation makes for very smooth, clean animations.
I have a project called RandomBlobs on Github that shows how to animate Bezier paths using CAShapeLayers and CABasicAnimations. It's written in Objective-C, but the techniques are directly applicable in Swift.
You can see a short video of the bezier path animation on Youtube.