Swift: Agar.io-like smooth SKCameraNode movement? - ios

If anyone here has played Agar.io before, you'll probably be familiar with the way the camera moves. It slides toward your mouse depending on the distance your mouse is from the center of the screen. I'm trying to recreate this movement using an SKCameraNode and touchesMoved:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let pos = touches.first!.location(in: view)
let center = view!.center
let xDist = pos.x - center.x
let yDist = pos.y - center.y
let distance = sqrt((xDist * xDist) + (yDist * yDist)) * 0.02
camera?.position.x += xDist * distance * 0.02
camera?.position.y -= yDist * distance * 0.02
}
Surprisingly, this doesn't look too bad. However, there are two problems with this.
The first is that I want to use a UIPanGestureRecognizer for this, since it'd work far better with multiple touches. I just can't imagine how this could be done, since I can't think of a way to use it to get the distance from the touch to the center of the screen.
The second problem is that this movement isn't smooth. If the user stops moving their finger for a single frame, there is a huge jump since the camera snaps to an absolute halt. I'm awful at maths, so I can't think of a way to implement a nice smooth decay (or attack).
Any help would be fantastic!
EDIT: I'm now doing this:
private var difference = CGVector()
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let pos = touches.first!.location(in: view)
let center = view!.center
difference = CGVector(dx: pos.x - center.x, dy: pos.y - center.y)
}
override func update(_ currentTime: TimeInterval) {
let distance = sqrt((difference.dx * difference.dx) + (difference.dy * difference.dy)) * 0.02
camera?.position.x += difference.dx * distance * 0.02
camera?.position.y -= difference.dy * distance * 0.02
}
All I need to know now is a good way to get the difference variable to smoothly increase from the time the user touches the screen.
EDIT 2: Now I'm doing this:
private var difference = CGVector()
private var touching: Bool = false
private var fade: CGFloat = 0
private func touched(_ touch: UITouch) {
let pos = touch.location(in: view)
let center = view!.center
difference = CGVector(dx: pos.x - center.x, dy: pos.y - center.y)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
touching = true
touched(touches.first!)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
touched(touches.first!)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
touching = false
touched(touches.first!)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
touching = false
}
override func update(_ currentTime: TimeInterval) {
if touching {
if fade < 0.02 { fade += 0.0005 }
} else {
if fade > 0 { fade -= 0.0005 }
}
let distance = sqrt((difference.dx * difference.dx) + (difference.dy * difference.dy)) * 0.01
camera?.position.x += difference.dx * distance * fade
camera?.position.y -= difference.dy * distance * fade
}
Can someone please help me before it gets any worse? The fade variable is incremented in an awful way and it's not smooth, and I just need someone to give me a slight hint of a better way to do this.

Try Linear Interpolation. Linear interpolation can make it so your object will slowly and smoothly speed up or slow down over time.

Related

SpriteKit: calculate angle of joystick and change sprite based on that

I am making a RPG Birds-eye style game with SpriteKit. I made a joystick because a D-Pad does not give the player enough control over his character.
I cannot wrap my brain around how I would calculate the neccessary data needed to change the Sprite of my character based on the angle of the joystick thumb Node.
Here is my code I am using
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
if isTracking == false && base.contains(location) {
isTracking = true
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location: CGPoint = touch.location(in: self)
if isTracking == true {
let v = CGVector(dx: location.x - base.position.x, dy: location.y - DPad.position.y)
let angle = atan2(v.dy, v.dx)
let deg = angle * CGFloat(180 / Double.pi)
let Length:CGFloat = base.frame.size.height / 2
let xDist: CGFloat = sin(angle - 1.57079633) * Length
let yDist: CGFloat = cos(angle - 1.57079633) * Length
print(xDist,yDist)
xJoystickDelta = location.x * base.position.x / CGFloat(Double.pi)
yJoystickDelta = location.y * base.position.y / CGFloat(Double.pi)
if base.contains(location) {
thumbNode.position = location
} else {
thumbNode.position = CGPoint(x: base.position.x - xDist, y: base.position.y + yDist)
}
}
}
}
Update method
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if xJoystickDelta > 0 && yJoystickDelta < 0 {
print("forward")
}
}
The way I have set up right now tests the positive or negative state of the Joystick position in a cross method based on where the thumb Node is inside of the four marked sections below
I dont want it to do that
How can I set it up so that it changes the sprite based on where the thumb node is actually pointing inside my joysticks base like so.
I have been struggling with this for 3 days now so any help would be appreciated.
That looks far too complicated. Just compare the x and y components
of the difference vector v. Something like this:
if v.dx > abs(v.dy) {
// right
} else if v.dx < -abs(v.dy) {
// left
} else if v.dy < 0 {
// up
} else if v.dy > 0 {
// down
}

Swift 3 Generate evenly-spaced SKSpriteNodes along path drawn by user

everyone! First of all, I'm aware that this question is very similar to Draw images evenly spaced along a path in iOS. However, that is in Objective-C (which I can't read) and it is in a normal ViewController working with CGImageRefs. I need it in swift and using SKSpriteNodes (not CGImageRefs). Here's my issue:
I'm trying to make a program that lets the user draw a simple shape (like a circle) and places SKSpriteNodes at fixed intervals along the path drawn by the user. I've got it working fine at a slow pace, but if the user draws too quickly then the nodes get placed too far apart. Here's an example of when I draw it slowly:
User-drawn path with nodes placed approximately 60 pixels apart from each other. Blue is the start node, purple is the end node.
The goal is that each node would have a physicsBody that kept entities from crossing the line drawn by the user (those entities wouldn't be able to squeeze in between evenly spaced nodes). If the user draws too fast, however, there will be a gap in defenses that I can't fix. For example:
Note the visibly larger gap between the 7th and 8th nodes. This occurred because I drew too quickly. Many people have questions that are slightly similar but are unhelpful for my task (e.g. place a specific amount of nodes evenly spaced along a path, rather than place as many nodes as neccessary to get them 60 pixels apart along the path).
In conclusion, here is my main question again: How can I place nodes perfectly spaced along a user-drawn path of any shape? Thank you in advance for your help! Here is my GameScene.swift file:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
let minDist: CGFloat = 60 //The minimum distance between one point and the next
var points: [CGPoint] = []
var circleNodes: [SKShapeNode] = []
override func didMove(to view: SKView) {
}
func getDistance (fromPoint: CGPoint, toPoint: CGPoint) -> CGFloat {
let deltaX = fromPoint.x - toPoint.x
let deltaY = fromPoint.y - toPoint.y
let deltaXSquared = deltaX*deltaX
let deltaYSquared = deltaY*deltaY
return sqrt(deltaXSquared + deltaYSquared) //Return the distance
}
func touchDown(atPoint pos : CGPoint) {
self.removeAllChildren()
//The first time the user touches, we need to place a point and mark that as the firstCircleNode
print(pos)
points.append(pos)
//allPoints.append(pos)
let firstCircleNode = SKShapeNode(circleOfRadius: 5.0)
firstCircleNode.fillColor = UIColor.blue
firstCircleNode.strokeColor = UIColor.blue
firstCircleNode.position = pos
circleNodes.append(firstCircleNode)
self.addChild(firstCircleNode)
}
func touchMoved(toPoint pos : CGPoint) {
let lastIndex = points.count - 1 //The index of the last recorded point
let distance = getDistance(fromPoint: points[lastIndex], toPoint: pos)
//vector_distance(vector_double2(Double(points[lastIndex].x), Double(points[lastIndex].y)), vector_double2(Double(pos.x), Double(pos.y))) //The distance between the user's finger and the last placed circleNode
if distance >= minDist {
points.append(pos)
//Add a box to that point
let newCircleNode = SKShapeNode(circleOfRadius: 5.0)
newCircleNode.fillColor = UIColor.red
newCircleNode.strokeColor = UIColor.red
newCircleNode.position = pos
circleNodes.append(newCircleNode)
self.addChild(newCircleNode)
}
}
func touchUp(atPoint pos : CGPoint) {
//When the user has finished drawing a circle:
circleNodes[circleNodes.count-1].fillColor = UIColor.purple //Make the last node purple
circleNodes[circleNodes.count-1].strokeColor = UIColor.purple
//Calculate the distance between the first placed node and the last placed node:
let distance = getDistance(fromPoint: points[0], toPoint: points[points.count-1])
//vector_distance(vector_double2(Double(points[0].x), Double(points[0].y)), vector_double2(Double(points[points.count - 1].x), Double(points[points.count - 1].y)))
if distance <= minDist { //If the distance is closer than the minimum distance
print("Successful circle")
} else { //If the distance is too far
print("Failed circle")
}
points = []
circleNodes = []
}
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) {
// Called before each frame is rendered
}
}
You could try resizing the vector:
func touchMoved(toPoint pos : CGPoint) {
let lastIndex = points.count - 1 //The index of the last recorded point
let distance = getDistance(fromPoint: points[lastIndex], toPoint: pos)
if distance >= minDist {
// find a new "pos" which is EXACTLY minDist distant
let vx = pos.x - points[lastIndex].x
let vy = pos.y - points[lastIndex].y
vx /= distance
vy /= distance
vx *= minDist
vy *= minDist
let newpos = CGPoint(x: vx, y:vy)
points.append(newpos)
//Add a box to that point
let newCircleNode = SKShapeNode(circleOfRadius: 5.0)
newCircleNode.fillColor = UIColor.red
newCircleNode.strokeColor = UIColor.red
newCircleNode.position = newpos // NOTE
circleNodes.append(newCircleNode)
self.addChild(newCircleNode)
}
}
It probably won't be perfect but might look better.
I've figured it out! I was inspired by Christian Cerri's suggestion so I used the following code to make what I wanted:
import SpriteKit
import GameplayKit
// MARK: - GameScene
class GameScene: SKScene {
// MARK: - Allows me to work with vectors. Derived from https://www.raywenderlich.com/145318/spritekit-swift-3-tutorial-beginners
func subtract(point: CGPoint, fromPoint: CGPoint) -> CGPoint {
return CGPoint(x: point.x - fromPoint.x, y: point.y - fromPoint.y) //Returns a the first vector minus the second
}
func add(point: CGPoint, toPoint: CGPoint) -> CGPoint {
return CGPoint(x: point.x + toPoint.x, y: point.y + toPoint.y) //Returns a the first vector minus the second
}
func multiply(point: CGPoint, by scalar: CGFloat) -> CGPoint {
return CGPoint(x: point.x * scalar, y: point.y * scalar)
}
func divide(point: CGPoint, by scalar: CGFloat) -> CGPoint {
return CGPoint(x: point.x / scalar, y: point.y / scalar)
}
func magnitude(point: CGPoint) -> CGFloat {
return sqrt(point.x*point.x + point.y*point.y)
}
func normalize(aPoint: CGPoint) -> CGPoint {
return divide(point: aPoint, by: magnitude(point: aPoint))
}
// MARK: - Properties
let minDist: CGFloat = 60
var userPath: [CGPoint] = [] //Holds the coordinates collected when the user drags their finger accross the screen
override func didMove(to view: SKView) {
}
// MARK: - Helper methods
func getDistance (fromPoint: CGPoint, toPoint: CGPoint) -> CGFloat
{
let deltaX = fromPoint.x - toPoint.x
let deltaY = fromPoint.y - toPoint.y
let deltaXSquared = deltaX*deltaX
let deltaYSquared = deltaY*deltaY
return sqrt(deltaXSquared + deltaYSquared) //Return the distance
}
func touchDown(atPoint pos : CGPoint) {
userPath = []
self.removeAllChildren()
//Get the first point the user makes
userPath.append(pos)
}
func touchMoved(toPoint pos : CGPoint) {
//Get every point the user makes as they drag their finger across the screen
userPath.append(pos)
}
func touchUp(atPoint pos : CGPoint) {
//Get the last position the user was left touching when they've completed the motion
userPath.append(pos)
//Print the entire path:
print(userPath)
print(userPath.count)
plotNodesAlongPath()
}
/**
Puts nodes equidistance from each other along the path that the user placed
*/
func plotNodesAlongPath() {
//Start at the first point
var currentPoint = userPath[0]
var circleNodePoints = [currentPoint] //Holds the points that I will then use to generate circle nodes
for i in 1..<userPath.count {
let distance = getDistance(fromPoint: currentPoint, toPoint: userPath[i]) //The distance between the point and the next
if distance >= minDist { //If userPath[i] is at least minDist pixels away
//Then we can make a vector that points from currentPoint to userPath[i]
var newNodePoint = subtract(point: userPath[i], fromPoint: currentPoint)
newNodePoint = normalize(aPoint: newNodePoint) //Normalize the vector so that we have only the direction and a magnitude of 1
newNodePoint = multiply(point: newNodePoint, by: minDist) //Stretch the vector to a length of minDist so that we now have a point for the next node to be drawn on
newNodePoint = add(point: currentPoint, toPoint: newNodePoint) //Now add the vector to the currentPoint so that we get a point in the correct position
currentPoint = newNodePoint //Update the current point. Next we want to draw a point minDist away from the new current point
circleNodePoints.append(newNodePoint) //Add the new node
}
//If distance was less than minDist, then we want to move on to the next point in line
}
generateNodesFromPoints(positions: circleNodePoints)
}
func generateNodesFromPoints(positions: [CGPoint]) {
print("generateNodesFromPoints")
for pos in positions {
let firstCircleNode = SKShapeNode(circleOfRadius: 5.0)
firstCircleNode.fillColor = UIColor.blue
firstCircleNode.strokeColor = UIColor.blue
firstCircleNode.position = pos //Put the node in the correct position
self.addChild(firstCircleNode)
}
}
// MARK: - Touch responders
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) {
// Called before each frame is rendered
}
}
And this results in the following:
No matter how quickly the user moves their finger, it places nodes evenly along their path. Thanks so much for your help, and I hope it helps more people in the future!

How do I limit the dragging area for image views

My first post and I am currently making an app in Xcode 8.1 using Swift 3
I have 9 images that I have made draggable with touchesBegan and touchesMoved functions.
However they are able to be dragged ANYWHERE on the screen and this can cause them to cover up other images I have. I would like to limit their movement by setting a boundary for them so that even when the user tries to drag the images out of that boundary they wont be able to.
I have created this code in draggedimageview.swift this allows the Image views to be dragged.
I have been spending a long time trying to figure out how to do this and if anyone can help I would appreciate it.
Thanks...
import UIKit
class DraggedImageView: UIImageView {
var startLocation: CGPoint?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
startLocation = touches.first?.location(in: self)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let currentLocation = touches.first?.location(in: self)
let dx = currentLocation!.x - startLocation!.x
let dy = currentLocation!.y - startLocation!.y
self.center = CGPoint(x: self.center.x+dx, y: self.center.y+dy)
}
}
You can do this:
let cx = self.center.x+dx
if (cx > 100) {
cx = 100
}
self.center = CGPoint(x: cx, y: self.center.y+dy)
But alter the if based on what you are trying to do. This clamps it so that it cannot be moved to a position where center.x > 100
Try to define your "allowed area" in a rect, such as:
import UIKit
class DraggedImageView: UIImageView {
var startLocation: CGPoint?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
startLocation = touches.first?.location(in: self)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let currentLocation = touches.first?.location(in: self)
let dx = currentLocation!.x - startLocation!.x
let dy = currentLocation!.y - startLocation!.y
// This is the area in which the dragging is allowed
let coolArea = CGRect(x: 0, y: 0, width: 100, height: 100)
let newCenter = CGPoint(x: self.center.x+dx, y: self.center.y+dy)
// If the allowed area contains the new point, we can assign it
if coolArea.contains(newCenter) {
self.center = newCenter
}
// else {
// print("Out of boundaries!")
// }
self.center = CGPoint(x: self.center.x+dx, y: self.center.y+dy)
}
}
You may want to change the code if you want something different to happen when user is dragging out of the bounds.
Taking the "allowed area" from the view that contains the UIImageView
import UIKit
class DraggedImageView: UIImageView {
var startLocation: CGPoint?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
startLocation = touches.first?.location(in: self)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let currentLocation = touches.first?.location(in: self)
let dx = currentLocation!.x - startLocation!.x
let dy = currentLocation!.y - startLocation!.y
let coolArea = (self.superview?.bounds)!
let newCenter = CGPoint(x: self.center.x+dx, y: self.center.y+dy)
// If the allowed area contains the new point, we can assign it
if coolArea.contains(newCenter) {
self.center = newCenter
print("touchesMoved")
}
else {
print("Out of boundaries!")
}
}
}

Creating a UIView with PanGestureRecognizer and activating it without lifting the finger

I've created a circle on the screen by creating a UIView at the finger's location when touchesBegan():
In ViewController:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let pos = touch.locationInView(view)
let radius: CGFloat = touch.majorRadius * 2
let circleView = CircleView(frame: CGRectMake(pos.x - radius / 2, pos.y - radius / 2, radius, radius))
view.addSubview(circleView)
}
}
In CircleView:
override init(frame: CGRect) {
super.init(frame: frame)
let recognizer = UIPanGestureRecognizer(target: self, action: Selector("handlePan:"))
recognizer.delegate = self
self.addGestureRecognizer(recognizer)
}
This creates the circle, but does not move it immediately when I move my finger. Instead, I have to pick my finger up and place it back on the circle before handlePan() kicks in.
Is there a way to start tracking a pan gesture without lifting the finger that spawned it's parent view, taking into account there may be multiple fingers touching and moving around the screen?
The issue here is that you're using both touchesBegan, and a UIPanGestureRecognizer. For best results, use one or the other. If you're going to do it with just a pan gesture (which is what I would do), do the following:
func handlePan(gesture: UIPanGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.Began {
//handle creating circle
} else if gesture.state == UIGestureRecognizerState.Changed {
//handle movement
}
}
Hope this helps!
If you have already created the circle view successfully in method touchesBegan:withEvent:
you can
Make that circle view a property
Move that circle view directly in method touchesMoved:withEvent:
This won't require you pick your finger up first
I was able to accomplish multiple independent touches by keeping a dictionary of all active touches and creating the circle views in touchesBegan() and updating their position in touchesMoved() by querying that dictionary.
var fingerTouches = [UITouch: CircleView]()
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let pos = touch.locationInView(view)
let radius: CGFloat = touch.majorRadius * 2
let circle = CircleView(frame: CGRectMake(pos.x - radius / 2, pos.y - radius / 2, radius, radius))
view.addSubview(circle)
fingerTouches[touch] = circle
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let pos = touch.locationInView(view)
let radius: CGFloat = touch.majorRadius * 2
fingerTouches[touch]!.frame = CGRectMake(pos.x - radius / 2, pos.y - radius / 2, radius, radius)
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
fingerTouches[touch]!.removeFromSuperview()
}
}

Node rotation doesn't follow a finger

I'm trying to rotate an arrow to follow a finger movement but it performs weirdly. It is definitely not following it. I'm trying to do it in touchesMoved. I tried to do this:
var fingerLocation = CGPoint()
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch: AnyObject in touches {
fingerLocation = touch.locationInNode(self)
let currentOrient = arrow.position
let angle = atan2(currentOrient.y - fingerLocation.y, currentOrient.x - fingerLocation.y)
let rotateAction = SKAction.rotateToAngle(angle + CGFloat(M_PI*0.5), duration: 0.0)
arrow.runAction(rotateAction)
}
}
And also tried this:
var fingerLocation = CGPoint()
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch: AnyObject in touches {
fingerLocation = touch.locationInNode(self)
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
var radians = atan2(fingerLocation.x, fingerLocation.y)
arrow.zRotation = -radians
}
I also tried SKConstraint.orientToPoint but had no luck in it either. What am I doing wrong? Every answer to similar question is suggestion atan2, but it doesn't seem to work for me.
If you want to rotate the sprite towards to touch location, it should be simple as :
let touchLocation = touch.locationInNode(self)
var dx = hero.position.x - positionInScene.x;
var dy = hero.position.y - positionInScene.y ;
var angle = atan2(dy,dx) + CGFloat(M_PI_2)
hero.zRotation = angle
It worked when I tried, so it can give you an basic idea where to start. Or I misunderstood what you are trying to achieve...
EDIT:
Currently what you will get if you try to convert angle to degrees is angle in range from -90 to 270 degrees. Its described here why. If you want to work with angle in range of 0 to 360, you can change to code above to:
var dx = missile.position.x - positionInScene.x ;
var dy = missile.position.y - positionInScene.y;
var angleInRadians = atan2(dy,dx) + CGFloat(M_PI_2)
if(angleInRadians < 0){
angleInRadians = angleInRadians + 2 * CGFloat(M_PI)
}
missile.zRotation = angleInRadians
var degrees = angleInRadians < 0 ? angleInRadians * 57.29577951 + 360 : angleInRadians * 57.29577951
Here is the result with debugging data:

Resources