I faced a similar elasticity bug with a rope simulation and could finally come up with a workaround.
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
Rope Implementation:
#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 to showcase how to use the Rope:
#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 was the actual workaround for the elastic bug.
Complete demo code is here. I hope this helps!
SKPhysicsJointPin
s have some inherent elasticity that is not accessible to developers. – Fulltime