RPG Movement Demo
In this demo we are going to synchronize basic movement of a character using Photon. The new PhotonTransformView
component gives us many options to dial in the movement and make it look as smooth for a remote character as it is for the local one.
This demo also shows how animation can be synchronized without sending any animation specific data. The animator is set up to react to changes in movement and since the movement is synchronized, the appropriate animations can still be played.
The character for the demo is the prefab "Robot Kyle Mecanim". On it you see PUN's components and the script IdleRunJump, which applies user-input to the character.
The Mecanim Demo makes use of the PhotonAnimatorView
which explicitly sends animation data.
The PhotonTransformView
The PhotonTransformView
can be added to any GameObject which also has a PhotonView
. After adding a PhotonTransformView
, you should add it to the "Observed Components" list of the PhotonView
(click the '+' below the list and drag and drop the PhotonTransformView
to the new field).
The PhotonTransformView
component has three checkboxes that allow you to enable/disable synchronization of position, rotation and scale. Once you choose to synchronize one of these, the component reveals more options that help you dial in how the data should be synchronized.
Let's go into detail what each of these options mean and how they help you create a smooth networked experience.
Synchronize Position
Position has the most varied options because games depend on position updates the most.
Position updates (like any other) are sent ten times per second. In a game with 100 frames per second, this means remote players get updates every 10 frames. In some cases, an update might arrive late or can get lost completely.
There are two concepts that try to counter both effects and make movement of remote-controlled objects as smooth as a local character:
- Interpolation: Since we cannot send position data each and every frame, the data we receive from another client is spotty and far from smooth. Interpolation takes into account that a character moved smoothly between individual updates. Instead of "teleporting" to the latest known position, a remote object is moved towards that location over the next few frames.
- Extrapolation: Extrapolation tries to estimate where the synchronized character actually should be "now", based on previously received data. It might help avoid stuttering when updates are late or lost but is always just a guess. It might need corrections and if it works well, depends on the type of game you develop.
Depending on your type of game, different inter- and extrapolation options work better and you should always experiment a little bit to see what looks best.
Interpolate Options
- Disabled: No Interpolation is done. Whenever a new position update arrives, it is immediately applied to the GameObject.
- FixedSpeed: Whenever a new position update arrives it is stored in a temporary variable and the character is being moved towards it using a fixed speed. This is useful for scenarios where you know the speed at which the synchronized object is travelling, which then results in very smooth movements. If the actual speed is changing however, or if the MoveTowards Speed value is different than the actual speed the object is travelling at, the movement will be jumpy.
- EstimatedSpeed: Again, the character is smoothly moved towards the last received position update. However this mode tries to estimate the objects speed by looking at the position difference of the newest update and the last one. This works best for objects that only change their speed slowly like vehicles.
- SynchronizeValues: In this mode the original object is sending its actual speed with each update so it can be used for interpolation. This works great if your character has sudden changes in velocity like in a jump and run where you can move left and right at will. This mode will send more data though since the movement speed and turn speed is synchronized as well. A script on the GameObject should call
SetSynchronizedValues(Vector3 speed, float turnSpeed)
to modify speed and turnspeed at runtime and update the synchronized values. As your movement script often already knows the speed at which the GameObject is moving, this mode often gives the smoothest results. - Lerp: Similar to MoveTowardsWithFixedSpeed but instead of using the MoveTowards function, the Lerp function is used. This results in a rubber band effect for the synchronized object.
Extrapolate Options
Extrapolation is a guess where the object actually is based on the last received position, its speed and the time passed since the last received update. This usually works best when you object does not change its speed very often. Objects that can change their direction instantaneously should not use extrapolation since it will result in ugly jittering when the wrongly guessed position is updated with a new one.
- Disabled: No Extrapolation is done.
- SynchronizeValues: This mode multiplies the time that passed since the last update with a synchronized speed and turn speed that the original update is sending. This is the most accurate option, but it also means more data is being sent. Remember that you have to define what the speed and turn speed is on the local GameObject by calling
SetSynchronizedValues( Vector3 speed, float turnSpeed)
. - EstimatedSpeed: In this mode the speed is estimated by calculating the distance between the most recent position update and the previous one.
- FixedSpeed: This mode allows you to define a fixed speed that is used to extrapolate the objects position.
Synchronize Rotation and Scale
Rotation and Scale only have a couple of interpolation options. In most games, the actual rotation and scale of the object is purely aesthetic and does not affect the gameplay at all. The interpolation options you have are:
- Disabled: Whenever a new value is received it is applied to the object immediately.
- RotateTowards/MoveTowards: The object is slowly rotated/scaled towards the target rotation/scale.
- Lerp: The Lerp function is used to rotate/scale the object towards its intended values.
Mecanim Animator Setup
This demo shows how you can synchronize animation without actually sending any animation specific data. The RPGMovement
class implements the movement and animation behaviour for this demo and its Update()
function is setup like this:
C#
void Update()
{
if( m_PhotonView.isMine == true )
{
UpdateRotateMovement();
UpdateForwardMovement();
UpdateBackwardMovement();
UpdateStrafeMovement();
UpdateGravity();
}
UpdateAnimation();
}
The movement is only handled if this object actually belongs to the player and the networked movement is handled by PhotonTransformView
However, we still update the animations for remote objects. The Animator has several parameters setup, the ones that are relevant for the movment animation are Speed
and Direction
.
We calculate these values based on the difference between the position the character was in during the last update and the most current position.
C#
void UpdateAnimation()
{
Vector3 movementVector = transform.position - m_LastPosition;
float speed = Vector3.Dot( movementVector.normalized, transform.forward );
float direction = Vector3.Dot( movementVector.normalized, transform.right );
m_Animator.SetFloat( "Speed", speed );
m_Animator.SetFloat( "Direction", direction );
m_LastPosition = transform.position;
}
Using the character's forward vector, we can then determine differences in speed and direction and pass them on to the Animator. By calculating these values based on the position, any position update received from PhotonTransformView
is also used in this calculation and therefore the animation works for synchronized objects.