I am trying to write a function where I can increase SKShapeNode radius every second, but don't know how:(
var eRadius: CGFloat = 20
var eCircle = SKShapeNode()
override func didMove(to view: SKView) {
super.didMove(to: view)
eCircle = SKShapeNode(circleOfRadius: eRadius)
eCircle.strokeColor = .black
eCircle.glowWidth = 1.0
eCircle.fillColor = .white
eCircle.fillTexture = SKTexture(imageNamed: "e")
addChild(eCircle)
gameTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(addRadius), userInfo: nil, repeats: true)
}
#objc func addRadius(){
eRadius += 1
???? :(
}
Thank you!
Here's a working solution:
import SpriteKit
class GameScene: SKScene {
private var eRadius: CGFloat = 20
private var eCircle = SKShapeNode()
override func didMove(to view: SKView) {
eCircle.strokeColor = .black
eCircle.glowWidth = 1.0
eCircle.fillColor = .white
eCircle.fillTexture = SKTexture(imageNamed: "e")
addChild(eCircle)
run(SKAction.repeatForever(SKAction.sequence([SKAction.run {
self.updateCirclePath()
self.eRadius += 1
}, SKAction.wait(forDuration: 1.0)])))
}
private func updateCirclePath() {
eCircle.path = UIBezierPath(ovalIn: CGRect(x: -eRadius * 0.5, y: -eRadius * 0.5, width: eRadius, height: eRadius)).cgPath
}
}
Basically, you create the circle using a UIBezierPath and then update it as your radius changes. For the updating you should use a forever-repeating SKAction sequence that both sets the path, then changes the radius. I chose to do the path first, then update the radius for the next upcoming turn, for clarity.
You can also do it the opposite way, so run the update function first to set the initial path, then wait one second, after which you change the radius, then update the path based on that radius.
In SpriteKit, you should use SKActions for timers instead of scheduling or dispatch queues. With SKActions, everything conforms to the same game time, which you can then control by pausing and whatever.
Also, there are multiple ways to make a circle using UIBezierPath, including ovals, rounded rectangles and arcs: https://developer.apple.com/documentation/uikit/uibezierpath.
Related
I am currently trying to build a loading scene into my SpriteKit game with my developer logo on it and need to transition to the MainMenuScene after say 5 seconds. How would I go about that.
My code right now looks like this, which is basically just the background/logo image.
import SpriteKit
class LoadingScene: SKScene {
override func didMove(to view: SKView) {
let background = SKSpriteNode(imageNamed: "fatscoprion")
background.position = CGPoint (x: self.size.width / 2, y: self.size.height / 2)
background.zPosition = -1
self.addChild(background)
}
}
You can create a Scheduled Timer and configure a function to call your method that's creates and present the new scene.
Example:
class LoadingScene: SKScene {
var timer = Timer()
override func didMove(to view: SKView) {
let background = SKSpriteNode(imageNamed: "")
background.position = CGPoint (x: self.size.width / 2, y: self.size.height / 2)
background.zPosition = -1
self.addChild(background)
//Create a Scheduled timer thats will fire a function after the timeInterval
timer = Timer.scheduledTimer(timeInterval: 5.0,
target: self,
selector: #selector(presentNewScene),
userInfo: nil, repeats: false)
}
#objc func presentNewScene() {
//Configure the new scene to be presented and then present.
let newScene = SKScene(size: .zero)
view?.presentScene(newScene)
}
deinit {
//Stops the timer.
timer.invalidate()
}
}
I am trying to create a program where the longer you hold down the screen the higher an object will move. I want to do it where the longer you hold down the longer the function repeats increasing the distance. However, for some reason it is not working.
import SpriteKit
import GameplayKit
var calculateTimer = Timer()
var distance = 10
var choice = 1
class GameScene: SKScene, SKPhysicsContactDelegate {
var planet = SKNode()
override func didMove(to view: SKView) { // viewdidload
let planetTexture = SKTexture(imageNamed: "mercury.jpg")
planet = SKSpriteNode(texture: planetTexture)
planet.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
addChild(planet)
}
func calculateDistance() {
distance += 10
print(distance)
print(choice)
}
func touchDown(atPoint pos : CGPoint) {
calculateTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.calculateDistance), userInfo: nil, repeats: true)
print("test")
}
func touchUp(atPoint pos : CGPoint) {
planet.physicsBody!.isDynamic = true
//planet.physicsBody?.velocity = (CGVector(dx: 0, dy: 0))
planet.physicsBody!.applyImpulse(CGVector(dx: 0, dy: distance))
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
This is my code. If anyone has any suggestions they would be much appreciated!
In touchDown, set a flag to indicate a touch has started.
In touchUp, clear/remove the flag.
Whilst the flag is set, keep performing your movement code.
Running into some trouble with my code. I'm trying to make zombies follow my player around with the following code:
class GameScene: SKScene, SKPhysicsContactDelegate {
func Enemies() {
Enemy = SKSpriteNode(imageNamed: "Spaceship")
Enemy.size = CGSize(width: 50, height: 50)
Enemy.color = UIColor(red: 0.9, green: 0.1, blue: 0.1, alpha: 1.0)
Enemy.colorBlendFactor = 1.0
//physics
Enemy.physicsBody = SKPhysicsBody(rectangleOf: Enemy.size)
Enemy.physicsBody?.isDynamic = true
Enemy.physicsBody?.affectedByGravity = false
Enemy.name = "Enemy"
Enemy.position.y = -frame.size.height/2
let positionX = arc4random_uniform(UInt32(frame.size.width))
Enemy.position.x = CGFloat(positionX)
addChild(Enemy)
}
override func didMove(to view: SKView) {
enemyTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(GameScene.Enemies), userInfo: nil, repeats: true)
override func update(_ currentTime: TimeInterval) {
Enemy.run(SKAction.move(to: ship.position, duration: 3))
}
If I run this code, I can spawn the zombies, but they will not follow my main player, they will just go to the position that he was at when they were spawned (i.e. zombie spawned at time = 0 will go to the ship position at time = 0, zombie spawned at time = 1 will go to the ship position at time = 1, and so on). However, if I run this code while only spawning one zombie like so:
override func didMove(to view: SKView) {
Enemies()
}
The lone zombie will follow my player around. Any idea why the code works for one zombie, but not for multiple zombies?
I would not recommend constantly adding actions on your update cycle, you are going to suffer from performance loss due to all the things that happen behind the scenes. Instead, use an SKAction.customAction that you would only add to your sprite once.
Here is an example of a custom action that will do what you want, remember only assign it once. (Code is not tested, so may need edits)
let customActionBlock =
{
(node,elapsedTime) in
let dx = ship.x - node.position.x
let dy = ship.y - node.position.y
let angle = atan2(dx,dy)
node.position.x += sin(angle) * speedPerFrame
node.position.y += cos(angle) * speedPerFrame
}
let duration = TimeInterval(Int.max) //want the action to run infinitely
let followPlayer = SKAction.customAction(withDuration:duration,actionBlock:customActionBlock)
Enemy.run(action:followPlayer,withKey:"follow")
Maybe you should removeAllActions() on Enemy before readding an action. It seems that you have actions that take 3 seconds, but you add an action every frame, so it has at most 180 actions for a node at once.
I'm trying to get my "Player" (A circle in the middle) to increase in size once the screen is touched by running a timer.
Once the timer is over 0 seconds, it increases in size. Once the timer is over 3 seconds, it decreases to its original scale size and once the timer is over 7 seconds, it resets and this repeats forever.
What am I doing wrong?
import SpriteKit
class GameScene: SKScene {
var Center = SKSpriteNode()
var Player = SKSpriteNode()
var timer = NSTimer()
var seconds = 0
override func didMoveToView(view: SKView) {
Center = SKSpriteNode(imageNamed: "Center")
Center.size = CGSize(width: 80, height: 80)
Center.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
self.addChild(Center)
Player = SKSpriteNode(imageNamed: "Player")
Player.size = CGSize(width: 80, height: 80)
Player.position = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
self.addChild(Player)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
timer = NSTimer.scheduledTimerWithTimeInterval(4.0, target: self, selector: #selector(GameScene.playerScaleUp), userInfo: nil, repeats: true)
}
func playerScaleUp(){
if seconds > 0{
Player.runAction(SKAction.scaleBy(4, duration: 2))
}
}
func playerScaleDown(){
if seconds > 3{
Player.runAction(SKAction.scaleBy(-4, duration: 2))
}
}
func resetScale(){
if seconds > 7{
timer.invalidate()
}
}
override func update(currentTime: CFTimeInterval) {
}
}
This can be done in a few different ways:
1) Using SKActions (which my example uses)
2) Using update: method and its passed currentTime parameter
3) Using NSTimer which I wouldn't recommend because it is not affected by scene's, view's or node's paused state, so it can lead you into troubles in the future. Read more in this StackOverflow answer
4) Well, probably some more, but I will stop here.
My example uses SKAction. This means that I don't really check how much time is passed, but rather I organize actions into sequences (where actions are executed sequentially) and into groups (where actions are organized parallel). Means I use SKActions like I am playing with LEGO's :)
Here is the code ...I left debugging code intentionally, because it can help you to learn how you can use SKActions in different situations.
class GameScene: SKScene, SKPhysicsContactDelegate {
let player = SKSpriteNode(color: .blackColor(), size: CGSize(width: 50, height: 50))
var timePassed = 0.0 {
didSet {
self.label.text = String(format: "%.1f",timePassed)
}
}
let label = SKLabelNode(fontNamed: "ArialMT")
override func didMoveToView(view: SKView) {
/* Debugging - Not really needed, I added it just because of a better example */
let wait = SKAction.waitForDuration(0.1)
let updateLabel = SKAction.runBlock({[unowned self] in self.timePassed += wait.duration})
self.label.position = CGPoint(x: frame.midX, y: frame.midY+100.0)
addChild(self.label)
let timerSequence = SKAction.sequence([updateLabel, wait])
self.runAction(SKAction.repeatActionForever(timerSequence), withKey: "counting")
//This is used later, at the end of scaleUpAndDownSequence to reset the timer
let resetTimer = SKAction.runBlock({[unowned self] in self.timePassed = 0.0})
/* End Debugging */
self.player.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(self.player)
let waitBeforeScaleToOriginalSize = SKAction.waitForDuration(3.0)
let waitBeforeRestart = SKAction.waitForDuration(4.0)
let scaleUp = SKAction.scaleTo(2.0, duration: 1)
let scaleDown = SKAction.scaleTo(1.0, duration: 1)
let scaleUpGroup = SKAction.group([waitBeforeScaleToOriginalSize, scaleUp])
let scaleDownGroup = SKAction.group([scaleDown, waitBeforeRestart])
//One cycle, scale up, scale down, reset timer
let scaleUpAndDownSequence = SKAction.sequence([scaleUpGroup, scaleDownGroup, resetTimer])
let loop = SKAction.repeatActionForever(scaleUpAndDownSequence)
self.player.runAction(loop, withKey: "scalingUpAndDown")
}
}
So here, I have two groups of actions:
1) scaleUpGroup
2) scaleDownGroup
scaleUpGroup group of actions has two actions in it: a scaleUp action, and an action which says how much to wait before the scale down action should occur. Because we want scaling up to happen immediately, we run it in parallel with the waitBeforeScaleToOriginalSize.
Same logic goes for scaleDownGroup. When scaleUpGroup is finished (its duration is determined by the longest action in the group) we start scaleDownGroup which scales down the player to its default size, and waits certain amount of time to repeat the whole thing.
Here is the result:
I start an animation on touch ( I've removed that code) and as you can see, scale up animation starts immediately, then after 3 seconds the player get scaled down to its original size, and after 4 seconds the whole animation repeats (player gets scaled up again etc).
Im trying to make it so that the program will print something to the console when an instance of the sprite is in the center of the screen from top to bottom
var sprite = SKSpriteNode()
override func didMoveToView(view: SKView) {
//spawns an instance of the sprite
NSTimer.scheduledTimerWithTimeInterval(0.3, target: self, selector: Selector("spawnObject"), userInfo: nil, repeats: true)
//call the check middle function
checkMiddle()
}
//spawns a sprite
func spawnObject() {
let size = CGSizeMake(self.frame.size.width/3, self.frame.size.width3)
self.physicsWorld.gravity = CGVectorMake(0, -CGFloat(objectSpeed))
sprite = SKSpriteNode(imageNamed: "spriteImage")
sprite.position = CGPointMake(CGFloat(100), self.frame.height)
sprite.size = size
sprite.physicsBody = SKPhysicsBody(circleOfRadius: self.frame.size.width)
sprite.physicsBody?.affectedByGravity = true
addChild(self.sprite)
}
//PRINT WHEN SPRITE IS IN THE CENTER OF SCREEN FROM TOP TO BOTTOM
func checkMiddle() {
if sprite.position.y == self.frame.size.height/2 {
print("center")
}
}
if this is for debugging purposes you can use :
override func update(currentTime: NSTimeInterval)
{
checkMiddle()
}
The update() function is called before every frame so you need to be careful what you put in there
If you don't need it to be checked as frequently as that (60 x per second), you could also use an action:
let checkCenterAction = SKAction.runBlock(){ self.checkMiddle() }
let waitAction = SKAction.waitForDuration( 0.1 ) // every 1/10th of a second
let checkAndWaitAction = SKAction.sequence([checkCenterAction, waitAction])
sprite.runAction(SKAction.repeatForever(checkAndWaitAction))
And yet another option is to use the SKPhysicsBody to detect collisions