0

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 :/

02fentym
  • 1,762
  • 2
  • 16
  • 29
  • Try grouping wheel animation action along with train moving forward action. Something like this, `SKAction *trainMovent = [SKAction group:@[wheelAnimatingAction,trainForwardMovementAction]]; [self runAction:trainMovent];` – Jaffer Sheriff Aug 12 '15 at 07:30
  • Yeah I thought of that, but isn't there supposed to be some benefit to having nodes as children? I mean, in this case, sure it's only a couple of nodes I'd have to group, but when I have more cars in the train it becomes a nuisance. As stated in the question, I'm looking for a method that relies more on the physics interaction if possible. – 02fentym Aug 12 '15 at 07:39
  • Try implementing Train class with wheels as its child and also contains array of all of its wheels. When you run move forward action on train all wheels will move forward as they are child of Train Class. Also implement wheel rotating animation an array of wheels that you have while moving train forward. – Jaffer Sheriff Aug 12 '15 at 07:50
  • Did you solved this Issue ? – Jaffer Sheriff Aug 12 '15 at 13:42
  • I tried it the way that you suggested, but every time the train is moved left x units and then right x units, it doesn't always move that amount. I also tried subclassing SKSpriteNode by creating a Train class, but the problem I kept running into is that the train's body will move left and right, but rotating the wheels results in a mess. I add each node to the train object separately. I ran the move left/right action on `self` and I ran the rotate action on the wheels that are child nodes of `self`. There has to be a way, but it continues to elude me. – 02fentym Aug 12 '15 at 22:07

1 Answers1

0

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.

Jaffer Sheriff
  • 1,444
  • 13
  • 33
  • I know how to do that as well. Take a look at the question title, I'm looking for a solution that involves using physics bodies for a more realistic look. I know it's possible, I'm just not sure how to solve the problem I have. Your solution doesn't take physics into account at all. For example: [spritekit car](http://youtu.be/-PtMlelU90w) – 02fentym Aug 13 '15 at 12:01