Increasing the size of a spriteNode using spriteKit in update function (Swift) - swift

I'm having troubles with animations, physics and stuff like these.
Anyway, what I'm trying to do is creating a pulsing effect for a sphere where the pulse itself is able to collide with elements on the screen, so I created another spriteNode (child of the sphere) that I want to scale continuously, without depending on touches.
I read that in these cases is better not to use SKActions.
My question is: how do I scale or increase the size of a spriteNode via update function?
The code represents how I worked so far for movement for example.
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var entities = [GKEntity]()
var graphs = [String: GKGraph]()
var movingSquareLv1 = SKSpriteNode()
var pulsingSphereLv2 = SKSpriteNode()
var lv2SpherePulse = SKSpriteNode()
var xVeloicty: CGFloat = 100
override func sceneDidLoad() {
movingSquareLv1 = self.childNode(withName: "movingSquare") as!SKSpriteNode
movingSquareLv1.physicsBody = SKPhysicsBody(rectangleOf: movingSquareLv1.size)
movingSquareLv1.physicsBody ? .affectedByGravity = false
pulsingSphereLv2 = self.childNode(withName: "pulsingSphere") as!SKSpriteNode
pulsingSphereLv2.physicsBody ? .affectedByGravity = false
lv2SpherePulse = pulsingSphereLv2.childNode(withName: "lv2SpherePulse") as!SKSpriteNode
lv2SpherePulse.physicsBody ? .affectedByGravity = false
}
func touchDown(atPoint pos: CGPoint) {
}
func touchMoved(toPoint pos: CGPoint) {
}
func touchUp(atPoint pos: CGPoint) {
}
override func touchesBegan(_ touches: Set < UITouch > , with event: UIEvent ? ) {
for t in touches {
self.touchDown(atPoint: t.location( in: self))
}
}
override func touchesMoved(_ touches: Set < UITouch > , with event: UIEvent ? ) {
for t in touches {
self.touchMoved(toPoint: t.location( in: self))
}
}
override func touchesEnded(_ touches: Set < UITouch > , with event: UIEvent ? ) {
for t in touches {
self.touchUp(atPoint: t.location( in: self))
}
}
override func touchesCancelled(_ touches: Set < UITouch > , with event: UIEvent ? ) {
for t in touches {
self.touchUp(atPoint: t.location( in: self))
}
}
override func update(_ currentTime: TimeInterval) {
rectMovement()
}
func rectMovement() {
if movingSquareLv1.frame.maxX >= self.size.width / 2 {
xVeloicty = -100
} else if movingSquareLv1.frame.minX <= -self.size.width / 2 {
xVeloicty = 100
}
let rate: CGFloat = 0.5
let relativeVelocity: CGVector = CGVector(dx: xVeloicty - movingSquareLv1.physicsBody!.velocity.dx, dy: 0)
movingSquareLv1.physicsBody!.velocity = CGVector(dx: movingSquareLv1.physicsBody!.velocity.dx + relativeVelocity.dx * rate, dy: 0)
}
}
This is the object composed of two sprites, the one I want to work on is the largest one.

Whoever told you to not use SKActions is wrong. This is exactly when you want to use an SKAction, and if you have multiple circles pulsing, you use the same action on all circles.
You really want to keep your update function as small as possible. Trust in appleā€™s optimizations because they have better access to the API than you do.

I don't know why whatever you read said that actions aren't appropriate, but if you want to change a node's size from update explicitly, then you can set its scaling.
myNode.setScale(2.5) // 2.5x as big as normal
myNode.setScale(0.5) // half size
myNode.xScale = 1.5
myNode.yScale = myNode.xScale * 2 // Set x and y separately for non-uniform scaling
See the documentation in the "Scaling and Rotation" section of SKNode:
https://developer.apple.com/documentation/spritekit/sknode

Related

How to make one line follow touch instead of drawing infinite in spritekit?

I am trying to make one single, straight line follow my finger after I touch a certain sprite object. I have it working so far except instead of one single line being drawn, infinite lines are drawn following the touch...
My Code:
import SpriteKit
import GameplayKit
class WireDrawTest: SKScene{
var drawingLayer: CAShapeLayer!
var redBox = SKSpriteNode()
var redBoxPoint = CGPoint(x: 445, y: 800)
var redBoxTouched:Int = -1
var currentTouch = touchesMoved
func drawLine(from: CGPoint, to: CGPoint) {
let line = SKShapeNode()
let path = CGMutablePath()
path.addLines(between: [from, to])
line.path = path
line.strokeColor = UIColor.yellow
line.lineWidth = 13
addChild(line)
}
override func didMove(to view: SKView) {
redBox = self.childNode(withName: "redBox") as! SKSpriteNode
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print(redBoxTouched)
}
override func touchesMoved(_ 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 == "redBox" {
if redBoxTouched == -1 {
redBoxTouched = 1
}
}
if redBoxTouched == 1 {
drawLine(from: redBoxPoint, to: location)
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print(redBoxTouched)
if redBoxTouched == 1 {
redBoxTouched = -1
}
}
}
This is a screenshot of the current result:
screenshot
TIA!!
Every time you call drawLine, you're creating a new shape node and adding it to the scene. However, you're never removing any of these shape nodes, so the number of lines is increasing as you move your finger.
One fix is to have an optional shape node that remembers the previous line, if any. Then just remove and discard the previous shape node if there is one whenever the touch moves. And when the touch ends, remove the line.
Or you could create one shape node and add it to the scene when the touch begins, change its path as the touch moves, and remove it from the scene when the touch ends. If I recall correctly, the shape node makes a copy of the path internally, so just having a mutable path and changing the path won't update the shape node. But I believe you can make a new path and assign to the shape node's path property to update the node.
I figured out the solution to this with thanks to bg2b settings me on the right track...
First of all instead of 'let line = SKShapeNode' being contained inside of the drawWire function, it needs to be outside of it, in the main scene setup.
From there I just added 'line.removeFromParent' to touchesEnded. Now it works! I don't know why this works but it does! LOL!

How to control sprite using swiping gestures so sprite can move along x axis

At the moment I have a game in which the sprite can be controlled and moved along the x axis (with a fixed y position) using the accelerometer of the device. I wish to change this so the user can control the sprite by dragging on the screen like in popular games such as snake vs.block.
I've already tried using the touches moved method which gives the correct effect, although the sprite first moves to the location of the users touch which I don't want.
Below is the environment I've been using to experiment, the touches moved gives the correct type of control I want although I can't seem to figure out how to stop the sprite first moving to the location of the touch before the swipe/drag
import SpriteKit
import GameplayKit
var player = SKSpriteNode()
var playerColor = UIColor.orange
var background = UIColor.black
var playerSize = CGSize(width: 50, height: 50)
var touchLocation = CGPoint()
var shape = CGPoint()
class GameScene: SKScene {
override func didMove(to view: SKView) {
self.backgroundColor = background
spawnPlayer()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let touchLocation = touch.location(in: self)
player.position.x = touchLocation.x
}
}
func spawnPlayer(){
player = SKSpriteNode(color: playerColor, size: playerSize)
player.position = CGPoint(x:50, y: 500)
self.addChild(player)
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
In summary, I'm looking for the same method of controlling a sprite as in snakes vs blocks
Hey so you have the right idea, you need to store a previous touchesMovedPoint. Then use it to determine how far your finger has moved each time touchesMoved has been called. Then add to the player's current position by that quantity.
var lastTouchLoc:CGPoint = CGPoint()
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let touchLocation = touch.location(in: self)
let dx = touchLocation.x - lastTouchLoc.x
player.position.x += dx // Change the players current position by the distance between the previous touchesMovedPoint and the current one.
lastTouchLoc.x = touchLocation.x // Update last touch location
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
// When releasing the screen from point "E" we don't want dx being calculated from point "E".
// We want dx to calculate from the most recent touchDown point
let initialTouchPoint = touch.location(in: self)
lastTouchLoc.x = initialTouchPoint.x
}
}
You want to use SKAction.move to perform what you want.
func distance(from lhs: CGPoint, to rhs: CGPoint) -> CGFloat {
return hypot(lhs.x.distance(to: rhs.x), lhs.y.distance(to: rhs.y))
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let touchLocation = touch.location(in: self)
let speed = 100/1 //This means 100 points per second
let duration = distance(player.position,touchLocation) / speed //Currently travels 100 points per second, adjust if needed
player.run(SKAction.move(to:touchLocation, duration:duration),forKey:"move")
}
}
What this does is it will move your "snake" to the new location at a certain duration. To determine duration, you need to figure out how fast you want your "snake" to travel. I currently have it set up to where it should take 1 second to move 100 points, but you may want to adjust that as needed.

Get node last position

It's the third time I'm looking for help in this question.
I have a class name BallNode of kind SKShapeNode. Inside my code I also have a function the spawn ball every 3 second from the top side of the screen. Now, I want to set a function that locate the ball position every 1 second, and so, if the ball.position.y > 200 to print a message to the console.
The purpose of this is that if any ball will be at this position (not while it's falling down) the I will call another function. I tried to do it via SKAction, update(_ currentTime: CFTimeInterval), Timer but I didn't succeed and I really have no idea what to do...
update - my current code:
var timeType1: CFTimeInterval = 0.0
var timeType2: CFTimeInterval = 2.0
override func update(_ currentTime: CFTimeInterval) {
if (currentTime - timeType1 > timeType2){
print("time")
self.checkBallsPosition()
self.timeType1 = currentTime
}
self.enumerateChildNodes(withName: "color.BallNode") { node, _ in
self.checkBallsPosition()
}
}
func checkBallsPosition() {
self.enumerateChildNodes(withName: "BALL") { (node: SKNode, nil) in
let x = self.createTopBorder()
x.isHidden = true
let wait2 = SKAction.wait(forDuration: 1)
let action2 = SKAction.run {
let position = Double(node.position.y)
if position < 200 {
}
else if position > 200 {
print("bolbo")
node.removeFromParent()
}
}
self.run(SKAction.sequence([wait2,action2]))
}
}
thats what I try do to so far, as I said the problem is that I want to get the ball last position. because the ball fall down the screen the last position should be when it touch the bottom border of the screen or when it touches another ball. if I set it at update I get the ball position every frame or (as I did) every second - but not the last. another problem is that the ball position can always change depends on another balls (when collision occurs).
update #2 - another functions:
func spawnBalls() {
let wait = SKAction.wait(forDuration: 3)
let action = SKAction.run {
self.createBall()
}
run(SKAction.repeatForever((SKAction.sequence([wait, action]))))
}
func createBall(){
let ball = BallNode(radius: 65)
print(ball.Name)
print(ball._subName!)
ball.position.y = ((frame.size.height) - 200)
let ballXPosition = arc4random_uniform(UInt32(frame.size.width))
ball.position.x = CGFloat(ballXPosition)
ball.physicsBody?.categoryBitMask = PhysicsCategory.ball
ball.physicsBody?.collisionBitMask = PhysicsCategory.ball
ball.physicsBody?.contactTestBitMask = PhysicsCategory.topBorder
ball.delegate = self
addChild(ball)
}
You did not post the entire code you are using. Not knowing how you are spawning your balls, how is the hierarchy and how you are moving them, I can't reproduce it here.
I am not sure if I understand it right, but I believe it might be simpler than what you are doing. See if the code below helps you, I am making the balls fall with SKAction (you can do it with physics also), and checking when they go below zero, when I remove them.
private let ballSpeed : CGFloat = 400
private let ballRadius : CGFloat = 10
private func spawnBall(atPoint pos : CGPoint){
let b = SKShapeNode(circleOfRadius: ballRadius)
b.name = "ball"
b.position = pos
addChild(b)
b.run(SKAction.moveTo(y: -size.height, duration: TimeInterval((pos.y+size.height)/400)))
}
override func update(_ currentTime: TimeInterval) {
super.update(currentTime)
enumerateChildNodes(withName: "ball") { (node, _) in
if node.position.y < 0 {
node.removeFromParent()
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let pos = touch.location(in: self)
spawnBall(atPoint: pos)
}
}

Detect object proximity to walls

I am trying to write something really simple - and use SpriteKit on this instance.
The way I did it on other platforms, is by having an invisible child "stick" sticking out a bit. By detecting collision between the invisible "sticks" I can tell wherether the object is close to the wall or not.
I am trying to replicate the same thing using SpriteKit. Of course, I'd prefer to have an invisible "beam" coming out of the object and giving me distance - but that's probably too much hassle.
I'd appriciate any ways to improve on my silly project I got so far.
My Project so far
Thanks.
Here is what I came up with that doesn't involve physics...
Drag the mouse to move the car, and the label in the center updates telling you the distance to the closest wall. Release mouse to reset car.
Very simple example, can be updated to give more accurate measurements.
class GameScene: SKScene {
let car = SKSpriteNode(color: .blue, size: CGSize(width: 50, height: 100))
let label = SKLabelNode(text: "")
func findNearestWall() -> CGFloat {
// you can make this more advanced by using the CGPoint of the frame borders of the car, or even 6+ points for more accuracy
let closestX: CGFloat = {
if car.position.x < 0 { // left wall
return abs(frame.minX - car.position.x)
} else { // right wall
return abs(frame.maxX - car.position.x)
}
}()
let closestY: CGFloat = {
if car.position.y < 0 { // bottom wall
return abs(frame.minY - car.position.y)
} else { // top wall
return abs(frame.maxY - car.position.y)
}
}()
if closestX < closestY {
return closestX.rounded() // as closest wall distance
} else {
return closestY.rounded() // as closest wall distance
}
}
override func didMove(to view: SKView) {
removeAllChildren()
label.fontSize *= 2
addChild(car)
addChild(label)
}
override func mouseDown(with event: NSEvent) {
}
override func mouseDragged(with event: NSEvent) {
let location = event.location(in: self)
car.position = location
}
override func mouseUp(with event: NSEvent) {
car.position = CGPoint.zero
}
override func didEvaluateActions() {
label.text = String(describing: findNearestWall())
}
}

(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)
}
}