Networked Platformer

This weekend I decided I wanted to try my hand at making a non-turn-based game using Photon Unity Networking. The only multiplayer games I have ever designed have been basically “call a REST service with move data, have the server validate the move, respond with result” type of stuff. Nothing built for actual real-time gameplay.

Digging into Photon has been pretty easy. The docs are decent enough, and there are a moderate amount of examples online. Photon really lets you get up and running using the Photon Cloud to start testing out your networked game.

So here’s a shot at what I have been making:

Super Coin Alien

Basically its a simple multiplayer version of Super Crate Box. Players can move left and right, jump, shoot each other, shoot enemies, and collect coins. This weekend I have been focusing on the multiplayer aspects of it - trying to make sure that if I booted up two instances of the game, that gameplay looked alright. Again, reiterating that I have never done real-time multiplayer before, I had no idea how tricky this stuff can get.

So the basics of Photon comes down to two aspects - RPCs and serializing data. Since the game is a fast-paced 2d platformer, I currently don’t have much RPCs I need, so this post will only focus on the serialization aspect.

What you do is attach a component called a PhotonView to your entity, and match it up with a Component that this view is observing. So in my case, I attached a PhotonView to each of the players and pointed it to the component that controls them.

Photon View

In this observed component you then write a serialize view method:

void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
  if (stream.isWriting) { // this our component, we are writing out
    stream.SendNext(transform.position);
  } else { // this is not our component, we are reading
    Vector3 newPosition = (Vector3) stream.ReceiveNext();
    transform.position = newPosition;
  }
}

In the above example, the stream.isWriting is called a couple times a frame via the PhotonView (adjustable via the sendRateOnSerialize property) and sends out the position of the entity.

If this component is attached to an entity that the client does not own, it will instead read in data from the stream and deserialize it. In this case we read in the entity’s position data and immediately set the entity’s current position to it.

The problem here is that updates are not smooth - they come unreliably at different intervals depending on latency and other factors, so the entity appears extremely jittery.

One way most people seem to handle this is to do linear interpolation between the entity’s read value and its current value, somewhat like this.

Vector3 syncPosition = new Vector3.zero;

void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
  if (stream.isWriting) { // this our component, we are writing out
    stream.SendNext(transform.position);
  } else { // this is not our component, we are reading
    syncPosition = (Vector3) stream.ReceiveNext();
  }
}

void Update() {
  if (photonView.isMine) {
    // ... update normally
  } else {
    transform.position = Vector3.Lerp(transform.position, syncPosition, 10f * Time.deltaTime);
  }
}

This seems like a fine enough solution, but I found for my game that it still suffered from jitters.

The current solution I am trying attempt to extrapolate the next position from the velocity, so that rending happens a lot more smoothly. Of course, I lose some accuracy doing this - characters rubber-band between positions a bit, they sometimes leap into the ground before being corrected, etc. But for a prototype it works well enough. Here’s an example of this:

public float interpolationSmoothing = 1000f;
public float teleportatThreshold = 2f;

void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
  if (stream.isWriting) {
    stream.SendNext(transform.position);
    stream.SendNext(velocity);
  } else {
    syncPosition = (Vector3) stream.ReceiveNext();
    syncVelocity = (Vector3) stream.ReceiveNext();

    lastSyncTime = PhotonNetwork.time;
  }
}

void Update() {
  if (!photonView.isMine) {
    float timeSinceLastUpdate = (float) (PhotonNetwork.time - lastSyncTime);

    float speed = syncVelocity.magnitude >= 1 ? syncVelocity.magnitude : interpolationSmoothing;

    Vector3 extrapolatedPosition = syncPosition + syncVelocity * timeSinceLastUpdate;
    Vector3 newPosition = Vector3.MoveTowards(transform.position, extrapolatedPosition, speed * Time.deltaTime);

    if (Vector3.Distance(transform.position, extrapolatedPosition) > teleportThreshold) {
      newPosition = extrapolatedPosition;
    }

    transform.position = newPosition;
}

Again, not perfect but good for a prototype. I don’t think I can do much prediction (even though I kind of am in the extrapolation) since players can jump or turn around at any moment.

Any suggestions? Anything I could read or look over in regards to this? Like I said I have this good for a prototype, but I’m not completely happy with this. Any more learning I can do on the subject would be great!

Thanks for reading!