Using SKPhysicsBody to create a vehicle

174 views Asked by At

I am trying to make a vehicle (in this case a train) move based on player input. If I move the train via an SKAction, the wheels do not rotate. I could use the applyForce method on its physics body, but it I need more control. I need to make it move a certain distance over a certain amount of time. How can this be accomplished?

-(void)didMoveToView:(SKView *)view {
    SKTexture *trainBodyTexture = [SKTexture textureWithImageNamed:@"levelselect_trainbody"];
    SKSpriteNode *trainBody = [[SKSpriteNode alloc] initWithTexture:trainBodyTexture];
    trainBody.zPosition = 0;
    trainBody.position = CGPointMake(300, 500);
    trainBody.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:trainBody.size];
    [self addChild:trainBody];

    SKTexture *trainWheelTexture = [SKTexture textureWithImageNamed:@"levelselect_trainwheel"];
    SKSpriteNode *trainWheel1 = [[SKSpriteNode alloc] initWithTexture:trainWheelTexture];
    trainWheel1.zPosition = 1;
    trainWheel1.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:trainWheel1.size.width/2];
    trainWheel1.physicsBody.allowsRotation = YES;
    trainWheel1.position = CGPointMake(220, 400);
    [self addChild:trainWheel1];

    SKSpriteNode *trainWheel2 = [[SKSpriteNode alloc] initWithTexture:trainWheelTexture];
    trainWheel2.zPosition = 1;
    trainWheel2.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:trainWheel2.size.width/2];
    trainWheel2.physicsBody.allowsRotation = YES;
    trainWheel2.position = CGPointMake(380, 400);
    [self addChild:trainWheel2];

    SKShapeNode *dot = [SKShapeNode shapeNodeWithCircleOfRadius:10];
    dot.zPosition = 2;
    dot.fillColor = [NSColor redColor];
    dot.position = CGPointMake(0, -20);
    [trainWheel1 addChild:dot];

    SKPhysicsJointPin *pin = [SKPhysicsJointPin jointWithBodyA:trainBody.physicsBody bodyB:trainWheel1.physicsBody anchor:trainWheel1.position];
    SKPhysicsJointPin *pin2 = [SKPhysicsJointPin jointWithBodyA:trainBody.physicsBody bodyB:trainWheel2.physicsBody anchor:trainWheel2.position];

    [self.scene.physicsWorld addJoint:pin];
    [self.scene.physicsWorld addJoint:pin2];

    //[trainWheel1 runAction:[SKAction moveByX:300 y:0 duration:3]];
    [trainBody.physicsBody applyForce:CGVectorMake(3000, 0)];

}



UPDATE: Implemented With A Train Class (suggested by Jaffer Sheriff)

Train.h

#import <SpriteKit/SpriteKit.h>

@interface Train : SKSpriteNode

-(void) createPhysics;
-(void) moveLeft;
-(void) moveRight;

@end

Train.m

#import "Train.h"

@interface Train()

@property SKSpriteNode *trainBody, *trainWheelFront, *trainWheelRear;
@property SKPhysicsWorld *physicsWorld;

@end

@implementation Train

-(instancetype) init {
    if (self = [super init]) {
        [self initTrainBody];
        [self initWheels];
    }
    return self;
}

-(void) initTrainBody {
    SKTexture *trainBodyTexture = [SKTexture textureWithImageNamed:@"levelselect_trainbody"];
    _trainBody = [[SKSpriteNode alloc] initWithTexture:trainBodyTexture];
    _trainBody.zPosition = 0;
    _trainBody.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:_trainBody.size];
    [self addChild:_trainBody];
}

-(void) initWheels {
    SKTexture *_trainWheelTexture = [SKTexture textureWithImageNamed:@"levelselect_trainwheel"];
    _trainWheelFront = [[SKSpriteNode alloc] initWithTexture:_trainWheelTexture];
    _trainWheelFront.zPosition = 1;
    _trainWheelFront.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:_trainWheelFront.size.width/2];
    _trainWheelFront.physicsBody.allowsRotation = YES;
    _trainWheelFront.position = CGPointMake(-80, -82);
    [self addChild:_trainWheelFront];

    _trainWheelRear = [[SKSpriteNode alloc] initWithTexture:_trainWheelTexture];
    _trainWheelRear.zPosition = 1;
    _trainWheelRear.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:_trainWheelRear.size.width/2];
    _trainWheelRear.physicsBody.allowsRotation = YES;
    _trainWheelRear.position = CGPointMake(80, -82);
    [self addChild:_trainWheelRear];

    //dot used to see if wheels are rotating, no other point
    SKShapeNode *dot = [SKShapeNode shapeNodeWithCircleOfRadius:10];
    dot.zPosition = 2;
    dot.fillColor = [NSColor redColor];
    dot.position = CGPointMake(0, -20);
    [_trainWheelFront addChild:dot];
}

//this method is called after the train node is added to the scene in GameScene otherwise will get error adding joints before node is in scene
-(void) createPhysics {
    SKPhysicsJointPin *pin = [SKPhysicsJointPin jointWithBodyA:_trainBody.physicsBody bodyB:_trainWheelFront.physicsBody anchor:_trainWheelFront.position];
    SKPhysicsJointPin *pin2 = [SKPhysicsJointPin jointWithBodyA:_trainBody.physicsBody bodyB:_trainWheelRear.physicsBody anchor:_trainWheelRear.position];

    [self.scene.physicsWorld addJoint:pin];
    [self.scene.physicsWorld addJoint:pin2];
}

-(void) moveLeft {
    SKAction *rotateLeft = [SKAction rotateByAngle:6*M_PI duration:0.2];
    [_trainWheelFront runAction:rotateLeft];
    [_trainWheelRear runAction:rotateLeft];
}

-(void) moveRight {
    SKAction *rotateRight = [SKAction rotateByAngle:-6*M_PI duration:0.2];
    [_trainWheelFront runAction:rotateRight];
    [_trainWheelRear runAction:rotateRight];
}


@end

GameScene.m

#import "GameScene.h"
#import "Train.h"

@interface GameScene()

@property Train *train;

@end

@implementation GameScene

-(void)didMoveToView:(SKView *)view {
    [self initTrain];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyPressed:) name:@"KeyPressedNotificationKey" object:nil]; //using notifications and custom view class to handle key presses

}

-(void) initTrain {
    _train = [[Train alloc] init];
    _train.position = CGPointMake(500, 300);
    [self addChild:_train];
    [_train createPhysics];
}

-(void)update:(CFTimeInterval)currentTime {
    /* Called before each frame is rendered */
}

-(void) keyPressed:(NSNotification*)notification {
    NSNumber *keyCodeObject = notification.userInfo[@"keyCode"];
    NSInteger keyCode = keyCodeObject.integerValue;
    NSLog(@"keycode = %lu", keyCode);
    switch (keyCode) {
        case 123:
            [self leftArrowPressed];
            break;
        case 124:
            [self rightArrowPressed];
            break;
    }
}

-(void) leftArrowPressed {
    SKAction *moveLeft = [SKAction moveByX:-200 y:0 duration:0.2];
    [_train runAction:moveLeft];
    [_train moveLeft];
}

-(void) rightArrowPressed {
    SKAction *moveRight = [SKAction moveByX:200 y:0 duration:0.2];
    [_train runAction:moveRight];
    [_train moveRight];
}

@end

Note: This solution causes the entire train to flip and freak out when the left/right keys are pressed. It seems like my pin joints are incorrect, but they seem correct to me :/

1

There are 1 answers

1
Jaffer Sheriff On

Default Initializer of SkSpriteNode is - (instancetype)initWithTexture:(SKTexture *)texture color:(SKColor *)color size:(CGSize)size; Try this,

@interface Train : SKSpriteNode
- (instancetype)initTrainWithColor:(UIColor *) color andSize:(CGSize) size;
-(void) animateWheelsWithTime:(float) time;
@end


@interface Train ()
{
   SKSpriteNode *wheel1;
   SKSpriteNode *wheel2;
   NSMutableArray *wheelsArray;
 }
@end

@implementation Train
- (instancetype)initTrainWithColor:(UIColor *) color andSize:(CGSize) size
 {
 self = [super initWithTexture:nil color:color size:size]; 
 if (self)
 {
     [self addWheels];
 }
return self;
}
  -(void)addWheels
  {
   wheelsArray = [[NSMutableArray alloc]init];
   wheel1 = [SKSpriteNode spriteNodeWithImageNamed:@"wheel"];
   [wheel1 setPosition:CGPointMake(-(self.frame.size.width/2.0f-wheel1.frame.size.width/2.0f), - (self.frame.size.height/2.0f - wheel1.frame.size.height/2.0f))];
 [self addChild:wheel1];

 wheel2 = [SKSpriteNode spriteNodeWithImageNamed:@"wheel"];
 [wheel2 setPosition:CGPointMake((self.frame.size.width/2.0f-wheel2.frame.size.width/2.0f), - (self.frame.size.height/2.0f - wheel2.frame.size.height/2.0f))];
[self addChild:wheel2];

[wheelsArray addObject:wheel1];
[wheelsArray addObject:wheel2];
}

-(void) animateWheelsWithTime:(float) time
{
    for (SKSpriteNode *wheel in wheelsArray)
      {
        SKAction *act = [SKAction rotateByAngle:3*M_PI duration:time];
        [wheel runAction:act];
     }
}
@end

In GameScene.m add train like this,

  -(void) addTrain
   {
      Train *train1 = [[Train alloc]initTrainWithColor:[UIColor yellowColor] andSize:CGSizeMake(150, 100)];
      [train1 setPosition:CGPointMake(self.size.width/2.0f, self.size.height/2.0f)];
     [self addChild:train1];

   SKAction *act = [SKAction moveTo:CGPointMake(self.size.width/2.0f, self.size.height - 50) duration:2];
    [train1 runAction:act];
    [train1 animateWheelsWithTime:2];
 }

I tried and it works.