Double M
Double M

Reputation: 1503

SKFieldNode positioned wrong after iOS 9 update

I've some code that worked perfectly fine in iOS 8, however since iOS 9 a few SKFieldNodes behave very strangely. The image below shows a scene with two SKFieldNodes (marked with red sprites) and a SKEmitterNode emitting gold-ish particles.

enter image description here

In iOS 8, the fields would attract the particles and "suck" them towards the magnet's tips. Since iOS 9, the particles accelerate towards the upper right corner, like illustrated by the red arrows. Also, I had to increase the strength of the fields to even make the effect visible.

Here's the corresponding code:

            SKSpriteNode* magnet = [SKSpriteNode spriteNodeWithImageNamed:@"StarMagnet_main"];

            //Add field node
            SKFieldNode* field = [SKFieldNode radialGravityField];
            field.strength = 10.0;
            field.falloff = 0.1;

            //Add a red circle to visualize the node
            SKShapeNode* debugNode = [SKShapeNode shapeNodeWithCircleOfRadius:20.0];
            debugNode.fillColor = [UIColor redColor];
            [field addChild:debugNode];

            field.position = CGPointMake(magnet.size.width * 0.3, magnet.size.height * 0.45);

            field.categoryBitMask = LECategoryDust;
            [magnet addChild:field];

            //Add second field
            field = [field copy];
            field.position = CGPointMake(-field.position.x, field.position.y);
            [magnet addChild:field];

            //Add particle emitter
            SKEmitterNode* emitter = [SKEmitterNode emitterNamed:@"MagneticParticleEmitter"];
            emitter.fieldBitMask = LECategoryDust;
            emitter.position = CGPointMake(0, magnet.size.height/2.0);
            [magnet addChild:emitter];

It seems as if the field nodes were located somewhere near the upper right corner, yet the red circle child nodes show different.

Upvotes: 2

Views: 321

Answers (1)

Double M
Double M

Reputation: 1503

After thoroughly investigating the issue, I conclude that it is a SpriteKit bug introduced with iOS 9. Below you can find a test scene that consists of a SKEmitterNode, SKFieldNode and two SKShapeNodes marking the initial viewport and the field's position. All nodes are descendants of a rootNode used to translate the "world".

Although the field's position does not change, the center of its effects does.

The force calculations are only correct if the SKEmitterNode's targetNode property is set to be the scene, which is not always desired.

The view's initial state.

The yellow circle marks the field's position. Yet, particles are drawn towards the upper right corner. Here are some positions at this point:

Root scene pos: 160x, 284y | parent pos: 160x, 284y
Field scene pos: 80x, 142y | parent pos: -80x, -142y
Target scene pos: 80x, 142y | parent pos: -80x, -142y

As you can see the field and the yellow circle have the same position (and parent).

When moving the scene (that is, the rootNode only), the particles are drawn towards another point that moves closer to the lower left corner while translating into the opposite position.

enter image description here

Again some positions after translating:

Root scene pos: 84x, 149y | parent pos: 84x, 149y
Field scene pos: 4x, 7y | parent pos: -80x, -142y
Target scene pos: 4x, 7y | parent pos: -80x, -142y

The field's position has not changed, yet the center of gravity did.

Video

To better illustrate the behaviour, I've uploaded a screencast: https://youtu.be/EJs4vPYMndU

Code

Anyone willing to try this out on their own is welcome, here's the code of the test scene used in this experiment:

#import "LEDebugScene.h"
#import "LEPhysicsCategories.h"
    
@implementation LEDebugScene

SKNode* rootNode;
SKFieldNode* fieldNode;
SKShapeNode* targetPosShape;

- (void) didMoveToView:(SKView *)view {
    self.backgroundColor = [SKColor blackColor];
    
    // This will be the desired field's position (in parent space)
    CGPoint fieldPosition = CGPointMake(-self.size.width * 0.25, -self.size.height * 0.25);
    
    // 1. Set up a root node to move around to translate the "world"
    rootNode = [SKNode node];
    rootNode.position = CGPointMake(self.size.width/2.0, self.size.height/2.0);
    [self addChild:rootNode];
    
    // 2. Mark the target position of the field with a circle
    targetPosShape = [SKShapeNode shapeNodeWithCircleOfRadius:20.0];
    targetPosShape.fillColor = [UIColor yellowColor];
    targetPosShape.strokeColor = [UIColor brownColor];
    [rootNode addChild:targetPosShape];
    targetPosShape.position = fieldPosition;
    
    // 3. Mark the initially visible portion of the scene
    SKShapeNode* initialViewportShape = [SKShapeNode shapeNodeWithRect: CGRectMake(-self.size.width/2.0,
                                                                        -self.size.height/2.0,
                                                                        self.size.width,
                                                                        self.size.height)];
    initialViewportShape.fillColor = [UIColor clearColor];
    initialViewportShape.strokeColor = [UIColor redColor];
    initialViewportShape.lineWidth = 3.0;
    [rootNode addChild:initialViewportShape];
    
    // 4. Add an emitter at the center of the initial viewport
    NSString* emitterPath = [[NSBundle mainBundle] pathForResource:@"DebugEmitter" ofType:@"sks"];
    SKEmitterNode* emitterNode = [NSKeyedUnarchiver unarchiveObjectWithFile:emitterPath];
    emitterNode.fieldBitMask = LECategoryDust;
    emitterNode.particlePositionRange = CGVectorMake(self.size.width, self.size.height);
    emitterNode.position = CGPointZero;
    [rootNode addChild:emitterNode];
    
    // 5. Create a field at the desired position
    fieldNode = [SKFieldNode radialGravityField];
    fieldNode.userData = [NSMutableDictionary dictionary];
    fieldNode.categoryBitMask = LECategoryDust;
    fieldNode.falloff = 1.8;
    fieldNode.minimumRadius = 20.0;
    fieldNode.strength = 2.5;
    fieldNode.position = fieldPosition;
    [rootNode addChild:fieldNode];
}

- (void) touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    //Get the touch movement vector
    UITouch* touch = [touches anyObject];
    CGPoint prevLoc = [touch previousLocationInNode:self];
    CGPoint location = [touch locationInNode:self];
    CGVector delta = CGVectorMake(location.x - prevLoc.x, location.y - prevLoc.y);
    
    //Translate the world by repositioning the root node
    rootNode.position = CGPointMake(rootNode.position.x + delta.dx, rootNode.position.y + delta.dy);
    
    CGPoint rootScenePos = [self convertPoint:rootNode.position fromNode:rootNode.parent];
    CGPoint fieldScenePos = [self convertPoint:fieldNode.position fromNode:fieldNode.parent];
    CGPoint targetShapeScenePos = [self convertPoint:targetPosShape.position fromNode:targetPosShape.parent];

    NSLog(@"");
    NSLog(@"Root scene pos: %.0fx, %.0fy | parent pos: %.0fx, %.0fy",
          rootScenePos.x, rootScenePos.y, rootNode.position.x, rootNode.position.y);
    NSLog(@"Field scene pos: %.0fx, %.0fy | parent pos: %.0fx, %.0fy",
          fieldScenePos.x, fieldScenePos.y, fieldNode.position.x, fieldNode.position.y);
    NSLog(@"Target scene pos: %.0fx, %.0fy | parent pos: %.0fx, %.0fy",
          targetShapeScenePos.x, targetShapeScenePos.y, targetPosShape.position.x, targetPosShape.position.y);
}

@end

Despite trying for hours, I did not manage to compensate for the calculation errors by repositioning the field node. If anyone does, it would be great if they could share it.

EDIT: I also filed a bug report (23063175).

Upvotes: 0

Related Questions