How to create a rope in SpriteKit?

前端 未结 4 756
别那么骄傲
别那么骄傲 2020-12-28 18:54

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

相关标签:
4条回答
  • 2020-12-28 19:11

    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

    0 讨论(0)
  • 2020-12-28 19:13

    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];
    }
    
    0 讨论(0)
  • 2020-12-28 19:13

    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)
    
    
    }
    
    0 讨论(0)
  • 2020-12-28 19:27

    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

    0 讨论(0)
提交回复
热议问题