Create an array of SKShapeNodes - ios

I'm new to Swift development and I'm trying to create a simple racing game. The road in the game has lane markers that animate from the top of the screen to the bottom then loop back up to the top. I am using SKShapeNodes for the lane line markers.
I have been able to get one lane marker to work and now I just need to create an array(I think?) of lane markers so that I can have 2 columns of lanes and enough to give the game the illusion of being a real road.
I'm able to create the array but I get a run-time error when I try to add a SKShapeNode to it. The error I get is:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP,subcode=0x0)
fatal error: Cannot index empty buffer
Here is the relevant source code:
class PlayScene: SKScene {
let road = SKSpriteNode(imageNamed: "road8")
let car = SKSpriteNode(imageNamed: "car")
let laneLineNode = SKShapeNode()
var laneLineNodeArray = [[SKShapeNode]]()
var groundSpeed = 25
}
override func didMoveToView(view: SKView) {
//draw a lane line
var path = CGPathCreateMutable();
var laneLineWidth = self.size.width / 85
var laneLineHeight = self.size.height / 11
var laneLine = CGRect(x: ((self.frame.size.width / 5) * 2), y:
(self.frame.size.height + laneLineHeight),
width: laneLineWidth, height: laneLineHeight)
laneLineNode.lineWidth = 0
laneLineNode.path = CGPathCreateWithRect(laneLine, nil)
laneLineNode.fillColor = SKColor.yellowColor()
laneLineNode.zPosition = 2
//THIS IS THE LINE I GET THE ERROR ON
laneLineNodeArray[0][0] = laneLineNode
self.addChild(self.road)
self.addChild(self.laneLineNode)
self.addChild(self.car)
}
override func update(currentTime: CFTimeInterval) {
//move the lane lines
scrollLaneLines(self.scene!)
}
}
Thanks for any help you can give me.

First of all... You are closing your class too soon with this code:
var groundSpeed = 25
} // <--
Second... Your are not initializing your array before using it. In my test, I got the error because of that.
You should use this syntax to initialize your Array or use append as #Arnab suggest.
let laneLineNode = SKShapeNode()
var laneLineNodeArray = [[laneLineNode]]

You have created an empty array so far. Use "append" to add data to the array before you access the elements by index.

Thank you! I was able to get it to work with this in Class PlayScene:
var laneLineNodeArray:[[SKShapeNode]] = []
and this in func didMoveToView:
//draw a lane line
var path = CGPathCreateMutable();
var laneLineWidth = self.size.width / 85
var laneLineHeight = self.size.height / 11
var laneLine = CGRect(x: 0, y: (self.frame.size.height + laneLineHeight),
width: laneLineWidth, height: laneLineHeight)
var laneLineRowNumber: Int
var laneLineY: CGFloat
for r in 0...6 {
var laneLineRow:[LaneLine] = []
laneLineRowNumber = r
for c in 0...1 {
let laneLineNode = LaneLine()
laneLineNode.lineWidth = 0
laneLineNode.path = CGPathCreateWithRect(laneLine, nil)
laneLineNode.fillColor = SKColor.yellowColor()
laneLineNode.zPosition = 2
//lanelineY determines the Y placement of the lane.
laneLineY = self.frame.size.height - (CGFloat((laneLineHeight) * 2)
* CGFloat(r))
if c == 0 {
laneLineNode.position = CGPointMake(laneWidth * 2, laneLineY)
}
else if c == 1 {
laneLineNode.position = CGPointMake(laneWidth * 3, laneLineY)
}
self.addChild(laneLineNode)
laneLineRow.append(laneLineNode)
}
laneLineNodeArray.append(laneLineRow)
}

Related

Swift SKPhysicsBody falling through static SKPhysicsBody with matching bitmasks

I am making a platforming game in Swift with SpriteKit involving a main character that jumps around. However, once the level loads the player immediately falls through the ground. You can see it in action here.
I am using SKTilemapNode to create the ground, and looping through the tiles when a level loads to create an SKPhysicsBody on a child node of the tile map. This is very similar to what is demoed in the "What's new in SpriteKit" video at WWDC 2016:
So, here we've got a little platform that I built. A little guy that can run around. And you can see that I got the parallax scrolling going on in the background. And you'll note that I'm colliding with the tiles here. And I achieve this by leveraging custom user data that we can put on each of our tiles. Here, I'll show you in our tile set. Select one of the variants here.
And you can see that we have some user data over here. And I just have a value called edgeTile which is a Boolean, and I set to 1.
So, in code, I'm going through the tile map in our platform demo here, and I'm looking for all of these edge tiles.
And whenever I find one, I create some physics data to allow the player to collide with it.
My function to create a physics body based off of an SKTilemapNode is as follows:
extension SKTileMapNode {
//In order for this to work, edge tile definitions must have the "edge" property in user data
func createPhysicsBody() -> SKPhysicsBody {
var physicsBodies = [SKPhysicsBody]()
for row in 0 ..< self.numberOfRows {
for column in 0 ..< self.numberOfColumns {
if self.tileDefinition(atColumn: column, row: row)?.userData?["edge"] != nil {
physicsBodies.append(SKPhysicsBody(rectangleOf: self.tileSize, center: self.centerOfTile(atColumn: column, row: row)))
}
}
}
let body = SKPhysicsBody(bodies: physicsBodies)
body.affectedByGravity = false
body.isDynamic = false
body.allowsRotation = false
body.pinned = true
body.restitution = 0
body.collisionBitMask = 0b1111
body.categoryBitMask = 0b1111
body.contactTestBitMask = 0b1000
return body
}
func initializePhysicsBody() {
let node = SKNode()
node.name = "Tilemap"
node.physicsBody = createPhysicsBody()
addChild(node)
}
}
So, in my scene setup all I have to do is call tileMap.initializePhysicsBody() to do everything that I need.
The SKPhysicsBody for my player is as follows:
let rect = CGSize(width: 16 * xScale, height: 24 * yScale)
let physics = SKPhysicsBody(rectangleOf: rect)
physics.isDynamic = true
physics.allowsRotation = false
physics.pinned = false
physics.affectedByGravity = true
physics.friction = 0
physics.restitution = 0
physics.linearDamping = 0
physics.angularDamping = 0
physics.density = 100
physics.categoryBitMask = 0b0001
physics.collisionBitMask = 0b0001
physics.contactTestBitMask = 0b0011
physics.usesPreciseCollisionDetection = true
physicsBody = physics
I'm not sure what the problem is here, but if I set the SKTilemapNode's physics body to be dynamic, it works. This is how I had the game working up until this point, however, this creates a lot of jitter in the ground because it's moving as a result of the player hitting it. So, thanks for reading this far at least, and any suggestions would be appreciated.
EDIT.
I think the ERROR Here is not using UInt 32
body.categoryBitMask: UInt32 = 2
body.collisionBitMask: UInt32 = 1
body.contactTestBitMask: UInt32 = 1
And Player
physics.categoryBitMask: UInt32 = 1
physics.collisionBitMask: UInt32 = 2
physics.contactTestBitMask: UInt32 = 2
This should definitely Work
Also try this way for the tileMapNode (given below) rather than creating the extension. This was given in a apple developer forum by dontangg
self.tileMap = self.childNode(withName: "Tile Map") as? SKTileMapNode
guard let tileMap = self.tileMap else { fatalError("Missing tile map for the level") }
let tileSize = tileMap.tileSize
let halfWidth = CGFloat(tileMap.numberOfColumns) / 2.0 * tileSize.width
let halfHeight = CGFloat(tileMap.numberOfRows) / 2.0 * tileSize.height
for col in 0..<tileMap.numberOfColumns {
for row in 0..<tileMap.numberOfRows {
let tileDefinition = tileMap.tileDefinition(atColumn: col, row: row)
let isEdgeTile = tileDefinition?.userData?["edgeTile"] as? Bool
if (isEdgeTile ?? false) {
let x = CGFloat(col) * tileSize.width - halfWidth
let y = CGFloat(row) * tileSize.height - halfHeight
let rect = CGRect(x: 0, y: 0, width: tileSize.width, height: tileSize.height)
let tileNode = SKShapeNode(rect: rect)
tileNode.position = CGPoint(x: x, y: y)
tileNode.physicsBody = SKPhysicsBody.init(rectangleOf: tileSize, center: CGPoint(x: tileSize.width / 2.0, y: tileSize.height / 2.0))
tileNode.physicsBody?.isDynamic = false
tileNode.physicsBody?.collisionBitMask = playerCollisionMask | wallCollisionMask
tileNode.physicsBody?.categoryBitMask = wallCollisionMask
tileMap.addChild(tileNode)
}
}
}

SpriteKit joint: follow the body

I've been asked to simplify this question, so that's what I'm doing.
I'm struggling in SpriteKit's physic joints (and possibly physic body properties). I tried every single subclass and many configurations but seams like nothing works or I'm doing something wrong.
I'm developing Snake game. User controls head of snake which should move at constant speed ahead and user can turn it clockwise or anticlockwise. All the remaining snake's pieces should follow the head - they should travel exactly the same path that head was some time ago.
I think for this game the Pin joint should be the answer, which anchor point is exactly in the centre between elements.
Unfortunately the result is not perfect. The structure should make the perfect circle, but it doesn't. I'm attaching the code, and gif showing the current effect. Is anyone experience enough to give me any suggestion what properties of physic body and or joints should are apply here for desired effect?
My code:
class GameScene: SKScene {
private var elements = [SKNode]()
override func didMove(to view: SKView) {
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
let dummyTurnNode = SKNode()
dummyTurnNode.position = CGPoint(x: size.width / 2 - 50, y: size.height / 2)
let dummyTurnBody = SKPhysicsBody(circleOfRadius: 1)
dummyTurnBody.isDynamic = false
dummyTurnNode.physicsBody = dummyTurnBody
addChild(dummyTurnNode)
for index in 0..<5 {
let element = SKShapeNode(circleOfRadius: 10)
let body = SKPhysicsBody(circleOfRadius: 10)
body.linearDamping = 0
// body.mass = 0
element.physicsBody = body
element.position = CGPoint(x: size.width / 2, y: size.height / 2 - 30 * CGFloat(index))
elements.append(element)
addChild(element)
let label = SKLabelNode(text: "A")
label.fontSize = 10
label.fontName = "Helvetica-Bold"
element.addChild(label)
if index == 0 {
element.fillColor = UIColor.blue()
body.velocity = CGVector(dx: 0, dy: 30)
let dummyTurnJoint = SKPhysicsJointPin.joint(withBodyA: dummyTurnBody, bodyB: body, anchor: dummyTurnNode.position)
physicsWorld.add(dummyTurnJoint)
} else {
body.linearDamping = 1
element.fillColor = UIColor.red()
let previousElement = elements[index - 1]
let connectingJoint = SKPhysicsJointPin.joint(withBodyA: previousElement.physicsBody!, bodyB: body, anchor: CGPoint(x: size.width / 2, y: size.height / 2 - 30 * CGFloat(index) + CGFloat(15)))
physicsWorld.add(connectingJoint)
}
}
}
override func update(_ currentTime: TimeInterval) {
let head = elements.first!.physicsBody!
var velocity = head.velocity
velocity.normalize()
velocity.multiply(30)
head.velocity = velocity
}
}
extension CGVector {
var rwLength: CGFloat {
let xSq = pow(dx, 2)
let ySq = pow(dy, 2)
return sqrt(xSq + ySq)
}
mutating func normalize() {
dx /= rwLength
dy /= rwLength
}
mutating func multiply(_ factor: CGFloat) {
dx *= factor
dy *= factor
}
}
"All the remaining snake's pieces should follow the head - they should travel exactly the same path that head was some time ago."
You should note that with Physics joints you are likely going to have variance no matter what you do. Even if you have it close to perfect you'll have rounding errors under the hood making the path not exact.
If all the tail parts are equal you can also use a different approach, this is something I've done for a comet tail. Basically the idea is that you have an array of tail objects and per-frame move move the last tail-object always to the same position as the head-object. If the head-object has a higher z-position the tail is drawn below it.
If you need to keep your tail in order you could vary the approach by storing an array of head-positions (per-frame path) and then place the tail objects along that path in your per-frame update call to the snake.
See my code below for example:
These are you head-object variables:
var tails = [SKEmitterNode]()
var tailIndex = 0
In your head init function instantiate the tail objects:
for _ in 0...MAX_TAIL_INDEX
{
if let remnant = SKEmitterNode(fileNamed: "FireTail.sks")
{
p.tails.append(remnant)
}
}
Call the below per-frame:
func drawTail()
{
if tails.count > tailIndex
{
tails[tailIndex].resetSimulation()
tails[tailIndex].particleSpeed = velocity() / 4
tails[tailIndex].emissionAngle = zRotation - CGFloat(M_PI_2) // opposite direction
tails[tailIndex].position = position
tailIndex = tailIndex < MAX_TAIL_INDEX ? tailIndex + 1 : 0
}
}
The resulting effect is actually really smooth when you call it from the scene update() function.

Trying to delay spawn nodes using waitForDuration in didMoveToView with SpriteKit Scene (SWIFT) but not working

I'm trying to stop my nodes from falling just for a second or two at the start of my game. So my problem is when I push start the nodes are already halfway down the screen. I also tried changing how high the nodes start but it seems like a costly solution since I want to be careful not to let my FPS get too low. In my code I am trying to do this in the didMoveToView and I am using waitForDuration but it doesn't work.
Example Image of Nodes Falling Down
Any SpriteKit masters know what I should do? I'm using Swift.
Here is my code:
override func didMoveToView(view: SKView) {
let wait = SKAction.waitForDuration(2.5)
let run = SKAction.runBlock {
self.spawnNumbers()
}
numContainer.runAction(SKAction.sequence([wait, run]))
}
func spawnNumbers() {
let minValue = self.size.width / 8
let maxValue = self.size.width - 36
let spawnPoint = CGFloat(arc4random_uniform(UInt32(maxValue - minValue)))
let action = SKAction.moveToY(-300, duration: 2)
numContainer = SKSpriteNode(imageNamed: "Circle")
numContainer.name = "Circle"
numContainer.size = CGSize(width: 72, height: 72)
numContainer.anchorPoint = CGPointMake(0, 0)
numContainer.position = CGPoint(x: spawnPoint, y: self.size.height)
numContainer.runAction(SKAction.repeatActionForever(action))
numContainer.zPosition = 2
let numberLabel = SKLabelNode(fontNamed: "AvenirNext-Bold")
numberLabel.text = "\(numToTouch)"
numberLabel.name = "Label"
numberLabel.zPosition = -1
numberLabel.position = CGPointMake(CGRectGetMidX(numContainer.centerRect) + 36, CGRectGetMidY(numContainer.centerRect) + 36)
numberLabel.horizontalAlignmentMode = .Center
numberLabel.verticalAlignmentMode = .Center
numberLabel.fontColor = UIColor.whiteColor()
numberLabel.fontSize = 28
addChild(numContainer)
numContainer.addChild(numberLabel)
numContainerArray.append(numContainer)
numToTouch += 1
}
I think you have to cast the 2.5 seconds to an NSTimeInterval:
let wait = SKAction.waitForDuration(NSTimeInterval(2.5))
You could also try putting the actions in the init function for your scene:
override init (size: CGSize) {
super.init(size: size)
//your code from before
}
Also, not sure if it matters, but this is what I normally do and it works,
let wait = SKAction.waitForDuration(NSTimeInterval(2.5))
let action = SKAction.runBlock({() in self.spawnNumbers()})
let actionThenWait = SKAction.sequence([wait, action])
self.runAction(actionThenWait)

GKObstacleGraph not finding path when obstacle introduced

I'm working on a Gameplaykit pathfinding proof-of-concept and I can't get GKObstacleGraph to find paths correctly.
In the following code snippet (it should work in an Xcode 7.2 playground), path2 is always an empty array if there is an obstacle provided when the graph is created. If I create the obGraph object with an empty array of obstacles the findPathFromNode returns a correct path.
The obstacle created should be a simple U shaped polygon with the end point being inside the U.
import UIKit
import GameplayKit
let pts = [vector_float2(2,2),
vector_float2(3,2),
vector_float2(3,6),
vector_float2(7,6),
vector_float2(7,2),
vector_float2(8,3),
vector_float2(8,7),
vector_float2(2,7),
vector_float2(2,2)]
let obstacle1 = GKPolygonObstacle(points: UnsafeMutablePointer(pts) ,
count: pts.count)
let obGraph = GKObstacleGraph(obstacles: [obstacle1], bufferRadius: 0)
let startPt = GKGraphNode2D(point: vector_float2(5,9))
let endPt = GKGraphNode2D(point: vector_float2(5,5))
let pt3 = GKGraphNode2D(point: vector_float2(0,0))
let pt4 = GKGraphNode2D(point: vector_float2(0,9))
let pt5 = GKGraphNode2D(point: vector_float2(5,0))
let pt6 = GKGraphNode2D(point: vector_float2(10,0))
obGraph.connectNodeUsingObstacles(startPt)
obGraph.connectNodeUsingObstacles(endPt)
obGraph.connectNodeUsingObstacles(pt3)
obGraph.connectNodeUsingObstacles(pt4)
obGraph.connectNodeUsingObstacles(pt5)
obGraph.connectNodeUsingObstacles(pt6)
startPt.connectedNodes
endPt.connectedNodes
pt3.connectedNodes
let path2 = obGraph.findPathFromNode(startPt, toNode: endPt)
print(path2)
I was having the same problem as Jack. I started with Will's code example and translated it to Swift 5.0 in Xcode 10.3. I added it into Xcode's Game project template. I still had the same result: an empty array from findPath(from:to:).
After playing around with the code, I realized that anything physics-related is going to impact pathing. The only way to show code that will work for everyone is to include the creation of SKScene and all SKNode instances. Notice I set gravity to 0 in SKPhysicsWorld and I add no SKPhysicsBody to anything.
Run this in a Playground. You activate the animation by tapping anywhere in the scene.
import PlaygroundSupport
import SpriteKit
import GameKit
class GameScene: SKScene {
let nodeToMove:SKShapeNode = {
let n = SKShapeNode(circleOfRadius: 10)
n.lineWidth = 2
n.strokeColor = UIColor.orange
n.position = CGPoint(x: -200, y: 150)
return n
}()
override func sceneDidLoad() {
addChild(nodeToMove)
let nodeToFind = SKShapeNode(circleOfRadius: 5)
nodeToFind.lineWidth = 2
nodeToFind.strokeColor = UIColor.red
addChild(nodeToFind)
nodeToFind.position = CGPoint(x: 200, y: -150)
let nodeToAvoid = SKShapeNode(rectOf: CGSize(width: 100, height: 100))
nodeToAvoid.lineWidth = 4
nodeToAvoid.strokeColor = UIColor.blue
addChild(nodeToAvoid)
nodeToAvoid.position = CGPoint.zero
let polygonObstacles = SKNode.obstacles(fromNodeBounds: [nodeToAvoid])
let graph = GKObstacleGraph(obstacles: polygonObstacles, bufferRadius: 10.0)
let end = GKGraphNode2D(point: vector2(Float(nodeToMove.position.x), Float(nodeToMove.position.y)))
let start = GKGraphNode2D(point: vector2(Float(nodeToFind.position.x), Float(nodeToFind.position.y)))
graph.connectUsingObstacles(node: end)
graph.connectUsingObstacles(node: start)
graphNodes = graph.findPath(from: end, to: start) as! [GKGraphNode2D]
print("graphNodes = \(graphNodes)")
}
var graphNodes = [GKGraphNode2D]()
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
touches.first.flatMap {_ in
let newActions: [SKAction] = graphNodes.map { n in
return SKAction.move(to: CGPoint(x: CGFloat(n.position.x), y: CGFloat(n.position.y)), duration: 2)
}
nodeToMove.run(SKAction.sequence(newActions))
}
}
}
let sceneView = SKView(frame: CGRect(x:0 , y:0, width: 640, height: 480))
let scene = GameScene(size: CGSize(width: 640, height: 480))
scene.scaleMode = .aspectFill
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
scene.backgroundColor = UIColor.purple
scene.physicsWorld.gravity = CGVector(dx: 0.0, dy: 0.0)
sceneView.presentScene(scene)
PlaygroundSupport.PlaygroundPage.current.liveView = sceneView
The output in the console is:
graphNodes = [GKGraphNode2D: {-200.00, 150.00}, GKGraphNode2D: {62.85, 62.85}, GKGraphNode2D: {200.00, -150.00}]
The start state:
The end state:
Warning: I do not know why it takes an entire second between the user tap and the start of the animation. Performance tuning is a separate topic.
(sorry in advance for obj-c not swift)
It is my impression that adding each vertex point as a connection is not necessary, merely telling the GKObstacleGraph about the GKPolygonObstacle will be enough for it to avoid the generated polgyon shape. I have used the following and received 3 nodes to create a pathway around my obstacle (with a buffer of 10.0f) seen here:
- (void)findPathWithNode:(SKNode *)nodeToFindPath {
NSMutableArray *obstaclesToAvoid = [NSMutableArray array];
for (SKNode *objectInScene in chapterScene.children) {
if ([objectInScene.name isEqualToString:#"innerMapBoundary"]) {
[obstaclesToAvoid addObject:objectInScene];
}
}
/* FYI: The objectInScene is just a black SKSpriteNode with a
square physics body 100 x 100 rotated at 45°
*/
NSArray *obstacles = [SKNode obstaclesFromNodePhysicsBodies:[NSArray arrayWithArray:obstaclesToAvoid]];
GKObstacleGraph *graph = [GKObstacleGraph graphWithObstacles:obstacles bufferRadius:10.0f];
GKGraphNode2D *end = [GKGraphNode2D nodeWithPoint:vector2((float)character.position.x, (float)character.position.y)];
GKGraphNode2D *start = [GKGraphNode2D nodeWithPoint:vector2((float)nodeToFindPath.position.x, (float)nodeToFindPath.position.y)];
[graph connectNodeUsingObstacles:end];
[graph connectNodeUsingObstacles:start];
NSArray *pathPointsFound = [graph findPathFromNode:enemy toNode:target];
NSLog(#"Path: %#", pathPointsFound);
GKPath *pathFound;
// Make sure that there were at least 2 points found before creating the path
if (pathPointsFound.count > 1) {
for (GKGraphNode2D *nodeFound in pathPointsFound) {
// This is just to create a visual for the points found
vector_float2 v = (vector_float2){(float)nodeFound.position.x, (float)nodeFound.position.y};
CGPoint p = CGPointMake(v.x, v.y);
SKShapeNode *shapetoadd = [SKShapeNode shapeNodeWithCircleOfRadius:4];
shapetoadd.name = #"shapeadded";
shapetoadd.fillColor = [UIColor redColor];
shapetoadd.position = p;
[chapterScene addChild:shapetoadd];
}
pathFound = [GKPath pathWithGraphNodes:pathPointsFound radius:10.0];
}
}
Hopefully this points you in the right direction!

Spawning a circle in a random spot on screen

I've been racking my brain and searching here and all over to try to find out how to generate a random position on screen to spawn a circle. I'm hoping someone here can help me because I'm completely stumped. Basically, I'm trying to create a shape that always spawns in a random spot on screen when the user touches.
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenHeight = screenSize.height
let screenWidth = screenSize.width
let currentBall = SKShapeNode(circleOfRadius: 100)
currentBall.position = CGPointMake(CGFloat(arc4random_uniform(UInt32(Float(screenWidth)))), CGFloat(arc4random_uniform(UInt32(Float(screenHeight)))))
self.removeAllChildren()
self.addChild(currentBall)
}
If you all need more of my code, there really isn't any more. But thank you for whatever help you can give! (Just to reiterate, this code kind of works... But a majority of the spawned balls seem to spawn offscreen)
The problem there is that you scene is bigger than your screen bounds
let viewMidX = view!.bounds.midX
let viewMidY = view!.bounds.midY
print(viewMidX)
print(viewMidY)
let sceneHeight = view!.scene!.frame.height
let sceneWidth = view!.scene!.frame.width
print(sceneWidth)
print(sceneHeight)
let currentBall = SKShapeNode(circleOfRadius: 100)
currentBall.fillColor = .green
let x = view!.scene!.frame.midX - viewMidX + CGFloat(arc4random_uniform(UInt32(viewMidX*2)))
let y = view!.scene!.frame.midY - viewMidY + CGFloat(arc4random_uniform(UInt32(viewMidY*2)))
print(x)
print(y)
currentBall.position = CGPoint(x: x, y: y)
view?.scene?.addChild(currentBall)
self.removeAllChildren()
self.addChild(currentBall)
First: Determine the area that will be valid. It might not be the frame of the superview because perhaps the ball (let's call it ballView) might be cut off. The area will likely be (in pseudocode):
CGSize( Width of the superview - width of ballView , Height of the superview - height of ballView)
Once you have a view of that size, just place it on screen with the origin 0, 0.
Secondly: Now you have a range of valid coordinates. Just use a random function (like the one you are using) to select one of them.
Create a swift file with the following:
extension Int
{
static func random(range: Range<Int>) -> Int
{
var offset = 0
if range.startIndex < 0 // allow negative ranges
{
offset = abs(range.startIndex)
}
let mini = UInt32(range.startIndex + offset)
let maxi = UInt32(range.endIndex + offset)
return Int(mini + arc4random_uniform(maxi - mini)) - offset
}
}
And now you can specify a random number as follows:
Int.random(1...1000) //generate a random number integer somewhere from 1 to 1000.
You can generate the values for the x and y coordinates now using this function.
Given the following random generators:
public extension CGFloat {
public static var random: CGFloat { return CGFloat(arc4random()) / CGFloat(UInt32.max) }
public static func random(between x: CGFloat, and y: CGFloat) -> CGFloat {
let (start, end) = x < y ? (x, y) : (y, x)
return start + CGFloat.random * (end - start)
}
}
public extension CGRect {
public var randomPoint: CGPoint {
var point = CGPoint()
point.x = CGFloat.random(between: origin.x, and: origin.x + width)
point.y = CGFloat.random(between: origin.y, and: origin.y + height)
return point
}
}
You can paste the following into a playground:
import XCPlayground
import SpriteKit
let view = SKView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
XCPShowView("game", view)
let scene = SKScene(size: view.frame.size)
view.presentScene(scene)
let wait = SKAction.waitForDuration(0.5)
let popIn = SKAction.scaleTo(1, duration: 0.25)
let popOut = SKAction.scaleTo(0, duration: 0.25)
let remove = SKAction.removeFromParent()
let popInAndOut = SKAction.sequence([popIn, wait, popOut, remove])
let addBall = SKAction.runBlock { [unowned scene] in
let ballRadius: CGFloat = 25
let ball = SKShapeNode(circleOfRadius: ballRadius)
var popInArea = scene.frame
popInArea.inset(dx: ballRadius, dy: ballRadius)
ball.position = popInArea.randomPoint
ball.xScale = 0
ball.yScale = 0
ball.runAction(popInAndOut)
scene.addChild(ball)
}
scene.runAction(SKAction.repeatActionForever(SKAction.sequence([addBall, wait])))
(Just make sure to also paste in the random generators, too, or to copy them to the playground's Sources, as well as to open the assistant editor so you can see the animation.)

Resources