用触摸旋转精灵并限制旋转速度

7

我想让我的spriteNode在手指触摸时旋转。

到目前为止,我已经做到了,但是我希望我的节点有一个“旋转速度”。 因此,我计算角度的长度,然后设置不同的时间来旋转它(如果弧长很长,它将需要更多的时间…)。

这是我的代码:

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")
    }
}

主要问题是将多个SKAction添加到节点会阻止移动。

我想知道可能的解决方案是什么?如果可能的话,使用SKAction,因为我想避免在每一帧上进行更新以计算移动(但如果这是唯一的解决方案...)

回答后注意事项

当我收到答案时,我再次阅读了SpriteKit文档,并发现this clear note

何时不应使用操作

尽管操作很有效,但创建和执行它们是有成本的。如果您在每个动画帧中更改节点的属性,并且这些更改需要在每个帧中重新计算,则最好直接对节点进行更改,而不使用操作进行更改。有关在游戏中可能需要执行此操作的更多信息,请参见高级场景处理。


我有点困惑你想要实现什么。你是想在玩家移动手指时(使用touchesMoved)改变旋转角度,还是想阻止任何进一步的操作直到移动完成? - sangony
都不是。我有一个炮塔,它有一个“最大旋转速度”,应该瞄准手指位置,但不是立即瞄准,需要时间来旋转。我已经做到了这一点,但由于touchesMoved每秒钟调用60次,每秒钟添加60个动作会阻碍移动(动作不运行...)。 - Tancrede Chazallet
所以你是在寻找炮塔来跟踪触摸位置,即使它在视图中移动? - sangony
它只是自己打开了,但是它会瞄准手指,不过“以它的速度”。 - Tancrede Chazallet
4个回答

8
我有一个炮塔,应该瞄准手指位置,但不是立即瞄准,需要时间才能转动。
像这样的问题您无法使用SKActions解决。您可以尝试,但会非常混乱和低效。像这样的问题需要实时运动控制,因为炮塔的角速度需要根据触摸位置不断变化。
所以我为您编写了一个快速示例项目,展示如何计算角速度。该项目还处理所有特殊情况,例如防止角度跳过目标旋转。
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


1
我的想法完全一样。SKAction不是处理这个问题的正确方式。更新方法(update method)是逐步处理旋转变化的唯一方法。我今天本来要添加这段代码,但你比我先完成了 :) 为"史诗级"解决方案点赞。 - sangony
1
这就是正确的答案。我仔细再次阅读了SpriteKit文档,并找到了这个链接:https://developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/AddingActionstoSprites/AddingActionstoSprites.html#//apple_ref/doc/uid/TP40013043-CH11-SW13 它再也不能更清晰了,感谢您详细的回答。 - Tancrede Chazallet

1

我会尝试一下。但是现在无法测试。这是一个 Objective-C(伪)代码。

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

没有问题,对于Obj-C来说。但你的代码与我的相比并没有太大变化,你只是在每个touchesMoved调用中运行一个操作,这也是我做的。 - Tancrede Chazallet

1
我已经创建了一段代码,可以使节点0精灵以恒定速度朝向触摸点,无论角度如何。您在问题中指出您更喜欢使用SKAction而不是在更新方法中编写代码。
以下是GameScene.m文件的示例项目代码:
#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");
        }
    }
}

我个人认为最好使用更新方法来完成这项任务。这将使您在逐步移动对象的zRotation方面具有更大的控制力,而不是使用SKAction。


我已经做过了,我的重点不是仅在touchesBegan上执行,而是在touchesMoved上执行,整个重点是炮塔随着手指的移动而移动。试着把你的代码也放到touchesMoved中,你会看到我的问题;) - Tancrede Chazallet
@AncAinu - 你是否坚决要使用SKAction来完成这个任务? - sangony
绝对不,我更喜欢,但也许在这种情况下使用SKAction根本不可能。我只想知道是否有一种方法可以使用SKAction来实现它(因为在SpriteKit中似乎已经完成了)。如果不可能,我想知道为什么(如果有人知道的话)。如果没有人能告诉我,那么我会得出结论:这是不可能的(或者我没有找到足够好的SpriteKit开发者)。这个问题超出了我的问题范围,只是为了看看这是否是SpriteKit的限制 :) - Tancrede Chazallet

1
您不需要执行removeActionForKey("rotation")操作 - 这就是阻止您运动的原因。只需查看以下方法:
  • speedBy:duration:
  • speedTo:duration:
以及文档中的动画属性:

speed - 速度因子可调整动作动画的运行速度。例如,速度因子为2.0表示动画运行速度加倍。

在您的情况下很难确定解决方案,但创建序列动作也是一种解决方法,这样它们将依次执行。 希望这能帮到您。

你试过序列了吗?你可以将动作收集到一个序列中,并每秒运行它。例如,创建一个NSTimer,然后每秒执行runAction(seq),其中seq是你的动作序列。每个动作的持续时间将为1秒/seq.Count()。 - Kateryna Gridina
变量 actions = Array<SKAction>(); 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
我已经尝试在执行操作时加入“空格”,使用序列和SKAction.waitForDuration效果有所改善,但仍然表现不佳。因此这不是一个解决方案。 - Tancrede Chazallet
也许你可以尝试一下cocos2d? 它更容易处理图形。或许这个答案能在一定程度上帮助到你 https://dev59.com/OVXTa4cB1Zd3GeqPzjgC - Kateryna Gridina
如果我在SpriteKit上提问,那并不是要使用Cocos2D,我不想重新做整个项目,我在寻找一个明确的解决方案。也许这不是SKAction的问题,但在这种情况下,我想知道为什么以及如何解决。 抱歉,我并不是有意无礼,只是这不是我的问题目标。 - Tancrede Chazallet
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接