I am trying to generate a random point on the screen to create an SKSpriteNode:
let px : CGFloat = CGFloat(drand48())
let py : CGFloat = CGFloat(drand48())
let screenSize : CGRect = self.frame
let location = CGPoint(x: screenSize.width*px,y: screenSize.height*py)
let sprite = SKSpriteNode(imageNamed: "BlackCircle.png")
Although this generates points correctly within the frame, sometimes the node itself is not created because the location in the frame does not correctly translate to a location within the GameScene. I tested positions using:
print(touch.locationInNode(self))
print(touch.locationInView(self.view))
I know there should be some offset for the x and y coordinates, but I cannot figure them out. Am I using the wrong CGRect? I tried using:
UIApplication.sharedApplication().keyWindow?.frame
and
view!.frame
as screenSize instead, but I had the same problem. What CGRect should I use?
Related
how works constraints in spriteKit? I have a global var
let screenSize: CGRect = UIScreen.main.bounds
then, i am trying to add my sknode.
personage = SKSpriteNode(imageNamed: "Bro")
personage.position.y = CGFloat(-screenSize.height)
personage.position.x = CGFloat(-screenSize.width)
personage.size = CGSize(width: screenSize.width/3, height: screenSize.height/3)
addChild(personage)
It is perfect on small size iphone, but when i open 13 pro max i cant see my personage, whats wrong? How do i change my code to see the same sizes of personage on all devices
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.
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)
}
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.
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.