mercredi 28 novembre 2012

Tutorial: La mise en réseau sous Unity3D - Annexe 1

Salut tout le monde !

Désolé pour le retard pris dans mes publications du tutorial. Avant de passer à la partie 4 du tuto j'aimerais vous faire partager une petite découverte faite par Pascal Monnier, un grand merci à lui.

Si, contrairement à moi, vous avez testé le projet en réseau, vous aurez probablement constaté pas mal de problèmes de lags. Cela est dû, entre autre, au fait que nous déplaçons notre personnage en touchant directement à sa position plutôt qu'en lui appliquant des forces.

Du coup, sa position est envoyée régulièrement sur le réseau, mais entre chaque envoi, le personnage est tout simplement immobile tant qu'il ne reçoit pas de nouvelles instructions  Si un retard survient dans l'envoi ou que des packets se perdent sur le réseau, votre personnage recevra une position beaucoup plus éloignée que la sienne et fera un petit bond de téléportation. C'est moche et surtout ça ruine le gameplay.

Notez qu'on a pas ce problème avec les projectiles tirés puisqu'on synchronise leur rigidbody et donc leur vitesse. Entre deux reçus de données, le projectile continue donc son chemin.

Mais on va remédier à ce problème en "trichant" un peu. Entre deux reçus de position, on va tenter de deviner la prochaine position du player adverse, avant de la recevoir et de corriger les quelques imprécisions.

Ce processus s'appelle l'interpolation et vu qu'il est assez compliqué on va se contenter de copier/coller un script donné dans le tutorial réseau d'Unity. Je vous recommande la lecture de cet article pour mieux comprendre le principe. C'est expliqué de manière totalement accessible et en français donc ne vous en privez pas.

Je vous encourage aussi à lire le script et à essayer de le comprendre quand vous aurez le skill pour. Mais il y a de fortes chances que ce soit du chinois pour vous actuellement.

Vous pouvez le télécharger ici: NetworkRigidbody.cs

Je vous le copie en fin d'article au cas où le lien viendrait à mourir.

Je m'excuse pour la mise en page un peu moisie, j'ai copier/coller le truc du PDF Unity et évidemment c'est pas super.

La suite est simple. On va simplement placer le script sur notre "Player" et l'assigner à la NetworkView à la place du Transform de l'objet.


Et voila, en principe c'est bon :)

Une fois encore je n'ai pas le matériel pour tester ça sur un réseau donc faites moi savoir s'il manque une étape ou si ça ne fonctionne pas correctement.

Et comme promis voici le script. Attention, c'est à coller dans un script en C#:

______________________________________


using UnityEngine;
using System.Collections;

public class NetworkRigidbody : MonoBehaviour
{
public double m_InterpolationBackTime = 0.1;
public double m_ExtrapolationLimit = 0.5;
internal struct  State
  {
internal double timestamp;
internal Vector3 pos;
internal Vector3 velocity;
internal Quaternion rot;
internal Vector3 angularVelocity;
  }
  // We store twenty states with "playback" information
  State[] m_BufferedState = new State[20];
  // Keep track of what slots are used
  int m_TimestampCount;
  void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info) {
  // Send data to server
  if (stream.isWriting)
  {
Vector3 pos = rigidbody.position;
Quaternion rot = rigidbody.rotation;
Vector3 velocity = rigidbody.velocity;
Vector3 angularVelocity = rigidbody.angularVelocity;
stream.Serialize(ref pos);
stream.Serialize(ref velocity);
stream.Serialize(ref rot);
stream.Serialize(ref angularVelocity);
  }
  // Read data from remote client
else
{
  Vector3 pos = Vector3.zero;
  Vector3 velocity = Vector3.zero;
  Quaternion rot = Quaternion.identity;
  Vector3 angularVelocity = Vector3.zero;
  stream.Serialize(ref pos);
  stream.Serialize(ref velocity);
  stream.Serialize(ref rot);
  stream.Serialize(ref angularVelocity);
 
  // Shift the buffer sideways, deleting state 20
  for (int i = m_BufferedState.Length-1; i >= 1; i--)
  {
    m_BufferedState[i] = m_BufferedState[i-1];
  }
 
  // Record current state in slot 0
  State state;
  state.timestamp = info.timestamp;
  state.pos = pos;
  state.velocity = velocity;
  state.rot = rot;
  state.angularVelocity = angularVelocity;
  m_BufferedState[0] = state;
 
  // Update used slot count, however never exceed the buffer size
  // Slots aren't actually freed so this just makes sure the buffer is
  // filled up and that uninitalized slots aren't used.
  m_TimestampCount = Mathf.Min(m_TimestampCount + 1, m_BufferedState.Length);
  // Check if states are in order, if it is inconsistent you could reshuffel or
  // drop the out-of-order state. Nothing is done here
  for (int i=0;i   {
   if (m_BufferedState[i].timestamp < m_BufferedState[i+1].timestamp)
    Debug.Log("State inconsistent");
  }
  }
  }

 // We have a window of interpolationBackTime where we basically play
 // By having interpolationBackTime the average ping, you will usually use interpolation.
 // And only if no more data arrives we will use extra polation
 void Update () {
  // This is the target playback time of the rigid body
  double interpolationTime = Network.time - m_InterpolationBackTime;
 
  // Use interpolation if the target playback time is present in the buffer
  if (m_BufferedState[0].timestamp > interpolationTime)
  {
   // Go through buffer and find correct state to play back
   for (int i=0;i   {
    if (m_BufferedState[i].timestamp <= interpolationTime || i == m_TimestampCount-1)
    {
     // The state one slot newer (<100ms best="best" p="p" playback="playback" state="state" than="than" the="the">     State rhs = m_BufferedState[Mathf.Max(i-1, 0)];
     // The best playback state (closest to 100 ms old (default time))
     State lhs = m_BufferedState[i];
   
     // Use the time between the two slots to determine if interpolation is necessary
     double length = rhs.timestamp - lhs.timestamp;
     float t = 0.0F;
     // As the time difference gets closer to 100 ms t gets closer to 1 in
     // which case rhs is only used
     // Example:
     // Time is 10.000, so sampleTime is 9.900
     // lhs.time is 9.910 rhs.time is 9.980 length is 0.070
     // t is 9.900 - 9.910 / 0.070 = 0.14. So it uses 14% of rhs, 86% of lhs
     if (length > 0.0001)
      t = (float)((interpolationTime - lhs.timestamp) / length);
   
     // if t=0 => lhs is used directly
     transform.localPosition = Vector3.Lerp(lhs.pos, rhs.pos, t);
     transform.localRotation = Quaternion.Slerp(lhs.rot, rhs.rot, t);
     return;
    }
   }
  }
  // Use extrapolation
  else
  {
  State latest = m_BufferedState[0];

  float extrapolationLength = (float)(interpolationTime - latest.timestamp);
  // Don't extrapolation for more than 500 ms, you would need to do that carefully
  if (extrapolationLength < m_ExtrapolationLimit)
  {
   float axisLength = extrapolationLength * latest.angularVelocity.magnitude * Mathf.Rad2Deg;
   Quaternion angularRotation = Quaternion.AngleAxis(axisLength, latest.angularVelocity);
 
   rigidbody.position = latest.pos + latest.velocity * extrapolationLength;
   rigidbody.rotation = angularRotation * latest.rot;
   rigidbody.velocity = latest.velocity;
   rigidbody.angularVelocity = latest.angularVelocity;
    }
  }
  }
}

______________________________________

Nous voila armés pour la partie 4, hiaaarrr !