I am playing around with SpriteKit and the TileMapNode and i've got an annoying problem.
That's how it should look like.
That's how its actually looking in the simulator/device.
The white spaces are terrible and i have no idea, how to get rid of them.
My Tiles are about 70x70 in the "Sprite Atlas - Part" of the assets, i've configured my tilemapnode with a scale of 0.5 and tile size of 70x70.
While testing some cases, i figured out that this part of code triggers the error, but i have no idea, what could be wrong. Changing the SKPhysicsBody size to a smaller one, did not helped.
guard let tilemap = childNode(withName: "LevelGround") as? SKTileMapNode else { return }
let tileSize = tilemap.tileSize
let halfWidth = CGFloat(tilemap.numberOfColumns) / 2.0 * tileSize.width
let halfHeight = CGFloat(tilemap.numberOfRows) / 2.0 * tileSize.height
for row in 0..<tilemap.numberOfRows {
for col in 0..<tilemap.numberOfColumns {
if tilemap.tileDefinition(atColumn: col, row: row) != nil {
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(rectangleOf: CGSize(width: 70, height: 70), center: CGPoint(x: tileSize.width / 2.0, y: tileSize.height / 2.0))
tileNode.physicsBody?.isDynamic = false
tileNode.physicsBody?.collisionBitMask = 2
tileNode.physicsBody?.categoryBitMask = 1
tileNode.physicsBody?.contactTestBitMask = 2 | 1
tileNode.name = "Ground"
tilemap.addChild(tileNode)
}
}
}
tileNode.strokeColor = .clear
solved the problem
Update:
problem not solved... just moved :/
When checking, if ground & player are in contact, every new tile the status is switching between "contact" and "no contact".
When using a cube instead a circle, the cube begins to rotate. It seems, the corner of the cube get's stuck at the minimal space between the tiles.
Related
I want to equally distribute an n amount of square SKShapeNodes over the screen to create a square grid. I tried searching for other examples of this, but I cannot find the solution I'm looking for as most solutions only focus on only distributing views horizontally.
I'm creating the squares in a for loop, however I can only get them to move on either the x or the y axes, not both. The below example gives me the best result, but as expected from the code only the first two squares are visible, the third one is created off screen:
amountOfTiles is a CGFloat set to 4
tile is an SKShapeNode
func addTiles() {
let screenWidth = self.view?.frame.size.width
tileWidth = screenWidth! / (amountOfTiles / 2)
tileHeight = tileWidth
tileX = 0
tileY = 0
tileRect = CGRect(x: tileX, y: tileY, width: tileWidth, height: tileHeight)
for _ in 0...Int(amountOfTiles) {
tile = SKShapeNode(rect: tileRect)
tile.position = CGPoint(x: tileX, y: tileY)
tile.fillColor = .red
addChild(tile)
tileX += tileWidth
tileY += tileHeight
}
}
My guess is that I should not update the Y and X positions of the squares at the same time, but I'm not sure how to solve this.
Fixed it myself. I removed amountOfTiles and added two separate CGFloats for x and y, which I update in a separate loop:
func addTiles() {
let xAmount:CGFloat = 4
let yAmount:CGFloat = 4
let screenWidth = self.view?.frame.size.width
tileWidth = screenWidth! / (xAmount)
tileHeight = tileWidth
tileY = 0
tileRect = CGRect(x: tileX, y: tileY, width: tileWidth, height: tileHeight)
for _ in 0..<Int(xAmount) {
tileX = 0
for _ in 0..<Int(yAmount) {
tile = SKShapeNode(rect: tileRect)
tile.fillColor = .red
addChild(tile)
tile.position = CGPoint(x: tileX, y: tileY)
tileX += tileWidth
}
tileY += tileHeight
}
}
I'm trying to create a game in which i have a object 1 rotatiing in a circle and another object appears and places itself ontop of object 1. currently the object just rotates around object 1 without stacking ontop of it. how do i get the object to stack itself on top and then follow it's orbit? here's my code now.
let player = SKSpriteNode(imageNamed: "Light")
override func didMoveToView(view: SKView) {
// 2
backgroundColor = SKColor.whiteColor()
// 3
player.position = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
// 4
player.size = CGSize(width:70, height:60 )
addChild(player)
let dx = player.position.x - self.frame.width / 2
let dy = player.position.y - self.frame.height / 2
let rad = atan2(dy, dx)
circle = UIBezierPath(arcCenter: CGPoint(x: size.width / 2, y: size.height / 2), radius: 120, startAngle: rad, endAngle: rad + CGFloat(M_PI * 4), clockwise: true)
let follow = SKAction.followPath(circle.CGPath, asOffset: false, orientToPath: true, speed: 100)
player.runAction(SKAction.repeatActionForever(follow))
}
func addMonster() {
let monster = SKSpriteNode(imageNamed: "plate")
// Determine where to spawn the monster along the Y axis
let actualY = random(min: monster.size.height/1, max: size.height - monster.size.height/1)
// Position the monster slightly off-screen along the right edge,
// and along a random position along the Y axis as calculated above
monster.position = CGPoint(x: size.width * 0.5 + monster.size.width/2, y: actualY)
// Add the monster to the scene
addChild(monster)
// Determine speed of the monster
let actualDuration = random(min: CGFloat(2.0), max: CGFloat(3.0))
// Create the actions
let actionMove = SKAction.moveTo(CGPoint(x: -monster.size.width/2, y: actualY), duration: NSTimeInterval(actualDuration))
let follow = SKAction.followPath(circle.CGPath, asOffset: false, orientToPath:true, speed: 100)
monster.runAction(SKAction.repeatActionForever(follow))
}
Your verbal description appears to have something like the following in mind (you can copy and paste this into an iOS playground):
import UIKit
import SpriteKit
import XCPlayground
let scene = SKScene(size: CGSize(width: 400, height: 400))
let view = SKView(frame: CGRect(origin: .zero, size: scene.size))
view.presentScene(scene)
XCPlaygroundPage.currentPage.liveView = view
scene.backgroundColor = .darkGrayColor()
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let player = SKShapeNode(rectOfSize: CGSize(width: 70, height: 60), cornerRadius: 5)
let radius: CGFloat = 120
let circle = CGPathCreateWithEllipseInRect(CGRect(x: -radius, y: -radius, width: radius * 2, height: radius * 2), nil)
let followCircle = SKAction.followPath(circle, asOffset: false, orientToPath: true, speed: 100)
player.runAction(.repeatActionForever(followCircle))
let monsterSize = CGSize(width: 35, height: 30)
let monster = SKShapeNode(rectOfSize: monsterSize, cornerRadius: 4)
monster.fillColor = .redColor()
monster.runAction(.repeatActionForever(followCircle))
scene.addChild(player)
scene.addChild(monster)
Where you make use of a random(min:max:) function that is probably implemented along the lines of:
public func random(min min: CGFloat, max: CGFloat) -> CGFloat {
return min + (CGFloat(arc4random()) / CGFloat(UInt32.max)) * (max - min)
}
But you also have the code that seems to be saying:
let y = random(
min: scene.size.height / -2 + monsterSize.height / 2,
max: scene.size.height / 2 - monsterSize.height / 2
)
let xFrom = scene.size.width / 2
let xTo = scene.size.width / -2
monster.position = CGPoint(x: xFrom, y: y)
monster.runAction(
.moveToX(xTo, duration: NSTimeInterval(random(min: 2, max: 3)))
)
But this is unused. Instead the monster is set to follow the same circle path as the player. And so I am not sure really what exactly you are trying to achieve. (By the way, the simplest way of getting the monster "stack on top" of the player would be to make it the child node of the player...)
Hope some of this guesswork helps you in some way (to refine your question and code sample if nothing else).
So I am experimenting with Sprite-kit, to build circular path where the main character can follow and collect coins. I have successfully positioned my character and made him follow my circular path.
What I'm trying to achieve is the following:
red ball is the main character [done]
white polygons are the coins
// Adding the big circle
let runway = SKSpriteNode(imageNamed: "runway")
runway.position = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame))
addChild(runway)
// Adding the player
player = SKSpriteNode(imageNamed: "player")
player.position = CGPointMake( CGRectGetMidX(frame) , (CGRectGetMidY(frame) + runway.size.width/2) )
// Calculating the initial position of the player and creating a circular path around it
let dx = player.position.x - frame.width / 2
let dy = player.position.y - frame.height / 2
let radian = atan2(dy, dx)
let playerPath = UIBezierPath(
arcCenter: CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMidY(frame)),
radius: (runway.frame.size.width / 2) - 20,
startAngle: radian,
endAngle: radian + CGFloat(M_PI * 4.0),
clockwise: true)
let follow = SKAction.followPath(playerPath.CGPath, asOffset: false, orientToPath: true, speed: 200)
player.runAction(SKAction.repeatActionForever(follow))
My problem now, is how to position my coins on that same path ??
Is there a way to generate their positions using the same path or should I create a specific path for each coin and extract the path currentPoint each time ??
Is there a simpler way to solving my problem ??
Thanks
Like I said, you need to know where is the center of the path (in your case that is CGPoint(x: frame.midX, y:frame.midY) which is "center" of the screen) and you have to know the radius (you have it already calculated when you was creating the path) and you need an angle that the ray from center (frame.midX,frame.midY) to the point on the circumference (coinX,coinY) makes with positive x axis:
import SpriteKit
class GameScene: SKScene {
let player = SKSpriteNode(color: .purpleColor(), size: CGSize(width: 30, height: 30))
override func didMoveToView(view: SKView) {
/* Setup your scene here */
// Adding the big circle
let runway = SKSpriteNode(color: .orangeColor(), size: CGSize(width: 150, height: 150))
runway.position = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame))
addChild(runway)
// Adding the player
player.position = CGPointMake( CGRectGetMidX(frame) , (CGRectGetMidY(frame) + runway.size.width/2) )
addChild(player)
// Calculating the initial position of the player and creating a circular path around it
let dx = player.position.x - frame.width / 2
let dy = player.position.y - frame.height / 2
let radius = (runway.frame.size.width / 2) - 20.0
let radian = atan2(dy, dx)
let playerPath = UIBezierPath(
arcCenter: CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMidY(frame)),
radius: radius,
startAngle: radian,
endAngle: radian + CGFloat(M_PI * 4.0),
clockwise: true)
let follow = SKAction.followPath(playerPath.CGPath, asOffset: false, orientToPath: true, speed: 200)
player.runAction(SKAction.repeatActionForever(follow))
let numberOfCoins = 8
for i in 0...numberOfCoins {
let coin = SKSpriteNode(color: .yellowColor(), size: CGSize(width: 10, height: 10))
let angle = 2 * M_PI / Double(numberOfCoins) * Double(i)
let coinX = radius * cos(CGFloat(angle))
let coinY = radius * sin(CGFloat(angle))
coin.position = CGPoint(x:coinX + frame.midX, y:coinY + frame.midY)
addChild(coin)
}
}
}
Just a sidenote : In Sprite-Kit the angle of 0 radians specifies the positive x axis. And the positive angle is in the counterclockwise direction. The source - Building Your Scene.
The answer that Whirlwind posted is valid, but needs to be migrated to work with Xcode 11.
The following is an updated version of that answer, along with an animation showing it in action.
override func didMove(to view: SKView) {
/* Setup your scene here */
// Adding the big circle
let runway = SKSpriteNode(color: .orange, size: CGSize(width: 150, height: 150))
runway.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(runway)
// Adding the player
player.position = CGPoint(x: frame.midX, y: frame.midY + runway.size.width / 2)
addChild(player)
// Calculating the initial position of the player and creating a circular path around it
let dx = player.position.x - frame.width / 2
let dy = player.position.y - frame.height / 2
let radius = (runway.frame.size.width / 2) - 20.0
let radian = atan2(dy, dx)
let playerPath = UIBezierPath(
arcCenter: CGPoint(x: frame.midX, y: frame.midY),
radius: radius,
startAngle: radian,
endAngle: radian + CGFloat(.pi * 4.0),
clockwise: true)
let follow = SKAction.follow(playerPath.cgPath, asOffset: false, orientToPath: true, speed: 200)
player.run(SKAction.repeatForever(follow))
let numberOfCoins = 8
for i in 0...numberOfCoins {
let coin = SKSpriteNode(color: .yellow, size: CGSize(width: 10, height: 10))
let angle = 2 * .pi / Double(numberOfCoins) * Double(i)
let coinX = radius * cos(CGFloat(angle))
let coinY = radius * sin(CGFloat(angle))
coin.position = CGPoint(x:coinX + frame.midX, y:coinY + frame.midY)
addChild(coin)
}
}
How it appears in action:
What is the best way how to draw a matrix of squares on iOS using SpriteKit? I am complete iOS beginner and I'm not sure what is the correct approach.
I was thinking creating a sprite representing a picture of a square and than add that sprite several times to specific locations. The data in the matrix will change over time and I need to reflect that in what is being drawn on the screen, though the shape of the matrix will remain the same.
This will get a basic 2d matrix of squares on the scree. If you need to keep track of these tile sprites, you can create an array of tiles and modify them elsewhere in your code. Hope this is helpful.
func setupMap() {
let tilesWide = 10
let tilesTall = 10
for i in 0..<tilesWide {
for j in 0..<tilesTall {
let tile = SKSpriteNode(color: SKColor.redColor(), size: CGSize(width: 5, height: 5))
tile.anchorPoint = CGPointZero
let x = CGFloat(i) * tile.size.width
let y = CGFloat(j) * tile.size.height
tile.position = CGPoint(x: x, y: y)
self.addChild(tile)
}
}
}
update for Swift3 :
func setupMap() {
let tilesWide = 10
let tilesTall = 10
for i in 0..<tilesWide {
for j in 0..<tilesTall {
let tile = SKSpriteNode(color: SKColor.red, size: CGSize(width: 5, height: 5))
tile.anchorPoint = .zero
let x = CGFloat(i) * tile.size.width
let y = CGFloat(j) * tile.size.height
tile.position = CGPoint(x: x, y: y)
self.addChild(tile)
}
}
}
I am trying to find the best method to do a vertically scrolling background in Swift,
without using the Update Method. The Background is 1 Image that should loop infinite.
The Image should be in the middle of the screen (full-size), moving down, and on top of that image should always be another image spawned.
However, mine doesn't work quite right, it spawns 2 images, and than after a delay they pop up and start the Actions again.
Also, it breaks sometimes down from 30 FPS to 26, which I want to avoid.
I hope somebody, who is more into Swift can help me.
let Background = SKTexture(imageNamed: "Background.png")
Background.filteringMode = .Nearest
let BackgroundMove = SKAction.moveByX(0, y: -self.frame.size.height, duration: 10)
let BackgroundReset = SKAction.moveByX(0, y: self.frame.size.height, duration: 0.0)
let BackgroundMoveAndResetForever = SKAction.repeatActionForever(SKAction.sequence([BackgroundMove,BackgroundReset]))
for var i:CGFloat = 0; i < 2.0 + self.frame.size.height / (Background.size().height * 2); ++i {
let sprite = SKSpriteNode(texture: Background)
sprite.size = CGSize(width: self.frame.size.width / 2, height: self.frame.size.height)
sprite.position = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2 * i)
sprite.zPosition = 1
sprite.runAction(BackgroundMoveAndResetForever)
self.addChild(sprite)
}
This is the code I used to make a background that moves right to left across the screen. It used a loop to generate three backgrounds, all of which formed a line which passed through the screen and returned to their original position when they were done. To convert it to a screen that moves top to bottom, you would have to replace the moveByX SKActions to moveToY, and exchange changing x values with changing y values. Hope this helps!
func makeBackground() {
var backgroundTexture = SKTexture(imageNamed: "img/bg.png")
//move background right to left; replace
var shiftBackground = SKAction.moveByX(-backgroundTexture.size().width, y: 0, duration: 9)
var replaceBackground = SKAction.moveByX(backgroundTexture.size().width, y:0, duration: 0)
var movingAndReplacingBackground = SKAction.repeatActionForever(SKAction.sequence([shiftBackground,replaceBackground]))
for var i:CGFloat = 0; i<3; i++ {
//defining background; giving it height and moving width
background=SKSpriteNode(texture:backgroundTexture)
background.position = CGPoint(x: backgroundTexture.size().width/2 + (backgroundTexture.size().width * i), y: CGRectGetMidY(self.frame))
background.size.height = self.frame.height
background.runAction(movingAndReplacingBackground)
self.addChild(background)
}
}
I wrote this one... It's probably not as short as it could be, but it only uses 2 copies of the image, unlike the one posted above as best-answer. It also allows you to specify a direction (true to scroll right, false to scroll left) and a speed (indicates how many seconds it takes for an image to scroll across the screen, lower numbers = faster). It requires you to specify the view as well, and I haven't tested it with images that aren't the same size as the desired output (although I included scaling, so it probably works)
func scrollingBackground (view: SKView, rightward: Bool, speed: CGFloat) {
let background = SKSpriteNode(imageNamed: "space")
let background2 = SKSpriteNode(imageNamed: "space")
// variables to manage x position, will be set based on which
// direction this will be moving
var b1startX, b2startX, destination, resetPos: CGFloat
// Set values based on desired direction
if(rightward){
b1startX = frame.size.width * -0.5 // value to start offscreen
b2startX = frame.size.width * 0.5 // value to start on screen
destination = frame.size.width * 1.5 // position where image will disappear
resetPos = frame.size.width * -0.5 // position where image will reappear
}
else{
b1startX = frame.size.width * 1.5
b2startX = frame.size.width * 0.5
destination = frame.size.width * -0.5
resetPos = frame.size.width * 1.5
}
background.position = CGPoint(x: b1startX, y: frame.size.height / 2)
background.scale(to: view.bounds.size)
background.zPosition = -1
background2.position = CGPoint(x: b2startX, y: frame.size.height / 2)
background2.scale(to: view.bounds.size)
background2.zPosition = -1
// Define actions based on desired direction
let scrollFromMid = SKAction.moveTo(x: destination, duration: TimeInterval(speed))
let scrollFromLeft = SKAction.moveTo(x: destination, duration: TimeInterval(speed * 2.0))
let resetPosition = SKAction.moveTo(x: resetPos, duration: 0)
//background starts in the default starting position, totally offscreen
background.run(SKAction.repeatForever(SKAction.sequence([scrollFromLeft, resetPosition])))
// background2 starts fully displayed, moves offscreen, then into default loop
background2.run(SKAction.sequence([scrollFromMid, resetPosition,
SKAction.repeatForever(
SKAction.sequence([scrollFromLeft, resetPosition]))
]))
addChild(background)
addChild(background2)
}
when you create the BackgroundReset action you set the y change to a negative value, this will move the background further down the screen. Remove the minus sign to move the background back up the screen.
let BackgroundReset =
SKAction.moveByX(0, y: /* removed minus sign here */ self.frame.size.height * 2, duration: 0.0)