Flipped x-scale breaks collision handling (SpriteK

2019-01-23 18:27发布

问题:

I use SKNode's xScale property to flip my sprites horizontally. Now, after updating iOS to version 7.1 horizontal flip causes my objects to sink inside the ground. (See animation below). The problem occurs only with xScale property. Vertical flips work fine.

// Init
{
    SKSpriteNode* ground = [SKSpriteNode spriteNodeWithColor:[UIColor blackColor] size:CGSizeMake(winSize.width, 150)];
    ground.position = CGPointMake(winSize.width/2, ground.size.height/2);
    ground.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:ground.size center:CGPointZero];
    ground.physicsBody.dynamic = NO;
    ground.physicsBody.categoryBitMask = 0x01;
    ground.physicsBody.collisionBitMask = 0x02;
    ground.physicsBody.contactTestBitMask = 0x02;
    [self.world addChild:ground];

    SKSpriteNode* turtle = [SKSpriteNode spriteNodeWithImageNamed:@"turtle.png"];
    turtle.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:turtle.size.width/2];
    turtle.physicsBody.categoryBitMask = 0x02;
    turtle.physicsBody.collisionBitMask = 0x01;
    turtle.physicsBody.contactTestBitMask = 0x01;
    turtle.position = CGPointMake(winSize.width/2, winSize.height/2);
    [self.world addChild:turtle];
    self.turtle = turtle;
}

// Somewhere else
{
    self.turtle.xScale *= -1;
}

回答1:

I'm convinced this is a bug in SpriteKit.

Anyway, here is one solution for the problem (Actually, this is more a workaround than a real solution but...): Wrap the sprite in a container node. Also, container node holds the physicsBody while the child node is merely a graphics node. This way you can safely flip the sprite using xScale without affecting the physics of the node.

// Init
{
    SKSpriteNode* turtleSprite = [SKSpriteNode spriteNodeWithImageNamed:@"turtle.png"];
    self.turtleSprite = turtleSprite;

    SKNode* turtleWrapper = [SKNode node]; 
    turtleWrapper.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:turtleSprite.size.width/2];
    turtleWrapper.physicsBody.categoryBitMask = 2;
    turtleWrapper.physicsBody.collisionBitMask = 1;
    turtleWrapper.physicsBody.contactTestBitMask = 1;

    [turtleWrapper addChild:turtleSprite];
    [self.world addChild:turtleWrapper];
}

// Elsewhere
{
    self.turtleSprite.xScale *= -1;
}


回答2:

I encountered this problem a couple of days ago. I wanted to invert the sprite based on it's movement (right or left) and found that setting xScale disables any collisions/contacts.

However, I used this line every time I set the xScale property and everything went back to normal.

node.xScale = -1.0
node.physicsBody = node.physicsBody;


回答3:

Alternative Solution

In my case I generally subclass SKSpriteNode to represent nodes in my scene and then encapsulate their behaviour (animation and movement) in the subclass. The Wrap sprite in container node solution doesn't work in my case as I have actions that animate the sprites texture and also move the sprite in sequence.

For example:

class Player: SKSpriteNode{

    override init() {
        ...
        super.init(texture: texture, color: nil, size: texture.size())
        physicsBody = SKPhysicsBody(rectangleOfSize: CGSizeMake(70,75))
    }

    func attack(){
        let action = SKAction.sequence([
            SKAction.moveTo(enemyPosition, duration: 0),
            SKAction.animateWithTextures(frames, timePerFrame: 0.5)
            ])
        runAction(action)
    }
}

Workaround

In my case I solved this by adding these methods to my 'Player' class:

class Player: SKSpriteNode{
    private var flipped: Bool
    ...

    func flip(){
        flipped = !flipped
        position = CGPointMake(-position.x, position.y)
    }

    //MARK: Workaround for iOS7 -xScale physics bugs
    func willSimulatePhysics(){
        xScale = 1
    }

    func didSimulatePhysics(){
        xScale = flipped ? -1 : 1
    }
}

And then override SKScene methods:

class MyScene: SKScene{
    ...

    override func update(currentTime: CFTimeInterval) {
        super.update(currentTime)
        player.willSimulatePhysics()
    }

    override func didSimulatePhysics() {
        super.didSimulatePhysics()
        player.didSimulatePhysics()
    }
}

This works because the node flip is disabled just before physics are simulated for the scene. The node flip is then applied just before the frame is rendered.