How do I replace an image of an SKSpriteNode for the duration of an action then return it to it's original texture atlas loop - swift

I am trying to make a basic run and jump game in SpriteKit.
When the view loads I wish for the sprite node to Run using images from a texture atlas. This I have managed to do.
When the screen is touched I wish this image to change to another image in the texture atlas called Jump.
When the character returns to the ground I wish it to go back to the original texture atlas loop.
So far I have coded the following:
import UIKit
import SpriteKit
class Level1: SKScene {
var Hero : SKSpriteNode!
//Creates an object for the Hero character.
let textureAtlas = SKTextureAtlas(named:"RunImages.atlas")
//Specifies the image atlas used.
var spriteArray = Array<SKTexture>();
//Creates a variable for the image atlas of him running.
var HeroBaseLine = CGFloat (0)
//This is where the Hero character sits on top of the ground.
var onGround = true
//Creates a variable to specify if Hero is on the ground.
var velocityY = CGFloat (0)
//Creates a variable to hold a three decimal point specification for velocity in the Y axis.
let gravity = CGFloat (0.6)
//Creates a non variable setting for gravity in the scene.
let movingGround = SKSpriteNode (imageNamed: "Ground")
//Creates an object for the moving ground and assigns the Ground image to it.
var originalMovingGroundPositionX = CGFloat (0)
//Sets a variable for the original ground position before it starts to move.
var MaxGroundX = CGFloat (0)
//Sets a variable for the maximum
var groundSpeed = 4
//Sets the ground speed. This number is how many pixels it will move the ground to the left every frame.
override func didMoveToView(view: SKView) {
//Misc setup tasks.
backgroundColor = (UIColor.blackColor())
//Sets the background colour when the view loads.
//Ground related tasks.
self.movingGround.anchorPoint = CGPointMake(0, 0.5)
//Positions the Ground image hard left in the X axis.
self.movingGround.position = CGPointMake(CGRectGetMinX(self.frame), CGRectGetMinY(self.frame) + (self.movingGround.size.height / 2))
//Positions the Ground image at the bottom of the screen relative to half the height of the image.
self.addChild(self.movingGround)
//Creates an instance of the Ground image that follows the parameters set in the lines above when the view loads.
self.originalMovingGroundPositionX = self.movingGround.position.x
//Sets the starting position for the ground image in the x before it start to move.
self.MaxGroundX = self.movingGround.size.width - self.frame.size.width
//Sets the maximum ground size minus the width of the screen to create the loop point in the image.
self.MaxGroundX *= -1
//This multiplies the size of the ground by itself and makes the max ground size a negative number as the image is moving towards the left in x which is negative.
//Hero related tasks.
spriteArray.append(textureAtlas.textureNamed("Run1"));
spriteArray.append(textureAtlas.textureNamed("Run2"));
spriteArray.append(textureAtlas.textureNamed("Run3"));
spriteArray.append(textureAtlas.textureNamed("Run2"));
Hero = SKSpriteNode(texture:spriteArray[0]);
self.HeroBaseLine = self.movingGround.position.y + (self.movingGround.size.height / 2) + 25
//self.Hero.position = CGPointMake(CGRectGetMinX(self.frame) + 50, self.HeroBaseLine)
self.Hero.position = CGPointMake(CGRectGetMinX(self.frame) + 50, self.HeroBaseLine)
//Sets where the character will appear exactly.
self.Hero.xScale = 0.15
self.Hero.yScale = 0.15
addChild(self.Hero);
//Adds an instance of Hero to the screen.
let animateAction = SKAction.animateWithTextures(self.spriteArray, timePerFrame: 0.15);
let moveAction = SKAction.moveBy(CGVector(dx: 0,dy: 0), duration: 0.0);
//Although currently set to 0, the above line controls the displacement of the character in the x and y axis if required.
let group = SKAction.group([ animateAction,moveAction]);
let repeatAction = SKAction.repeatActionForever(group);
self.Hero.runAction(repeatAction);
//Animation action to make him run. Here we can affect the frames and x, y movement, etc.
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if self.onGround {
self.velocityY = -18
self.onGround = false
}
}
//This block specifies what happens when the screen is touched.
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if self.velocityY < -9.0 {
self.velocityY = -9.0
}
}
//This block prevents Hero from jumping whilst already jumping.
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if self.movingGround.position.x <= MaxGroundX {
self.movingGround.position.x = self.originalMovingGroundPositionX
}
//This is how the ground is positioned at the beginning of each update (each frame refresh)
movingGround.position.x -= CGFloat (self.groundSpeed)
//This is how the ground is moved relative to the ground speed variable set at the top. The number in the variable is how many pixels the frame is being moved each frame refresh.
self.velocityY += self.gravity
self.Hero.position.y -= velocityY
if self.Hero.position.y < self.HeroBaseLine {
self.Hero.position.y = self.HeroBaseLine
velocityY = 0.0
self.onGround = true
}
//This is the code for making Hero jump in accordance to the velocity and gravity specified at the top of the class in relation to the base line.
}
}
I have tried to add code in the touchesBegan section to change the image texture of the sprite node to another image in my image atlas called Jump.
I then wish for the texture atlas to return to animating once the touches ended action is called.
Any help would be greatly appreciated.
Update:
I have implemented what you have suggested but it still doesn't work quite correclty. The hero is changing to the jumping image but does not actually jump and is stuck in the jump image.
I created a JumpImages.atlas and added "Jump" image to that folder.
I have modified the code to the following:
import UIKit
import SpriteKit
class Level1: SKScene {
//Creates an object for the Hero character.
var Hero : SKSpriteNode!
//Specifies the image atlas used.
let textureAtlas = SKTextureAtlas(named:"RunImages.atlas")
//Creates a variable for the image atlas of him running.
var spriteArray = Array<SKTexture>();
var jumpArray = Array<SKTexture>();
let jumpAtlas = SKTextureAtlas(named:"JumpImages.atlas")
//jumpArray.append(jumpAtlas.textureNamed("Jump")) Didn't work in this area, moved it to the did move to view.
//This is where the Hero character sits on top of the ground.
var HeroBaseLine = CGFloat (0)
//Creates a variable to specify if Hero is on the ground.
var onGround = true
//Creates a variable to hold a three decimal point specification for velocity in the Y axis.
var velocityY = CGFloat (0)
//Creates a non variable setting for gravity in the scene.
let gravity = CGFloat (0.6)
//Creates an object for the moving ground and assigns the Ground image to it.
let movingGround = SKSpriteNode (imageNamed: "Ground")
//Sets a variable for the original ground position before it starts to move.
var originalMovingGroundPositionX = CGFloat (0)
//Sets a variable for the maximum
var MaxGroundX = CGFloat (0)
//Sets the ground speed. This number is how many pixels it will move the ground to the left every frame.
var groundSpeed = 4
override func didMoveToView(view: SKView) {
//Misc setup tasks.
//Sets the background colour when the view loads.
backgroundColor = (UIColor.blackColor())
//Ground related tasks.
//Positions the Ground image hard left in the X axis.
self.movingGround.anchorPoint = CGPointMake(0, 0.5)
//Positions the Ground image at the bottom of the screen relative to half the height of the image.
self.movingGround.position = CGPointMake(CGRectGetMinX(self.frame), CGRectGetMinY(self.frame) + (self.movingGround.size.height / 2))
//Creates an instance of the Ground image that follows the parameters set in the lines above when the view loads.
self.addChild(self.movingGround)
//Sets the starting position for the ground image in the x before it start to move.
self.originalMovingGroundPositionX = self.movingGround.position.x
//Sets the maximum ground size minus the witdth of the screen to create the loop point in the image.
self.MaxGroundX = self.movingGround.size.width - self.frame.size.width
//This multiplies the size of the ground by itself and makes the max ground size a negative number as the image is moving towards the left in x which is negative.
self.MaxGroundX *= -1
//Hero related tasks.
spriteArray.append(textureAtlas.textureNamed("Run1"));
spriteArray.append(textureAtlas.textureNamed("Run2"));
spriteArray.append(textureAtlas.textureNamed("Run3"));
spriteArray.append(textureAtlas.textureNamed("Run2"));
Hero = SKSpriteNode(texture:spriteArray[0]);
self.HeroBaseLine = self.movingGround.position.y + (self.movingGround.size.height / 2) + 25
//Sets where the character will appear exactly.
self.Hero.position = CGPointMake(CGRectGetMinX(self.frame) + 50, self.HeroBaseLine)
//Scales the image to an appropriate size.
self.Hero.xScale = 0.15
self.Hero.yScale = 0.15
//Adds an instance of Hero to the screen.
addChild(self.Hero);
//Added this here as it didn't appear to work in the place recommended.
jumpArray.append(jumpAtlas.textureNamed("Jump"));
//I added this so that he runs when the view loads.
if self.onGround {
run()
}
}
//Animation function to make him run. Here we can affect the frames and x, y movement, etc.
func run() {
let animateAction = SKAction.animateWithTextures(self.spriteArray, timePerFrame: 0.15);
//Although currently set to 0, the above line controls the displacement of the character in the x and y axis if required.
let moveAction = SKAction.moveBy(CGVector(dx: 0,dy: 0), duration: 0.0);
let group = SKAction.group([animateAction,moveAction]);
let repeatAction = SKAction.repeatActionForever(group);
self.Hero.runAction(repeatAction);
}
//Animation function to make him jump.
func jump() {
self.velocityY = -18
self.onGround = false
let jumpAnimation = SKAction.animateWithTextures(jumpArray, timePerFrame: 0.15)
self.Hero.runAction(SKAction.repeatActionForever(jumpAnimation))
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
//This block specifies what happens when the screen is touched.
if self.onGround {
jump()
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
//This block prevents Hero from jumping whilst already jumping.
if self.velocityY < -9.0 {
self.velocityY = -9.0
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
//This is how the ground is positioned at the beginning of each update (each frame refresh)
if self.movingGround.position.x <= MaxGroundX {
self.movingGround.position.x = self.originalMovingGroundPositionX
}
//This is how the ground is moved relative to the ground speed variable set at the top. The number in the variable is how many pixels the frame is being moved each frame refresh.
movingGround.position.x -= CGFloat (self.groundSpeed)
//This is the code for making Hero jump in accordance to the velocity and gravity specified at the top of the class in realation to the base line and run when he hits the ground.
if self.Hero.position.y < self.HeroBaseLine {
self.Hero.position.y = self.HeroBaseLine
velocityY = 0.0
if self.onGround == false {
self.onGround = true
run()
}
}
}
}
Is there anything obvious I am doing wrong? Thanks for your help.

Since you have already made your sprite run, to jump is not a hard thing. Just replace the texture of run animation with the texture of jump animation in proper place.
Firstly, I wrap the code of run animation for reuse later.
func run() {
let animateAction = SKAction.animateWithTextures(self.spriteArray, timePerFrame: 0.15);
let moveAction = SKAction.moveBy(CGVector(dx: 0,dy: 0), duration: 0.0);
let group = SKAction.group([animateAction,moveAction]);
let repeatAction = SKAction.repeatActionForever(group);
self.Hero.runAction(repeatAction);
}
Next step is for texture atlas of Jump. For demo, I just add one frame animation for jumping. Add these line after you create textureAtlas and spriteArray for Run.
var jumpArray = Array<SKTexture>()
let jumpAtlas = SKTextureAtlas(named:"JumpImages.atlas")
jumpArray.append(jumpAtlas.textureNamed("Jump"))
After you write function jump(), you can call it in touchesBegan.
func jump() {
self.velocityY = -18
self.onGround = false
println("jump over ground")
let jumpAnimation = SKAction.animateWithTextures(jumpArray, timePerFrame: 0.15)
self.Hero.runAction(SKAction.repeatActionForever(jumpAnimation))
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent?) {
if self.onGround {
jump()
}
}
Last but not least, resume running animation after back to the ground in update.
override func update(currentTime: CFTimeInterval) {
...
if self.Hero.position.y < self.HeroBaseLine {
self.Hero.position.y = self.HeroBaseLine
velocityY = 0.0
if self.onGround == false {
self.onGround = true
println("on the ground")
run()
}
}
}
Now you should get the result below. If you have any problem with the code, just let me know.

Related

Moving Background Swift SKSpriteKit

I'm currently working on an application where I'm trying to set up a moving background. I have a transparent image full of clouds that I'm currently using. My problem is, how can I make it move more smoother? I've tried to play with the speeds but it still looks laggy. Any help would be a blessing.
Here's a video of what I got going. http://sendvid.com/78ggkzcj
And here's a picture of the cloud image. Cloud Image
Here's my code. What you think I should change or do differently?
class GameScene: SKScene {
// Background
let background = SKSpriteNode(texture:SKTexture(imageNamed: "background"))
// Clouds
var mainCloud = SKSpriteNode()
var cloud1Next = SKSpriteNode()
// Time of last frame
var lastFrameTime : TimeInterval = 0
// Time since last frame
var deltaTime : TimeInterval = 0
override func didMove(to view: SKView) {
background.position = CGPoint(x: 0, y: 0)
// Prepare the clouds sprites
mainCloud = SKSpriteNode(texture:
SKTexture(imageNamed: "cloudbg1"))
mainCloud.position = CGPoint(x: 0, y: 0)
cloud1Next = mainCloud.copy() as! SKSpriteNode
cloud1Next.position =
CGPoint(x: mainCloud.position.x + mainCloud.size.width,
y: mainCloud.position.y)
// Add the sprites to the scene
self.addChild(background)
self.addChild(mainCloud)
self.addChild(cloud1Next)
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
// First, update the delta time values:
// If we don't have a last frame time value, this is the first frame,
// so delta time will be zero.
if lastFrameTime <= 0 {
lastFrameTime = currentTime
}
// Update delta time
deltaTime = currentTime - lastFrameTime
// Set last frame time to current time
lastFrameTime = currentTime
// Next, move each of the four pairs of sprites.
// Objects that should appear move slower than foreground objects.
self.moveSprite(sprite: mainCloud, nextSprite:cloud1Next, speed:100)
}
// Move a pair of sprites leftward based on a speed value;
// when either of the sprites goes off-screen, move it to the
// right so that it appears to be seamless movement
func moveSprite(sprite : SKSpriteNode,
nextSprite : SKSpriteNode, speed : Float) -> Void {
var newPosition = CGPoint.zero
// For both the sprite and its duplicate:
for spriteToMove in [sprite, nextSprite] {
// Shift the sprite leftward based on the speed
newPosition = spriteToMove.position
newPosition.x -= CGFloat(speed * Float(deltaTime))
spriteToMove.position = newPosition
// If this sprite is now offscreen (i.e., its rightmost edge is
// farther left than the scene's leftmost edge):
if spriteToMove.frame.maxX < self.frame.minX {
// Shift it over so that it's now to the immediate right
// of the other sprite.
// This means that the two sprites are effectively
// leap-frogging each other as they both move.
spriteToMove.position =
CGPoint(x: spriteToMove.position.x +
spriteToMove.size.width * 2,
y: spriteToMove.position.y)
}
}
}
}
Your code looks fine. You are getting low fps because you are running your game in the simulator. If you run on a real device, it should be smooth.

(Swift) pan & zoom constrained to a certain size image

I am trying to pan and zoom across an image background in spritekit, I have managed to get the zoom working ok and manually entered some restrictions on how far you can pan the image, however the problem is when you pan the screen right to the edge of the image and then zoom out the background shows.
I want the camera to restrict only to the image on screen and not any blank background. Any ideas on how I should do this or any better solutions?
Here is what I got so far
class GameScene:SKScene{
var cam: SKCameraNode!
var scaleNum:CGFloat=1
override func didMove(to view: SKView){
cam=SKCameraNode()
cam.setScale(CGFloat(scaleNum))
self.camera=cam
self.addChild(cam)
let gesture=UIPinchGestureRecognizer(target: self, action: #selector(zoomIn(recognizer:)))
self.view!.addGestureRecognizer(gesture)
}
func zoomIn(recognizer: UIPinchGestureRecognizer){
if recognizer.state == .changed{
cam.setScale(recognizer.scale)
scaleNum=recognizer.scale
if cam.xScale<1 || cam.yScale<1{
cam.setScale(1)
}
if cam.xScale>3 || cam.yScale > 3{
cam.setScale(3)
}
// recognizer.scale=1
test()
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let firstTouch=touches.first
let location=(firstTouch?.location(in: self))!
let previousLocation=firstTouch?.previousLocation(in: self)
cam?.position.x -= location.x - (previousLocation?.x)!
cam?.position.y -= location.y - (previousLocation?.y)!
test()
}
func test(){
if cam.position.x < 1000*scaleNum{
cam.position.x=1000*scaleNum
}
if cam.position.x > 9200*scaleNum{
cam.position.x=9200*scaleNum
}
if cam.position.y<617*scaleNum{
cam.position.y=617*scaleNum
}
if cam.position.y>4476*scaleNum{
cam.position.y=4476*scaleNum
}
}
}
First of all, I would change your zoomIn function to this:
func zoomIn(recognizer: UIPinchGestureRecognizer){
if recognizer.state == .changed {
scaleNum = recognizer.scale
if scaleNum < 1 { scaleNum = 1 }
if scaleNum > 3 { scaleNum = 3 }
cam.setScale(scaleNum)
test()
}
}
It is easier to understand, you're not setting the camera scale twice, and most importantly, when you clamp the camera scale, scaleNum reflects that clamped value. This was not the case before, and in fact, that small change might be your entire problem.
Now I don't have much experience with UIPinchGestureRecognizer but I think the reason your zoom gesture works "ok" is because you are assigning directly from recognizer.scale to cam scale. Correct me if I'm wrong, but I think UIGestureRecognizer always starts with a scale of 1 for each new gesture, but your camera scale maintains its last value.
As an example, imagine your camera is at a scale of 1. A user zooms in to a scale of 2, the scene zooms in perfectly. The user lifts their fingers ending the gesture. Then the user tries to zoom in more, so they begin a new gesture, starting with a scale of 1, but your scene is still at a scale of 2. You can't assign the gesture scale directly or the image scale will 'jump' back to 1 for each new gesture. You have to convert from the gesture scale space to the camera scale space.
How exactly you do this is a design and feel choice. With no experience, my advice would be to change the line in my zoomIn function from
`scaleNum = recognizer.scale'
to
`scaleNum *= recognizer.scale`
Try both versions, and let me know how they work. If there is still a problem, then it most likely resides in your test() function. If so, I will try and help out with that as needed.
Thanks for the answer above, I managed to get it working, code below. Still needs a bit of tweaking but you can pan and zoom anywhere on the background image but the view should be constrained within the background image and not move into empty space beyond the image
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var cam: SKCameraNode!
var scaleNum: CGFloat=1
var background: SKSpriteNode!
var playableRect: CGRect!
override func didMove(to view: SKView) {
background=self.childNode(withName: "clouds") as! SKSpriteNode
cam=SKCameraNode()
cam.setScale(CGFloat(scaleNum))
self.camera=cam
self.addChild(cam)
self.isUserInteractionEnabled=true
let gesture=UIPinchGestureRecognizer(target: self, action: #selector(zoomIn(recognizer:)))
self.view!.addGestureRecognizer(gesture)
let maxAspectRatio:CGFloat=16.0/9.0
let playableHeight=size.width/maxAspectRatio
let playableMargin=(size.height-playableHeight)/2.0
playableRect=CGRect(x:0, y: playableMargin, width: size.width, height: playableHeight)
}
func zoomIn(recognizer: UIPinchGestureRecognizer){
if recognizer.state == .changed{
let savedScale=scaleNum
scaleNum=recognizer.scale
if scaleNum<1{
scaleNum=1
}
else if scaleNum>3{
scaleNum=3
}
if testcamera(posX: cam.position.x, posY: cam.position.y){
cam.setScale(scaleNum)
}
else{
scaleNum=savedScale
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let firstTouch=touches.first
let location=(firstTouch?.location(in: self))!
var posX=cam.position.x
var posY=cam.position.y
let previousLocation=firstTouch?.previousLocation(in: self)
posX -= location.x - (previousLocation?.x)!
posY -= location.y - (previousLocation?.y)!
if testcamera(posX: posX, posY: posY){
cam.position.x=posX
cam.position.y=posY
}
}
func testcamera(posX: CGFloat, posY: CGFloat)->Bool{
var cameraRect : CGRect {
let xx = posX - size.width/2*scaleNum
let yy = posY - playableRect.height/2*scaleNum
return CGRect(x: xx, y: yy, width: size.width*scaleNum, height: playableRect.height*scaleNum)
}
let backGroundRect=CGRect(x: background.position.x-background.frame.width/2, y: background.position.y-background.frame.height/2, width: background.frame.width, height: background.frame.height)
return backGroundRect.contains(cameraRect)
}
}

How to move sprite to center while is in a path and then return to the original path?

I am making a game in which a sprite is rotating around a big circle and I want that when touches began the sprite moves to another circle that is in the center of the screen and now the sprite rotatate around it, and when the touches began occurs again the sprite return to the big circle. In my code the sprite moves to the -sprite.position.x * 2 so that makes that the sprite move to the oposite side of the big circle. How can I solve that?
One of the easiest ways to move an object in a circular path is to
Create a SKNode container
Create a sprite
Add the container to the scene
Add the sprite to the container
Set the sprite's x position to the radius of the circular path
Rotate the container
If you want to move the sprite to the another circle with a different radius,
change the sprite's x position
move the container's position (optional)
If you want to change the direction of the rotation,
reverse the container's rotation
Here's an example:
// Define circle
struct Circle {
var position:CGPoint
var radius:CGFloat
}
class GameScene: SKScene {
// 1. Create container node
let node = SKNode()
// 2. Create a sprite
let sprite = SKSpriteNode(color:SKColor.blueColor(),size:CGSizeMake(10,10))
var rotation:CGFloat = CGFloat(M_PI)
var circles:[Circle] = []
override func didMoveToView(view: SKView) {
scaleMode = .ResizeFill
circles.append(Circle(position: view.center, radius: 80))
circles.append(Circle(position: view.center, radius: 40))
// 3. Add the container to the scene
addChild(node)
// 4. Add the sprite to the container
node.addChild(sprite)
// 5. Set the sprite's x position to the radius of the circular path
if let circle = nextCircle() {
node.position = circle.position
sprite.position = CGPoint(x:circle.radius, y:0)
// 6. Rotate the container
rotate()
}
}
// Rotate the container
func rotate() {
let action = SKAction.rotateByAngle(rotation, duration: 4)
node.runAction(SKAction.repeatActionForever(action),withKey: "rotate")
}
// 9. Reverse the container's rotation
func reverse() {
rotation = -rotation
}
// Stop rotating the container
func stopRotation() {
if node.actionForKey("rotate") != nil {
node.removeActionForKey("rotate")
}
}
// Returns the next circle's parameters and rotates the array
func nextCircle() -> Circle? {
guard let circle = circles.first else {
return nil
}
// Move the current circle to the back of the queue
circles.append(circles.removeAtIndex(0))
return circle
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Change the sprite's x-position */
if sprite.actionForKey("move") == nil {
stopRotation()
// 7. change the sprite's x position
// 8. move the container's position (optional)
if let circle = nextCircle() {
let radius = CGPoint(x:circle.radius, y:0)
let moveCenter = SKAction.moveTo(circle.position, duration: 3)
let move = SKAction.moveTo(radius, duration: 3)
let rotate = SKAction.runBlock {
self.rotate()
}
sprite.runAction(SKAction.sequence([move, rotate]), withKey: "move")
node.runAction(moveCenter, withKey: "move")
}
}
}
}

Hero jumps over walls after colliding with them

In the following code, the 'hero' starts inside of the level. As it moves around the map, the black area outside of the map becomes visible. As the hero comes up to the wall he stops. But then, if I touch the black area outside of the level, the hero jumps over the wall and into the outside-of-the-level area. Also, sometimes when the hero contacts the wall he bounces back in the opposite direction. (I'm not really sure what is causing that.) What I'm trying to do is keep the hero inside the level, and stop the bouncing back that is happening sometimes.
I'm not sure if the issue is that I'm not doing my collisions correctly or if I need to somehow stop the black area from being visible at all. I think stopping the area outside of the level from showing is what I need but playing around with let scene = GameScene(size: view.bounds.size) and changing it to let scene = GameScene(size: tileMap.frame.size) didn't work. Here is my code:
import SpriteKit
let tileMap = JSTileMap(named: "level2.tmx")
let hero = SKSpriteNode(imageNamed: "hero")
let theCamera: SKCameraNode = SKCameraNode()
class GameScene: SKScene, SKPhysicsContactDelegate {
enum ColliderType: UInt32 {
case Hero = 1
case Wall = 2
}
override func didMoveToView(view: SKView) {
self.physicsWorld.contactDelegate = self
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
hero.physicsBody = SKPhysicsBody(circleOfRadius: hero.size.height / 2)
hero.physicsBody?.affectedByGravity = false
hero.physicsBody!.dynamic = true
hero.physicsBody!.categoryBitMask = ColliderType.Hero.rawValue
hero.physicsBody!.contactTestBitMask = ColliderType.Wall.rawValue
hero.physicsBody!.collisionBitMask = ColliderType.Wall.rawValue
tileMap.zPosition = 1
tileMap.position = CGPoint(x: 0, y: 0)
self.addChild(tileMap)
self.addChild(hero)
self.addChild(theCamera)
self.camera = theCamera
camera?.position = hero.position
addWalls()
}
func didBeginContact(contact: SKPhysicsContact) {
print("Hero made contact with a wall")
}
func addWalls() {
//Go through every point up the tile map
for var a = 0; a < Int(tileMap.mapSize.width); a++ {
//Go through every point across the tile map
for var b = 0; b < Int(tileMap.mapSize.height); b++ {
//Get the first layer (you may want to pick another layer if you don't want to use the first one on the tile map)
let layerInfo:TMXLayerInfo = tileMap.layers[1] as! TMXLayerInfo
//Create a point with a and b
let point = CGPoint(x: a, y: b)
//The gID is the ID of the tile. They start at 1 up the the amount of tiles in your tile set.
let gid = layerInfo.layer.tileGidAt(layerInfo.layer.pointForCoord(point))
//My gIDs for the floor were 2, 9 and 8 so I checked for those values
if gid == 1 {
//I fetched a node at that point created by JSTileMap
let node = layerInfo.layer.tileAtCoord(point)
//I added a physics body
node.physicsBody = SKPhysicsBody(rectangleOfSize: node.frame.size)
node.physicsBody?.dynamic = false
node.physicsBody!.categoryBitMask = ColliderType.Wall.rawValue
node.physicsBody!.contactTestBitMask = ColliderType.Hero.rawValue
node.physicsBody!.collisionBitMask = ColliderType.Wall.rawValue
}
}
}
}
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 */
let action = SKAction.moveTo(hero.position, duration: 0.25)
theCamera.runAction(action)
}
}
My TMX file:
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.0" orientation="orthogonal" renderorder="right-down" width="24" height="42" tilewidth="32" tileheight="32" nextobjectid="13">
<tileset firstgid="1" name="grass-tiles-2-small" tilewidth="32" tileheight="32" tilecount="72">
<image source="grass-tiles-2-small.png" trans="ff00ff" width="384" height="192"/>
</tileset>
<layer name="Tile Layer 1" width="24" height="42">
<data encoding="base64" compression="zlib">
eJztzLEJAAAMw7Bs/f/jXhHIIINXJf2uNJ/P5/P5fD6fz+fz+Ut+swfI8xgR
</data>
</layer>
<layer name="Walls" width="24" height="42">
<data encoding="base64" compression="zlib">
eJztzLEJAAAMw7Dk/6d7RaCDDF7VJB2/is/n8/l8Pp/P5/P5/E/+8gMA/ACB
</data>
</layer>
</map>
Here is a video of both the bouncing when the hero touches the wall and the jumping over the wall Video of app sim
When a contact happens between a node and a physics body, SpriteKit will just detect the contact but won't take responsibility for stopping the node's action as we hitting an obstacle in the daily life. So you need to stop it manually.
Let's add a key value for SKAction of hero to distinguish the moving action from other actions which may be added in the future:
hero.runAction(action, withKey: "move")
Then, modify didBeginContact method to remove the action when contact happens, and I hope this will make what you want:
func didBeginContact(contact: SKPhysicsContact) {
print("Hero made contact with a wall")
// Stop your hero
hero.removeActionForKey("move")
}

How to drag a SKSpriteNode without touching it with Swift

I'm trying to move SKSpriteNode horizontally by dragging. To get the idea of what I try to achieve you can watch this. I want player to be able to drag sprite without touching it. But I don't really know how to implement it correctly.
I tried to do something like:
override func didMoveToView(view: SKView) {
/* Setup your scene here */
capLeft.size = self.capLeft.size
self.capLeft.position = CGPointMake(CGRectGetMinX(self.frame) + self.capLeft.size.height * 2, CGRectGetMinY(self.frame) + self.capLeft.size.height * 1.5)
capLeft.zPosition = 1
self.addChild(capLeft)
let panLeftCap: UIPanGestureRecognizer = UIPanGestureRecognizer(target: capLeft, action: Selector("moveLeftCap:"))
And when I'm setting a moveLeftCap function, code that I've found for UIPanGestureRecognizer is requiring "View" and gives me an error. I also wanted to limit min and max positions of a sprite through which it shouldn't go.
Any ideas how to implement that?
You probably get that error because you can't just access the view from any node in the tree. You could to refer to it as scene!.view or you handle the gesture within you scene instead which is preferable if you want to keep things simple.
I gave it a try and came up with this basic scene:
import SpriteKit
class GameScene: SKScene {
var shape:SKNode!
override func didMoveToView(view: SKView) {
//creates the shape to be moved
shape = SKShapeNode(circleOfRadius: 30.0)
shape.position = CGPointMake(frame.midX, frame.midY)
addChild(shape)
//sets up gesture recognizer
let pan = UIPanGestureRecognizer(target: self, action: "panned:")
view.addGestureRecognizer(pan)
}
var previousTranslateX:CGFloat = 0.0
func panned (sender:UIPanGestureRecognizer) {
//retrieve pan movement along the x-axis of the view since the gesture began
let currentTranslateX = sender.translationInView(view!).x
//calculate translation since last measurement
let translateX = currentTranslateX - previousTranslateX
//move shape within frame boundaries
let newShapeX = shape.position.x + translateX
if newShapeX < frame.maxX && newShapeX > frame.minX {
shape.position = CGPointMake(shape.position.x + translateX, shape.position.y)
}
//(re-)set previous measurement
if sender.state == .Ended {
previousTranslateX = 0
} else {
previousTranslateX = currentTranslateX
}
}
}
when you move you finger across the screen, the circle gets moves along the x-axis accordingly.
if you want to move the sprite in both x and y directions, remember to invert the y-values from the view (up in view is down in scene).