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 :)
You can assume SpriteKit is at Top of the SceneKit, As using SceneKit you can add 3D models into Augmented Reality while SpriteKit is used to add extra sprites onto the model. In short SpriteKit is revolution in Gaming.
SpriteKit is one of the best ways to make games on iOS. It's easy to learn, powerful, and is fully supported by Apple. Swift is an easy language to get started with, especially if you are a beginner to the iOS platform.
SceneKit, SpriteKit and Core Image all have some integration with Metal shaders and Metal rendering. There may be times when you don't want to write a full-blown 3D Metal app, but you want to take advantage of SceneKit.
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
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];
}
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)
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With