Preventing draggable view to go off screen - ios

I've got a button that is draggable and I don't want the user to be able to drag the view so the sides get cut off by the edges of the screen.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: self.superview)
self.center = CGPoint(x: position.x, y: position.y)
}
}
The way I'm doing it is a huge train of if / else statements checking against allowed areas on the screen.
let halfOfButton = CGFloat(85 / 2)
let heightOfTopBar = CGFloat(55) + halfOfButton
let widthOfScreen = CGFloat((self.superview?.frame.width)!) - halfOfButton
let heightOfScreen = CGFloat((self.superview?.frame.height)!) - heightOfTopBar
let heightOfBotBar = CGFloat((self.superview?.frame.height)! - heightOfTopBar)
if position.x > halfOfButton && position.x < widthOfScreen {
self.center = CGPoint(x: position.x, y: position.y)
} else {
if position.x < halfOfButton {
self.center = CGPoint(x: halfOfButton, y: position.y)
} else {
self.center = CGPoint(x: widthOfScreen, y: position.y)
}
}
if position.y > heightOfTopBar && position.y < heightOfScreen && position.x > halfOfButton && position.x < widthOfScreen {
self.center = CGPoint(x: position.x, y: position.y)
} else {
if position.y < heightOfTopBar {
self.center = CGPoint(x: position.x, y: heightOfTopBar)
} else {
self.center = CGPoint(x: position.x, y: heightOfScreen)
}
}
}
Is there a better way to accomplish this though? That code above doesn't even fully work yet. It has a lot of flaws in the logic where I'm saying "if the Y is okay, then just allow the center of your touch to be the center of the button" but then that overrides the code I'm writing in the X where even though the Y might be okay, the X isn't.
I can figure this out logic'ing it out this way, but surely there's a way better way of doing this?

Try something like this:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: self.superview)
var x:CGFloat = position.x
var y:CGFloat = position.y
let parentWidth = button.superView!.frame.size.width
let parentHeight = button.superView!.frame.size.height
let width = button.frame.size.width / 2
let height = button.frame.size.height / 2
x = max(width, x)
x = min(x, parentWidth - width)
y = max(height, y)
y = min(y, parentHeight - height)
self.center = CGPoint(x: x, y: y)
}
}

I dealt with this issue awhile back and solved it with this approach.
First, to account for the size of the button, and not wanting to clip parts of it, I create a view (for this example, let's call it containerView) that is placed over the area you want the button to appear in. This view needs to have its margins reduced from the superview such that its top & bottom margins are half of the button's height, and it's leading & trailing edges are half of the button's width.
With that set up, it is now fairly trivial to contain its movement with a built-in function that offers better performance without the if statements.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let touch = touches.first else {
return
}
let location = touch.location(in: containerView)
if containerView.point(inside: location, with: event) {
button.center = location
}
}
Now, depending on the coordinate systems you are using, it is possible that the button will not be properly positioned where you want it with this technique. If that does happen, you should be able to resolve it by modifying the location value prior to assigning it to the button.center property.
The modification should be to shift it right or down based on your leading & top margins. Ideally, you would use storyboard constraints for this, which you can then link to your controller, allowing you to directly get these values for the adjustment.

Related

Making a "Joy stick" swift

What I have been trying to do is create a "Joy stick" that moves a player around. Here is what I have so far:
import UIKit
import SpriteKit
import SceneKit
class GameViewController: UIViewController, SCNSceneRendererDelegate {
var isTracking = false
var firstTrackingLocation = CGPoint.zero
var trackingVelocity = CGPoint.zero
var trackingDistance : CGFloat = 0.0
var previousTime : NSTimeInterval = 0.0
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene(named: "art.scnassets/level.scn")!
let scnView = self.view as! SCNView
scnView.delegate = self
scnView.scene = scene
scnView.showsStatistics = true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if isTracking == false {
for touch in touches {
isTracking = true
let location = touch.locationInView(self.view)
firstTrackingLocation = location
}
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if isTracking {
trackingVelocity = touches.first!.locationInView(self.view)
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
isTracking = false
trackingVelocity = CGPoint.zero
}
func renderer(renderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) {
if isTracking == true {
let scnView = self.view as! SCNView
let character = scnView.scene!.rootNode.childNodeWithName("person", recursively: true)
let deltaTime = time - previousTime
let pointsPerSecond: CGFloat = 1.0 * CGFloat(deltaTime)
var xResult:CGFloat = 0.0
var yResult:CGFloat = 0.0
let point = firstTrackingLocation
let endPoint = trackingVelocity
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
if direction.x > direction.y {
let movePerSecond = pointsPerSecond/direction.x
xResult = direction.x*movePerSecond
yResult = direction.y*movePerSecond
} else {
let movePerSecond = pointsPerSecond/direction.y
xResult = direction.x*movePerSecond
yResult = direction.y*movePerSecond
}
character!.position = SCNVector3(CGFloat(character!.position.x) + (xResult), CGFloat(character!.position.y), CGFloat(character!.position.z) + (yResult))
let camera = scnView.scene?.rootNode.childNodeWithName("camera", recursively: true)
camera?.position = SCNVector3(CGFloat(camera!.position.x) + (xResult), CGFloat(camera!.position.y), CGFloat(camera!.position.z) + (yResult))
}
previousTime = time
}
override func shouldAutorotate() -> Bool {
return true
}
override func prefersStatusBarHidden() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Landscape
}
}
Now this works except if you drag your finger to the other side of the phone the character moves 10 times faster then it would if you barely moved your finger. So what I would like to have, is a Joy stick that moves the character the same speed if you drag a little bit or to the other side of the screen. And I would also like if you changed direction at the other side of the screen that the character would move the other way. So, my guess is that there needs to be a lastPoint saved then when the touchesMoved gets called that somehow we calculate a direction from lastPoint to the currentPoint and then move the character in renderer. I understand that most of this code is probably rubbish, but thanks in advance.
Your joystick should be a value from 0 to 1, you need to determine the radius of your joystick, then calculate distance of (point touched to center of control) and the angle of the control with arc tan.
Now we need to ensure we never go past maxRadius, so if our distance is > maxRadius, we just set it to max radius, then we divide this value by our maxRadius to get out distance ratio.
Then we just take the cos and sin of our angle, and multiply it by our distance ratio, and get the x and y ratio values. (Should be between 0 and 1)
Finally, take this x and y value, and multiply it to the speed at which your object should be moving at.
let maxRadius = 100 //Your allowable radius
let xDist = (p2.x - p1.x)
let yDist = (p2.y - p1.y)
let distance = (sqrt((xDist * xDist) + (yDist * yDist))
let angle = atan2(yDist , xDist )
let controlDistanceRatio = (distance > maxRadius) ? 1 : distance / maxRadius
let controllerX = cos(angle) * controlDistanceRatio
let controllerY = sin(angle) * controlDistanceRatio
This seems like a good case to use a custom UIGestureRecognizer. See Apple API Reference.
In this particular case you would create a continuous gesture. The resultant CGVector would be calculated from the center (origin) point of your joystick on screen. The recognizer would fail if the joystick node isn't selected and end if unselected (deselected). The resultant CGVector will be updated while the gesture's state is moved.
Now the tricky part to figure out would be moving the node image in such a way that allows the user to have the feeling of a joystick. For this you may need to update the node texture and make slight adjustments to the node position to give the appearance of moving a node around.
See if this helps you: Single Rotation Gesture Recognizer
Let me know if this points you in the right direction.
Your stated problem is the character moves at a variable rate depending on the how far from the start point the user drags their finger. The crucial point in the code seems to be this
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
The difference between endPoint and point is variable and so you are getting a variable magnitude in your direction. To simplify, you could just put in a constant value like
let direction = CGPoint(x: 10, y: 10)
That gets the character moving at a constant speed when the user presses the joystick, but the character is always moving the same direction.
So somehow you've got to bracket in the variable directional values. The first thing that comes to mind is using min and max
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
direction.x = min(direction.x, 10)
direction.x = max(direction.x, -10)
direction.y = min(direction.y, 10)
direction.y = max(direction.y, -10)
That seems like it would keep the magnitude of the direction values between -10 and 10. That doesn't make the speed completely constant, and it allows faster travel along diagonal lines than travel parallel to the x or y axis, but maybe it is closer to what you want.

How do I change the texture of my node when Im moving the joystick to the left or right?

I have this node that is controlled by a joystick and when I move the joystick to the left I would like to change the image of the node and the same thing when I move the joystick to the right. How would I do that? Here is the code I have:
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
if stickActive == true {
//EDIT Code to change texture when moving joystick to left or right.
if joyStick.position.x < base.position.x {
plane.texture = SKTexture(imageNamed: "planeleft")
} else {
plane.texture = SKTexture(imageNamed: "planeright")
}
plane.removeActionForKey("stopaction")
plane.physicsBody?.affectedByGravity = false
xJoystickDelta = location.x - base.position.x
yJoystickDelta = location.y - base.position.y
let v = CGVector(dx: location.x - base.position.x, dy: location.y - base.position.x)
let angle = atan2(v.dy, v.dx)
let length: CGFloat = base.frame.size.height / 2
let xDist: CGFloat = sin(angle - 1.57079633) * length
let yDist: CGFloat = cos(angle - 1.57079633) * length
if (CGRectContainsPoint(base.frame, location)) {
joyStick.position = location
}else {
base.alpha = 0.5 //sets the opacity to 0.5 when the joystick is touched
joyStick.position = CGPointMake(base.position.x - xDist, base.position.y + yDist)
}
}
}
}
override func update(currentTime: CFTimeInterval) {
let xScale = 0.08
let yScale = 0.08
let xAdd = self.xJoystickDelta * CGFloat(xScale)
let yAdd = self.yJoystickDelta * CGFloat(yScale)
self.plane.position.x += xAdd
self.plane.position.y += yAdd
}
I'm not familiar with this, but I know the logic you should use for this.
Complicated:
To detect if the joystick is on the left, you should use a range of the furthest left the joystick can go to the centre of the joystick when it's in the middle. The right side would be from the centre to the furthest right it can go.
Simple:
For the left, make it when the X value if the joystick is less than the default centre position. The right would be an X position greater than the middle.
These would be if statements inside the update function so it's constantly checking for this. In the statements would be something like this:
plane.texture = SKTexture(imageNamed: "TextureName")
Note: This all assumes that the joystick is in a fixed position and has a set centre
Hope this helps!

Detect the degrees in a circle with users touch

Im looking to create a swift function to return the degrees (Float) of the users touch on an image (SKSpritenode). In touchesBegan, I know how to detect the x & y positions of my image. The idea is to create a function that takes in these positions and returns the degrees.
Amended - The following code now works:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
self.anchorPoint = CGPointMake(0.5, 0.5)
myNode.position = CGPointMake(0, -myNode.frame.height / 2)
self.addChild(myNode)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
let location = touch.locationInNode(self)
if myNode.containsPoint(location) {
print("tapped!")
let origin = myNode.position
let touch = touch.locationInNode(myNode.parent!)
let diffX = touch.x - origin.x
let diffY = touch.y - origin.y
let radians = atan2(diffY, diffX)
let degrees = radians * CGFloat(180 / M_PI)
print("degrees = \(degrees)")
}
}
}
You need to compare the user's touch position to an origin point, which might be the centre of your sprite node for example. Here's some code to get you started:
let origin = CGPoint(x: 0, y: 0)
let touch = CGPoint(x: 100, y: 100)
let diffX = touch.x - origin.x
let diffY = touch.y - origin.y
let radians = atan2(diffY, diffX)
let degrees = radians * CGFloat(180 / M_PI)
That last value – degrees – is the one you want if you want to show users information. If you want to do more calculations, you should probably stick with radians.

Draw a grid with SpriteKit

What would be the best way to draw a grid like this by using the SpriteKit 2D game engine?
Requirements:
Input programatically the number of columns and rows (5x5, 10x3, 3x4 etc.).
Draw it programmatically using something like SKSpriteNode or SKShapeNode, since just using images of a square like this doesn't seem very efficient to me.
The squares should have a fixed size (let's say each is 40x40).
The grid should be vertically and horizontally centred in the view.
I'm planning to use a SKSpriteNode (from an image) as a player moving in different squares in this grid.
So, I'll save in a 2 dimensional array the central point (x,y) of each square and then move from the player's current position to that position. If you have a better suggestion for this too, I'd like to hear it.
I would appreciate a solution in Swift (preferably 2.1), but Objective-C would do too. Planning on using this only on iPhone devices.
My question is close to this one. Any help is appreciated.
I suggest you implement the grid as a texture of an SKSpriteNode because Sprite Kit will renders the grid in a single draw call. Here's a example of how to do that:
class Grid:SKSpriteNode {
var rows:Int!
var cols:Int!
var blockSize:CGFloat!
convenience init?(blockSize:CGFloat,rows:Int,cols:Int) {
guard let texture = Grid.gridTexture(blockSize: blockSize,rows: rows, cols:cols) else {
return nil
}
self.init(texture: texture, color:SKColor.clear, size: texture.size())
self.blockSize = blockSize
self.rows = rows
self.cols = cols
}
class func gridTexture(blockSize:CGFloat,rows:Int,cols:Int) -> SKTexture? {
// Add 1 to the height and width to ensure the borders are within the sprite
let size = CGSize(width: CGFloat(cols)*blockSize+1.0, height: CGFloat(rows)*blockSize+1.0)
UIGraphicsBeginImageContext(size)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
let bezierPath = UIBezierPath()
let offset:CGFloat = 0.5
// Draw vertical lines
for i in 0...cols {
let x = CGFloat(i)*blockSize + offset
bezierPath.move(to: CGPoint(x: x, y: 0))
bezierPath.addLine(to: CGPoint(x: x, y: size.height))
}
// Draw horizontal lines
for i in 0...rows {
let y = CGFloat(i)*blockSize + offset
bezierPath.move(to: CGPoint(x: 0, y: y))
bezierPath.addLine(to: CGPoint(x: size.width, y: y))
}
SKColor.white.setStroke()
bezierPath.lineWidth = 1.0
bezierPath.stroke()
context.addPath(bezierPath.cgPath)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return SKTexture(image: image!)
}
func gridPosition(row:Int, col:Int) -> CGPoint {
let offset = blockSize / 2.0 + 0.5
let x = CGFloat(col) * blockSize - (blockSize * CGFloat(cols)) / 2.0 + offset
let y = CGFloat(rows - row - 1) * blockSize - (blockSize * CGFloat(rows)) / 2.0 + offset
return CGPoint(x:x, y:y)
}
}
And here's how to create a grid and add a game piece to the grid
class GameScene: SKScene {
override func didMove(to: SKView) {
if let grid = Grid(blockSize: 40.0, rows:5, cols:5) {
grid.position = CGPoint (x:frame.midX, y:frame.midY)
addChild(grid)
let gamePiece = SKSpriteNode(imageNamed: "Spaceship")
gamePiece.setScale(0.0625)
gamePiece.position = grid.gridPosition(row: 1, col: 0)
grid.addChild(gamePiece)
}
}
}
Update:
To determine which grid square was touched, add this to init
self.isUserInteractionEnabled = true
and this to the Grid class:
override func touchesBegan(_ touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let position = touch.location(in:self)
let node = atPoint(position)
if node != self {
let action = SKAction.rotate(by:CGFloat.pi*2, duration: 1)
node.run(action)
}
else {
let x = size.width / 2 + position.x
let y = size.height / 2 - position.y
let row = Int(floor(x / blockSize))
let col = Int(floor(y / blockSize))
print("\(row) \(col)")
}
}
}

(SpriteKit + Swift 2) - Remove sprite when user tapped on it

I am trying to make a simple bubble tapped game. I have a created a SKSpriteKid node in my code. The problem is I want to sprite to disappear when user tap on it. I can't seem to removeparent() the node. How do I tackle this?
I have created a sprite with bubble1 as the name.
func bubblePressed(bubble1: SKSpriteNode) {
print("Tapped")
bubble1.removeFromParent()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let touchLocation = touch.locationInNode(self)
let touchedNode = self.nodeAtPoint(touchLocation)
if (touchedNode.name == "bubble1") {
touchedNode.removeFromParent()
print("hit")
}
}
}
//the node's creation.
func bubblesinView() {
//create the sprite
let bubble1 = SKSpriteNode(imageNamed: "bubble_green.png")
//spawn in the X axis min is size, max is the total height minus size bubble
let width_bubble_1 = random(min: bubble1.size.width/2, max: size.width - bubble1.size.height/2)
//y: size.height * X, X is 0 at bottom page to max
//spawn position, let x be random
bubble1.position = CGPoint(x: width_bubble_1, y: size.height)
// Add the monster to the scene
addChild(bubble1)
// Determine speed of the monster from start to end
let bubblespeed = random(min: CGFloat(1.1), max: CGFloat(4.0))
//this tell the bubble to move to the given position, changeble x must be a random value
let moveAction = SKAction.moveTo(CGPoint(x: width_bubble_1, y: size.height * 0.1), duration: NSTimeInterval(bubblespeed))
let actionMoveDone = SKAction.removeFromParent()
bubble1.runAction(SKAction.sequence([moveAction, actionMoveDone]))
}
I am really trying to make this work. Thanks in advance!
If you want to use the name property of the node, you should set it to something. The runtime cannot figure out the variable name as the name. So in bubblesInView function after
let bubble1 = SKSpriteNode(imageNamed: "bubble_green.png")
you should do
bubble1.name = "bubble1"
You just forgot to name the node :
//create the sprite
let bubble1 = SKSpriteNode(imageNamed: "bubble_green.png")
bubble1.name = "bubble1"

Resources