AND (&&) operator not working as expected in SWIFT - swift

Im trying to randomly generate nodes on the screen without overlapping so I decided to find the range/distance of each node from each other and continuously randomly generate a position where the range from each node is greater than 100, once the range is over 100 it'll break out of the while loop and add the node to the view
var p2Dp1: CGFloat = 0
var p3Dp1: CGFloat = 0
var p3Dp2: CGFloat = 0
planet1.position = CGPoint(x: Int(arc4random_uniform(220) + 80), y: Int(arc4random_uniform(500) + 100))
while p2Dp1 < 100.0 {
planet2.position = CGPoint(x: Int(arc4random_uniform(220) + 80), y: Int(arc4random_uniform(500) + 100))
p2Dp1 = SDistanceBetweenPoints(planet2.position, p2: planet1.position)
print(" planet 2 distance from 1: \(p2Dp1) ")
}
while p3Dp1 < 100.0 && p3Dp2 < 100.0 {
planet3.position = CGPoint(x: Int(arc4random_uniform(220) + 80), y: Int(arc4random_uniform(500) + 100))
p3Dp1 = SDistanceBetweenPoints(planet3.position, p2: planet1.position)
print(" planet 3 distance from 1: \(p3Dp1) ")
p3Dp2 = SDistanceBetweenPoints(planet3.position, p2: planet2.position)
print(" planet 3 distance from 2: \(p3Dp2) ")
}
self.addChild(planet1)
self.addChild(planet2)
self.addChild(planet3)
it starts by randomly giving the first node(planet1) a position on the view and then randomly give node2(planet2) a position where the distance from planet 1 and 2 are greater than 100. The first while loops seems to be working fine, its the second while loops thats giving me trouble. According to the console, the "&&" operator doesn't seem to be working
Console/Log:
planet 2 distance from 1: 96.1665222413705 planet 2 distance from 1: 185.407658957229 planet 3 distance from 1: 136.30847369111 planet 3 distance from 2: 55.1724568965349 EWWWW WE'RE TOUCHING
I set up the didBeginContact(contact: SKPhysicsContact) where it prints out if contact has happened
this is the method im using to find the distance:
func SDistanceBetweenPoints(p1: CGPoint, p2: CGPoint) -> CGFloat {
return hypot(p2.x - p1.x, p2.y - p1.y);
}
can someone please tell me what I might be doing wrong? I'm planning to add 5 planets in total but just for now im testing it out with 3

The condition of your while loop is:
p3Dp1 < 100.0 && p3Dp2 < 100.0
This means that the while loop will only continue while both conditions are true. As soon as either part of the condition is false (i.e. the distance is greater than 100) the loop will stop executing.
You probably want to change the condition to an or (||), for example:
while p3Dp1 < 100.0 || p3Dp2 < 100.0 {
This will keep running the while loop until both distances are greater than 100.

Change && to ||
You are now saying keep trying while both p2 and p3 are less than 100 so as soon as p2 is greater than 100 the while fails and you stop caring about p3.

Related

How to exclude positions in a field in SpriteKit?

I have a function that spawns little balls, randomly positioned, on the screen. The problem I face is that I want to distribute the balls randomly, but when I do so, some balls spawn on top of each other. I want to exclude all the positions that are already taken (and maybe a buffer of a few pixels around the balls), but I don't know how to do so. I worked around this by giving the balls a Physicsbody, so they move off from one another if they happen to spawn on top of each other. But I want them to not spawn on top of each other in the first place. My code for now is the following:
spawnedBalls = [Ball]()
level = Int()
func setupLevel() {
let numberOfBallsToGenerate = level * 2
let boundary: CGFloat = 26
let rightBoundary = scene!.size.width - boundary
let topBoundary = scene!.size.height - boundary
while spawnedBalls.count < numberOfBallsToGenerate {
let randomPosition = CGPoint(x: CGFloat.random(in: boundary...rightBoundary), y: CGFloat.random(in: boundary...topBoundary))
let ball = Ball()
ball.position = randomPosition
ball.size = CGSize(width: 32, height: 32)
ball.physicsBody = SKPhysicsBody(circleOfRadius: ball.size.width)
ball.physicsBody?.affectedByGravity = false
ball.physicsBody?.allowsRotation = false
ball.physicsBody?.categoryBitMask = 1
ball.physicsBody?.collisionBitMask = 1
spawnedBalls.append(ball)
self.addChild(ball)
}
}
I don't know if this problem should be solved by having an array that stores all taken positions, or if I should use some kind of FiledNode, where occupied space can be sort of subtracted, but sadly I am unfamiliar with FieldNodes, so I don't know if that's the right way to face the problem.
Step 1) Replace
let randomPosition = ....
with
let randomPosition = randomPositionInOpenSpace()
Step 2) Write the randomPositionInOpenSpace function:
Idea is:
a) generate a random position
b) is it in open space? if so return that
c) repeat until OK
Then Step 3) write the 'is it in open space' function
For that you need to know if the proposed coordinate is near any of the other balls. For circles, you can test the distance between their centers is greater than (radiuses + margins). Distance between centers is pythagoras: sqrt of the x delta squared plus the y delta squared.

Enemies following in SpriteKit get closer overtime

This is code that I had to update from and outdated blog post
import SpriteKit
class CreateEnemies {
var enemySprites: [SKSpriteNode] = []
static var lowerRate = 120
static var upperRate = 250
func spawnEnemy(targetSprite: SKNode) -> SKSpriteNode {
// create a new enemy sprite
let newEnemy = SKSpriteNode(imageNamed:"sharkshark")
enemySprites.append(newEnemy)
newEnemy.size = CGSize(width: 35, height: 35)
let randomXStart = CGFloat(arc4random_uniform(UInt32(2000 - (-2000)))) + CGFloat(-2000)
let randomYStart = CGFloat(arc4random_uniform(UInt32(2000 - (-2000)))) + CGFloat(-2000)
let xInt = Int(randomXStart)
let yInt = Int(randomYStart)
newEnemy.position = CGPoint(x: xInt, y: yInt)
// Define Constraints for orientation/targeting behavior
let i = enemySprites.count-1
let rangeForOrientation = SKRange(constantValue:CGFloat(M_2_PI*7))
let orientConstraint = SKConstraint.orient(to: targetSprite, offset: rangeForOrientation)
let rangeToSprite = SKRange(lowerLimit: CGFloat(CreateEnemies.lowerRate), upperLimit: CGFloat(CreateEnemies.upperRate))
var distanceConstraint: SKConstraint
// First enemy has to follow spriteToFollow, second enemy has to follow first enemy, ...
if enemySprites.count-1 == 0 {
distanceConstraint = SKConstraint.distance(rangeToSprite, to: targetSprite)
} else {
distanceConstraint = SKConstraint.distance(rangeToSprite, to: enemySprites[i-1])
}
newEnemy.constraints = [orientConstraint, distanceConstraint]
return newEnemy
}
}
I was wondering how I would make the enemies get closer to the target gradually the longer they exist.
I tried changing the variables every 5 seconds using a
let wait5 = SKAction.wait(forDuration: 5)
let getCloser = SKAction.repeatForever(SKAction.sequence([wait5, SKAction.run {self.changeRates()}]))
self.run(getCloser)
and attaching it to run a function
func changeRates() {
CreateEnemies.lowerRate -= 25
CreateEnemies.upperRate -= 25
}
A few things here:
1) your random is based on a square not a circle, so you are going to find your enemies spawning more on the corners than in the center of each side of your screen. I would recommend making a random angle between 0 and 2 pi, a random radius of I am guessing between 0 and 2000, and then setting your position using CGPoint(x:cos(angle) * radius, y:sin(angle) * radius)
2) your lowerRate is going to hit negative in 25 seconds, That may become a problem. I would use a percentage instead:
lowerRate = lowerRateStart * percentage
percentage *= 0.90
This will reduce your sprites 10 percent of their current location, so 90 81 73 64 58 52 47 42 38 34 31 28 25 22 20 18 16 14 13 12 11 10 9 8 7 6 5 4..... then be between 4 and 0 for a while. This basically assures that your number approaches zero, but will never become 0.
3) Your random start and your distance constraint are not correlating correctly, and this may cause problems. I would recommend setting the position somewhere inside of your distance constraint radius, so your rates should be:
let radius = CGFloat(arc4random_uniform(UInt32(upperRate - lowerRate)))
newEnemy.position = CGPoint(x:cos(angle) * radius + lowerRate + targetSprite.x, y:sin(angle) * radius + lowerRate + targetSprite.y)

Connect Physicsbodies on TileMap in SpriteKit

I use the following function to append physicsbodies on tiles from a SKTileMapNode:
static func addPhysicsBody(to tileMap: SKTileMapNode, and tileInfo: String){
let tileSize = tileMap.tileSize
let halfWidth = CGFloat(tileMap.numberOfColumns) / 2 * tileSize.width
let halfHeight = CGFloat(tileMap.numberOfRows) / 2 * tileSize.height
for row in 0..<tileMap.numberOfColumns{
for column in 0..<tileMap.numberOfRows{
let tileDefinition = tileMap.tileDefinition(atColumn: column, row: row)
let isCorrectTile = tileDefinition?.userData?[tileInfo] as? Bool
if isCorrectTile ?? false && tileInfo == "wall"{
let x = CGFloat(column) * tileSize.width - halfWidth
let y = CGFloat(row) * tileSize.height - halfHeight
let tileNode = SKNode()
tileNode.position = CGPoint(x: x, y: y)
tileNode.physicsBody = SKPhysicsBody.init(rectangleOf: tileSize, center: CGPoint(x: tileSize.width / 2, y: tileSize.height / 2))
tileNode.physicsBody!.isDynamic = false
tileNode.physicsBody!.restitution = 0.0
tileNode.physicsBody!.categoryBitMask = Constants.PhysicsCategories.wall
tileNode.physicsBody!.collisionBitMask = Constants.PhysicsCategories.player | Constants.PhysicsCategories.npc | Constants.PhysicsCategories.enemy
nodesForGraph.append(tileNode)
tileMap.addChild(tileNode)
}
}
}
}
However if I use this, I have a physicsbody per tile. I want to connect physicsbodies to bigger ones to get a better performance. I know that this can be with init(bodies: [SKPhysicsBody]). But how can I do that?
How can I find out which body is next to another body to group them?
The physicsbodies in the tileMap aren't all next to each other. Some are big blocks of physicsbodies, some are single physicsbodies with no bodies next to them. So I can't simply put every physicsbody in an array and group them.
Here's an image that shows how it looks like at the moment.
I hope the explanation is clear enough. If not, I will try to explain it better.
Has anyone done this before and can point me in the right direction? I would appreciate any help.
EDIT:
Before I tried this:
static var bodies = [SKPhysicsBody]()
static func addPhysicsBody(to tileMap: SKTileMapNode, and tileInfo: String){
let tileSize = tileMap.tileSize
let halfWidth = CGFloat(tileMap.numberOfColumns) / 2 * tileSize.width
let halfHeight = CGFloat(tileMap.numberOfRows) / 2 * tileSize.height
for column in 0..<tileMap.numberOfColumns{
for row in 0..<tileMap.numberOfRows{
let tileDefinition = tileMap.tileDefinition(atColumn: column, row: row)
let isCorrectTile = tileDefinition?.userData?[tileInfo] as? Bool
if isCorrectTile ?? false && tileInfo == "wall"{
let x = CGFloat(column) * tileSize.width - halfWidth
let y = CGFloat(row) * tileSize.height - halfHeight
let tileNode = SKNode()
tileNode.position = CGPoint(x: x, y: y)
tileNode.physicsBody = SKPhysicsBody.init(rectangleOf: tileSize, center: CGPoint(x: tileSize.width / 2, y: tileSize.height / 2))
tileNode.physicsBody!.isDynamic = false
tileNode.physicsBody!.restitution = 0.0
tileNode.physicsBody!.categoryBitMask = Constants.PhysicsCategories.wall
tileNode.physicsBody!.collisionBitMask = Constants.PhysicsCategories.player | Constants.PhysicsCategories.npc | Constants.PhysicsCategories.enemy
//nodesForGraph.append(tileNode)
bodies.append(tileNode.physicsBody!)
tileMap.addChild(tileNode)
}
}
}
tileMap.physicsBody = SKPhysicsBody(bodies: bodies)
}
But when I do this, the physicsbodies are totally messed up..
I recommend applying a line sweep algorithm to merge the tiles together.
You can do this in four steps;
Iterate through the position of the tiles in your SKTileMap.
Find the tiles that are adjacent to one another.
For each group of adjacent tiles, collect:
a down-left corner coordinate and
an up-right corner coordinate.
Draw a square, and move on to the next group of tiles until you run out of tile coordinates.
The first step: creating an array containing all of your position nodes.
func tilephysics() {
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 {
if (tileMap.tileDefinition(atColumn: col, row: row)?.userData?.value(forKey: "ground") != nil) {
let tileDef = tileMap.tileDefinition(atColumn: col, row: row)!
let tile = SKSpriteNode()
let x = round(CGFloat(col) * tilesize.width - halfwidth + (tilesize.width / 2))
let y = round(CGFloat(row) * tilesize.height - halfheight + (tilesize.height / 2))
tile.position = CGPoint(x: x, y: y)
tile.size = CGSize(width: tileDef.size.width, height: tileDef.size.height)
tileArray.append(tile)
tilePositionArray.append(tile.position)
}
}
}
algorithm()
}
The second and third step: finding adjacent tiles, collecting the two corner coordinates, and adding them to an array:
var dir = [String]()
var pLoc = [CGPoint]()
var adT = [CGPoint]()
func algorithm(){
let width = tileMap.tileSize.width
let height = tileMap.tileSize.height
let rWidth = 0.5 * width
let rHeight = 0.5 * height
var ti:Int = 0
var ti2:Int = 0
var id:Int = 0
var dl:CGPoint = CGPoint(x: 0, y: 0)
var tLE = [CGPoint]()
var tRE = [CGPoint]()
for t in tilePositionArray {
if (ti-1 < 0) || (tilePositionArray[ti-1].y != tilePositionArray[ti].y - height) {
dl = CGPoint(x: t.x - rWidth, y: t.y - rHeight)
}
if (ti+1 > tilePositionArray.count-1) {
tLE.append(dl)
tRE.append(CGPoint(x: t.x + rWidth, y: t.y + rHeight))
} else if (tilePositionArray[ti+1].y != tilePositionArray[ti].y + height) {
if let _ = tRE.first(where: {
if $0 == CGPoint(x: t.x + rWidth - width, y: t.y + rHeight) {id = tRE.index(of: $0)!}
return $0 == CGPoint(x: t.x + rWidth - width, y: t.y + rHeight)}) {
if tLE[id].y == dl.y {
tRE[id] = CGPoint(x: t.x + rWidth, y: t.y + rHeight)
} else {
tLE.append(dl)
tRE.append(CGPoint(x: t.x + rWidth, y: t.y + rHeight))
}
} else {
tLE.append(dl)
tRE.append(CGPoint(x: t.x + rWidth, y: t.y + rHeight))
}
}
ti+=1
}
The fourth step: drawing a rectangle and moving on to the next shape:
for t in tLE {
let size = CGSize(width: abs(t.x - tRE[ti2].x), height: abs(t.y - tRE[ti2].y))
let loadnode = SKNode()
loadnode.physicsBody = SKPhysicsBody(rectangleOf: size)
loadnode.physicsBody?.isDynamic = false
loadnode.physicsBody?.affectedByGravity = false
loadnode.physicsBody?.restitution = 0
loadnode.physicsBody?.categoryBitMask = 2
loadnode.position.x = t.x + size.width / 2
loadnode.position.y = t.y + size.height / 2
scene.addChild(loadnode)
ti2 += 1
}
}
Apply these steps correctly, and you should see that your tiles are merged together in large squares; like so:
Screenshot without visuals for comparison
Screenshot without visuals showing the physicsbodies
I had a lot of fun solving this problem. If I have helped you, let me know.
I only recently started coding and am looking for new challenges. Please reach out to me if you have challenges or projects I could possibly contribute to.
As Knight0fDragon pointed out, there is no way to do exactly what you have asked. Unfortunately, tile maps in SpriteKit leave much to be desired. But you might try this technique to reduce the number of physics bodies.
Idea #1 - Manually Draw Your Physics Bodies
Create your tile map in the editor. Just paint your tile textures onto the map; don't assign any physics bodies to them. Then keep working in the editor to drag Color Sprites (SKSpriteNodes) over parts of your map that need a physics body. Shape the nodes to make the largest rectangle possible for areas that need physics bodies. This works best for for large, flat surfaces like walls, floors, ceilings, platforms, crates, etc. It's tedious but you end up with far fewer physics bodies in your simulation than if you automatically assign bodies to all tiles like you are doing.
Idea #2 - Use No Physics Bodies
This idea would probably require even more work, but you could potentially avoid using physics bodies altogether. First, create your tile map in the editor. Analyze your map to identify which tiles mark a barrier, beyond which the player should not cross. Assign a user data identifier to that type of tile. You would need different categories of identifiers for different types of barriers, and you may also need to design your artwork to fit this approach.
Once your barrier tiles are sufficiently identified, write code which checks the user data value for the tile currently occupied by the player sprite and restrict the sprite's movement accordingly. For example, if the player enters a title that marks an upper boundary, your movement code would not allow the player sprite to move up. Likewise, if the player enters a tile that marks the leftmost boundary, your movement code will not let the player travel left.
You can check out this related post where I basically suggest the same ideas. Unfortunately, SpriteKit's tile maps have no perfect solution for this problem.

What's the most efficient way to get the minimum and maximum value of two properties of a set of objects

I'm writing an app where performance is most important and I have to iterate through a set of workstations which all have positions with x- and y-coordinates. This is the relevant part of the workstation and the position:
struct Workstation {
let position: Position
}
struct Position {
let x, y: Int
func distance(to otherPosition: Position) -> Int {
return abs(self.x - otherPosition.x) + abs(self.y - otherPosition.y)
}
}
The goal in this specific case is to get half the length of the diagonal of the rectangle surrounding all workstations, but I have to do some other calculations later on as well (like getting the center coordinate of all workstations).
I came up with two possible solutions (note: layout.workstations is of type Set<Workstation>):
Solution 1
private func getSurroundingRectangeScore(for layout: FactoryLayout) -> Double {
var minX = Int.max
var maxX = Int.min
var minY = Int.max
var maxY = Int.min
for workstation in layout.workstations {
let pos = workstation.position
if pos.x < minX { minX = pos.x }
if pos.x > maxX { maxX = pos.x }
if pos.y < minY { minY = pos.y }
if pos.y > maxY { maxY = pos.y }
}
let minPosition = Position(x: minX, y: minY)
let maxPosition = Position(x: maxX, y: maxY)
return Double(minPosition.distance(to: maxPosition)) / 2
}
Solution 2
private func getSurroundingRectangeScore(for layout: FactoryLayout) -> Double {
let xValues = layout.workstations.map { $0.position.x }
let yValues = layout.workstations.map { $0.position.y }
guard let minX = xValues.min(), let maxX = xValues.max(), let minY = yValues.min(), let maxY = yValues.max() else {
fatalError("Minima or Maxima could not be determined!")
}
let minPosition = Position(x: minX, y: minY)
let maxPosition = Position(x: maxX, y: maxY)
return Double(minPosition.distance(to: maxPosition)) / 2
}
My understanding is that solution 1 iterates through the workstations only once and fills all needed coordinate variables in one swoop. Solution 2 is better readable, but has to iterate the workstation set two times and the resulting arrays four times, so I guess this is by far worse. So my question would be, if my assumption is right and if there's an even more efficient way to calculate here?
First, don't guess about which one is worse. Run it, and measure it. (There's also an option besides your two, which is using the built-in sort routine and then looking at the ends of the result.)
Second, if it's a requirement that accessing this information always be as fast as possible, then use a data structure that supports the requirement. Find the values for the initial collection once, then update them each time an element is added. Instead of throwing away information and repeatedly doing the same N comparisons, do just four each time a new Workstation is added. Then you have a constant-time read of the extremities whenever you need them.

Spritekit rotating multiple nodes around center

I would like to apply a rotation to multiple nodes (selected nodes in my game) using the UIRotationGesture, based on the center point of all those nodes. I can already rotate a single node simply changing it's zRotation.
The problem with multiple nodes is that it changes position and zRotation based on a center node, and I cannot seem to understand how to manage that.
I would like something like this:
What I have to rotate a single node is this:
During the rotation gesture
theRotation = CGFloat(sender.rotation) + self.offset
theRotation = theRotation * -1
node.rotation = theRotation
After the rotation gesture
self.offset = theRotation * -1
Would you have an idea on how set the correct position and angle for my nodes during the rotation?
What I tried:
I tried to add a node in the center (where the white dot is in my pictures, which represents the center) and change the parent of my nodes to be this one, then apply the zRotation on this node, and then replace the right parents. This did not work as I cannot seem to change a parent (my nodes disappear), this is another one of my Stack Questions.
I tried to change the anchor point of my nodes to fit the center point and than rotate them using theRotation. It did not work as I cannot seem to set the anchor point at the center position (that I have). I tried changing the coordinates system of the center's position to fit the node's one, but this is still not working. node.convertPoint(center, fromNode: Self) gives me coordinated like -58;-74 when it's about -1;-.5 (or something like that). I do not understand this.
So now I am thinking to calculate the position and rotation myself, as those did not work, but I would need an idea on how to calculate those as I am not very good with trigonometry/linear algebra, sadly enough.
Thank you for you help!
How I calculate my center:
var maxX = nodesSelected[0].position.x
var minX = nodesSelected[0].position.x
var maxY = nodesSelected[0].position.y
var minY = nodesSelected[0].position.y
for node in nodesSelected{
if node.position.x > maxX{
maxX = node.position.x
}
if node.position.x < minX{
minX = node.position.x
}
if node.position.y > maxY{
maxY = node.position.y
}
if node.position.y > maxY{
minY = node.position.y
}
}
return CGPoint(x: (maxX-minX)/2+minX, y: (maxY-minY)+minY/2)
How I calculate the radius of the rotation (distance between a node and the center):
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y))))
}
How I get my rotation:
sender.rotation
Given a rotationAngle, you can calculate the new position of each node with the code below, you need to know a bit of trigonometry to understand the code.
Here I have an array of SKShapeNode that I called dots (It would be the equivalent of your green nodes in the image). And the centralDot would be your central SKSpriteNode.
for dot in dots {
let dx = dot.position.x - centralDot!.position.x // Get distance X from center
let dy = dot.position.y - centralDot!.position.y // Get distance Y from center
let current_angle = atan(dy / dx) // Current angle is the arctan of dy / dx
let next_angle = current_angle - rotationAngle // Sum how much you want to rotate in radians
// the new x is: center + radius*cos(x)
// the new y is: center + radius*sin(y)
// if dx < 0 you need to get the oposite value of the position
let new_x = dx >= 0 ? centralDot!.position.x + rotationRadius * cos(next_angle) : centralDot!.position.x - rotationRadius * cos(next_angle)
let new_y = dx >= 0 ? centralDot!.position.y + rotationRadius * sin(next_angle) : centralDot!.position.y - rotationRadius * sin(next_angle)
let new_point = CGPoint(x: new_x, y: new_y)
let action = SKAction.moveTo(new_point, duration: 0.2)
dot.runAction(action)
}
Hope this helps
Update:
The first code didn't helped, so I tried another one. This one worked better on my tests.
for i in 0..<dots.count {
let dot = dots[i]
let angle = rotationAngle + CGFloat(M_PI_2 * Double(i))
let new_x = rotationRadius * cos(angle) + centralDot!.position.x
let new_y = rotationRadius * sin(angle) + centralDot!.position.y
let new_point = CGPoint(x: new_x, y: new_y)
let action = SKAction.moveTo(new_point, duration: 1/60)
dot.runAction(action)
}
rotationRadius is a constant, the distance you want between the center and the green node.