How to create a rope in SpriteKit?
Asked Answered
D

4

15

I want to create a rope like the one that is shown in this video. What is the best way to develop a rope like this?

I've already tried to start and I think the best way to do it, is to make many little "rope"-parts and connect them with a pin joint (is that really the best one?!?). But I didn't know how to start.

Maybe someone can give me some example code for that.. THX :)

Delimitate answered 28/12, 2013 at 6:56 Comment(3)
I like the background music.Racism
Do you mean limit joint?Delimitate
Use SKPhysicsJointPin. Google Box2d rope, and do the same thing with sprite kit.Frissell
G
18

I am the author of that video. Because of the big demand for the source code I have released it on the Github.

You can find it here

Geochemistry answered 15/2, 2014 at 14:59 Comment(0)
R
11

I've also done a rope with single parts connected with PinJoints. I think it's the only way to display a "rope". I think in the video it's the same, you can see the single chain links. You don't even need so much jointed elements, just let the sprite overlap a bit the physics body to make it look very real. Heres my example method... just replace the image names n stuff...

+(void) addRopeJointItems:(CGPoint)leftStartPosition
   countJointElements:(int)countJointElements
                 game:(SKScene*)game 
{
     int itemJointWidth = 25;

     //Left Physics Anchor
     SKSpriteNode * leftAnchor = [SKSpriteNode spriteNodeWithImageNamed:@"dummypixel_transparent.png"];
     leftAnchor.position = CGPointMake(leftStartPosition.x, leftStartPosition.y);
     leftAnchor.size = CGSizeMake(1, 1);
     leftAnchor.zPosition = 2;
     leftAnchor.physicsBody = [SKPhysicsBody
                          bodyWithRectangleOfSize:
                          leftAnchor.size];
     leftAnchor.physicsBody.affectedByGravity = false;
     leftAnchor.physicsBody.mass = 99999999999;
     [game addChild:leftAnchor];

     //add RopeElements
     for (int i=0; i<countJointElements; i++) 
     {
           SKSpriteNode * item = [SKSpriteNode spriteNodeWithImageNamed:@"suspensionrope.png"];
           item.name = [NSString stringWithFormat:@"ropeitem_%d", i];
           item.position = CGPointMake(leftStartPosition.x + (i*itemJointWidth) + itemJointWidth/2, leftStartPosition.y+60);
           item.size = CGSizeMake(itemJointWidth + 5, 5);
           item.zPosition = 2;
           item.physicsBody = [SKPhysicsBody
                        bodyWithRectangleOfSize:
                        item.size];
           item.physicsBody.categoryBitMask = kNilOptions;
           [game addChild:item];

         //Add Joint to the element before
           SKPhysicsBody* bodyA;
           if (i == 0) 
           {
               bodyA = leftAnchor.physicsBody;
           } 
           else 
           {
               bodyA = [game childNodeWithName:[NSString stringWithFormat:@"ropeitem_%d", i-1]].physicsBody;
           }

           SKPhysicsJointPin* joint = [SKPhysicsJointPin jointWithBodyA:bodyA bodyB:item.physicsBody anchor:CGPointMake((item.position.x - item.size.width/2) + 5, item.position.y)];
          [game.physicsWorld addJoint:joint];
    }

    //Right Physics Anchor
    SKSpriteNode * rightAnchor = [SKSpriteNode spriteNodeWithImageNamed:@"dummypixel_transparent.png"];
    rightAnchor.position = CGPointMake((leftStartPosition.x + (countJointElements*itemJointWidth)),
                                   leftStartPosition.y+60);
    rightAnchor.size = CGSizeMake(1, 1);
    rightAnchor.zPosition = 2;
    rightAnchor.physicsBody = [SKPhysicsBody
                           bodyWithRectangleOfSize:
                           rightAnchor.size];
    rightAnchor.physicsBody.affectedByGravity = false;
    rightAnchor.physicsBody.mass = 99999999999;
    [game addChild:rightAnchor];

    //Add the Last Joint
    SKPhysicsJointPin* jointLast = [SKPhysicsJointPin jointWithBodyA:[game childNodeWithName:[NSString stringWithFormat:@"ropeitem_%d", countJointElements - 1]].physicsBody
                                                           bodyB:rightAnchor.physicsBody
                                                          anchor:rightAnchor.position];
    [game.physicsWorld addJoint:jointLast];
}
Rubetta answered 31/12, 2013 at 9:34 Comment(1)
fwiw, I would modify to this: leftAnchor.physicsBody.mass = CGFLOAT_MAX;Frae
I
7

in swift

 func addRopeJointItems(leftStartPosition: CGPoint, numOfJoints countJointElements:Int, andScene game:SKScene ){

    var itemJointWidth = 25

    var leftAnchor = SKSpriteNode(imageNamed: "rope_ring.png")
    leftAnchor.position = CGPointMake(leftStartPosition.x, leftStartPosition.y)
    leftAnchor.size = CGSizeMake(1, 1);
    leftAnchor.zPosition = 2;
    leftAnchor.physicsBody = SKPhysicsBody(rectangleOfSize: leftAnchor.size)
    leftAnchor.physicsBody?.affectedByGravity = false
    leftAnchor.physicsBody?.mass = 999999999;
    game.addChild(leftAnchor)

   for index in 0...countJointElements {

    var item = SKSpriteNode(imageNamed: "rope_ring.png")
    item.name = "ropeitem_" + String(index)

    item.position = CGPointMake(leftStartPosition.x + CGFloat((index * itemJointWidth)) + CGFloat(itemJointWidth / 2) , leftStartPosition.y + 60)

    item.size = CGSizeMake(CGFloat(itemJointWidth + 5), 5);
    item.zPosition = 2;

    item.physicsBody = SKPhysicsBody(rectangleOfSize: item.size)
    item.physicsBody?.categoryBitMask = 0;
    game.addChild(item)


    var bodyA = SKPhysicsBody()


    if (index == 0)
    {
        bodyA = leftAnchor.physicsBody!;
    }
    else
    {

        var nameString = "ropeitem_" + String(index - 1)


        var node = game.childNodeWithName(nameString) as SKSpriteNode
        bodyA = node.physicsBody!

    }

    var joint = SKPhysicsJointPin.jointWithBodyA(bodyA, bodyB: item.physicsBody, anchor: CGPointMake((item.position.x - item.size.width/2) + 5, item.position.y))

    game.physicsWorld.addJoint(joint)

    }


    var rightAnchor = SKSpriteNode(imageNamed: "rope_ring.png")
    rightAnchor.position = CGPointMake(leftStartPosition.x + CGFloat((countJointElements * itemJointWidth)), CGFloat(leftStartPosition.y + 60))
    rightAnchor.size = CGSizeMake(1, 1);
    rightAnchor.zPosition = 2;
    rightAnchor.physicsBody = SKPhysicsBody(rectangleOfSize: rightAnchor.size)
    rightAnchor.physicsBody?.affectedByGravity = false
    rightAnchor.physicsBody?.mass = 999999999;
    game.addChild(rightAnchor)


      var nameString = NSString(format: "ropeitem_%d", countJointElements - 1)

    var node = game.childNodeWithName(nameString)


      var jointLast = SKPhysicsJointPin.jointWithBodyA(node!.physicsBody!, bodyB: rightAnchor.physicsBody, anchor: rightAnchor.position)

    game.physicsWorld.addJoint(jointLast)


}
Incorrigible answered 15/10, 2014 at 8:21 Comment(0)
F
6

I just released my own rope version inspired by mraty's but with a workaround for the elastic effect "bug".

Here's my rope interface:

#import <SpriteKit/SpriteKit.h>

@interface ALRope : NSObject

@property(nonatomic, readonly) NSArray *ropeRings;

@property(nonatomic) int ringCount;

@property(nonatomic) CGFloat ringScale;

@property(nonatomic) CGFloat ringsDistance;

@property(nonatomic) CGFloat jointsFrictionTorque;

@property(nonatomic) CGFloat ringsZPosition;

@property(nonatomic) CGPoint startRingPosition;

@property(nonatomic) CGFloat ringFriction;

@property(nonatomic) CGFloat ringRestitution;

@property(nonatomic) CGFloat ringMass;


@property(nonatomic) BOOL shouldEnableJointsAngleLimits;

@property(nonatomic) CGFloat jointsLowerAngleLimit;

@property(nonatomic) CGFloat jointsUpperAngleLimit;



-(instancetype)initWithRingTexture:(SKTexture *)ringTexture;


-(void)buildRopeWithScene:(SKScene *)scene;

-(void)adjustRingPositions;

-(SKSpriteNode *)startRing;

-(SKSpriteNode *)lastRing;

@end

Implementation code:

#import "ALRope.h"

@implementation ALRope

{
    SKTexture *_ringTexture;

    NSMutableArray *_ropeRings;
}

static CGFloat const RINGS_DISTANCE_DEFAULT = 0;

static CGFloat const JOINTS_FRICTION_TORQUE_DEFAULT = 0;

static CGFloat const RING_SCALE_DEFAULT = 1;

static int const RING_COUNT_DEFAULT = 30;

static CGFloat const RINGS_Z_POSITION_DEFAULT = 1;

static BOOL const SHOULD_ENABLE_JOINTS_ANGLE_LIMITS_DEFAULT = NO;

static CGFloat const JOINT_LOWER_ANGLE_LIMIT_DEFAULT = -M_PI / 3;

static CGFloat const JOINT_UPPER_ANGLE_LIMIT_DEFAULT = M_PI / 3;

static CGFloat const RING_FRICTION_DEFAULT = 0;

static CGFloat const RING_RESTITUTION_DEFAULT = 0;

static CGFloat const RING_MASS_DEFAULT = -1;


-(instancetype)initWithRingTexture:(SKTexture *)ringTexture
{
    if(self = [super init]) {
        _ringTexture = ringTexture;

        //apply defaults
        _startRingPosition = CGPointMake(0, 0);
        _ringsDistance = RINGS_DISTANCE_DEFAULT;
        _jointsFrictionTorque = JOINTS_FRICTION_TORQUE_DEFAULT;
        _ringScale = RING_SCALE_DEFAULT;
        _ringCount = RING_COUNT_DEFAULT;
        _ringsZPosition = RINGS_Z_POSITION_DEFAULT;
        _shouldEnableJointsAngleLimits = SHOULD_ENABLE_JOINTS_ANGLE_LIMITS_DEFAULT;
        _jointsLowerAngleLimit = JOINT_LOWER_ANGLE_LIMIT_DEFAULT;
        _jointsUpperAngleLimit = JOINT_UPPER_ANGLE_LIMIT_DEFAULT;
        _ringFriction = RING_FRICTION_DEFAULT;
        _ringRestitution = RING_RESTITUTION_DEFAULT;
        _ringMass = RING_MASS_DEFAULT;
    }
    return self;
}


-(void)buildRopeWithScene:(SKScene *)scene
{
    _ropeRings = [NSMutableArray new];
    SKSpriteNode *firstRing = [self addRopeRingWithPosition:_startRingPosition underScene:scene];

    SKSpriteNode *lastRing = firstRing;
    CGPoint position;
    for (int i = 1; i < _ringCount; i++) {
        position = CGPointMake(lastRing.position.x, lastRing.position.y - lastRing.size.height - _ringsDistance);
        lastRing = [self addRopeRingWithPosition:position underScene:scene];
    }

    [self addJointsWithScene:scene];
}

-(SKSpriteNode *)addRopeRingWithPosition:(CGPoint)position underScene:(SKScene *)scene
{
    SKSpriteNode *ring = [SKSpriteNode spriteNodeWithTexture:_ringTexture];
    ring.xScale = ring.yScale = _ringScale;
    ring.position = position;
    ring.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:ring.size.height / 2];
    ring.physicsBody.allowsRotation = YES;
    ring.physicsBody.friction = _ringFriction;
    ring.physicsBody.restitution = _ringRestitution;
    if(_ringMass > 0) {
        ring.physicsBody.mass = _ringMass;
    }

    [scene addChild:ring];
    [_ropeRings addObject:ring];
    return ring;
}

-(void)addJointsWithScene:(SKScene *)scene
{
    for (int i = 1; i < _ropeRings.count; i++) {
        SKSpriteNode *nodeA = [_ropeRings objectAtIndex:i-1];
        SKSpriteNode *nodeB = [_ropeRings objectAtIndex:i];
        SKPhysicsJointPin *joint = [SKPhysicsJointPin jointWithBodyA:nodeA.physicsBody
                                                               bodyB:nodeB.physicsBody
                                                              anchor:CGPointMake(nodeA.position.x,
                                                                                 nodeA.position.y - (nodeA.size.height + _ringsDistance) / 2)];
        joint.frictionTorque = _jointsFrictionTorque;
        joint.shouldEnableLimits = _shouldEnableJointsAngleLimits;
        if(_shouldEnableJointsAngleLimits) {
            joint.lowerAngleLimit = _jointsLowerAngleLimit;
            joint.upperAngleLimit = _jointsUpperAngleLimit;
        }
        [scene.physicsWorld addJoint:joint];
    }
}

//workaround for elastic effect should be called from didSimulatePhysics
-(void)adjustRingPositions
{
    //based on zRotations of all rings and the position of start ring adjust the rest of the rings positions starting from top to bottom
    for (int i = 1; i < _ropeRings.count; i++) {
        SKSpriteNode *nodeA = [_ropeRings objectAtIndex:i-1];
        SKSpriteNode *nodeB = [_ropeRings objectAtIndex:i];
        CGFloat thetaA = nodeA.zRotation - M_PI / 2,
        thetaB = nodeB.zRotation + M_PI / 2,
        jointRadius = (_ringsDistance + nodeA.size.height) / 2,
        xJoint = jointRadius * cosf(thetaA) + nodeA.position.x,
        yJoint = jointRadius * sinf(thetaA) + nodeA.position.y,
        theta = thetaB - M_PI,
        xB = jointRadius * cosf(theta) + xJoint,
        yB = jointRadius * sinf(theta) + yJoint;
        nodeB.position = CGPointMake(xB, yB);
    }
}

-(SKSpriteNode *)startRing
{
    return _ropeRings[0];
}

-(SKSpriteNode *)lastRing
{
    return [_ropeRings lastObject];
}

@end

Scene Code:

#import "ALRopeDemoScene.h"
#import "ALRope.h"

@implementation ALRopeDemoScene
{
    __weak SKSpriteNode *_branch;

    CGPoint _touchLastPosition;

    BOOL _branchIsMoving;

    ALRope *_rope;
}

-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        /* Setup your scene here */

        self.backgroundColor = [SKColor colorWithRed:0.2 green:0.5 blue:0.6 alpha:1.0];

        [self buildScene];
    }
    return self;
}

-(void) buildScene {
    SKSpriteNode *branch = [SKSpriteNode spriteNodeWithImageNamed:@"Branch"];
    _branch = branch;
    branch.position = CGPointMake(CGRectGetMaxX(self.frame) - branch.size.width / 2,
                                  CGRectGetMidY(self.frame) + 200);
    branch.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(2, 10)];
    branch.physicsBody.dynamic = NO;
    [self addChild:branch];
    _rope = [[ALRope alloc] initWithRingTexture:[SKTexture textureWithImageNamed:@"rope_ring"]];

    //configure rope params if needed
    //    _rope.ringCount = ...;//default is 30
    //    _rope.ringScale = ...;//default is 1
    //    _rope.ringsDistance = ...;//default is 0
    //    _rope.jointsFrictionTorque = ...;//default is 0
    //    _rope.ringsZPosition = ...;//default is 1
    //    _rope.ringFriction = ...;//default is 0
    //    _rope.ringRestitution = ...;//default is 0
    //    _rope.ringMass = ...;//ignored unless mass > 0; default -1
    //    _rope.shouldEnableJointsAngleLimits = ...;//default is NO
    //    _rope.jointsLowerAngleLimit = ...;//default is -M_PI/3
    //    _rope.jointsUpperAngleLimit = ...;//default is M_PI/3

    _rope.startRingPosition = CGPointMake(branch.position.x - 100, branch.position.y);//default is (0, 0)

    [_rope buildRopeWithScene:self];

    //attach rope to branch
    SKSpriteNode *startRing = [_rope startRing];
    CGPoint jointAnchor = CGPointMake(startRing.position.x, startRing.position.y + startRing.size.height / 2);
    SKPhysicsJointPin *joint = [SKPhysicsJointPin jointWithBodyA:branch.physicsBody bodyB:startRing.physicsBody anchor:jointAnchor];
    [self.physicsWorld addJoint:joint];
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInNode:self];
    if(CGRectContainsPoint(_branch.frame, location)) {
        _branchIsMoving = YES;
        _touchLastPosition = location;
    }
}

-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    _branchIsMoving = NO;

}

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

    if(_branchIsMoving) {
        UITouch *touch = [touches anyObject];
        CGPoint location = [touch locationInNode:self],
        branchCurrentPosition = _branch.position;
        CGFloat dx = location.x - _touchLastPosition.x,
        dy = location.y - _touchLastPosition.y;
        _branch.position = CGPointMake(branchCurrentPosition.x + dx, branchCurrentPosition.y + dy);
        _touchLastPosition = location;
    }
}

-(void)didSimulatePhysics
{
    //workaround for elastic effect
    [_rope adjustRingPositions];
}


@end

Notice the [rope adjustRingPositions] call from [scene didSimulatePhysics]. That is my actual fix for the elastic bug.

Complete demo code is here

Fahy answered 18/1, 2015 at 21:16 Comment(1)
Could you add the relevant code instead of giving a link?Duro

© 2022 - 2024 — McMap. All rights reserved.