Is there a way to spawn nodes uniformly? - swift

I have a game where circles are moving up and down lines like so:
http://i.stack.imgur.com/5zKS4.png
, and Im using a .plist file to generate them, but its a little monotonous.Is there another way to generate several columns of circles uniformly without using a .plist file? Will post code if necessary.

You could write a function like this:
func createCirclesOnLine(line : CGFloat){
//Set this to however low you want the circles to start.
var currentY : CGFloat = -20.0
//Set this to however high you want the circles to go
let maxY = self.size.height + 100
//Set this to the spacing you want between the circles
let spacing : CGFloat = 50
while currentY < maxY {
let circle = SKShapeNode(circleOfRadius: 20)
circle.position = CGPoint(x: line, y: currentY)
self.addChild(circle)
currentY += spacing
}
}
And then pass it the line (think longitude) of wherever you want the circles to be drawn:
let line = self.size.width / 2
createCirclesOnLine(line)
Is that what you're looking for?
EDIT: To answer your question about how to use this multiple times:
If you want multiple lines of circles on the screen, you can simply decide what lines (again, think longitude) you want to place the circles on. For instance, if I wanted three equally spaced lines of circles on the screen, I would do the following:
let line = self.size.width / 4
createCirclesOnLine(line)
createCirclesOnLine(line * 2)
createCirclesOnLine(line * 3)
If you try this and don't see all of the lines, you might need to add this
scene.size = skView.bounds.size
to the viewDidLoad method in your ViewController.swift right under
skView.ignoresSiblingOrder = true
since you are running the app in portrait mode.

Related

Convert between UIKit and SpriteKit coordinate systems

I'm new to iOS programming, and have almost no experience with SpriteKit, so please forgive me if this is a ridiculous question.
I've been trying to make a basic grid with a 2D array, and I would prefer to work with it from top-left being 0, 0.
After researching the differences in coordinate systems between UIKit and SpriteKit, I came across this answer about Converting Between View and Scene Coordinates but it doesn't seem to change the y value the way I thought it would. I am guessing that I'm not using it right, or maybe this is not what it's meant to do, I don't know.
When I try this:
let convertedCoordinates = convert(cellCoordinates, to: grid)
print(cellCoordinates.y, convertedCoordinates.y)
it doesn't seem to have any effect on the y value.
I have found that when I change to "y: -cy" in the line let cellCoordinates = CGPoint(x: cx, y: cy)
Then it does seem to work the way I am hoping for, but I don't know if that's the only solution or if doing this will work as expected under more complicated situations.
Here is the code I am working with:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
var background: SKShapeNode!
background = SKShapeNode(rectOf: CGSize(width: frame.size.width, height: frame.size.height))
background.fillColor = SKColor.lightGray
self.addChild(background)
let margin = CGFloat(50)
let width = frame.size.width - margin
let height = frame.size.height - margin
let centerX = frame.midX - width / 2
let centerY = frame.midY - height / 2
var grid: SKShapeNode!
grid = SKShapeNode(rectOf: CGSize(width: width, height: height))
grid.strokeColor = SKColor.clear
self.addChild(grid)
let numRows = 2
let numCols = 3
let cellWidth = width / CGFloat(numCols)
for r in 0..<numRows {
for c in 0..<numCols {
let cx = centerX + (cellWidth / 2) + (CGFloat(c) * cellWidth)
let cy = centerY + (cellWidth / 2) + (CGFloat(r) * cellWidth)
//***
let cellCoordinates = CGPoint(x: cx, y: cy)
//***
let cellNode = SKShapeNode(rectOf: CGSize(width: cellWidth, height: cellWidth))
let convertedCoordinates = convert(cellCoordinates, to: grid)
print(cellCoordinates.y, convertedCoordinates.y)
cellNode.strokeColor = SKColor.black
cellNode.lineWidth = 5
cellNode.fillColor = SKColor.darkGray
cellNode.position = convertedCoordinates
let textNode = SKLabelNode(text: String("\(r),\(c)"))
textNode.fontName = "Menlo"
textNode.fontSize = 60
textNode.verticalAlignmentMode = .center
textNode.position = convertedCoordinates
grid.addChild(cellNode)
grid.addChild(textNode)
}
}
}
}
This is more a philosophical answer than an implementation one. As far as somehow flipping SpriteKit's coordinate system, well, you're going to be fighting it constantly. Better to just embrace the system as it is.
The essence of your question though is more one of separation of model and view. When you say
I would prefer to work with it from top-left being 0, 0
what you mean is that mentally you're thinking of the game as a grid of cells with 0,0 at the top left. That's perfectly fine and natural. That's your model of the game. But what are you writing in the code?
let cx = centerX + (cellWidth / 2) + (CGFloat(c) * cellWidth)
let cy = centerY + (cellWidth / 2) + (CGFloat(r) * cellWidth)
let cellCoordinates = CGPoint(x: cx, y: cy)
let convertedCoordinates = convert(cellCoordinates, to: grid)
That's your view struggling to get out. You have the abstract model grid that you're indexing with r,c with 0,0 at the upper left and whose coordinates increase in unit steps down and to the right. Then there's the view of the model, which might depend on screen resolution, aspect ratio, device orientation, whatever. If you keep the two mentally separate, you'll usually find that you can isolate the translation between the two systems to a small interface. In those places you may have to do things like scale the axes or flip one of them, or stretch things in one direction to match aspect ratios.
In a case like this, if you start with your mental model with your preferred 0,0 in the upper left and think about how the game operates, it'll often be in terms of the cells. OK, that suggests that maybe a 2D array or an array of arrays is natural. Maybe the cells will eventually become a class in your game. They'll probably have a node property that stores the SpriteKit node. You might wind up with something like this:
struct boardPosition {
let row: Int
let col: Int
}
class Cell {
let pos: boardPosition
let node: SKNode
init(pos: boardPosition, in board: Board) {
self.pos = pos
node = SKShapeNode(...)
board.node.addChild(node)
}
}
class Board {
let cells: [[Cell]]
let node: SKNode
init(numRows: Int, numColumns: Int) {
...
}
func movePiece(from: boardPosition, to: boardPosition) {
let piece = cell[from.row][from.col].removePiece()
cell[to.row][to.col].addPiece(piece)
}
}
The operation of the game will be in terms of your mental model. The fact that the y-coordinates of the cells' SKNode nodes happen to decrease as the row index increases will be completely buried.
Set all nodes applicable and scene’s anchor point to 0,1 to get it to mount to the top left corner and set your world node’s (if you do not have one, I recommend adding it, it is a basic SKNode that you use to place all of your game nodes in, allowing you to use a separate node for things not applicable to the game world, like hud and overlays) yScale to -1 to have y increment downward instead of upward.
Edit:
When dealing with SKShapeNodes, you do not have to worry about the images being inversed unless you have an obscure shape. When designing the CGPath for the obscure shape, just flip it.
shape.path = shape.path!.copy(using:CGAffineTransform(scaleX:1,y:-1))
The bigger problem is SKShapeNode does not have anchor points. You instead need to move the entire CGPath
To do this, add the following line:
shape.path = shape.path!.copy(using:CGAffineTransform(translationX:shape.frame.width/2,y:shape.frame.height/2))
If dealing with SKSprite nodes in the future....
This will cause your assets to be upside down, so all you would need to do is have your assets flipped before import, use a secondary node to flip the y axis, or assign all nodes with a yScale of -1. Flipping all of your assets prior to import vertically would be the cheapest method, I believe you can flip it inside xcassets as well, but I need to verify that when I get back on a MacOS again.

Creating a progress indicator with a rounded rectangle

I am attempting to create a rounded-rectangle progress indicator in my app. I have previously implemented a circular indicator, but not like this shape. I would like it to look something like this (start point is at the top):
But I get this with 0 as the .strokeStart property of the layer:
My current code place in viewDidLoad():
let queueShapeLayer = CAShapeLayer()
let queuePath = UIBezierPath(roundedRect: addToQueue.frame, cornerRadius: addToQueue.layer.cornerRadius)
queueShapeLayer.path = queuePath.cgPath
queueShapeLayer.lineWidth = 5
queueShapeLayer.strokeColor = UIColor.white.cgColor
queueShapeLayer.fillColor = UIColor.clear.cgColor
queueShapeLayer.strokeStart = 0
queueShapeLayer.strokeEnd = 0.5
view.layer.addSublayer(queueShapeLayer)
addToQueue is the button which says 'Upvote'.
Unlike creating a circular progress indicator, I cannot set the start and end angle in the initialisation of a Bezier path.
How do I make the progress start from the top middle as seen in the first image?
Edit - added a picture without corner radius on:
It seems that the corner radius is creating the issue.
If you have any questions, please ask!
I found a solution so the loading indicator works for round corners:
let queueShapeLayer = CAShapeLayer()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Queue timer
let radius = addToQueue.layer.cornerRadius
let diameter = radius * 2
let totalLength = (addToQueue.frame.width - diameter) * 2 + (CGFloat.pi * diameter)
let queuePath = UIBezierPath(roundedRect: addToQueue.frame, cornerRadius: radius)
queueShapeLayer.path = queuePath.cgPath
queueShapeLayer.lineWidth = 5
queueShapeLayer.strokeColor = UIColor.white.cgColor
queueShapeLayer.fillColor = UIColor.clear.cgColor
queueShapeLayer.strokeStart = 0.25 - CGFloat.pi * diameter / 3 / totalLength // Change the '0.25' to 0.5, 0.75 etc. wherever you want the bar to start
queueShapeLayer.strokeEnd = queueShapeLayer.strokeStart + 0.5 // Change this to the value you want it to go to (in this case 0.5 or 50% loaded)
view.layer.addSublayer(queueShapeLayer)
}
After I had did this though, I was having problems that I couldn't animate the whole way round. To get around this, I created a second animation (setting strokeStart to 0) and then I placed completion blocks so I could trigger the animations at the correct time.
Tip:
Add animation.fillMode = CAMediaTimingFillMode.forwards & animation.isRemovedOnCompletion = false when using a CABasicAnimation for the animation to wait until you remove it.
I hope this formula helps anyone in the future!
If you need help, you can always message me and I am willing to help. :)

Making sprite appear at random position

I'm trying to make a sprite "comet" appear at a random position at random times. So far, I wanted to test if my random position code works, however, I can't seem to even see the sprite. This is my code:
func spawnAtRandomPosition() {
let height = self.view!.frame.height
let width = self.view!.frame.width
let randomPosition = CGPointMake(CGFloat(arc4random()) % height, CGFloat(arc4random()) % width)
let sprite = SKSpriteNode(imageNamed: "comet")
sprite.position = randomPosition
self.addChild(sprite)
}
As I said, I'm not seeing anything. Any help? If you already know how to make it appear at a random time that would be appreciated as well, because that's having problems of its own, however this is my focus right now. Thanks!
Your code for setting the random position is incorrect. Additionally, your code has issues that should make it impossible to compile in Swift 3. Your full function should look like this:
func spawnAtRandomPosition() {
let height = UInt32(self.size.height)
let width = UInt32(self.size.width)
let randomPosition = CGPoint(x: Int(arc4random_uniform(width)), y: Int(arc4random_uniform(height)))
let sprite = SKSpriteNode(imageNamed: "comet")
sprite.position = randomPosition
self.addChild(sprite)
}
Note the changes to randomPosition and the height and width:
let randomPosition = CGPoint(x: Int(arc4random_uniform(width)), y: Int(arc4random_uniform(height)))
This determines a random value between 0 and your width and does the same thing for the height.
As for the height and width, see #Whirlwind's comment on the question explaining the difference between the view and the scene.
Additionally, you may want to check if you're setting up your node properly (set size, try with fixed location, etc) before you test the random positioning, to determine where the problem truly lies.
I came up with this bit of code and it works for me:
let randomNum = CGPoint(x:Int (arc4random() % 1000), y: 1)
let actionMove = SKAction.moveTo(randomNum, duration: 1)
I only wanted the x to be randomized, however if you need "y" to be randomized as well, you can just copy the specification to x to y.

How to generate a new node when another one hits a certain y value

I have a circle moving up a line, and when that circle reaches a certain y point, how can I make it so that another node would generate from below?
Here is the code I currently have for populating the circles, but I am not able to use it with a physics body, as it generates too many nodes and slows down my app:
func createCirclesOnLine(line: CGFloat) {
var currentY : CGFloat = -110
let maxY = self.size.width * 15
let spacing : CGFloat = 120
while currentY < maxY {
let circle = SKSpriteNode(imageNamed: "first#2x")
circle.physicsBody?.dynamic = false
circle.position = CGPointMake(line, currentY)
//circle.physicsBody?.restitution = -900
circle.size = CGSizeMake(75, 75)
// circle.physicsBody = SKPhysicsBody(rectangleOfSize: circle.size)
let up = SKAction.moveByX(0, y: 9000, duration: 90)
circle.runAction(up)
foregroundNode.addChild(circle)
currentY += CGFloat((random() % 400) + 70)
}
Will post more code if necessary.
There are two ways you can go about this. One is to simply check every circle's y position to see if it's above the screen. You'll need a reference to the circles so...
class GameScene: SKScene {
var circles = Array<SKSpriteNode>()
...
In your createCirlcesOnLine function, add each circle to the array as you create it.
...
self.addChild(circle)
circles.append(circle)
Then, in your update method, enumerate through the circles to see if any of them are above the top of the screen.
override func update(currentTime: NSTimeInterval) {
for circle in circles {
if circle.position.y > self.size.height + circle.size.height/2 {
//Send circle back to the bottom using the circle's position property
}
}
}
This solution will work but causes a lot of unnecessary checks on every update cycle.
A second more efficient (and slightly more complicated) recommendation is to add an invisible node above the top of the screen that stretches the screen width. When the circle collides with it, just move it to the bottom of the screen. Look into implementing the SKPhysicsContactDelegate protocol and what needs to happen for that to work. If you run into problems with this solution, post a separate question with those issues.

Why does the position not change for the sprite nodes?

I've commented the code. This is my first game so my knowledge is very shallow. I don't even know if there is a better way of doing this.
Now, the problem I am running into is that although the animation works, the position property I am printing is always 0. Why? My idea was that in the update method, I will check the position property and if it is above a certain height on the screen, I'll move the top sprite to the bottom (like a queue data structure - pull out from one end and add to the other end) and adjust the middle to be the top and the bottom to be the middle. That way, I just need 3 sprites to animate a scroll wheel of numbers.
override func didMoveToView(view: SKView) {
/* Idea is to have a scrolling wheel of numbers animate.
The approach is to have 3 'number' sprite nodes inside a crop node. Only one sprite number node is visible at a given time
because of the crop node's mask.
Initially, number 0, 1 and 2 one under the other and a crop sprite to crop out everything but just one number
that needs to be displayed. Then, we animate number 1 in and push out number 0 and then animate number 2 in and push
out number 1. When the position of the sprite containing 0 moves above a certain position 'y', we move it to the bottom
and have number 4 replace 0 so it can roll in next. So we have a continuous scrolling wheel of numbers */
//A slot to hold the numbers
let numSlot = SKSpriteNode(color: SKColor.clearColor(), size: CGSizeMake(100, 180))
//Create a crop node
let cn = SKCropNode.init()
//Add the crop node mask so only 1 number is visible
cn.maskNode = SKSpriteNode(color: SKColor.redColor(), size: CGSizeMake(100, 180))
//Add the number slot sprite to the crop node sprite at a certain position
numSlot.addChild(cn)
numSlot.position = CGPointMake(size.width/2, size.height/2 - 25)
self.addChild(numSlot)
//The three images that should live inside the numSlot node (with only 1 visible at a given time)
let zeroSprite = SKSpriteNode.init(imageNamed: "0.png", normalMapped: false)
let oneSprite = SKSpriteNode.init(imageNamed: "1.png", normalMapped: false)
let twoSprite = SKSpriteNode.init(imageNamed: "2.png", normalMapped: false)
//Add the three numbers to the numberHolderSprite and then add the numberHolderSprite to the crop Nod
let numberHolderSprite = SKSpriteNode.init()
numberHolderSprite.addChild(zeroSprite)
oneSprite.position = CGPointMake(0, -120)
numberHolderSprite.addChild(oneSprite)
twoSprite.position = CGPointMake(0, -240)
numberHolderSprite.addChild(twoSprite)
//Add the number holder sprite to the crop node
cn.addChild(numberHolderSprite)
top = zeroSprite
middle = oneSprite
bottom = twoSprite
var numSlotSpriteAction1 = SKAction.moveToY(top!.position.y + 500, duration: 0.5)
var numSlotSpriteAction2 = SKAction.moveToY(middle!.position.y + 500, duration: 0.5)
var numSlotSpriteAction3 = SKAction.moveToY(bottom!.position.y + 500, duration: 0.5)
println("Position \(top!.position.y)")
var numSlotGroupAction = SKAction.group([numSlotSpriteAction1, numSlotSpriteAction2, numSlotSpriteAction3])
numberHolderSprite.runAction(numSlotGroupAction)
println("Position \(top!.position.y)")
//Just a simple background image
let bg = SKSpriteNode.init(imageNamed: "bg.png", normalMapped: false)
bg.position = CGPointMake(size.width/2, size.height/2)
self.addChild(bg)
}
println("Position \(top!.position.y)")
is being executed prior to any SKActions being run. top is really zeroSprite, and has the default position of 0, 0.
However, even if your SKActions were run, it would not affect top. This is because the SKActions are run on the numberHolderSprite node. This means numberHolderSprite's position will change, not the sub-nodes. That action will also not be run until the next animation loop is processed.
So I am guessing this code will not do what you expect it to do.