How it's done: Spaceship control and physics



In this post I am going to talk abut two important things in the game: spaceship control and physics.

Zirthian makes full use of the iOS's Sprite Kit Physics Engine. I must say working with this engine was a bit rough to start with, but once you understand what's going on, you can create amazing things and very believable behaviour with minimal code.

Let's have a look:



If you'd like to try this out, become a tester! Plus you get free in-game content (I plan to release the game in the last quarter of 2020).

And now, here is what the control feels like and how this stuff actually works (spoiler alert: Objective-C code coming up!)

Control

To control the spaceship, you put your finger anywhere on the screen and then move it up and down to control the vertical motion. Because of the physics engine, what actually happens is that a vertical physical force is applied onto the spaceship, making the resulting movement a bit delayed and accelerated. As you play the game, this "feels more right" than if the vertical (y) position of the spaceship was changed directly.

Another thing you can do is slide your finger to the right to speed up and slide to the left to slow down. I wanted to give the player the option to whizz through the world (I eventually always crash!) or slow down the movement when they find the current situation too overwhelming.

The way the movement through the world actually works is that the spaceship stays in the same horizontal (x) position, while everything else moves at a negative speed of the spaceship - i.e. towards the spaceship!

Physics

In order to make the spaceship (or any other object in the world) be able to use the physics engine, I subclass everything from SKShapeNode and specify the self.path and self.physicalBody properties. In my code, all the in-game objects inherit from BaseInLevelSpriteWithBody class, which looks as follows:

@interface BaseInLevelSpriteWithBody : SKShapeNode {
...
}

@implementation BaseInLevelSprite

- (id) initWithPos:(CGPoint)pos_ size:(CGSize)size_ {
  if (self == [super init] ) {
    self.size = CGSizeMake(size_.width, size_.height);
    // can be any shape, a rectangle is a good start
    self.path = CGPathCreateWithRect(CGRectMake(0, 0, _size.width, _size.height), nil);
    self.position = pos_;
    self.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:self.path];
    ...
  }
  return self;
}


The hardest part was actually making the physics engine work with the kind of control that I described above. To create the illusion of everything moving towards the spaceship and to prevent crashes with other objects from moving the spaceship backwards, I have to manipulate the ship's x coordinate directly. However, the SpriteKit engine doesn't quite like that. Instead, you should really apply forces onto different objects, like I do for the up and down movement control. I had to put a few workarounds in for what happens when the player collides and is pushed backwards, as well as to prevent the spaceship from being completely stuck when it hits the terrain head on.

The whole spaceship movement code goes roughly like this:

- (void) update {

  [super update];

  ...

  //-- apply vertical force to move up and down
  [self.physicsBody applyForce:CGVectorMake(0, _ySpeed)];

  //-- handle crashes: the acceleration in x direction becomes negative.
  // After collision is resolved, the player accelerates to full speed
  // Note: Remember that _xSpeed doesn't actually move the spaceship.
  // Instead, all the other world objects move by negative _xSpeed
  if (_isColliding) {
    if (_isBouncingBackFromCollission) {
      //-- beginning of collision, go a bit back
      _xSpeed = -1;
    } else {
      //-- collision has been taking too long, stop
      _xSpeed = 0;
    }

  } else {
    //-- no collision, accelerate
    _xSpeed += 1;

    //-- add user speed control. When player is not controlling x speed
    // (not moving their finger horizontally), accelerate up to DEFAULT_SPEED
    // when user wants to speed up / slow down, accelerate / decelerate more,
    // within bounds of MIN_SPEED; MAX_SPEED
    if (_extraXSpeedFromPlayerControl == 0) {
      //-- player is not controlling the x speed
      if (_xSpeed > DEFAULT_SPEED) { _xSpeed = DEFAULT_SPEED; }
    } else {
      //-- player wants to speed up / slow down
      // (_extraXSpeedFromPlayerControl can be positive or negative)
      _xSpeed += _extraXSpeedFromPlayerControl;
      if (_xSpeed > MAX_SPEED) { _xSpeed = MAX_SPEED; }
      if (_xSpeed < MIN_SPEED) { _xSpeed = MIN_SPEED; }
    }
  }

  //-- make sure the player always continues flying forward
  // (i.e. is not sent backwards by the terrain)
  if (!_isColliding) {
    if (self.position.x != self.originalSpawningPosition.x) {
      self.position = CGPointMake(self.spawningPosition.x, self.position.y);
      //... but be careful with setting self.position directly
      //- can clash with what the physics engine wants to do,
      // more logic may be needed for special situations
    }
  }

  //-- straighten the ship to point forward if not colliding anymore
  if (!_isColliding) {
    if (fabs(self.zRotation) > 0.1) {
      [self.physicsBody applyTorque:-10*self.zRotation];
    } else if (fabs(self.zRotation) > 0.04) {
      [self.physicsBody applyTorque:-self.zRotation];
    } else {
      self.zRotation = 0;
    }
  }

  ...
}


And here is one more trick: In order to test for collisions with other in-game objects and get a reference to those objects at the same time, the code doesn't make use of SKPhysicsBody's collision masks. Instead, I directly test for the class of the object that the spaceship collides with in the update method. By obtaining a pointer like this, and casting it to the proper BaseInLevelSpriteWithBody class, I can manipulate the collided objects as well, e.g., tell them to take damage, to explode, or whatever else:

//-- test for collissions
_isColliding = false;
for (int i=0; i<[self.physicsBody.allContactedBodies count]; i++) {
  SKPhysicsBody* contactedBody = self.physicsBody.allContactedBodies[i];
  if (contactedBody != NULL && [contactedBody.node isKindOfClass:[BaseInLevelSpriteWithBody class]]) {
    _isColliding = true;
    //-- process the collision in a separate method that accepts
    // BaseInLevelSpriteWithBody* as an argument
    [self onCollision:(BaseInLevelSpriteWithBody*)contactedBody.node];
  }
}


And that's it for spaceship control and physics! The rule of thumb is basically to try to let the SpriteKit physics engine do as much as possible in order to prevent weird glitches in behaviour. If that's not convenient, you can still change the position and rotation of your in-game objects directly, but triple-test for special cases, especially for what happens when collisions of multiple objects are involved.

If you'd like more news and coding tricks, don't forget to sign up for the Zirthian project updates!