Skip to content

Instantly share code, notes, and snippets.

@cnsoft
Last active December 28, 2015 23:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cnsoft/7576631 to your computer and use it in GitHub Desktop.
Save cnsoft/7576631 to your computer and use it in GitHub Desktop.
StrictCharacter Logic . (use timestamp slots to store and cache the movement data.
// (c)2011 MuchDifferent. All Rights Reserved.
using System;
using System.Collections.Generic;
using UnityEngine;
using uLink;
/// 检查是否移动过快.并进行纠正. :)
/// <summary>
/// A script example that can be use for players' objects in a 3d game without gravity.
/// The object is floating in space just like a spaceship or a submarine does.
/// </summary>
/// <remarks>
/// When using this example script, it should be added as a component to the game object that a player controls.
/// The server should be authoritative when using this script (uLink.Network.isAuthoritativeServer = true).
/// The basic idea is that the server simulates all physics and checks if any player tries to cheat by
/// sending movment orders as an RPC (The RPC name is ServerMove) with false coordinates to move faster than allowed in the game.
/// The server checks the incoming ServerMove RPC from the client and sends two kinds of RPCs back to the client.
/// If the client did move too fast (due to a cheating attempt or a bug or whatever) the server sends an RPC named
/// AdjustOwnerPos. If the position is good, the server sends an RPC named GoodOwnerPos. They are both sent as unreliable
/// RPCs from the server to the client to minimize server resources.
///
/// This script component also makes sure interpolation and extrapolation is used for the state synchronozation sent from
/// the server to clients. The state synchronization, arriving at the client, is stored in an internal array and the
/// public properties interpolationBackTime and extrapolationLimit can be used to tune the correct behavior for every game.
/// Please read the code for more details.
/// </remarks>
[AddComponentMenu("uLink Utilities/Strict Character")]
[RequireComponent(typeof(uLinkNetworkView))]
public class uLinkStrictCharacter : uLink.MonoBehaviour
{
private struct State
{
public double timestamp;
public Vector3 pos;
public Vector3 vel;
public Quaternion rot;
}
private struct Move : IComparable<Move>
{
public double timestamp;
public float deltaTime;
public Vector3 vel;
public static bool operator ==(Move lhs, Move rhs) { return lhs.timestamp == rhs.timestamp; }
public static bool operator !=(Move lhs, Move rhs) { return lhs.timestamp != rhs.timestamp; }
public static bool operator >=(Move lhs, Move rhs) { return lhs.timestamp >= rhs.timestamp; }
public static bool operator <=(Move lhs, Move rhs) { return lhs.timestamp <= rhs.timestamp; }
public static bool operator >(Move lhs, Move rhs) { return lhs.timestamp > rhs.timestamp; }
public static bool operator <(Move lhs, Move rhs) { return lhs.timestamp < rhs.timestamp; }
public override bool Equals(object other)
{
if (other == null || !(other is Move))
return false;
return this == (Move)other;
}
public override int GetHashCode()
{
return timestamp.GetHashCode();
}
public int CompareTo(Move other)
{
if (this > other)
return 1;
if (this < other)
return -1;
return 0;
}
}
public double interpolationBackTime = 0.2;
public double extrapolationLimit = 0.5;
public float sqrMaxServerError = 300.0f;
public float sqrMaxServerSpeed = 1000.0f;
private CharacterController character;
// We store twenty states with "playback" information
State[] proxyStates = new State[20];
// Keep track of what slots are used
int proxyStateCount;
List<Move> ownerMoves = new List<Move>();
double serverLastTimestamp = 0;
void Awake()
{
character = GetComponent<CharacterController>();
}
void uLink_OnSerializeNetworkView(uLink.BitStream stream, uLink.NetworkMessageInfo info)
{
if (stream.isWriting)
{
Vector3 pos = transform.position;
Quaternion rot = transform.rotation;
Vector3 velocity = character.velocity;
stream.Serialize(ref pos);
stream.Serialize(ref velocity);
stream.Serialize(ref rot);
}
else
{
Vector3 pos = Vector3.zero;
Vector3 velocity = Vector3.zero;
Quaternion rot = Quaternion.identity;
stream.Serialize(ref pos);
stream.Serialize(ref velocity);
stream.Serialize(ref rot);
// Shift the buffer sideways, deleting state 20
for (int i = proxyStates.Length - 1; i >= 1; i--)
{
proxyStates[i] = proxyStates[i - 1];
}
// Record current state in slot 0
State state;
state.timestamp = info.timestamp;
state.pos = pos;
state.vel = velocity;
state.rot = rot;
proxyStates[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.
proxyStateCount = Mathf.Min(proxyStateCount + 1, proxyStates.Length);
// Check if states are in order
if (proxyStates[0].timestamp < proxyStates[1].timestamp)
Debug.LogError("Timestamp inconsistent: " + proxyStates[0].timestamp + " should be greater than " + proxyStates[1].timestamp);
}
}
// 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()
{
if (uLink.Network.isAuthoritativeServer && uLink.Network.isServerOrCellServer)
{
return;
}
// This is the target playback time of the rigid body
double interpolationTime = uLink.Network.time - interpolationBackTime;
//Mark:cnsoft 用最佳插槽点 来同步坐标.面向等等..处理基于延迟的位置同步.
// Use interpolation if the target playback time is present in the buffer
if (proxyStates[0].timestamp > interpolationTime)
{
// Go through buffer and find correct state to play back
for (int i=0;i<proxyStateCount;i++)
{
if (proxyStates[i].timestamp <= interpolationTime || i == proxyStateCount-1)
{
// The state one slot newer (<100ms) than the best playback state
State rhs = proxyStates[Mathf.Max(i-1, 0)];
// The best playback state (closest to 100 ms old (default time))
State lhs = proxyStates[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 = proxyStates[0];
float extrapolationLength = (float)(interpolationTime - latest.timestamp);
// Don't extrapolation for more than 500 ms, you would need to do that carefully
if (extrapolationLength < extrapolationLimit)
{
transform.position = latest.pos + latest.vel * extrapolationLength;
transform.rotation = latest.rot;
character.SimpleMove(latest.vel);
}
}
}
void LateUpdate()
{
if (!uLink.Network.isAuthoritativeServer || uLink.Network.isServerOrCellServer || !networkView.isMine)
{
return;
}
// TODO: optimize by not sending rpc if no input and rotation. also add idleTime so server's timestamp is still in sync
Move move;
move.timestamp = uLink.Network.time;
move.deltaTime = (ownerMoves.Count > 0) ? (float)(move.timestamp - ownerMoves[ownerMoves.Count - 1].timestamp) : 0.0f;
move.vel = character.velocity;
ownerMoves.Add(move);
networkView.UnreliableRPC("ServerMove", uLink.NetworkPlayer.server, transform.position, move.vel, transform.rotation);
}
[RPC]
void ServerMove(Vector3 ownerPos, Vector3 vel, Quaternion rot, uLink.NetworkMessageInfo info)
{
if (info.timestamp <= serverLastTimestamp || !character.isGrounded)
{
return;
}
transform.rotation = rot;
if (vel.sqrMagnitude > sqrMaxServerSpeed)
{
vel.x = vel.y = vel.z = Mathf.Sqrt(sqrMaxServerSpeed) / 3.0f;
}
float deltaTime = (float)(info.timestamp - serverLastTimestamp);
Vector3 deltaPos = vel * deltaTime;
character.Move(deltaPos);
serverLastTimestamp = info.timestamp;
Vector3 serverPos = transform.position;
Vector3 diff = serverPos - ownerPos;
if (Vector3.SqrMagnitude(diff) > sqrMaxServerError)
{
networkView.UnreliableRPC("AdjustOwnerPos", uLink.RPCMode.Owner, serverPos);
}
else
{
networkView.UnreliableRPC("GoodOwnerPos", uLink.RPCMode.Owner);
}
}
[RPC]
void GoodOwnerPos(uLink.NetworkMessageInfo info)
{
Move goodMove;
goodMove.timestamp = info.timestamp;
goodMove.deltaTime = 0;
goodMove.vel = Vector3.zero;
int index = ownerMoves.BinarySearch(goodMove);
if (index < 0) index = ~index;
ownerMoves.RemoveRange(0, index);
}
[RPC]
void AdjustOwnerPos(Vector3 pos, uLink.NetworkMessageInfo info)
{
GoodOwnerPos(info);
transform.position = pos;
foreach (Move move in ownerMoves)
{
character.Move(move.vel * move.deltaTime);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment