SpriteKit partial texture mapping - sprite-kit

Is it possible to create a SKSpriteNode that displays only a part of a texture?
For example, can I create a square with size 100x100 displaying the specific region of a texture of size 720x720 like from x1=300 to x2=400 and y1=600 to y2=700?
Thanks for your help.

Try something like this:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
let visibleArea = SKSpriteNode(color: .black, size: CGSize(width:100,height:100))
let parentNode = SKSpriteNode(color: .white, size: CGSize(width:200, height:200))
override func didMove(to view: SKView) {
let cropNode = SKCropNode()
let texture = SKSpriteNode(imageNamed: "Spaceship")
visibleArea.position = CGPoint(x: 0, y: 100)
cropNode.maskNode = visibleArea
cropNode.addChild(texture)
addChild(cropNode)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let previousPosition = touch.previousLocation(in: self)
let translation = CGPoint(x: location.x - previousPosition.x , y: location.y - previousPosition.y )
visibleArea.position = CGPoint(x: visibleArea.position.x + translation.x , y: visibleArea.position.y + translation.y)
}
}
}
Overriden touchesMoved method is there just because of better example. What I did here, is:
created SKCropNode
added a texture to it which will be masked
defined visible area which is SKSpriteNode and assigned it to crop node's mask property, which actually does the magic
Here is the result:

If you want to break up a texture into smaller chunks of textures to be used as puzzle pieces, then you want to use SKTexture(rect: in texture:)
Here is an example of how to use it:
let texture = SKTexture(...) //How ever you plan on getting main texture
let subTextureRect = CGRect(x:0,y:0.width:10,height:10) // The actual location and size of where you want to grab the sub texture from main texture
let subTexture = SKTexture(rect:subTextureRect, in:texture);
You now have a chunk of the sub texture to use in other nodes.

Related

Is there a way I can add more than one sprite in SpriteKit globally?

I am struggling with one issue. Global declaration of my sprite so that I can interact with it. In this game, I have created a local sprite called enemy featured below:
func spawnEnemy() {
let enemy = SKSpriteNode(imageNamed: "as")
let yPosition = CGFloat(frame.maxY - enemy.size.height)
let getXvalue = GKRandomDistribution(lowestValue: Int(frame.minX + enemy.size.width), highestValue: Int(frame.maxX - enemy.size.width))
let xPosition = CGFloat(getXvalue.nextInt())
enemy.position = CGPoint(x: xPosition, y: yPosition)
enemy.name = "asteroid"
enemy.zPosition = 100
addChild(enemy)
let animationDuration:TimeInterval = 6
var actionArray = [SKAction]()
actionArray.append(SKAction.move(to: CGPoint(x: xPosition, y: 0), duration: animationDuration))
actionArray.append(SKAction.self.removeFromParent())
enemy.run(SKAction.sequence(actionArray))
}
I want to tap the enemy to make it disappear from the screen. The variable is declared locally and not globally so the touchesBegan function does not "see" enemy. However, when I move the statement:
let enemy = SKSpriteNode(imageNamed: "as")
outside of local declaration and into global. It works until the code tries to spawn in another enemy and i get an error of "Tried to add an SKNode who already has a parent" This is the code I have running in my view did load:
run(SKAction.repeatForever(SKAction.sequence([SKAction.run{self.spawnEnemy()
}, SKAction.wait(forDuration: 1.0)])))
Every time it spawns a new enemy it crashes and says that the SKNode already has a parent which i understand. However, for my game to function I need the player to be able to touch the individual instance of that enemy and remove it. Hence my code for
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
if let location = touch?.location(in:self) {
let nodesArray = self.nodes(at:location)
if nodesArray.first?.name == "asteroid" {
print("Test")
enemy.removeFromParent()
print("Test Completed")
}
}
}
Now the error says unresolved use of "enemy" because the enemy is not global. I have been going back and forth on this issue for quite some time. If anyone has any potential solution or work around I would be very grateful, and thank you for your help.
Move your enemies to their own class and handle the touch for each of those enemies in their own class. This cleans up your GameScene and keeps your code more organized. You can now add as many instances of enemy as you want.
FYI not related to this question but somethings to consider after you get this working
when game over or level change or win make sure you have a clean up function to remove all enemies
you should strongly consider recycling your objects vs creating them on the fly...better performance
try to separate as much code to your objects class as possible
class enemy: SKSpriteNode {
init() {
super.init(texture: nil, color: .clear, size: CGSize.zero)
setup()
}
func setup() {
isUserInteractionEnabled = true
name = "asteroid"
zPosition = 100
let image = SKSpriteNode(imageNamed: "as")
imagine.zPosition = 1
addChild(image)
self.size = image.size
animate()
}
func animate() {
let animationDuration: TimeInterval = 6
let move = SKAction.move(to: CGPoint(x: xPosition, y: 0), duration: animationDuration)
let remover = SKAction.self.removeFromParent()
run(SKAction.sequence(move, remover))
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
removeFromParent()
}
}
class GameScene: SKScene {
override func didMove(to view: SKView) {
let sequence = SKAction.sequence([SKAction.run{ self.spawnEnemy()
}, SKAction.wait(forDuration: 1.0)])
run(SKAction.repeatForever(sequence))
}
func spawnEnemy() {
let enemy = Enemy()
let yPosition = CGFloat(frame.maxY - enemy.size.height)
let getXvalue = GKRandomDistribution(lowestValue: Int(frame.minX + enemy.size.width), highestValue: Int(frame.maxX - enemy.size.width))
let xPosition = CGFloat(getXvalue.nextInt())
enemy.position = CGPoint(x: xPosition, y: yPosition)
addChild(enemy)
}
}

Display upperLimit SKRange

So here is my code:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var ship1 = [2,1]
var ship2 = [1,2]
let jonahSpriteNode = SKSpriteNode(imageNamed: "jonah_spaceship")
let georgeSpriteNode = SKSpriteNode(imageNamed: "george_spaceship")
override func didMove(to view: SKView) {
//var jonahFrames = [SKTexture]()
jonahSpriteNode.position = CGPoint(x: 30, y: frame.midY)
jonahSpriteNode.size = CGSize(width: 100.0, height: 100.0)
addChild(jonahSpriteNode)
georgeSpriteNode.position = CGPoint(x: 628, y: frame.midY)
georgeSpriteNode.size = CGSize(width: 100.0, height: 100.0)
addChild(georgeSpriteNode)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
var touchLocation = touch.location(in: self)
var angle1 = atan2(jonahSpriteNode.position.y - touchLocation.y , jonahSpriteNode.position.x - touchLocation.x)
var angle = angle1 - CGFloat(Double.pi / 1)
makeCircularRange(to: jonahSpriteNode)
let rotate = SKAction.rotate(toAngle: angle, duration: 1.0)
let move = SKAction.move(to: CGPoint(x: touchLocation.x, y: touchLocation.y), duration: 2.5)
let sequence = SKAction.sequence([rotate, move])
jonahSpriteNode.run(sequence)
}
}
func makeCircularRange(to node: SKNode) {
let range = SKRange(lowerLimit: 0, upperLimit: 400)
let constraint = SKConstraint.distance(range, to: .zero)
node.constraints = [constraint]
}
}
I wanted to display the SKRange by showing upperLimit in a certain color. I tried making nodes around the limit and then coloring the nodes but it just showed a bunch of errors. If you have any ideas please answer.
Something like this:
The sprite node will be in the center and the circle will show where it can move.
You can make a circular shape using range.upperLimit as a radius an add to scene.
func drawCircularRangeBy(range: SKRange) {
let radius = range.upperLimit
let node = SKShapeNode(circleOfRadius: radius)
node.strokeColor = .white
addChild(node)
}
Take a look of this example: https://github.com/Maetschl/SpriteKitExamples/blob/master/CircularRange/CircularRange/GameScene.swift
If you really need dots instead of a line please see this answer: Drawing dashed line in Sprite Kit using SKShapeNode

Cut a hole on SKNode and update its physicsBody in Swift

I'm creating an app which will contains some holes on the image.
Since the code is very big I've created this simple code which has the same idea.
This code has an image with it's physicsBody set already.
What I would like is in the touchesBegan function to draw some transparent circles at the touched location and update the image physicsBody (making a hole on the image).
I've found several codes in objective C and UIImage, can someone help with Swift and SKSpriteNode?
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let texture = SKTexture(imageNamed: "Icon.png")
let node = SKSpriteNode(texture: texture)
node.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
addChild(node)
node.physicsBody = SKPhysicsBody(rectangleOfSize: texture.size())
node.physicsBody?.dynamic = false
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
}
}
One option is to apply a mask and render that to an image and update your SKSpriteNode's texture. Then use that texture to determine the physics body. This process however will not be very performant.
For example in touchesBegan you can say something like:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
guard let touch = touches.first else { return }
if let node = self.nodeAtPoint(touch.locationInNode(self.scene!)) as? SKSpriteNode{
let layer = self.layerFor(touch, node: node) // We'll use a helper function to create the layer
let image = self.snapShotLayer(layer) // Helper function to snapshot the layer
let texture = SKTexture(image:image)
node.texture = texture
// This will map the physical bounds of the body to alpha channel of texture. Hit in performance
node.physicsBody = SKPhysicsBody(texture: node.texture!, size: node.size)
}
}
Here we just get the node and look at the first touch, you could generalize to allow multi touch, but we then create a layer with the new image with transparency, then create a texture out of it, then update the node's physics body.
For creating the layer you can say something like:
func layerFor(touch: UITouch, node: SKSpriteNode) -> CALayer
{
let touchDiameter:CGFloat = 20.0
let layer = CALayer()
layer.frame = CGRect(origin: CGPointZero, size: node.size)
layer.contents = node.texture?.CGImage()
let locationInNode = touch.locationInNode(node)
// Convert touch to layer coordinate system from node coordinates
let touchInLayerX = locationInNode.x + node.size.width * 0.5 - touchDiameter * 0.5
let touchInLayerY = node.size.height - (locationInNode.y + node.size.height * 0.5) - touchDiameter * 0.5
let circleRect = CGRect(x: touchInLayerX, y: touchInLayerY, width: touchDiameter, height: touchDiameter)
let circle = UIBezierPath(ovalInRect: circleRect)
let shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: node.size.width, height: node.size.height)
let path = UIBezierPath(rect: shapeLayer.frame)
path.appendPath(circle)
shapeLayer.path = path.CGPath
shapeLayer.fillRule = kCAFillRuleEvenOdd
layer.mask = shapeLayer
return layer
}
Here we just set the current texture to the contents of a CALayer then we create a CAShapeLayer for the mask we want to create. We want the mask to be opaque for most of the layer but we want a transparent circle, so we create a path of a rectangle then add a circle to it. We set the fillRule to kCAFillRuleEvenOdd to fill everything but our circle.
Lastly, we render that layer to a UIImage that we can use to update our texture.
func snapShotLayer(layer: CALayer) -> UIImage
{
UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()
layer.renderInContext(context!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
Maybe someone will provide a more performant way of accomplishing this, but I think this will work for a lot of cases.

Get SKCameraNode to follow a hero Spritenode

I can't seem to figure this out. I've tried many different things and none of them seem to work. With my current code, the camera and the hero never line up and the scene seems to jump pretty far when I touch the screen. All I want to do is when I touch the screen have the hero move to the touch point and have the camera follow him. Is there some way to lock the camera to the hero spritenode?
import SpriteKit
let tileMap = JSTileMap(named: "level2.tmx")
let hero = SKSpriteNode(imageNamed: "hero")
let theCamera: SKCameraNode = SKCameraNode()
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
self.anchorPoint = CGPoint(x: 0, y: 0)
self.position = CGPoint(x: 0, y: 0)
hero.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
hero.xScale = 0.5
hero.yScale = 0.5
hero.zPosition = 2
tileMap.zPosition = 1
tileMap.position = CGPoint(x: 0, y: 0)
self.addChild(tileMap)
self.addChild(hero)
self.addChild(theCamera)
self.camera = theCamera
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
let location = touch.locationInNode(self)
let action = SKAction.moveTo(location, duration: 1)
hero.runAction(action)
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
self.camera?.position = hero.position
}
}
The reason why you saw the scene jumped pretty far is because the scene.size doesn't equal to the screen size. I guess you might initialize your first scene like this:
// GameViewController.swift
if let scene = GameScene(fileNamed:"GameScene") {...}
That code will load GameScene.sks whose size is 1024*768 by default. But since you add your SKSpriteNode programmatically, you can initialize the scene in this way to fit the screen size:
// GameViewController.swift
// Only remove if statement and modify
let scene = GameScene(size: view.bounds.size) ...
This will solve most of the problem you have. Moreover, I suggest moving the camera node using SKAction:
override func update(currentTime: CFTimeInterval) {
let action = SKAction.moveTo(hero.position, duration: 0.25)
theCamera.runAction(action)
}
The last thing, add this line to align the camera with your hero at the start:
self.camera?.position = hero.position

Swift SpriteKit get visible frame size

I have been trying to create a simple SpriteKit app using Swift. The purpose is to have a red ball re-locate itself on the screen when clicked on. But the variables self.frame.width and self.frame.height do not return the boundaries of the visible screen. Instead they return boundaries of the whole screen. Because I am choosing the location of the ball at random I need the visible boundaries. Couldn't find an answer after hours of research. How can I achieve this?
var dot = SKSpriteNode()
let dotScreenHeightPercantage = 10.0
let frameMarginSize = 30.0
override func didMoveToView(view: SKView) {
var dotTexture = SKTexture(imageNamed: "img/RedDot.png")
dot = SKSpriteNode(texture: dotTexture)
dot.size.height = CGFloat( Double(self.frame.height) / dotScreenHeightPercantage )
dot.size.width = dot.size.height
dot.name = "dot"
reCreateDot()
}
func reCreateDot() {
dot.removeFromParent()
let dotRadius = Double(dot.size.height / 2)
let minX = Int(frameMarginSize + dotRadius)
let maxX = Int(Double(self.frame.width) - frameMarginSize - dotRadius)
let minY = Int(frameMarginSize + dotRadius)
let maxY = Int(Double(self.frame.height) - frameMarginSize - dotRadius)
let corX = randomInt(minX, max: maxX)
let corY = randomInt(minY, max: maxY)
println("result: \(corX) \(corY)")
dot.position = CGPoint(x: corX, y: corY)
self.addChild(dot)
}
func randomInt(min: Int, max:Int) -> Int {
return min + Int(arc4random_uniform(UInt32(max - min + 1)))
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let node = nodeAtPoint(location)
if node.name == "dot" {
println("Dot tapped.")
reCreateDot()
}
}
}
Adding this bit of code to the GameViewController seems to have fixed the issue.
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
scene.size = skView.bounds.size
skView.presentScene(scene)
Thank you #ABakerSmith for pointing me in the right direction.
I tried with your code and setting the size of the scene fixed the issue. If you're unarchiving the scene, its size will be the size set in the sks file. Therefore you need to set the size of the scene to the size of your SKView.
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
scene.size = skView.frame.size
skView.presentScene(scene)
}
or in didMoveToView of your SKScene subclass:
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
self.size = view.frame.size
}