Oct 012014
 

This is part 2 of how my spaceship building/fighting game is structured. Find part 1 here.

Space network synchronisation

Each player’s ship consists of a parent object with a PlayerShip script and a Rigidbody 2D, and a bunch of children (one for each attached ship module). I very much like the fact that you can just add a selection of children with box colliders (i.e. the modules) to an object with a Rigidbody and the physics interactions Just Work (certainly well enough for my purposes).

With that in mind, the only objects created with a Network.Instantiate() are the parent ship objects, one for each player. The server owns all network-instantiated objects, and nothing is network-instantiated on a client. The server keeps track of which object belongs to which player.

The clients have already been told which modules make up all the ships, so they create them all locally and attach them as children of the ships. The parent PlayerShips are the only things in the game that use the NetworkView state synchronisation (which automatically updates position and rotation 15 times/second). This is very efficient as there is only one synchronised object per player.

Prediction and interpolation

The ships use some simple prediction to minimise the effects of lag. I’ve seen a few people asking about how this works, so here’s the serialisation code:

void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info)
{
  if (stream.isWriting)
  {
    Vector3 position = rigidbody2D.position;
    Vector3 velocity = rigidbody2D.velocity;
    float rotation = rigidbody2D.rotation;
    float rotSpeed = rigidbody2D.angularVelocity;

    stream.Serialize(ref position);
    stream.Serialize(ref velocity);
    stream.Serialize(ref rotation);
    stream.Serialize(ref rotSpeed);
  }
  else
  {
    stream.Serialize(ref syncPosition);
    stream.Serialize(ref syncVelocity);
    stream.Serialize(ref syncRotation);
    stream.Serialize(ref syncRotSpeed);

    syncPositionFrom = transform.position;
    syncRotationFrom = transform.rotation.eulerAngles.z;
    syncBlendTime = 0.0f;
  }
}

And here’s the update code to calculate the transform every frame:

void Update()
{
  if (!Network.isServer)
  {
    syncBlendTime += Time.deltaTime;
    float blend = Mathf.Min(1.0f, syncBlendTime / blendTimeMax);
    transform.position = Vector3.Lerp(syncPositionFrom, syncPosition, blend);

    float newRot = Mathf.LerpAngle(syncRotationFrom, syncRotation, blend);
    transform.rotation = Quaternion.Euler(0.0f, 0.0f, newRot);

    // Update the from and to values by velocity.
    syncPositionFrom += syncVelocity * Time.deltaTime;
    syncPosition += syncVelocity * Time.deltaTime;
    syncRotationFrom += syncRotSpeed * Time.deltaTime;
    syncRotation += syncRotSpeed * Time.deltaTime;
  }
}

This will predict the position/rotation in the frames following an update, and blend out previous prediction errors over blendTimeMax (set it the same as your time between updates). This will fix all positional discontinuities (nothing will pop to a new position) but there will still be first-order discontinuities (velocity will pop).

That’s not a problem at all for the other ships, as it’s not noticeable in a game like this with slow controls. The only issue is if the camera is fixed relative to your ship (which it currently the case), because a tiny change in the ship rotation leads to a large movement of the background at the edge of the screen. It’s still barely noticeable, but ideally the camera position/rotation needs to be slightly elastic.

Controlling the ship

The Space scene contains a PlayerControls script which takes input from the keyboard and sends it to the server. You have controls for applying thrust in four directions (forwards, backwards, left and right), firing in each of the for directions, and steering left and right. The PlayerControls sends an RPC to the server whenever any of the inputs change (e.g. started or stopped steering) to minimise server calls. On the server, the inputs are passed to the PlayerShip owned by that player.

Ships are controlled by applying physics forces to the Rigidbody. Every FixedUpdate(), the PlayerShip uses GetComponentsInChildren() to find all the Engine components (scripts attached to the engine module prefabs) and send them the net horizontal and vertical thrust. If the engine is facing the right way is applies a force to the parent Rigidbody with AddForceAtPosition().

Applying the force at the actual engine location results is wild spinning for even slightly unbalanced ships, so I blend the position nearly all the way back towards the centre of mass to make is more controllable (97% of the way in this case, and even then it’s hard to drive with off-centre engines).

Steering simply uses AddTorque() to rotate the ship.

shipgame1

A ship with unbalanced engines

Weapons

Weapons are fired in a slightly different way to engines. Because there are a variety of weapons systems, I use BroadcastMessage() to call a function on every child script that responds to it (scripts are added to each weapon module). Each weapon script keeps track of its own cooldown and fires if it can.

Firing weapons creates Projectile objects. Each weapon module prefab has a Projectile prefab referenced in it, and each Projectile can have different graphics, speed, lifetime, damage and particle effects. The projectile is created on all clients by doing a Network.Instantiate().

Because projectiles go in straight lines there is no need to synchronise the transforms over the network. The initial position and velocity are set precisely the same as on the server, and the parent ship velocity is added immediately after creation with an RPC (use the new object’s networkView.viewID to identify it on the client). The projectile can then move itself locally and be in exactly the right place.

Impacts and damage are all calculated on the server. OnTriggerEnter2D() is used to detect when a ShipModule has been hit. Network.Destroy() is called on the projectile, a particle effect is instantiated on all clients, and damage is applied to the ShipModule.

If the ShipModule has been destroyed it informs the parent PlayerShip which checks if any other components are no longer connect. RPCs are then used to tell clients to destroy the relevant modules. If the red central component is destroyed then you’re out of the game.

No physics calculations or collisions are processed on the clients at all. The game is entirely server-authoritative to minimise cheating possibilities – the clients simply sends off inputs and receive updates. Overall I’m pretty happy with how it all works, and very happy that the entire game comes in at under 3000 lines of code (total, including comments and blank space)!

Next it needs a load of polish – better graphics, add some sounds, a building tutorial etc, but it’s a decent start.

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

(required)

(required)

This site uses Akismet to reduce spam. Learn how your comment data is processed.