Skip to content

Instantly share code, notes, and snippets.

@James-Frowen
Last active October 1, 2020 22:16
Show Gist options
  • Save James-Frowen/fb65f82bb242f1208e431e9393659a9f to your computer and use it in GitHub Desktop.
Save James-Frowen/fb65f82bb242f1208e431e9393659a9f to your computer and use it in GitHub Desktop.
Working, but unfinished Projectile Manager for the Wolves games, built with Mirror
using Mirror;
using ProjectWolves.Networking;
using UnityEngine;
namespace ProjectWolves.Weapons
{
public class NetworkProjectile : MonoBehaviour
{
public uint ProjectileId { get; set; }
public uint LocalId { get; set; }
private Rigidbody _rigidBody;
public ProjectileBehaviour projectileBehaviour;
private ServerState _lastGoal;
private ServerState _currentGoal;
private void Awake()
{
this._rigidBody = this.GetComponent<Rigidbody>();
this.projectileBehaviour = this.GetComponent<ProjectileBehaviour>();
}
public void ApplyState(ProjectileState state, float serverTime)
{
Debug.Assert(state.projectileId == this.ProjectileId, "Wrong projectile id");
//this._rigidBody.position = state.position;
//this._rigidBody.rotation = state.rotation;
//this._rigidBody.velocity = state.velocity;
var nextGoal = new ServerState
{
position = state.position,
rotation = state.rotation,
velocity = state.velocity,
timeStamp = serverTime,
};
// is this first message?
if (this._currentGoal.timeStamp == 0)
{
var goal = new ServerState();
goal.position = this._rigidBody.position;
goal.rotation = this._rigidBody.rotation;
goal.velocity = this._rigidBody.velocity;
goal.timeStamp = (float)NetworkTime.time - (1f / WolvesNetworkManager.Instance.serverTickRate);
this._lastGoal = goal;
}
else
{
this._lastGoal = this._currentGoal;
this._currentGoal = nextGoal;
}
// client disables self if it collides
// if server didnt collide then it will send another sync package
// so re-enable projectile
if (!this.gameObject.activeSelf)
{
this.gameObject.SetActive(true);
}
}
public ProjectileState GetProjectileState()
{
return new ProjectileState
{
projectileId = this.ProjectileId,
position = this._rigidBody.position,
rotation = this._rigidBody.rotation,
velocity = this._rigidBody.velocity,
};
}
public void SendDestroy()
{
NetworkProjectileManager.DestroyProjectile(this);
}
public void SendRpcHitEffect(HitData data)
{
NetworkProjectileManager.SendRpcHitEffect(this, data);
}
public void CallRpcHitEffect(HitData data)
{
this.projectileBehaviour.RpcHitEffect(data);
}
private void FixedUpdate()
{
// server doesnt need to apply sync
if (NetworkServer.active) { return; }
// has not recieved data from server yet?
if (this._currentGoal.timeStamp == 0) { return; }
this.interpolatePosition();
this.interpolateRotation();
this.interpolateVelocity();
}
private void interpolateVelocity()
{
var msgDelta = this._currentGoal.timeStamp - this._lastGoal.timeStamp;
var localDelta = Time.fixedDeltaTime;
if (msgDelta == 0 || localDelta == 0) { return; }
var timeSinceMsg = (float)(NetworkTime.time - this._currentGoal.timeStamp);
var localTicksSinceMsg = timeSinceMsg / localDelta;
var msgAcceleration = (this._currentGoal.velocity - this._lastGoal.velocity) / msgDelta;
var deltaVel = msgAcceleration * localTicksSinceMsg;
var localVel = this._currentGoal.velocity + deltaVel;
this._rigidBody.velocity = localVel;
}
private void interpolatePosition()
{
var msgDelta = this._currentGoal.timeStamp - this._lastGoal.timeStamp;
var localDelta = Time.fixedDeltaTime;
if (msgDelta == 0 || localDelta == 0) { return; }
var timeSinceMsg = (float)(NetworkTime.time - this._currentGoal.timeStamp);
var localTicksSinceMsg = timeSinceMsg / localDelta;
var msgSpeed = (this._currentGoal.position - this._lastGoal.position) / msgDelta;
var posOneTickDelta = this._currentGoal.velocity + msgSpeed;
var localPos = Vector3.Lerp(this._currentGoal.velocity, posOneTickDelta, localTicksSinceMsg);
this._rigidBody.position = localPos;
}
private void interpolateRotation()
{
var msgDelta = this._currentGoal.timeStamp - this._lastGoal.timeStamp;
var localDelta = Time.fixedDeltaTime;
if (msgDelta == 0 || localDelta == 0) { return; }
var timeSinceMsg = (float)(NetworkTime.time - this._currentGoal.timeStamp);
var localTicksSinceMsg = timeSinceMsg / localDelta;
var localRot = Quaternion.Slerp(this._lastGoal.rotation, this._currentGoal.rotation, localTicksSinceMsg);
this._rigidBody.rotation = localRot;
}
private struct ServerState
{
public float timeStamp;
public Vector3 position;
public Quaternion rotation;
public Vector3 velocity;
}
}
}
using JamesFrowen;
using Mirror;
using ProjectWolves.Networking;
using System.Collections.Generic;
using UnityEngine;
namespace ProjectWolves.Weapons
{
public static class NetworkProjectileManager
{
static Dictionary<uint, NetworkProjectile> _local;
static Dictionary<uint, NetworkProjectile> _all;
// start at 1 so that 0 can be send for no local
private static uint _nextLocalIndex = 1;
private static uint _nextAllIndex = 1;
private static bool clientRegistered;
private static bool serverRegistered;
// shooters spawns projectiles
// shooter gives it local id
// sends server projectileinfo and local id
// Server spawns projectiles
// Server sends message to nonshooters to spawn projectile
// Server Sends link message with local id to shooter
// shooters links projectile
public static void Init()
{
WolvesLog.ProjectileLog("NetworkProjectileManager.init()");
_all = new Dictionary<uint, NetworkProjectile>();
_nextAllIndex = 1;
if (NetworkServer.active)
{
RegisterServerMessages();
}
if (NetworkClient.active)
{
RegisterClientMessages();
}
}
internal static void ShutDown()
{
WolvesLog.ProjectileLog("NetworkProjectileManager.shutDown()");
_all.Clear();
_all = null;
if (NetworkServer.active)
{
UnregisterServerMessages();
}
if (NetworkClient.active)
{
UnregisterClientMessages();
}
}
public static void RegisterServerMessages()
{
if (serverRegistered) { return; }
NetworkServer.RegisterHandler<SpawnProjectileMessage>(serverHandleSpawnProjectileMessage);
serverRegistered = true;
}
public static void RegisterClientMessages()
{
if (clientRegistered) { return; }
_local = new Dictionary<uint, NetworkProjectile>();
_nextLocalIndex = 1;
NetworkClient.RegisterHandler<SpawnProjectileMessage>(clientHandleSpawnProjectileMessage);
NetworkClient.RegisterHandler<LinkProjectileMessage>(handleLinkProjectileMessage);
NetworkClient.RegisterHandler<SyncProjectilesMessage>(handleSyncProjectilesMessage);
NetworkClient.RegisterHandler<DestroyProjectileMessage>(handleDestroyProjectileMessage);
NetworkClient.RegisterHandler<RpcHitEffectMessage>(HandleRpcHitEffect);
clientRegistered = true;
}
public static void UnregisterServerMessages()
{
if (!serverRegistered) { return; }
NetworkServer.UnregisterHandler<SpawnProjectileMessage>();
serverRegistered = false;
}
public static void UnregisterClientMessages()
{
if (!clientRegistered) { return; }
_local.Clear();
_local = null;
NetworkClient.UnregisterHandler<SpawnProjectileMessage>();
NetworkClient.UnregisterHandler<LinkProjectileMessage>();
NetworkClient.UnregisterHandler<SyncProjectilesMessage>();
NetworkClient.UnregisterHandler<DestroyProjectileMessage>();
NetworkClient.UnregisterHandler<RpcHitEffectMessage>();
clientRegistered = false;
}
public static void Update()
{
if (!NetworkServer.active) { Debug.LogWarning("NetworkProjectileManager.Update called on client"); return; }
// dont sync if nothing
if (_all.Count == 0) { return; }
var i = 0;
foreach (var item in _all.Values)
{
var state = item.GetProjectileState();
SyncProjectilesMessage.buffer[i] = state;
i++;
}
var msg = new SyncProjectilesMessage
{
count = i,
serverTime = (float)NetworkTime.time,
};
NetworkServer.SendToAll(msg);
}
public static void ClearProjectiles()
{
_all.Clear();
}
internal static void SendRpcHitEffect(NetworkProjectile networkProjectile, HitData data)
{
var msg = new RpcHitEffectMessage
{
projectileId = networkProjectile.ProjectileId,
hitData = data
};
NetworkServer.SendToAll(msg);
}
internal static void HandleRpcHitEffect(NetworkConnection conn, RpcHitEffectMessage message)
{
if (NetworkServer.active) { return; } // host
if (_all.TryGetValue(message.projectileId, out var projectile))
{
projectile.CallRpcHitEffect(message.hitData);
}
else
{
if (WolvesLog.Projectile) { WolvesLog.ProjectileWarn("Projectile with id {0} not in all", message.projectileId); }
}
}
private static void handleLinkProjectileMessage(NetworkConnection conn, LinkProjectileMessage message)
{
Debug.Assert(!NetworkServer.active, "handleLinkProjectileMessages should not be called on server");
var local = _local[message.localId];
_local.Remove(message.localId);
local.ProjectileId = message.projectileId;
_all.Add(message.projectileId, local);
}
private static void clientHandleSpawnProjectileMessage(NetworkConnection conn, SpawnProjectileMessage message)
{
Debug.Assert(!NetworkServer.active, "clientHandleSpawnProjectileMessage should not be called on server");
Debug.Assert(message.projectileId != 0, "SpawnProjectileMessage should bot have projectileId = 0 in clientHandleSpawnProjectileMessage");
// get projectile type
var playerIdentity = NetworkIdentity.spawned[message.ownerNetId];
var weaponHolder = playerIdentity.GetComponent<WeaponHolder>();
var activeWeapon = weaponHolder.ActiveWeapon;
if (activeWeapon.FireType is ProjectileFireType projectileFireType)
{
var projectileType = projectileFireType.ProjectileType;
// spawn projectile
var projectile = projectileType.RemoteClientSpawnProjectile(message, activeWeapon.Weapon, weaponHolder.Core);
// add to all
var networkProjectile = projectile.GetComponent<NetworkProjectile>();
networkProjectile.ProjectileId = message.projectileId;
_all.Add(message.projectileId, networkProjectile);
}
else
{
GameLog.LogError("Active weapon firetype was not a ProjectileFireType");
}
}
private static void serverHandleSpawnProjectileMessage(NetworkConnection conn, SpawnProjectileMessage message)
{
Debug.Assert(NetworkServer.active, "serverHandleSpawnProjectileMessage should not be called on client");
// get projectile type
var playerIdentity = NetworkIdentity.spawned[message.ownerNetId];
Debug.Assert(playerIdentity == conn.identity, "OwnerNetId should be the conn.identity");
var weaponHolder = playerIdentity.GetComponent<WeaponHolder>();
var activeWeapon = weaponHolder.ActiveWeapon;
if (activeWeapon.FireType is ProjectileFireType projectileFireType)
{
var projectileType = projectileFireType.ProjectileType;
// spawn projectile
var clone = projectileType.ServerSpawnProjectile(message, activeWeapon.Weapon, weaponHolder.Core, conn);
// call ServerSpawn
ServerSpawn(message, clone, conn);
}
else
{
GameLog.LogError("Active weapon firetype was not a ProjectileFireType");
}
}
private static void handleSyncProjectilesMessage(NetworkConnection conn, SyncProjectilesMessage message)
{
if (NetworkServer.active) { return; } // host
for (var i = 0; i < message.count; i++)
{
var state = SyncProjectilesMessage.buffer[i];
if (_all.TryGetValue(state.projectileId, out var projectile))
{
projectile.ApplyState(state, message.serverTime);
}
else
{
if (WolvesLog.Projectile) { WolvesLog.ProjectileWarn("Projectile with id {0} not in all", state.projectileId); }
}
}
}
//[Client]
private static void handleDestroyProjectileMessage(NetworkConnection conn, DestroyProjectileMessage message)
{
if (NetworkServer.active) { return; } // host
var projectile = _all[message.projectileId];
_all.Remove(message.projectileId);
projectile.projectileBehaviour.HandleDestroy();
}
public static void DestroyProjectile(NetworkProjectile networkProjectile)
{
var msg = new DestroyProjectileMessage
{
projectileId = networkProjectile.ProjectileId
};
NetworkServer.SendToAll(msg);
if (NetworkServer.active) // server and host
{
_all.Remove(networkProjectile.ProjectileId);
}
}
public static void ServerSpawn(SpawnProjectileMessage message, GameObject gameObject, NetworkConnection spawner)
{
var networkProjectile = gameObject.GetComponent<NetworkProjectile>();
if (networkProjectile == null)
{
Debug.LogError($"{nameof(NetworkProjectileManager)}.{nameof(ServerSpawn)} needs an object with NetworkProjectile");
}
ServerSpawn(message, networkProjectile, spawner);
}
public static void ServerSpawn(SpawnProjectileMessage message, NetworkProjectile projectile, NetworkConnection spawner)
{
// give id
// add to all
projectile.ProjectileId = _nextAllIndex++;
_all.Add(projectile.ProjectileId, projectile);
// send message to clients
message.projectileId = projectile.ProjectileId;
foreach (var conn in NetworkServer.connections.Values)
{
if (conn == spawner)
{
var linkMsg = new LinkProjectileMessage
{
localId = message.localId,
projectileId = message.projectileId,
};
conn.Send(linkMsg);
}
// dont send to host's client
else if (!isLocalConnection(conn))
{
Debug.Assert(message.projectileId != 0, "SpawnProjectileMessage should bot have projectileId = 0 in ServerSpawn");
conn.Send(message);
}
}
}
private static bool isLocalConnection(NetworkConnection conn)
{
return NetworkClient.active && NetworkClient.connection.connectionId == conn.connectionId;
}
public static void ClientSpawn(SpawnProjectileMessage message, GameObject gameObject)
{
var networkProjectile = gameObject.GetComponent<NetworkProjectile>();
if (networkProjectile == null)
{
Debug.LogError($"{nameof(NetworkProjectileManager)}.{nameof(ClientSpawn)} needs an object with NetworkProjectile");
}
ClientSpawn(message, networkProjectile);
}
public static void ClientSpawn(SpawnProjectileMessage message, NetworkProjectile projectile)
{
// gives it local id
projectile.LocalId = _nextLocalIndex++;
_local.Add(projectile.LocalId, projectile);
// sends server projectileinfo and local id
message.localId = projectile.LocalId;
message.expectedRtt = (float)NetworkTime.rtt;
NetworkClient.Send(message);
}
}
public struct Charge
{
public bool hasValue;
public float value;
public static implicit operator Charge(float? nullable)
{
return new Charge
{
hasValue = nullable.HasValue,
value = nullable.HasValue ? nullable.Value : 0
};
}
}
public struct SpawnProjectileMessage : IMessageBase
{
// spawn data
/// <summary>
/// Used for spawning on client
/// </summary>
public uint localId;
public float expectedRtt;
public uint projectileId;
// projectile data
public Vector3 position;
public Quaternion rotation;
public Charge charge;
public IWeaponTarget target;
// owner data
public uint ownerNetId;
public byte weaponIndex;
public void Deserialize(NetworkReader reader)
{
this.charge.hasValue = reader.ReadBoolean();
if (WolvesLog.Weapon) { WolvesLog.WeaponLog($"hasCharge: {this.charge.hasValue}"); }
this.charge.value = this.charge.hasValue
? reader.ReadFloatFromByte(0, 1)
: 0;
this.position = reader.ReadVector3();
this.rotation = reader.ReadQuaternion();
var hasTarget = reader.ReadBoolean();
this.target = null;
if (hasTarget)
{
var netID = reader.ReadUInt32();
var targetIdentity = NetworkIdentity.spawned[netID];
this.target = targetIdentity.GetComponent<IWeaponTarget>();
}
this.localId = reader.ReadUInt32();
if (this.localId != 0)
{
this.expectedRtt = reader.ReadSingle();
}
this.projectileId = reader.ReadUInt32();
this.ownerNetId = reader.ReadUInt32();
this.weaponIndex = reader.ReadByte();
}
public void Serialize(NetworkWriter writer)
{
writer.WriteBoolean(this.charge.hasValue);
if (this.charge.hasValue)
{
writer.WriteFloatToByte(this.charge.value, 0, 1);
}
writer.WriteVector3(this.position);
writer.WriteQuaternion(this.rotation);
var hasTarget = this.target != null;
uint netId = 0;
if (hasTarget)
{
var target = this.target.netIdentity;
hasTarget = target != null;
if (hasTarget)
{
netId = target.netId;
}
}
writer.WriteBoolean(hasTarget);
if (hasTarget)
{
writer.WriteUInt32(netId);
}
writer.WriteUInt32(this.localId);
if (this.localId != 0)
{
writer.WriteSingle((float)NetworkTime.rtt);
}
writer.WriteUInt32(this.projectileId);
writer.WriteUInt32(this.ownerNetId);
writer.WriteByte(this.weaponIndex);
}
}
public struct LinkProjectileMessage : IMessageBase
{
// spawn data
/// <summary>
/// Used for spawning on client
/// </summary>
public uint localId;
public uint projectileId;
public void Deserialize(NetworkReader reader)
{
this.localId = reader.ReadUInt32();
this.projectileId = reader.ReadUInt32();
}
public void Serialize(NetworkWriter writer)
{
writer.WriteUInt32(this.localId);
writer.WriteUInt32(this.projectileId);
}
}
public struct ProjectileState
{
public uint projectileId;
public Vector3 position;
public Quaternion rotation;
public Vector3 velocity;
}
public struct SyncProjectilesMessage : IMessageBase
{
public static ProjectileState[] buffer = new ProjectileState[1000];
public int count;
public float serverTime;
public void Deserialize(NetworkReader reader)
{
this.count = reader.ReadInt32();
this.serverTime = reader.ReadSingle();
for (var i = 0; i < this.count; i++)
{
buffer[i].projectileId = reader.ReadUInt32();
buffer[i].position = reader.ReadVector3();
buffer[i].rotation = reader.ReadQuaternion();
buffer[i].velocity = reader.ReadVector3();
}
}
public void Serialize(NetworkWriter writer)
{
writer.WriteInt32(this.count);
writer.WriteSingle(this.serverTime);
for (var i = 0; i < this.count; i++)
{
writer.WriteUInt32(buffer[i].projectileId);
writer.WriteVector3(buffer[i].position);
writer.WriteQuaternion(buffer[i].rotation);
writer.WriteVector3(buffer[i].velocity);
}
}
}
public struct DestroyProjectileMessage : IMessageBase
{
public uint projectileId;
public void Deserialize(NetworkReader reader)
{
this.projectileId = reader.ReadUInt32();
}
public void Serialize(NetworkWriter writer)
{
writer.WriteUInt32(this.projectileId);
}
}
public struct RpcHitEffectMessage : IMessageBase
{
public uint projectileId;
public HitData hitData;
public void Deserialize(NetworkReader reader)
{
this.projectileId = reader.ReadUInt32();
this.hitData = reader.ReadHitData();
}
public void Serialize(NetworkWriter writer)
{
writer.WriteUInt32(this.projectileId);
writer.WriteHitData(this.hitData);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment