7

I'm trying to make my spriteNode rotate over finger touch.

So far I can do it, but what I want is that my node have a "rotation speed". So I calculate the length of the angle then set a different timing to rotate with it (if the arc is long, it will take time...).

Here's my code :

override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
    _isTouched = true
    for touch in touches {
        let location:CGVector = touch.locationInNode(self) - miner.position
        
        miner.weaponRotation = location.angle() - CGFloat(M_PI_2)
    }
}

var wantedRotation: CGFloat {
    get { return _wantedRotation }
    set(rotation) {
        if rotation > CGFloat(M_PI) || rotation < -CGFloat(M_PI) {
            _wantedRotation = CGFloat(M_PI) + rotation % CGFloat(M_PI)
        }
        else {
            _wantedRotation = rotation
        }
        
        removeActionForKey("rotation")
        let arc: CGFloat = CGFloat(abs(_wantedRotation - zRotation))
        let shortestArc: CGFloat = min(arc, CGFloat(M_PI * 2.0) - arc)
        runAction(SKAction.rotateToAngle(_wantedRotation, duration: NSTimeInterval(shortestArc / CGFloat(M_PI) * rotationSpeed), shortestUnitArc: true), withKey: "rotation")
    }
}

The main problem is that adding several SKAction to the node block the movement.

I would like to know what could be the solution ? If possible by using SKAction, since I would like to avoid doing an update on each frame to calculate the movement (but if it's the only solution...)

NOTE AFTER ANSWER

As I received answers, I read again the SpriteKit documentation and found this clear note :

When You Shouldn’t Use Actions

Although actions are efficient, there is a cost to creating and executing them. If you are making changes to a node’s properties in every frame of animation and those changes need to be recomputed in each frame, you are better off making the changes to the node directly and not using actions to do so. For more information on where you might do this in your game, see Advanced Scene Processing.

Community
  • 1
  • 1
Tancrede Chazallet
  • 7,035
  • 6
  • 41
  • 62
  • I am a little confused on what you are trying to accomplish. Are you looking to change the angle rotation in mid movement in case the player moves his finger around (using thouchesMoved) or are you looking to block any further actions until the movement has completed? – sangony Apr 02 '15 at 17:52
  • Neither of this. I have a turret which have a "maximum rotation speed" and should target the finger position, but not immediately, by taking time to turn. I already do that, but since touchesMoved is called like 60time by seconds, adding 60 actions by seconds block the movement (actions don't run...) – Tancrede Chazallet Apr 02 '15 at 18:36
  • So you are you looking for the turret to track the touch location even as it moves around the view? – sangony Apr 02 '15 at 21:14
  • It just turn on itself, but yeah, it target the finger, but "at its speed" – Tancrede Chazallet Apr 03 '15 at 07:16

4 Answers4

8

I have a turret which ... should target the finger position, but not immediately, by taking time to turn.

You won't be able to get away with SKActions for something like this. You can try but it will be really messy and inefficient. You need real-time motion control for something like this because the angular velocity of your turret needs to change constantly depending on the touch position.

So I wrote you a quick example project showing how to calculate the angular velocity. The project handles all special cases as well, such as preventing the angle from jumping over your target rotation.

import SpriteKit

class GameScene: SKScene {
    let turret = SKSpriteNode(imageNamed: "Spaceship")
    let rotationSpeed: CGFloat = CGFloat(M_PI) //Speed turret rotates.
    let rotationOffset: CGFloat = -CGFloat(M_PI/2.0) //Controls which side of the sprite faces the touch point. I offset the angle by -90 degrees so that the top of my image faces the touch point.

    private var touchPosition: CGFloat = 0
    private var targetZRotation: CGFloat = 0

    override func didMoveToView(view: SKView) {
        turret.physicsBody = SKPhysicsBody(rectangleOfSize: turret.size)
        turret.physicsBody!.affectedByGravity = false
        turret.position = CGPoint(x: self.size.width/2.0, y: self.size.height/2.0)
        self.addChild(turret)
    }

    override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
        calculateAngleToTouch(touches.anyObject() as UITouch)
    }

    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        calculateAngleToTouch(touches.anyObject() as UITouch)
    }

    func calculateAngleToTouch(touch: UITouch) {
        let position = touch.locationInNode(self)
        let angle = atan2(position.y-turret.position.y, position.x-turret.position.x)

        targetZRotation = angle + rotationOffset
    }

    override func update(currentTime: NSTimeInterval) {
        var angularDisplacement = targetZRotation - turret.zRotation
        if angularDisplacement > CGFloat(M_PI) {
            angularDisplacement = (angularDisplacement - CGFloat(M_PI)*2)
        } else if angularDisplacement < -CGFloat(M_PI) {
            angularDisplacement = (angularDisplacement + CGFloat(M_PI)*2)
        }

        if abs(angularDisplacement) > rotationSpeed*(1.0/60.0) {
            let angularVelocity = angularDisplacement < 0 ? -rotationSpeed : rotationSpeed
            turret.physicsBody!.angularVelocity = angularVelocity
        } else {
            turret.physicsBody!.angularVelocity = 0
            turret.zRotation = targetZRotation
        }

    }


}

enter image description here

Epic Byte
  • 33,840
  • 12
  • 45
  • 93
  • 1
    My thoughts exactly. SKAction is not the right way of doing this. The update method is the only way of handling the rotation changes step by step. I was going to add the code for that today but you beat me to it :) +1 for an "Epic" solution. – sangony Apr 04 '15 at 13:27
  • 1
    This is the right answer. I read again carefully SpriteKit documentation and found this : https://developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/AddingActionstoSprites/AddingActionstoSprites.html#//apple_ref/doc/uid/TP40013043-CH11-SW13 It cannot be more clear, thanks for your detailed answer. – Tancrede Chazallet Apr 04 '15 at 16:06
1

You do not need to do that: removeActionForKey("rotation") - it is the reason, that you have movement blocking. Just take a look at these methods

  • speedBy:duration:
  • speedTo:duration:

and property of animation from documentation:

speed - The speed factor adjusts how fast an action’s animation runs. For example, a speed factor of 2.0 means the animation runs twice as fast.

It is hard to say in your case, but also solution would be to create sequence actions, so they will perform one by one. Hope this helps

Kateryna Gridina
  • 834
  • 9
  • 13
  • I tried with or without `removeActionForKey("rotation")` it doesn't change anything. And speed factor isn't really helpful since the rotation could change in any frame. – Tancrede Chazallet Apr 01 '15 at 06:11
  • When the user keep his finger moving on the game, `miner.weaponRotation` is called like 20 times each seconds but the angle can vary for each call. – Tancrede Chazallet Apr 01 '15 at 06:13
  • did you tried sequence? you can collect actions to a sequence and run it each second. For example make an NSTimer, than each second perform runAction(seq), where the seq is a sequence of your actions. Duration of each action will be 1(sec)/seq.Count() – Kateryna Gridina Apr 01 '15 at 08:22
  • var actions = Array(); func addAction(){ actions.append(SKAction.rotateToAngle(_wantedRotation, duration: 1/20); } func performAction(){ if (timeInterval = 1){ let sequence = SKAction.sequence(actions); runAction(sequence); } timeInterval = 0; actions.removeAll() } – Kateryna Gridina Apr 01 '15 at 08:24
  • I already tried to put "space" in the execution of actions, with sequence and `SKAction.waitForDuration` it worked a bit better but still shows bad responding. So it's not a solution. – Tancrede Chazallet Apr 02 '15 at 07:30
  • maybe you can try cocos2d? it is more easy to work with graphic. Maybe this answer helps you somehow http://stackoverflow.com/questions/5306451/rotating-sprite-with-finger-in-cocos2d – Kateryna Gridina Apr 02 '15 at 10:05
  • Well, if I ask a question on SpriteKit, it's not to use Cocos2D, I don't want to redo my whole project, I search for a clear solution. Maybe it isn't SKAction, but in this case I want to know why and how. Sorry I don't mean to be rude, it's just it's not the goal of my question. – Tancrede Chazallet Apr 02 '15 at 10:09
  • sure, I just try to help, did not know on what stage of proj you are :) – Kateryna Gridina Apr 02 '15 at 10:26
1

I'll give it a try. But I cann't test it now. this is an Objective-C (pseudo)code.

- (void)rotateSprite:(SKSpriteNode *)sprite toAngle:(float)angle inDuration:(NSTimeInterval)duration
{
    SKAction *rotation = [SKAction rotateToAngle:angle duration:duration]; 
    [sprite runAction:rotation completion:^{
        [sprite removeAllActions];
    }];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
   .
   .
   .
   // your sprite
   [node removeAllActions];                
   float angle = [self calcAngle]; // your angle
   NSTimeInterval duration = [self calcDuration]; // your duration
   [self rotateSprite:(SKSpriteNode *)node toAngle:angle inDuration:duration];
}
suyama
  • 262
  • 2
  • 15
1

I have created code to turn the node0 sprite towards the touch point at a constant speed regardless of angle. You had indicated in your question that you prefer using SKAction instead of having code in your update method.

Below is the sample project code for the GameScene.m file:

#import "GameScene.h"

@implementation GameScene {
    SKAction *objectAnimation;
    SKSpriteNode *node0;
}

-(void)didMoveToView:(SKView *)view {
    self.backgroundColor = [SKColor blackColor];

    node0 = [SKSpriteNode spriteNodeWithColor:[SKColor redColor] size:CGSizeMake(100, 10)];
    node0.position = CGPointMake(300, 300);
    node0.zRotation = 0.0;
    [self addChild:node0];
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    for (UITouch *touch in touches) {
        CGPoint touchLocation = [touch locationInNode:self];

        // calculate angle between object and touch
        float deltaY = touchLocation.y - node0.position.y;
        float deltaX = touchLocation.x - node0.position.x;
        float moveBydegrees = atan2(deltaY, deltaX) * 180 / 3.14159265359;

        // convert degrees to radians
        float moveByRadians = moveBydegrees * M_PI / 180;

        float myFloat0 = 0;
        if(moveByRadians < 0) {
            myFloat0 = fabs(moveByRadians);
        } else {
            myFloat0 = moveByRadians;
        }
        NSLog(@"myFloat0 = %f",myFloat0);

        float myFloat1 = 0;
        if(node0.zRotation < 0) {
            myFloat1 = fabs(node0.zRotation);
        } else {
            myFloat1 = node0.zRotation;
        }
        NSLog(@"myFloat1 = %f",myFloat1);

        float durationTime = fabs(myFloat0 - myFloat1);

        NSLog(@"durationTime = %f",durationTime);

        objectAnimation = [SKAction rotateToAngle:moveByRadians duration:durationTime shortestUnitArc:YES];
        [self startObjectAnimation];
    }
}

-(void)startObjectAnimation {
    [node0 removeActionForKey:@"animation"];

    if (![node0 actionForKey:@"animation"]) {
        if(objectAnimation != nil) {
            [node0 runAction:objectAnimation withKey:@"animation"];
            NSLog(@"runnign animation");
        } else {
            NSLog(@"enemy node animation is nil");
        }
    }
}

Personally I think it would be better to use the update method to accomplish this task. It would give you greater control in the step by step movement of the object's zRotation rather than using a SKAction.

sangony
  • 11,636
  • 4
  • 39
  • 55
  • I already did this, my point isn't to do it on `touchesBegan` only, but on `touchesMoved`, the whole point is that the turret follows the finger as it moves. Just try to put your code in `touchesMoved` too, you will see my problem ;) – Tancrede Chazallet Apr 03 '15 at 14:03
  • @AncAinu - Are you absolutely set on only doing this with a SKAction? – sangony Apr 03 '15 at 14:08
  • Absolutely not, I would prefer, but maybe it's not possible at all with SKAction for this case. I just want to know if there is a way to do it with SKAction (since it seems done for that in SpriteKit). If it's impossible, I want to know why (if someone know). If no one can tell, then I will conclude it's not possible (or I didn't find someone enough good into SpriteKit). The question is beyond my problem, to see if this is a limit of SpriteKit :) – Tancrede Chazallet Apr 03 '15 at 14:12