Looking for a method to rotate two sprites that are tires for a car.
Tried to use RotateByAngle, but that makes the tire go out of it's position to go in a circle much bigger then the car. I want the tire to stay in the exact same position and rotate 360 degrees forever. Like a normal tire should.
let wheel = SKAction.rotateByAngle(CGFloat(M_PI), duration:1)
playerWheelleft.runAction(SKAction.repeatActionForever(wheelSpeed))
If there are better solutions to this problem then to use rotateByAngle please let me know.
The reason the tire sprite is not the center is because i use SKPhysicsJoint to connect the two tires to the rest of the car
var playerJoint = SKPhysicsJointPin.jointWithBodyA(player.physicsBody!, bodyB:playerWheelleft.physicsBody!, anchor:player.position)
self.physicsWorld.addJoint(playerJoint)
playerJoint = SKPhysicsJointPin.jointWithBodyA(player.physicsBody!, bodyB:playerWheelright.physicsBody!, anchor:player.position)
self.physicsWorld.addJoint(playerJoint)
I tried to remove the code that connects the tires with the rest of the car and then the rotation works great?
How can I make the tires the center rotation point?
let playerWheelsTexture = SKTexture(imageNamed: "playerWheel")
playerWheelleft = SKSpriteNode(texture: playerWheelsTexture)
playerWheelleft.position = CGPoint(x: CGRectGetMidX(self.frame) - 50, y: CGRectGetMidY(self.frame) - 320)
playerWheelleft.physicsBody = SKPhysicsBody(texture: playerWheelsTexture, size: playerWheelsTexture.size())
let wheelSpeed = SKAction.rotateByAngle(CGFloat(M_PI), duration:0.5)
playerWheelleft.runAction(SKAction.repeatActionForever(wheelSpeed))
playerWheelright.runAction(SKAction.repeatActionForever(wheelSpeed))
Related
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.
is it somehow possible to destroy an object on contact? Like not just delete it from the screen with body.removeFromParent(), I would like to have an animation.
I have a player and walls, and when the player has a special powerup, I want it to be able to destroy the walls on contact. I could imagine that I have like the wall split up as many little physics bodies and they hold together on like an anchor point and when my player hits it, they get an impulse from the player (just set isDynamic to true I guess) and losen the anchor point so all the sprite Nodes will fly their way and so the wall will be destroyed.
Can you give me some help / advise of a good way of doing that?
You don't need to have the nodes making up the wall held together in any way - just place them on the screen. If the player doesn't have the power-up, turn off the bit for the player in the wall nodes' physicsBodies collisionBitMask so that the wall nodes do not collide with the player. Then when the player hits the wall, the player will be affected by the collision (and bounce off) but the wall nodes will be unaffected.
When the player has the power-up, make the wall nodes affected by the collision and also turn on contacts between the player and the wall (it's enough just to turn on the bit for the wall category in the player's contactTestBitMask). Then the wall nodes will be affected by the collision (and move or spin away) and your didBegin() will be called and you can run an action on each wall node comprising of the animation you want and ending with removeFromParent().
A guide to collision and contactTest bit masks:
https://stackoverflow.com/a/40596890/1430420
Manipulating bit masks to turn collision & contacts off and on.
https://stackoverflow.com/a/46495864/1430420
Edit: SK demo showing an object hitting a wall made up of blocks:
Create a new SK project and use this as the GameScene,swift:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
physicsWorld.gravity = CGVector(dx:0, dy:0)
let ball = SKSpriteNode.init(color: .red, size: CGSize(width: 50, height: 50))
ball.physicsBody = SKPhysicsBody.init(circleOfRadius: ball.size.width/2)
ball.position = CGPoint(x: 0, y: 0)
buildWall()
addChild(ball)
ball.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 50))
}
func buildWall() {
let xStart : CGFloat = ((scene?.size.width)!/2) * -0.9
var brickPosition = CGPoint(x: xStart, y: 500)
let brickSize = CGSize(width: 20, height:20)
for wallRow in 1...10 {
for wallColumn in 1...30 {
let brick = SKSpriteNode(color: .yellow, size: brickSize)
brick.physicsBody = SKPhysicsBody.init(rectangleOf: brick.size)
brick.position = brickPosition
addChild(brick)
brickPosition.x += brickSize.width + 1
}
brickPosition.x = xStart
brickPosition.y -= 11
}
}
}
Completely new to SpriteKit. Currently I have a UIView, and I want to add a sprite node to it (like a small UIImageView, but I want animation for it so using SpriteKit). Therefore I didn't initialize my project to be a game project, as found in almost all of tutorials for SpriteKit. I've found a note here: link and what I have now is sth like:
func initializeImage() {
let imageView = SKView()
imageView.frame = CGRect(x: self.frame.width / 2 - Constants.imageWidth / 2, y: self.frame.height - Constants.imageHeight, width: Constants.imageWidth, height: Constants.imageHeight)
// so place it somewhere in the bottom middle of the whole frame
let sheet = SpriteSheet(texture: ...)
let sprite = SKSpriteNode(texture: sheet.itemFor(column: 0, row: 0))
sprite.position = imageView.center //basically the same position as the imageView.frame's x and y value
let scene = SKScene(size: imageView.frame.size)
scene.backgroundColor = SKColor.clear
scene.addChild(sprite)
imageView.presentScene(scene)
self.frame.addSubview(imageView)
}
The SpriteSheet is similar to this: sprite sheet; it's essentially cutting an image atlas and divide it into smaller images. I tracked the process and this step is indeed giving the smaller image (the var 'sprite'). But if running I only have a black square now (should be the size as defined by Constants). If I set scene.backgroundColor to be white then it's white. May I know how I should proceed from here, as how should I make the sprite showing up?
All of your code looks good except for this:
sprite.position = imageView.center // basically the same position as the imageView.frame's x and y value
That is basically not the position you think it is. The coordinate system in SpriteKit is a) relative to the (SK)scene, not to whatever view the SKView is contained in, and b) flipped vertically relative to the UIKit coordinate system. If you want a sprite centered in the scene, you probably want to set its position based on the scene's size:
sprite.position = CGPoint(x: scene.size.width / 2, y: scene.size.height / 2)
By the way, the external SpriteSheet code might not be needed (and you're more likely to benefit from Apple's optimizations) if you slice up your sprite sheet and put it in an Xcode asset catalog.
I'm making my first game with SpriteKit and for the enemy aliens (SKSpriteNode subclass) physics bodies I have been using:
self.physicsBody = SKPhysicsBody(circleOfRadius: self.size.width/2)
this has worked so far as most of the aliens are circle-shaped. However, if I were to have ellipse or square aliens what would be the best method of designing their physics bodies? I tried this for square shape:
let squarePath = UIBezierPath(rect: CGRect(x: -64, y: -64, width: 128, height: 128))
self.physicsBody = SKPhysicsBody(polygonFromPath: squarePath.CGPath)
but I don't know if this is the most efficient, and also don't know how to exactly make ellipses. Any suggestions would be great!
Let SpriteKit draw the shape for you!
If you have a sprite and you need to create a PhysicsBody for it just let SpriteKit do it for you. SpriteKit can analyze the texture, find the borders (using the alpha/transparent channel) and finally create a physics body matching the image.
Let's say I add this sprite to my scene
let croc = SKSpriteNode(imageNamed: "croc_walk01")
croc.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(croc)
Automatically creating a PhysicsBody shape
Now I simply add this line
croc.physicsBody = SKPhysicsBody(texture: croc.texture!, size: croc.texture!.size())
And SpriteKit automatically creates the PhysicsBody for me.
Can you see the line line around the sprite? That's it. Let me hide the sprite so you can see it better
croc.hidden = true
Using paths is the only way to create complex physics bodys, however, you can create rectangles with SKPhysicsBody(rectangleOf: CGSize(width: 100, height: 100))
Edit:
Use this to help create efficient paths from images: http://insyncapp.net/SKPhysicsBodyPathGenerator.html
Note: this will only generate the paths for the image, you will need to clean it up your self by deleting paths and connecting the points.
I have a node named "user" and it runs an animation through user.atlas constantly. It is also pixel art (18x50) and I need it to stay looking sharp and how I designed it. If I remove the action and add
user.texture?.filteringMode = .Nearest
it looks sharp and clear and perfect - but as soon as I add the action, the texture don't seem to want to follow that rule.
Also!
The image is stretched, even if I set the size to (18 , 50) it still is stretched vertically and the pixels are longer than they are wide. This problem persists no matter the animation.
Anyone have any ideas? Thanks.
You should set filtering mode of the textures before you add to the sprite.
Here is an example:
// this creates a walking character with endless loop
// create textures of the sprite
let frame1 : SKTexture = SKTexture.init(imageNamed: "walk-left-1")
let frame2 : SKTexture = SKTexture.init(imageNamed: "walk-left-2")
let frame3 : SKTexture = SKTexture.init(imageNamed: "walk-left-3")
let frame4 : SKTexture = SKTexture.init(imageNamed: "walk-left-4")
// set filter for pixelart
frame1.filteringMode = .nearest
frame2.filteringMode = .nearest
frame3.filteringMode = .nearest
frame4.filteringMode = .nearest
// create the textures array
let walkFrames = [frame1, frame2, frame3, frame4]
// create sprite and add to the scene
let sprite = SKSpriteNode.init(texture: walkFrames[0])
sprite.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
self.addChild(sprite)
// create the walking animation
let animate = SKAction.animate(with: walkFrames, timePerFrame: 0.2)
//OPTIONAL: make sprite bigger if you need
sprite.setScale(4)
// start walking
sprite.run(SKAction.repeatForever(animate))