// TODO add true over-the-network movement tests. // but we need to split NetworkIdentity.spawned in server/client first. // atm we can't spawn an object on both server & client separately yet. using NUnit.Framework; using UnityEngine; namespace Mirror.Tests.NetworkTransform2k { // helper class to expose some of the protected methods public class NetworkTransformExposed : NetworkTransform { public new NTSnapshot ConstructSnapshot() => base.ConstructSnapshot(); public new void ApplySnapshot(NTSnapshot start, NTSnapshot goal, NTSnapshot interpolated) => base.ApplySnapshot(start, goal, interpolated); public new void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) => base.OnClientToServerSync(position, rotation, scale); public new void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) => base.OnServerToClientSync(position, rotation, scale); } public class NetworkTransform2kTests : MirrorTest { // networked and spawned NetworkTransform NetworkConnectionToClient connectionToClient; Transform transform; NetworkTransformExposed component; [SetUp] public override void SetUp() { // set up world with server & client // host mode for now. // TODO separate client & server after .spawned split. // we can use CreateNetworkedAndSpawn that creates on sv & cl. // then move on server, update, verify client position etc. base.SetUp(); NetworkServer.Listen(1); ConnectHostClientBlockingAuthenticatedAndReady(); connectionToClient = NetworkServer.localConnection; // create a networked object with NetworkTransform CreateNetworkedAndSpawn(out GameObject go, out NetworkIdentity _, out component, connectionToClient); // sync immediately component.syncInterval = 0; // remember transform for convenience transform = go.transform; } [TearDown] public override void TearDown() { base.TearDown(); NetworkClient.Disconnect(); } // TODO move to NTSnapshot tests? [Test] public void Interpolate() { NTSnapshot from = new NTSnapshot( 1, 1, new Vector3(1, 1, 1), Quaternion.Euler(new Vector3(0, 0, 0)), new Vector3(3, 3, 3) ); NTSnapshot to = new NTSnapshot( 2, 2, new Vector3(2, 2, 2), Quaternion.Euler(new Vector3(0, 90, 0)), new Vector3(4, 4, 4) ); // interpolate NTSnapshot between = NTSnapshot.Interpolate(from, to, 0.5); // note: timestamp interpolation isn't needed. we don't use it. //Assert.That(between.remoteTimestamp, Is.EqualTo(1.5).Within(Mathf.Epsilon)); //Assert.That(between.localTimestamp, Is.EqualTo(1.5).Within(Mathf.Epsilon)); // check position Assert.That(between.position.x, Is.EqualTo(1.5).Within(Mathf.Epsilon)); Assert.That(between.position.y, Is.EqualTo(1.5).Within(Mathf.Epsilon)); Assert.That(between.position.z, Is.EqualTo(1.5).Within(Mathf.Epsilon)); // check rotation // (epsilon is slightly too small) Assert.That(between.rotation.eulerAngles.x, Is.EqualTo(0).Within(Mathf.Epsilon)); Assert.That(between.rotation.eulerAngles.y, Is.EqualTo(45).Within(0.001)); Assert.That(between.rotation.eulerAngles.z, Is.EqualTo(0).Within(Mathf.Epsilon)); // check scale Assert.That(between.scale.x, Is.EqualTo(3.5).Within(Mathf.Epsilon)); Assert.That(between.scale.y, Is.EqualTo(3.5).Within(Mathf.Epsilon)); Assert.That(between.scale.z, Is.EqualTo(3.5).Within(Mathf.Epsilon)); } [Test] public void ConstructSnapshot() { // set unique position/rotation/scale transform.position = new Vector3(1, 2, 3); transform.rotation = Quaternion.identity; transform.localScale = new Vector3(4, 5, 6); // construct snapshot double time = NetworkTime.localTime; NTSnapshot snapshot = component.ConstructSnapshot(); Assert.That(snapshot.remoteTimestamp, Is.EqualTo(time).Within(0.01)); Assert.That(snapshot.position, Is.EqualTo(new Vector3(1, 2, 3))); Assert.That(snapshot.rotation, Is.EqualTo(Quaternion.identity)); Assert.That(snapshot.scale, Is.EqualTo(new Vector3(4, 5, 6))); } [Test] public void ApplySnapshot_Interpolated() { // construct snapshot with unique position/rotation/scale Vector3 position = new Vector3(1, 2, 3); Quaternion rotation = Quaternion.Euler(45, 90, 45); Vector3 scale = new Vector3(4, 5, 6); // apply snapshot with interpolation component.syncPosition = true; component.syncRotation = true; component.syncScale = true; component.interpolatePosition = true; component.interpolateRotation = true; component.interpolateScale = true; component.ApplySnapshot(default, default, new NTSnapshot(0, 0, position, rotation, scale)); // was it applied? Assert.That(transform.position, Is.EqualTo(position)); Assert.That(Quaternion.Angle(transform.rotation, rotation), Is.EqualTo(0).Within(Mathf.Epsilon)); Assert.That(transform.localScale, Is.EqualTo(scale)); } [Test] public void ApplySnapshot_Direct() { // construct snapshot with unique position/rotation/scale Vector3 position = new Vector3(1, 2, 3); Quaternion rotation = Quaternion.Euler(45, 90, 45); Vector3 scale = new Vector3(4, 5, 6); // apply snapshot without interpolation component.syncPosition = true; component.syncRotation = true; component.syncScale = true; component.interpolatePosition = false; component.interpolateRotation = false; component.interpolateScale = false; component.ApplySnapshot(default, new NTSnapshot(0, 0, position, rotation, scale), default); // was it applied? Assert.That(transform.position, Is.EqualTo(position)); Assert.That(Quaternion.Angle(transform.rotation, rotation), Is.EqualTo(0).Within(Mathf.Epsilon)); Assert.That(transform.localScale, Is.EqualTo(scale)); } [Test] public void ApplySnapshot_DontSyncPosition() { // construct snapshot with unique position/rotation/scale Vector3 position = new Vector3(1, 2, 3); Quaternion rotation = Quaternion.Euler(45, 90, 45); Vector3 scale = new Vector3(4, 5, 6); // apply snapshot without position sync should not apply position component.syncPosition = false; component.syncRotation = true; component.syncScale = true; component.interpolatePosition = false; component.interpolateRotation = true; component.interpolateScale = true; component.ApplySnapshot(default, default, new NTSnapshot(0, 0, position, rotation, scale)); // was it applied? Assert.That(transform.position, Is.EqualTo(Vector3.zero)); Assert.That(Quaternion.Angle(transform.rotation, rotation), Is.EqualTo(0).Within(Mathf.Epsilon)); Assert.That(transform.localScale, Is.EqualTo(scale)); } [Test] public void ApplySnapshot_DontSyncRotation() { // construct snapshot with unique position/rotation/scale Vector3 position = new Vector3(1, 2, 3); Quaternion rotation = Quaternion.Euler(45, 90, 45); Vector3 scale = new Vector3(4, 5, 6); // apply snapshot without position sync should not apply position component.syncPosition = true; component.syncRotation = false; component.syncScale = true; component.interpolatePosition = true; component.interpolateRotation = false; component.interpolateScale = true; component.ApplySnapshot(default, default, new NTSnapshot(0, 0, position, rotation, scale)); // was it applied? Assert.That(transform.position, Is.EqualTo(position)); Assert.That(transform.rotation, Is.EqualTo(Quaternion.identity)); Assert.That(transform.localScale, Is.EqualTo(scale)); } [Test] public void ApplySnapshot_DontSyncScale() { // construct snapshot with unique position/rotation/scale Vector3 position = new Vector3(1, 2, 3); Quaternion rotation = Quaternion.Euler(45, 90, 45); Vector3 scale = new Vector3(4, 5, 6); // apply snapshot without position sync should not apply position component.syncPosition = true; component.syncRotation = true; component.syncScale = false; component.interpolatePosition = true; component.interpolateRotation = true; component.interpolateScale = false; component.ApplySnapshot(default, default, new NTSnapshot(0, 0, position, rotation, scale)); // was it applied? Assert.That(transform.position, Is.EqualTo(position)); Assert.That(Quaternion.Angle(transform.rotation, rotation), Is.EqualTo(0).Within(Mathf.Epsilon)); Assert.That(transform.localScale, Is.EqualTo(Vector3.one)); } [Test] public void OnClientToServerSync_WithoutClientAuthority() { // call OnClientToServerSync without authority component.clientAuthority = false; component.OnClientToServerSync(Vector3.zero, Quaternion.identity, Vector3.zero); Assert.That(component.serverBuffer.Count, Is.EqualTo(0)); } [Test] public void OnClientToServerSync_WithClientAuthority() { // call OnClientToServerSync with authority component.clientAuthority = true; component.OnClientToServerSync(Vector3.zero, Quaternion.identity, Vector3.zero); Assert.That(component.serverBuffer.Count, Is.EqualTo(1)); } [Test] public void OnClientToServerSync_WithClientAuthority_BufferSizeLimit() { component.bufferSizeLimit = 1; // authority is required component.clientAuthority = true; // add first should work component.OnClientToServerSync(Vector3.zero, Quaternion.identity, Vector3.zero); Assert.That(component.serverBuffer.Count, Is.EqualTo(1)); // add second should be too much component.OnClientToServerSync(Vector3.zero, Quaternion.identity, Vector3.zero); Assert.That(component.serverBuffer.Count, Is.EqualTo(1)); } [Test] public void OnClientToServerSync_WithClientAuthority_Nullables_Uses_Last() { // set some defaults transform.position = Vector3.left; transform.rotation = Quaternion.identity; transform.localScale = Vector3.right; // call OnClientToServerSync with authority and nullable types // to make sure it uses the last valid position then. component.clientAuthority = true; component.OnClientToServerSync(new Vector3?(), new Quaternion?(), new Vector3?()); Assert.That(component.serverBuffer.Count, Is.EqualTo(1)); NTSnapshot first = component.serverBuffer.Values[0]; Assert.That(first.position, Is.EqualTo(Vector3.left)); Assert.That(first.rotation, Is.EqualTo(Quaternion.identity)); Assert.That(first.scale, Is.EqualTo(Vector3.right)); } // server->client sync should only work if client doesn't have authority [Test] public void OnServerToClientSync_WithoutClientAuthority() { // pretend to be the client object component.netIdentity.isServer = false; component.netIdentity.isClient = true; component.netIdentity.isLocalPlayer = true; // call OnServerToClientSync without authority component.clientAuthority = false; component.OnServerToClientSync(Vector3.zero, Quaternion.identity, Vector3.zero); Assert.That(component.clientBuffer.Count, Is.EqualTo(1)); } // server->client sync shouldn't work if client has authority [Test] public void OnServerToClientSync_WithoutClientAuthority_bufferSizeLimit() { component.bufferSizeLimit = 1; // pretend to be the client object component.netIdentity.isServer = false; component.netIdentity.isClient = true; component.netIdentity.isLocalPlayer = true; // client authority has to be disabled component.clientAuthority = false; // add first should work component.OnServerToClientSync(Vector3.zero, Quaternion.identity, Vector3.zero); Assert.That(component.clientBuffer.Count, Is.EqualTo(1)); // add second should be too much component.OnServerToClientSync(Vector3.zero, Quaternion.identity, Vector3.zero); Assert.That(component.clientBuffer.Count, Is.EqualTo(1)); } // server->client sync shouldn't work if client has authority [Test] public void OnServerToClientSync_WithClientAuthority() { // pretend to be the client object component.netIdentity.isServer = false; component.netIdentity.isClient = true; component.netIdentity.isLocalPlayer = true; // call OnServerToClientSync with authority component.clientAuthority = true; component.OnServerToClientSync(Vector3.zero, Quaternion.identity, Vector3.zero); Assert.That(component.clientBuffer.Count, Is.EqualTo(0)); } [Test] public void OnServerToClientSync_WithClientAuthority_Nullables_Uses_Last() { // set some defaults transform.position = Vector3.left; transform.rotation = Quaternion.identity; transform.localScale = Vector3.right; // pretend to be the client object component.netIdentity.isServer = false; component.netIdentity.isClient = true; component.netIdentity.isLocalPlayer = true; // call OnClientToServerSync with authority and nullable types // to make sure it uses the last valid position then. component.OnServerToClientSync(new Vector3?(), new Quaternion?(), new Vector3?()); Assert.That(component.clientBuffer.Count, Is.EqualTo(1)); NTSnapshot first = component.clientBuffer.Values[0]; Assert.That(first.position, Is.EqualTo(Vector3.left)); Assert.That(first.rotation, Is.EqualTo(Quaternion.identity)); Assert.That(first.scale, Is.EqualTo(Vector3.right)); } } }