Spritekit Add nodes without overlapping - swift

I am trying to add 20 nodes at random locations on the screen without any of the nodes overlapping. Ive got the adding of the nodes at random locations part but I sill get some that overlap. What I have done so far is: I would be greatful for a point in the right dirrection.
while i < 20 {
let bubbleSize = self.frame.width / 12
let bubble = SKShapeNode(circleOfRadius: bubbleSize)
let widthL = -self.frame.size.width / 2 + bubble.frame.size.width / 2
let widthH = self.frame.size.width / 2 - bubble.frame.size.width / 2
let heightL = -self.frame.size.height / 2 + bubble.frame.size.height/ 2
let heightH = self.frame.size.height / 2 - bubble.frame.size.height / 2
var randWidth = randomNumber(range: widthL..<widthH)
var randHeight = randomNumber(range: heightL..<heightH)
bubble.fillColor = SKColor.cyan
bubble.position = CGPoint(x: randWidth, y: randHeight)
self.addChild(bubble)
}
func randomNumber(range: Range<CGFloat>) -> CGFloat {
//function that gives a random number of a range of CGFloats entered
let min = range.lowerBound
let max = range.upperBound
return CGFloat(arc4random_uniform(UInt32(CGFloat(max - min)))) + min
}

This should get you started. It essentially divides the screen into a grid and places the bubbles in grid locations while ensuring no other bubble is at that position. there is lots more you could do with this, like add slight random placement to the location (random generate some small offsets -2 to +2 for x and y values so that they don't look so perfectly placed).
let bubbleSize = self.frame.width / 12
let minX = 0 - self.frame.size.width / 2
let maxX = self.frame.size.width / 2
let minY = 0 - self.frame.size.height/ 2
let maxY = self.frame.size.height/ 2
var usedIndexes: [Int]!
let xRange = maxX * 2
let yRange = maxY * 2
let cols = xRange / bubbleSize
let rows = yRange / bubbleSize
func createBubbles(count: Int) {
for _ in 0..<count {
createBubble()
}
}
func createBubble() {
let index = findSlot()
let posX = index % cols
let posY = index / cols
let bubble = SKShapeNode(circleOfRadius: bubbleSize)
bubble.fillColor = SKColor.cyan
bubble.position = CGPoint(x: posX, y: posY)
self.addChild(bubble)
}
func findSlot() -> Int {
//preventative measure to stop endless loop from happening
guard usedIndexes.count > rows * cols else { return 0 }
let randomX = randomNumber(range: 0..<cols)
let randomY = randomNumber(range: 0..<rows)
let index = randomX + randomY * cols
if usedIndexes.contains(index) {
return findSlot()
}
else {
usedIndexes.append(index)
}
return index
}

Related

Amoeba-shaped controls with SpriteKit

I am new to SpriteKit, and I am trying to create a distorted circle that acts like "amoeba". What I am trying to do is to use SKShapeNode initialised with with UIBezierPath created over 8-10 points on a circle with some randomness (something like):
let theta : Double = (Double(i) * Double.pi / 180.0) * (36.0 + Double.random(in: -1...1))
let radius : CGFloat = CGFloat(100.0 + Double.random(in: 0...20))
let a = CGFloat(cos(theta)) * radius
let b = CGFloat(sin(theta)) * radius
This part works, but then, I am starting to build the path using addCurve() method and the shapes come out really ugly - probably because I don't fully understand how the method works and what should I use for control points.
Appreciate if you have any better ideas or help me with using addCurve() in a better way.
Here is what I've done - posting as it might be useful to someone, parameters could be tweaked to your liking.
let path = UIBezierPath()
let numPoints = Int.random(in: 5...25)
let totalPoints = numPoints * 3
var pt : [CGPoint] = Array(repeating: CGPoint(), count: totalPoints)
var coef = 1.0
for i : Int in 0..<(totalPoints) {
let theta = (Double(i) * Double.pi / 180.0) * (360.0/Double(totalPoints) + Double.random(in: -1.0...1.0))
coef = (i % 3 == 2 ? 1 : 0.25)
let radius = CGFloat(100.0 * coef + Double.random(in: 0...20))
let a = CGFloat(cos(theta)) * radius
let b = CGFloat(sin(theta)) * radius
pt[i] = CGPoint(x: a, y: b)
// the code snippet below is to show the curve and critical points
let point : SKShapeNode = SKShapeNode(circleOfRadius: 5)
point.fillColor = i % 3 == 0 ? .green : .cyan
point.position = pt[i]
point.zPosition = 10
self.addChild(point)
// end of code snippet
}
path.move(to: CGPoint(x: self.view!.bounds.minX + pt[0].x, y: self.view!.bounds.minY + pt[0].y))
for i in 1...numPoints{
path.addCurve(to: pt[(i * 3) % (totalPoints)],
controlPoint1: pt[(i * 3 - 1)],
controlPoint2: pt[(i * 3 - 2)])
}
path.close()
let amoeba = SKShapeNode(path: path.cgPath)
amoeba.strokeColor = .orange
self.addChild(amoeba)

Normalising a CGFloat Array to UIView frame height

I am plotting an array of CGFloat values within a UIBezierPath. Now I want to Normalise the values so the max value of the array will occupy the whole height.
So far
let scaleFactor = (waveView.frame.height) * readFile.maxValue
for point in readFile.points {
let nextPoint = CGPoint(x: soundPath.currentPoint.x + sampleSpace, y: middleY - (point * scaleFactor) - 1.0)
soundPath.addLine(to: nextPoint)
soundPath.move(to: nextPoint)
}
But it does not seem to work...
EDIT
ReadFile:
class ReadFile {
public enum ReadFileError:Error{
case errorReading(String)
}
/* Player Interval Measured in Miliseconds (By now..) */
var beginPosition:Int32 = 0
var endPosition:Int32 = 0
/* Sample Rate -> Default 48KHz */
var sampleRate:Double = 48000
var samplesSeconds:CGFloat = 5
var maxValue:CGFloat = 0
var points:[CGFloat] = []
}
And
sampleSpace = 0.2
Thank you Andy for your answer but I have finally figured it out.
I am drawing a sound wave so it has positives and negatives values.
heightMax = waveView.frame.height/2
Applying a rule of three (Spanish translation) I end up with this:
func drawSoundWave(windowLength:Int32){
// Drawing code
print("\(logClassName): Drawing!!!")
print("\(logClassName): points COUNT = \(readFile.points.count)")
let soundPath = UIBezierPath()
soundPath.lineWidth = lineWidth
soundPath.move(to: CGPoint(x:0.0 , y: middleY))
print("\(logClassName) max ")
for point in readFile.points{
let normalizedHeight:CGFloat = (waveView.frame.height * point) / (2 * readFile.maxValue)
let nextPoint = CGPoint(x: soundPath.currentPoint.x + sampleSpace, y: middleY - (normalizedHeight))
soundPath.addLine(to: nextPoint)
soundPath.move(to: nextPoint)
}
let trackLayer = CAShapeLayer()
trackLayer.path = soundPath.cgPath
waveView.layer.addSublayer(trackLayer)
trackLayer.strokeColor = UIColor.red.cgColor
trackLayer.lineWidth = lineWidth
trackLayer.fillColor = UIColor.green.cgColor
trackLayer.lineCap = kCALineCapRound
}
where
let normalizedHeight:CGFloat = (waveView.frame.height * point) / (2 * readFile.maxValue)
is the normalize value given a readFile.maxValue and waveView.frame.height

Why does this position always return the same X value?

I am using the following code to try to creating a CGPoint for a ball along the top of the screen:
func randomBallPosition() -> CGPoint {
let random = CGFloat((arc4random_uniform(8) + 1) / 10)
let randomX = CGFloat(self.frame.size.width * random)
let staticY = CGFloat(self.frame.size.height * 0.95)
return CGPoint(x: randomX, y: staticY)
}
However the ball is always placed at (0,y) and I'm unsure why.
You just need to divide by 10 after converting your integer to CGFloat:
let random = CGFloat((arc4random_uniform(8) + 1)) / 10

Swift SpriteKit contain nodes within screen width

I have randomly spawning nodes moving vertically down the view.
Here is the code for doing this:
let meteTexture = SKTexture(imageNamed: "redmete.png")
let movementAmount = arc4random() % UInt32(self.frame.width)
let meteOffset = CGFloat(movementAmount) - self.frame.width / 2
let meteTime = arc4random_uniform(4) + 3;
let moveMete = SKAction.move(by: CGVector(dx: 0, dy: -2 * self.frame.height), duration: TimeInterval(meteTime))
redmete = SKSpriteNode(texture: meteTexture)
redmete.position = CGPoint(x: self.frame.midX + meteOffset, y: self.frame.midY + self.frame.height / 2)
My only problem is that as the meteOffset uses the centre of the sprite therefore it can occasionally spawn so 50% or so is out of the view.
I have tried
let movementAmount = arc4random() % UInt32(self.frame.width - meteTexture.size().width / 2)
I've also tried
let meteOffset = CGFloat(movementAmount) - meteTexture.size().width / 2 - self.frame.width / 2
But neither keep the whole of the sprite within the view. How can I do this?
You will want the initial position to be between the sub range of your frame height that is inset by half the height of the meteor.
You would need to have :
let meteorHeight = meteTexture.size().height
let verticalRange = self.frame.height - meteorHeight
let randomXPosition = meteorHeight/2 + arc4random() % verticalRange
Same goes for the horizontal position (if you want that to be random as well) :
let meteorWidth = meteTexture.size().width
let horizontalRange = self.frame.width - meteorWidth
let randomYPosition = meteorWidth/2 + arc4random() % horizontalRange
You can then set the position directly with the random XY coordinates
redmete.position = CGPoint(x:randomXPosition, y:randomYPosition)
If you don't want the meteor to appear too close to the edges, you can reduce the value of the verticalRange and horizontalRange further by subtracting a fixed offset or multiplying by a fraction.
SOLVED:
let meteTexture = SKTexture(imageNamed: "redmete.png")
let movementAmount = arc4random() % UInt32(self.frame.width / 2)
let meteOffset = CGFloat(movementAmount) + meteTexture.size().width/2 - self.frame.width / 2
let meteTime = arc4random_uniform(4) + 3;
let moveMete = SKAction.move(by: CGVector(dx: 0, dy: -2 * self.frame.height), duration: TimeInterval(meteTime))
redmete = SKSpriteNode(texture: meteTexture)
redmete.position = CGPoint(x: self.frame.midX + meteOffset, y: self.frame.midY + self.frame.height / 2)

How to make sprites follow a random pattern within a circle?

I am makeing a game in which I want that the enemies move following a random pattern within a circle. I already made that the enemies spawn randomly in all the sides of the screen, but the problem is that I dont know how to make the enemies move following a random pattern within a circle just like the image.
class GameScene: SKScene, SKPhysicsContactDelegate {
var circuloPrincipal = SKSpriteNode(imageNamed: "circulo")
var enemigoTimer = NSTimer()
}
override func didMoveToView(view: SKView) {
circuloPrincipal.size = CGSize(width: 225, height: 225)
circuloPrincipal.position = CGPoint(x: frame.width / 2, y: frame.height / 2)
circuloPrincipal.color = colorAzul
circuloPrincipal.colorBlendFactor = 1.0
circuloPrincipal.name = "circuloPrincipal"
circuloPrincipal.zPosition = 1.0
self.addChild(circuloPrincipal)
override func touchesBegan(touches: Set, withEvent event: UIEvent?) {
enemigoTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("enemigos"), userInfo: nil, repeats: true)
}
func enemigos() {
let enemigo = SKSpriteNode(imageNamed: "enemigo")
enemigo.size = CGSize(width: 25, height: 25)
enemigo.zPosition = 2.0
enemigo.name = "enemigo"
let posisionRandom = arc4random() % 4
switch posisionRandom {
case 0:
enemigo.position.x = 0
let posisionY = arc4random_uniform(UInt32(frame.size.height))
enemigo.position.y = CGFloat(posisionY)
self.addChild(enemigo)
break
case 1:
enemigo.position.y = 0
let posisionX = arc4random_uniform(UInt32(frame.size.width))
enemigo.position.x = CGFloat(posisionX)
self.addChild(enemigo)
break
case 2:
enemigo.position.y = frame.size.height
let posisionX = arc4random_uniform(UInt32(frame.size.width))
enemigo.position.x = CGFloat(posisionX)
self.addChild(enemigo)
break
case 3:
enemigo.position.x = frame.size.width
let posisionY = arc4random_uniform(UInt32(frame.size.height))
enemigo.position.y = CGFloat(posisionY)
self.addChild(enemigo)
break
default:
break
}
enemigo.runAction(SKAction.moveTo(circuloPrincipal.position, duration: 1.4))
}
Try to add this code:
let randomY = CGFloat(Int.random(-Int(circuloPrincipal.frame.height/2)...Int(circuloPrincipal.frame.height/2)))
let randomX = CGFloat(Int.random(-Int(circuloPrincipal.frame.width/2)...Int(circuloPrincipal.frame.width/2)))
let slopeToCirculoPrincipal = (enemigo.position.y - circuloPrincipal.position.y + randomY ) / (enemigo.position.x - circuloPrincipal.position.x + randomX)
let constant = enemigo.position.y - slopeToCirculoPrincipal * enemigo.position.x
let finalX : CGFloat = enemigo.position.x < circuloPrincipal.position.x ? 1500.0 : -1500.0 // Set it to somewhere outside screen size
let finalY = constant + slopeToCirculoPrincipal * finalX
let distance = (enemigo.position.y - finalY) * (enemigo.position.y - finalY) + (enemigo.position.x - finalX) * (enemigo.position.x - finalX)
let enemigoSpeed : CGFloat = 100.0
let timeToCoverDistance = sqrt(distance) / enemigoSpeed
let moveAction = SKAction.moveTo(CGPointMake(finalX, finalY), duration: NSTimeInterval(timeToCoverDistance))
let removeAction = SKAction.runBlock { () -> Void in
enemigo.removeFromParent()
}
enemigo.runAction(SKAction.sequence([moveAction,removeAction]))
Instead of:
enemigo.runAction(SKAction.moveTo(circuloPrincipal.position, duration: 1.4))
Also you need to put this extension somewhere in your project:
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
}
}