Rotate sprite by touch with a limited rotation spe

2019-01-14 14:19发布

问题:

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.

回答1:

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.zPosition = targetZRotation
        }

    }


}



回答2:

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



回答3:

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];
}


回答4:

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.