Object positioning is off - swift

Hi, I’m trying to get an object (in this case a green frog) to spawn in line with the player sprite (the red frog) on a platform which is as wide as the scene and what i mean by this, is getting the object to spawn so that when the player advances it doesn’t overlap the object. (The picture shows how the green frog is between two red frogs and not in line with one of the red frogs)
My code for positioning of the objects is as follows
obstacle.position = CGPointMake(-(backgroundSprite.size.width / 2) + CGFloat(randomX) + (spacing * CGFloat(i)), 0)
this currently spawns it on the left side the screen half off the scene. the background sprite is what the object is being added to which is defined like so:
let theSize:CGSize = CGSizeMake(levelUnitWidth, levelUnitHeight)
let tex:SKTexture = SKTexture(imageNamed: imageName)
backgroundSprite = SKSpriteNode(texture: tex, color: SKColor.blackColor(), size: theSize)
random x is also what spawns them randomly on the x axis of the background sprite (which I have also tried adjusting with no luck)
let randomX = arc4random_uniform( UInt32 (backgroundSprite.size.height) )
lastly spacing is the distance between the objects in the same level unit.
let spacing:CGFloat = 250
I have tried implementing the player sprites width as a reference and is hasn’t worked. Can some please tell me what i’m doing wrong here.
Here is the full code if you need to look at it all:
if (theType == LevelType.road) {
for (var i = 0; i < Int(numberOfObjectsInLevel); i++) {
let obstacle:Object = Object()
obstacle.theType = LevelType.road
obstacle.createObject()
addChild(obstacle)
let spacing:CGFloat = 250
obstacle.position = CGPointMake((backgroundSprite.size.width / 4) + CGFloat(randomX) + (spacing * CGFloat(i)), 0)
}
EDIT:
I have tried implementing that code you made in your edit post with code I had already and this is what I got.
if (theType == LevelType.road) {
let xAxisSpawnLocations: [CGFloat] = {
var spawnLocations:[CGFloat] = []
//Create 5 possible spawn locations
let numberOfNodes = 5
for i in 0...numberOfNodes - 1 {
/*
Spacing between nodes will change if:
1) number of nodes is changed,
2) screen width is changed,
3) node's size is changed.
*/
var xPosition = (frame.maxX - thePlayer.size.width) / CGFloat((numberOfNodes - 1)) * CGFloat(i)
//add a half of a player's width because node's anchor point is (0.5, 0.5) by default
xPosition += thePlayer.size.width/2.0
spawnLocations.append( xPosition )
}
return spawnLocations
}()
print(xAxisSpawnLocations)
let yAxisSpawnLocations: [CGFloat] = [0]
let obstacle:Object = Object()
obstacle.theType = LevelType.road
obstacle.createObject()
addChild(obstacle)
let randx = xAxisSpawnLocations[Int(arc4random_uniform(UInt32(xAxisSpawnLocations.count)))]
obstacle.position = CGPoint(x: randx, y: yAxisSpawnLocations[0] )
obstacle.zPosition = 200
}
EDIT 2:
so implemented the code again this time the right way and I got this:
So the player still isn't in line with the objects and for some reason it only spawns on the right side of the screen. I think it is because I have a worldNode that holds everything.
the worldNode holds the player which has a starting point of (0,0) in the worldNode and it also holds the level units which holds the objects. the camera position is centered on the player node I'm not sure if this is the problem but i'll provide the code below so you can have a look at it.
let startingPosition:CGPoint = CGPointMake(0, 0)
The woldNode Code:
let worldNode:SKNode = SKNode()
//creates the world node point to be in the middle of the screen
self.anchorPoint = CGPointMake(0.5, 0.5)
addChild(worldNode)
//adds the player as a child node to the world node
worldNode.addChild(thePlayer)
thePlayer.position = startingPosition
thePlayer.zPosition = 500
The camera positioning code:
override func didSimulatePhysics() {
self.centerOnNode(thePlayer)
}
//centers the camera on the node world.
func centerOnNode(node:SKNode) {
let cameraPositionInScene:CGPoint = self.convertPoint(node.position, fromNode: worldNode)
worldNode.position = CGPoint(x: worldNode.position.x , y:worldNode.position.y - cameraPositionInScene.y )
}
I pretty sure this is my problem but tell me what you think.

Like I said in comments, the key is to predefine coordinates for x (and y) axis and spawn nodes based on that. First, let's define a player inside your GameScene class:
let player = SKSpriteNode(color: .redColor(), size: CGSize(width: 50, height: 50))
Now, predefine spawn locations (for both x and y axis):
let xAxisSpawnLocations: [CGFloat] = [50.0, 125.0, 200.0, 275.0, 350.0, 425.0]
let yAxisSpawnLocations: [CGFloat] = [50.0, 125.0, 200.0, 275.0]
Now when we know possible positions, let position our player first and add it to the scene:
player.position = CGPoint(x: xAxisSpawnLocations[0], y: yAxisSpawnLocations[0])
player.zPosition = 10
addChild(player)
You could create those positions based on player's width and height and screen's size, but because of simplicity, I've hardcoded everything.
So, lets fill one row, right above the player with green frogs:
for xLocation in xAxisSpawnLocations {
let greenFrog = SKSpriteNode(color: .greenColor(), size: player.size)
greenFrog.position = CGPoint(x: xLocation, y: yAxisSpawnLocations[1])
addChild(greenFrog)
}
The result would be something like this:
Or, for example, move the player by one place to the right, and make a column of green frogs right above him:
player.position = CGPoint(x: xAxisSpawnLocations[1], y: yAxisSpawnLocations[0])
for yLocation in yAxisSpawnLocations {
let greenFrog = SKSpriteNode(color: .greenColor(), size: player.size)
greenFrog.position = CGPoint(x: xAxisSpawnLocations[1], y: yLocation)
addChild(greenFrog)
}
And it should look like this:
EDIT:
Based on your comments, this is how you could distribute nodes across the screen based on number of nodes, screen width and node's size:
let xAxisSpawnLocations: [CGFloat] = {
var spawnLocations:[CGFloat] = []
//Create 5 possible spawn locations
let numberOfNodes = 5
for i in 0...numberOfNodes - 1 {
/*
Spacing between nodes will change if:
1) number of nodes is changed,
2) screen width is changed,
3) node's size is changed.
*/
var xPosition = (frame.maxX - player.size.width) / CGFloat((numberOfNodes - 1)) * CGFloat(i)
//add a half of a player's width because node's anchor point is (0.5, 0.5) by default
xPosition += player.size.width/2.0
spawnLocations.append( xPosition )
}
return spawnLocations
}()
print(xAxisSpawnLocations)
You should handle what is happening when too much nodes are added, or if nodes are too big, but this can give you a basic idea how to distribute nodes along x axis and preserve the same distance between them.

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.

Aligning ARFaceAnchor with SpriteKit overlay

I'm trying to calculate SpriteKit overlay content position (not just overlaying visual content) over specific geometry points ARFaceGeometry/ARFaceAnchor.
I'm using SCNSceneRenderer.projectPoint from the calculated world coordinate, but the result is y inverted and not aligned to the camera image:
let vertex4 = vector_float4(0, 0, 0, 1)
let modelMatrix = faceAnchor.transform
let world_vertex4 = simd_mul(modelMatrix, vertex4)
let pt3 = SCNVector3(x: Float(world_vertex4.x),
y: Float(world_vertex4.y),
z: Float(world_vertex4.z))
let sprite_pt = renderer.projectPoint(pt3)
// To visualize sprite_pt
let dot = SKSpriteNode(imageNamed: "dot")
dot.size = CGSize(width: 7, height: 7)
dot.position = CGPoint(x: CGFloat(sprite_pt.x),
y: CGFloat(sprite_pt.y))
overlayScene.addChild(dot)
In my experience, the screen coordinates given by ARKit's projectPoint function are directly usable when drawing to, for example, a CALayer. This means they follow iOS coordinates as described here, where the origin is in the upper left and y is inverted.
SpriteKit has its own coordinate system:
The unit coordinate system places the origin at the bottom left corner of the frame and (1,1) at the top right corner of the frame. A sprite’s anchor point defaults to (0.5,0.5), which corresponds to the center of the frame.
Finally, SKNodes are placed in an SKScene which has its origin on the bottom left. You should ensure that your SKScene is the same size as your actual view, or else the origin may not be at the bottom left of the view and thus your positioning of the node from view coordinates my be incorrect. The answer to this question may help, in particular checking the AspectFit or AspectFill of your view to ensure your scene is being scaled down.
The Scene's origin is in the bottom left and depending on your scene size and scaling it may be off screen. This is where 0,0 is. So every child you add will start there and work its way right and up based on position. A SKSpriteNode has its origin in the center.
So the two basic steps to convert from view coordinates and SpriteKit coordinates would be 1) inverting the y-axis so your origin is in the bottom left, and 2) ensuring that your SKScene frame matches your view frame.
I can test this out more fully in a bit and edit if there are any issues
Found the transformation that works using camera.projectPoint instead of the renderer.projectPoint.
To scale the points correctly on the spritekit: set scaleMode=.aspectFill
I updated https://github.com/AnsonT/ARFaceSpriteKitMapping to demo this.
guard let faceAnchor = anchor as? ARFaceAnchor,
let camera = sceneView.session.currentFrame?.camera,
let sie = overlayScene?.size
else { return }
let modelMatrix = faceAnchor.transform
let vertices = faceAnchor.geometry.vertices
for vertex in vertices {
let vertex4 = vector_float4(vertex.x, vertex.y, vertex.z, 1)
let world_vertex4 = simd_mul(modelMatrix, vertex4)
let world_vector3 = simd_float3(x: world_vertex4.x, y: world_vertex4.y, z: world_vertex4.z)
let pt = camera.projectPoint(world_vector3, orientation: .portrait, viewportSize: size)
let dot = SKSpriteNode(imageNamed: "dot")
dot.size = CGSize(width: 7, height: 7)
dot.position = CGPoint(x: CGFloat(pt.x), y: size.height - CGFloat(pt.y))
overlayScene?.addChild(dot)
}

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.

how to set physics properties for a circle so it follows given path

The movement of a physics body circle is too erratic for what I want to achieve. I would like to restrict it so it follows a certain path touching specific points (or a range of points) as shown in the image below. How can I set the physics properties to traverse a similar path?
how to set physics properties for a circle so it follows given path
So essentially you are looking to move a node to a particular point using real-time motion. I have an answer here showing how to do this, however given the number of up votes this question has received, I will provide a more detailed answer.
What the answer I linked to doesn't provide is traversing a path of points. So below I have provided a solution showing how this can be done below. It simply just moves to each point in the path, and each time the node reaches a point, we increment the index to move to the next point. I also added a few variables for travel speed, rate (to make the motion more smooth or static) and whether or not the node should repeat the path. You could further expand upon this solution to better meet the needs of your game. I would definitely consider subclassing a node and building this behavior into it so you can re-use this motion for multiple nodes.
One final note, you may notice the calculation for the impulse varies between my solution below and the answer I linked to above. This is because I am avoiding using angle calculation because they are very expensive. Instead I am calculating a normal so that the calculation is more computationally efficient.
One final note, my answer here explains the use of the rate factor to smooth the motion and leave room for motion distortions.
import SpriteKit
class GameScene: SKScene {
var node: SKShapeNode! //The node.
let path: [CGPoint] = [CGPoint(x: 100, y: 100),CGPoint(x: 100, y: 300),CGPoint(x: 300, y: 300),CGPoint(x: 300, y: 100)] //The path of points to travel.
let repeats: Bool = true //Whether to repeat the path.
var pathIndex = 0 //The index of the current point to travel.
let pointRadius: CGFloat = 10 //How close the node must be to reach the destination point.
let travelSpeed: CGFloat = 200 //Speed the node will travel at.
let rate: CGFloat = 0.5 //Motion smoothing.
override func didMoveToView(view: SKView) {
node = SKShapeNode(circleOfRadius: 10)
node.physicsBody = SKPhysicsBody(circleOfRadius: 10)
node.physicsBody!.affectedByGravity = false
self.addChild(node)
}
final func didReachPoint() {
//We reached a point!
pathIndex++
if pathIndex >= path.count && repeats {
pathIndex = 0
}
}
override func update(currentTime: NSTimeInterval) {
if pathIndex >= 0 && pathIndex < path.count {
let destination = path[pathIndex]
let displacement = CGVector(dx: destination.x-node.position.x, dy: destination.y-node.position.y)
let radius = sqrt(displacement.dx*displacement.dx+displacement.dy*displacement.dy)
let normal = CGVector(dx: displacement.dx/radius, dy: displacement.dy/radius)
let impulse = CGVector(dx: normal.dx*travelSpeed, dy: normal.dy*travelSpeed)
let relativeVelocity = CGVector(dx:impulse.dx-node.physicsBody!.velocity.dx, dy:impulse.dy-node.physicsBody!.velocity.dy);
node.physicsBody!.velocity=CGVectorMake(node.physicsBody!.velocity.dx+relativeVelocity.dx*rate, node.physicsBody!.velocity.dy+relativeVelocity.dy*rate);
if radius < pointRadius {
didReachPoint()
}
}
}
}
I did this pretty quickly so I apologize if there is a mistake. I don't have time now but I will add a gif showing the solution later.
A note about collisions
To fix the erratic movement during a collision, after the 2 bodies collide set the "rate" property to 0 or preferably a very low number to reduce the travel velocity impulse which will give you more room for motion distortion. Then at some point in the future (maybe some time after the collision occurs or preferably when the body is moving slow again) set the rate back to its initial value. If you really want a nice effect, you can actually ramp up the rate value over time from 0 to the initial value to give yourself a smooth and gradual acceleration.
Quick implementation using followPath:duration:
override func didMoveToView(view: SKView) {
self.backgroundColor = UIColor.blackColor()
let xWidth: CGFloat = CGRectGetMaxX(self.frame)
let yHeight: CGFloat = CGRectGetMaxY(self.frame)
let ball = SKSpriteNode(color: UIColor.redColor(), size: CGSizeMake(50.0, 50.0))
let offset : CGFloat = ball.size.width / 2
// Moving path
let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, offset, yHeight / 2)
CGPathAddLineToPoint(path, nil, xWidth * 2 / 3, yHeight - offset)
CGPathAddLineToPoint(path, nil, xWidth - offset, yHeight * 2 / 3)
CGPathAddLineToPoint(path, nil, xWidth / 2, offset)
CGPathAddLineToPoint(path, nil, offset, yHeight / 2)
// Movement
let moveByPath = SKAction.followPath(path, asOffset: false, orientToPath: false, duration: 4.0)
let moveForever = SKAction.repeatActionForever(moveByPath)
ball.runAction(moveForever)
self.addChild(ball)
}
In GameViewController.swift, I changed the default GameScene.unarchiveFromFile method to let scene = GameScene(size: view.bounds.size) for creating the scene's size.
Preview: