First off, I have already seen and tried to implement the other answers to similar questions here, here and here. The problem is I started programming for iOS last year with Swift and (regrettably) I did not learn ObjC first (yes, it's now on my to-do list). ;-)
So please take a look and see if you might help me see my way thru this.
I can easily pinch to zoom the whole SKScene. I can also scale an SKSpiteNode up/down by using other UI Gestures (ie. swipes) and SKActions.
Based off this post I have applied the SKAction to the UIPinchGestureRecognizer and it works perfectly to zoom IN, but I cannot get it to zoom back OUT.
What am I missing?
Here is my code on a sample project:
class GameScene: SKScene {
var board = SKSpriteNode(color: SKColor.yellowColor(), size: CGSizeMake(200, 200))
func pinched(sender:UIPinchGestureRecognizer){
println("pinched \(sender)")
// the line below scales the entire scene
//sender.view!.transform = CGAffineTransformScale(sender.view!.transform, sender.scale, sender.scale)
sender.scale = 1.01
// line below scales just the SKSpriteNode
// But it has no effect unless I increase the scaling to >1
var zoomBoard = SKAction.scaleBy(sender.scale, duration: 0)
board.runAction(zoomBoard)
}
// line below scales just the SKSpriteNode
func swipedUp(sender:UISwipeGestureRecognizer){
println("swiped up")
var zoomBoard = SKAction.scaleBy(1.1, duration: 0)
board.runAction(zoomBoard)
}
// I thought perhaps the line below would scale down the SKSpriteNode
// But it has no effect at all
func swipedDown(sender:UISwipeGestureRecognizer){
println("swiped down")
var zoomBoard = SKAction.scaleBy(0.9, duration: 0)
board.runAction(zoomBoard)
}
override func didMoveToView(view: SKView) {
self.addChild(board)
let pinch:UIPinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: Selector("pinched:"))
view.addGestureRecognizer(pinch)
let swipeUp:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedUp:"))
swipeUp.direction = .Up
view.addGestureRecognizer(swipeUp)
let swipeDown:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedDown:"))
swipeDown.direction = .Down
view.addGestureRecognizer(swipeDown)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
// should I be using this function instead?
}
Thanks to the help from #sangony I have gotten this working finally. I thought I'd post the working code in case anyone else would like to see it in Swift.
var board = SKSpriteNode(color: SKColor.yellowColor(), size: CGSizeMake(200, 200))
var previousScale = CGFloat(1.0)
func pinched(sender:UIPinchGestureRecognizer){
if sender.scale > previousScale {
previousScale = sender.scale
if(board.size.height < 800) {
var zoomIn = SKAction.scaleBy(1.05, duration:0)
board.runAction(zoomIn)
}
}
if sender.scale < previousScale {
previousScale = sender.scale
if(board.size.height > 200) {
var zoomOut = SKAction.scaleBy(0.95, duration:0)
board.runAction(zoomOut)
}
}
I tried your code (in Objective C) and got it to zoom in and out using pinch. I don't think there's anything wrong with your code but you are probably not taking into account the scale factors as they are placed on the ever changing sprite size.
You can easily zoom so far out or in that it requires multiple pinch gestures to get the node back to a manageable size. Instead of using the scale property directly for your zoom factor, I suggest you use a step process. You should also have max/min limits for your scale size.
To use the step process you create a CGFloat ivar previousScale to store the last scale value as to determine whether the current pinch is zooming in or out. You then compare the new passed sender.scale to the ivar and zoom in or out based on the comparison.
Apply min and max scale limits to stop scaling once they are reached.
The code below is in Obj-C but I'm sure you can get the gist of it:
First declare your ivar float float previousScale;
- (void)handlePinch:(UIPinchGestureRecognizer *)sender {
NSLog(#"pinchScale:%f",sender.scale);
if(sender.scale > previousScale) {
previousScale = sender.scale;
// only scale up if the node height is less than 200
if(node0.size.height < 200) {
// step up the scale factor by 0.05
[node0 runAction:[SKAction scaleBy:1.05 duration:0]];
}
}
if(sender.scale < previousScale) {
previousScale = sender.scale;
// only scale down if the node height is greater than 20
if(node0.size.height > 20) {
// step down the scale factor by 0.05
[node0 runAction:[SKAction scaleBy:0.95 duration:0]];
}
}
}
Related
I am new to Swift and SpriteKit and am learning to understand the control in the game "Fish & Trip". The sprite node is always at the center of the view and it will rotate according to moving your touch, no matter where you touch and move (hold) it will rotate correspondingly.
The difficulty here is that it is different from the Pan Gesture and simple touch location as I noted in the picture 1 and 2.
For the 1st pic, the touch location is processed by atan2f and then sent to SKAction.rotate and it is done, I can make this working.
For the 2nd pic, I can get this by setup a UIPanGestureRecognizer and it works, but you can only rotate the node when you move your finger around the initial point (touchesBegan).
My question is for the 3rd pic, which is the same as the Fish & Trip game, you can touch anywhere on the screen and then move (hold) to anywhere and the node still rotate as you move, you don't have to move your finger around the initial point to let the node rotate and the rotation is smooth and accurate.
My code is as follow, it doesn't work very well and it is with some jittering, my question is how can I implement this in a better way? and How can I make the rotation smooth?
Is there a way to filter the previousLocation in the touchesMoved function? I always encountered jittering when I use this property, I think it reports too fast. I haven't had any issue when I used UIPanGestureRecoginzer and it is very smooth, so I guess I must did something wrong with the previousLocation.
func mtoRad(x: CGFloat, y: CGFloat) -> CGFloat {
let Radian3 = atan2f(Float(y), Float(x))
return CGFloat(Radian3)
}
func moveplayer(radian: CGFloat){
let rotateaction = SKAction.rotate(toAngle: radian, duration: 0.1, shortestUnitArc: true)
thePlayer.run(rotateaction)
}
var touchpoint = CGPoint.zero
var R2 : CGFloat? = 0.0
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches{
let previousPointOfTouch = t.previousLocation(in: self)
touchpoint = t.location(in: self)
if touchpoint.x != previousPointOfTouch.x && touchpoint.y != previousPointOfTouch.y {
let delta_y = touchpoint.y - previousPointOfTouch.y
let delta_x = touchpoint.x - previousPointOfTouch.x
let R1 = mtoRad(x: delta_x, y: delta_y)
if R2! != R1 {
moveplayer(radiant: R1)
}
R2 = R1
}
}
}
This is not an answer (yet - hoping to post one/edit this into one later), but you can make your code a bit more 'Swifty' by changing the definition for movePlayer() from:
func moveplayer(radian: CGFloat)
to
rotatePlayerTo(angle targetAngle: CGFloat) {
let rotateaction = SKAction.rotate(toAngle: targetAngle, duration: 0.1, shortestUnitArc: true)
thePlayer.run(rotateaction)
}
then, to call it, instead of:
moveplayer(radiant: R1)
use
rotatePlayerTo(angle: R1)
which is more readable as it describes what you are doing better.
Also, your rotation to the new angle is constant at 0.1s - so if the player has to rotate further, it will rotate faster. it would be better to keep the rotational speed constant (in terms of radians per second). we can do this as follows:
Add the following property:
let playerRotationSpeed = CGFloat((2 *Double.pi) / 2.0) //Radian per second; 2 second for full rotation
change your moveShip to:
func rotatePlayerTo(angle targetAngle: CGFloat) {
let angleToRotateBy = abs(targetAngle - thePlayer.zRotation)
let rotationTime = TimeInterval(angleToRotateBy / shipRotationSpeed)
let rotateAction = SKAction.rotate(toAngle: targetAngle, duration: rotationTime , shortestUnitArc: true)
thePlayer.run(rotateAction)
}
this may help smooth the rotation too.
I'm trying to make an app with swift, and I want to use front-facing camera.
I used AVFoundation and tried some codes. But I couldn't set front-facing zoom parameter. Is it possible? For back-camera, everything worked successfully.
I dont want to use Affine Transform. Because, it can be decrease image quality. So, how can I set this parameter programatically?
Thanks.
You'll need to add a zoomFactor variable to your camera.
var zoomFactor: CGFloat = 1.0
Next define a function zoom to be used in conjunction with a pinch recognizer. I assume you have created a front capture device and input. frontDevice is an optional capture device on my camera. Here's how I zoom that device.
public func zoom(pinch: UIPinchGestureRecognizer) {
guard let device = frontDevice else { return }
func minMaxZoom(_ factor: CGFloat) -> CGFloat { return min(max(factor, 1.0), device.activeFormat.videoMaxZoomFactor) }
func update(scale factor: CGFloat) {
do {
try device.lockForConfiguration()
defer { device.unlockForConfiguration() }
device.videoZoomFactor = factor
} catch {
debugPrint(error)
}
}
let newScaleFactor = minMaxZoom(pinch.scale * zoomFactor)
switch pinch.state {
case .began: fallthrough
case .changed: update(scale: newScaleFactor)
case .ended:
zoomFactor = minMaxZoom(newScaleFactor)
update(scale: zoomFactor)
default: break
}
}
Finally add a pinch recognizer to some view.
let pgr = UIPinchGestureRecognizer(target: self, action: #selector(zoom))
view.addGestureRecognizer(pgr)
The previous answer can be done without the internal methods, allowing it to be more straightforward and understandable.
To fully explain the code:
The zoom variable keeps track of what zoom you were at after the last gesture. Before any gesture happens there is no zoom, so you're at 1.0.
During a gesture the scale property of pinch holds the ratio of the pinch during the active gesture. This is 1.0 when your fingers haven't moved from their initial position and grows and shrinks with pinching. By multiplying this with the previously held zoom you get what scale to be at in the moment while the gesture is occurring. It's important to keep this scale in the range of [1, device.activeFormat.videoMaxZoomFactor] or you'll get a SIGABRT.
When the gesture finishes (pinch.state) you need to update zoom so that the next gesture starts at the current zoom level.
It's important to lock when modifying a camera property to avoid concurrent modification. defer will release the lock after the block of code no matter what, similar to a finally block.
var zoom: CGFloat = 1.0
#objc func pinch(_ pinch: UIPinchGestureRecognizer) {
guard let device = frontDevice
else { return }
let scaleFactor = min(max(pinch.scale * zoom, 1.0), device.activeFormat.videoMaxZoomFactor)
if pinch.state == .ended {
zoom = scaleFactor
}
do {
try device.lockForConfiguration()
defer { device.unlockForConfiguration() }
device.videoZoomFactor = scaleFactor
} catch {
print(error)
}
}
I am making a simple game in SpriteKit, and I have a scrolling background. What simply happens is that a few background images are placed adjacent to each other when the game scene is loaded, and then the image is moved horizontally when it scrolls out of the screen. Here is the code for that, from my game scene's didMoveToView method.
// self.gameSpeed is 1.0 and gradually increases during the game
let backgroundTexture = SKTexture(imageNamed: "Background")
var moveBackground = SKAction.moveByX(-self.frame.size.width, y: 0, duration: (20 / self.gameSpeed))
var replaceBackground = SKAction.moveByX(self.frame.size.width, y: 0, duration: 0)
var moveBackgroundForever = SKAction.repeatActionForever(SKAction.sequence([moveBackground, replaceBackground]))
for var i:CGFloat = 0; i < 2; i++ {
var background = SKSpriteNode(texture: backgroundTexture)
background.position = CGPoint(x: self.frame.size.width / 2 + self.frame.size.width * i, y: CGRectGetMidY(self.frame))
background.size = self.frame.size
background.zPosition = -100
background.runAction(moveBackgroundForever)
self.addChild(background)
}
Now I want to increase the speed of the scrolling background at certain points of the game. You can see that the duration of the background's horizontal scroll is set to (20 / self.gameSpeed). Obviously this does not work, because this code is only run once, and therefore the movement speed is never updated to account for a new value of the self.gameSpeed variable.
So, my question is simply: how do I increase the speed (reduce the duration) of my background images' movements according to the self.gameSpeed variable?
Thanks!
You could use the gameSpeed variable to set the velocity of the background. For this to work, firstly, you need to have a reference to your two background pieces (or more if you so wanted):
class GameScene: SKScene {
lazy var backgroundPieces: [SKSpriteNode] = [SKSpriteNode(imageNamed: "Background"),
SKSpriteNode(imageNamed: "Background")]
// ...
}
Now you need your gameSpeed variable:
var gameSpeed: CGFloat = 0.0 {
// Using a property observer means you can easily update the speed of the
// background just by setting gameSpeed.
didSet {
for background in backgroundPieces {
// Minus, because the background is moving from left to right.
background.physicsBody!.velocity.dx = -gameSpeed
}
}
}
Then position each piece correctly in didMoveToView. Also, for this method to work each background piece needs a physics body so you can easily change its velocity.
override func didMoveToView(view: SKView) {
for (index, background) in enumerate(backgroundPieces) {
// Setup the position, zPosition, size, etc...
background.physicsBody = SKPhysicsBody(rectangleOfSize: background.size)
background.physicsBody!.affectedByGravity = false
background.physicsBody!.linearDamping = 0
background.physicsBody!.friction = 0
self.addChild(background)
}
// If you wanted to give the background and initial speed,
// here's the place to do it.
gameSpeed = 1.0
}
You could update gameSpeed in update for example with gameSpeed += 0.5.
Finally, in update you need to check if a background piece has gone offscreen (to the left). If it has it needs to be moved to the end of the chain of background pieces:
override func update(currentTime: CFTimeInterval) {
for background in backgroundPieces {
if background.frame.maxX <= 0 {
let maxX = maxElement(backgroundPieces.map { $0.frame.maxX })
// I'm assuming the anchor of the background is (0.5, 0.5)
background.position.x = maxX + background.size.width / 2
}
}
}
You could make use of something like this
SKAction.waitforDuration(a certain amount of period to check for the updated values)
SKAction.repeatActionForever(the action above)
runAction(your action)
{ // this is the completion block, do whatever you want here, check the values and adjust them accordly
}
So i got a set of rings one inside the other. I want to be able to zoom then in and, when I zoom close enough the zooming should animate the snap in and then I should keep zooming again.
I've done the zooming part pretty easily, using CGAffineTransform and UIPinchGesture recognizer, here is how the scale method looks like:
func scale(gesture: UIPinchGestureRecognizer) {
if !animationInProgress {
for var i = 0; i < scrollViews.count; i++ {
var view = scrollViews[i]
var j = 0
for ; j < scrollViews.count; j++ {
if view == scrollViews[j] {
break
}
}
println("Animation in progress: \(animationInProgress)")
if view.frame.origin.x < -80 || view.frame.origin.y < -140 {
if !view.hidden {
currentViewIndex = j + 1
view.hidden = true
view.visible = false
println("new view got out of the screen")
snapIn = true
animateSnap(snapIn)
}
} else {
if view.hidden == true {
currentViewIndex = j
view.hidden = false
view.visible = true
println("new view got into the screen")
snapIn = false
animateSnap(snapIn)
}
}
view.transform = CGAffineTransformScale(view.transform, gesture.scale, gesture.scale)
println("transformed")
}
gesture.scale = 1
}
}
so we iterate through our views, the frame.origin x and y checks, whether the views is out of the screen and I set some flags, which turn of some calculations.
The idea is the following, when one circle gets out of the screen, the rest animate zoom in and zoom out, when the new view gets on the screen.
Here is a animateSnap function:
private func animateSnap(snapIn: Bool) {
let factor: CGFloat = snapIn ? 1.5 : 0.5
for var a = currentViewIndex; a < scrollViews.count; a++ {
let next = scrollViews[a]
var transformAnimation = CABasicAnimation(keyPath: "transform")
transformAnimation.duration = 1
transformAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
transformAnimation.removedOnCompletion = false
transformAnimation.fillMode = kCAFillModeForwards
transformAnimation.delegate = self
let transform = CATransform3DScale(next.layer.transform, factor, factor, 1)
transformAnimation.toValue = NSValue(CATransform3D:transform)
next.layer.addAnimation(transformAnimation, forKey: "transform")
}
}
The question is: does somebody know an elegant solution to this or see any flaw in my approach ? The problem happens when the animation ends the views kinda go back to where they were at the start. I don't know why because remove on completion is set to false. In addition, can the fact that I use CGAffineTransform for zooming and CATransform3D somehow affect the result ?
Thank you very much !
For everybody who might encounter the same problem, when you work with 2D use CGAffineTransform instead of CATransform3D. That solved the problem, in addition I suspect that they also change different things(for example, CATransform3D changes layer) that's why when animation finished the result did not look as expected.
I'm having an issue with some animations in a Swift iOS application. I am trying to allow a user to grab a UIImageView and drag (pan) it to a different point. Then if they push "animate" it shows the animation of the imageview along a path from the first point to the second.
Here is what I have so far, which is more so me just trying to hammer an early solution. I'm getting an error when the "animate" button is pressed that says:
CGPathAddLineToPoint(CGMutablePathRef, const CGAffineTransform *,
CGFloat, CGFloat): no current point.
Here is my code:
// There was some global stuff set up earlier, such as pathPlayer1 which is an
// array of CGPoints I am using to store the path; they are commented
override func viewDidLoad() {
super.viewDidLoad()
var panRecognizer1 = UIPanGestureRecognizer(target: self, action: "handlePanning1:")
playerWithBall.addGestureRecognizer(panRecognizer1)
pathPlayer1.append(playerWithBall.center)
}
func handlePanning1(recognizer: UIPanGestureRecognizer) {
var newTranslation: CGPoint = recognizer.translationInView(playerWithBall)
recognizer.view?.transform = CGAffineTransformMakeTranslation(lastTranslation1.x + newTranslation.x, lastTranslation1.y + newTranslation.y)
if recognizer.state == UIGestureRecognizerState.Ended {
// lastTranslation1 is a global
lastTranslation1.x += newTranslation.x
lastTranslation1.y += newTranslation.y
// another global to get the translation from imageview center in main view
// to the new point in main view
playerWithBallPos.x = playerWithBall.center.x + lastTranslation1.x
playerWithBallPos.y = playerWithBall.center.y + lastTranslation1.y
// add this point to the path to animate along
pathPlayer1.append(playerWithBallPos)
//This was to make sure the append was working
println(pathPlayer1)
}
}
#IBAction func animatePlay(sender: UIButton) {
var path = CGPathCreateMutable()
var i: Int = 0
for (i = 0; i < pathPlayer1.count; i++) {
var location: CGPoint! = pathPlayer1[i]
// I think if its the first time you need to call CGPathMoveToPoint?
if firstTime {
CGPathMoveToPoint(path, nil, location.x, location.y)
firstTime = false
} else {
CGPathAddLineToPoint(path, nil, location.x, location.y)
}
}
var pathAnimation: CAKeyframeAnimation = CAKeyframeAnimation(keyPath: "pos")
pathAnimation.path = path
pathAnimation.duration = 1.0
}
The panning is working just fine and it appears to be correctly getting the new point, but I have no experience using objective-c and a computer science class worth of knowledge of swift/iOS and I'm not familiar with these types of animations.
I would like to make my solution work so that I could extend it from a single imageview to multiple and animate each one simultaneously (think of a sports playbook and animating a play or something like that)