diff --git a/Experimental/DistributedAuthoritySample/Assets/Prefabs/Avatar.prefab b/Experimental/DistributedAuthoritySample/Assets/Prefabs/Avatar.prefab index bf4bddabc..9d1ff17f0 100644 --- a/Experimental/DistributedAuthoritySample/Assets/Prefabs/Avatar.prefab +++ b/Experimental/DistributedAuthoritySample/Assets/Prefabs/Avatar.prefab @@ -190,7 +190,7 @@ CapsuleCollider: m_Bits: 0 m_LayerOverridePriority: 0 m_IsTrigger: 0 - m_ProvidesContacts: 0 + m_ProvidesContacts: 1 m_Enabled: 1 serializedVersion: 2 m_Radius: 0.5 @@ -213,6 +213,7 @@ GameObject: - component: {fileID: 4903152183366241606} - component: {fileID: 8197190117666738524} - component: {fileID: 539700041658036575} + - component: {fileID: 4655812346339390596} m_Layer: 0 m_Name: Avatar m_TagString: Untagged @@ -292,18 +293,18 @@ MonoBehaviour: InLocalSpace: 0 Interpolate: 1 SlerpPosition: 0 - m_Rigidbody: {fileID: 4745766379562620470} + m_CollisionType: 1 + m_CollisionDamage: 0 + m_DebugCollisions: 0 + m_DebugDamage: 0 + m_Colliders: + - {fileID: 817796967242824188} + m_MaxAngularVelocity: 10 + m_MaxVelocity: 10 m_PlayerInput: {fileID: 4903152183366241606} m_AvatarInputs: {fileID: 8197190117666738524} - m_WalkSpeed: 6 - m_SprintSpeed: 10 - m_Acceleration: 50 - m_DragCoefficient: 4 - m_AirControlFactor: 0.5 - m_JumpImpusle: 7 - m_CustomGravityMultiplier: 2 - m_RotationSpeed: 700 - m_GroundCheckDistance: 1.1 + m_AvatarInteractions: {fileID: 539700041658036575} + m_PhysicsPlayerController: {fileID: 4655812346339390596} --- !u!54 &4745766379562620470 Rigidbody: m_ObjectHideFlags: 0 @@ -312,7 +313,7 @@ Rigidbody: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 5478015027486214707} serializedVersion: 4 - m_Mass: 1 + m_Mass: 10 m_Drag: 0 m_AngularDrag: 0.05 m_CenterOfMass: {x: 0, y: 0, z: 0} @@ -330,7 +331,7 @@ Rigidbody: m_IsKinematic: 1 m_Interpolate: 0 m_Constraints: 0 - m_CollisionDetection: 0 + m_CollisionDetection: 2 --- !u!114 &-8100831418301897270 MonoBehaviour: m_ObjectHideFlags: 0 @@ -358,8 +359,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 62899f850307741f2a39c98a8b639597, type: 3} m_Name: m_EditorClassIdentifier: - m_Actions: {fileID: -944628639613478452, guid: dcd1b4eb76b964f40afe6c0a3ca65c82, - type: 3} + m_Actions: {fileID: -944628639613478452, guid: dcd1b4eb76b964f40afe6c0a3ca65c82, type: 3} m_NotificationBehavior: 0 m_UIInputModule: {fileID: 0} m_DeviceLostEvent: @@ -389,15 +389,13 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 7e9f71718dd134b14b300e378e98c718, type: 3} m_Name: m_EditorClassIdentifier: + m_InteractActionReference: {fileID: 1781555164194001046, guid: dcd1b4eb76b964f40afe6c0a3ca65c82, type: 3} Move: {x: 0, y: 0} Look: {x: 0, y: 0} - Jump: 0 Sprint: 0 AnalogMovement: 0 CursorLocked: 1 CursorInputForLook: 1 - m_InteractActionReference: {fileID: 1781555164194001046, guid: dcd1b4eb76b964f40afe6c0a3ca65c82, - type: 3} --- !u!114 &539700041658036575 MonoBehaviour: m_ObjectHideFlags: 0 @@ -405,7 +403,7 @@ MonoBehaviour: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 5478015027486214707} - m_Enabled: 1 + m_Enabled: 0 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: b47efef44cef74d9ca00f4913607f988, type: 3} m_Name: @@ -415,8 +413,22 @@ MonoBehaviour: m_NetworkRigidbody: {fileID: -8100831418301897270} m_HoldTransform: {fileID: 3820763517226122587} m_InteractCollider: {fileID: 4881592427634173612} - m_MinTossForce: 5 - m_MaxTossForce: 10 + m_MinTossForce: 500 + m_MaxTossForce: 1000 +--- !u!114 &4655812346339390596 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5478015027486214707} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e222c86e1ae554b699fe6d68d3cf4ba9, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Rigidbody: {fileID: 4745766379562620470} + m_PhysicsPlayerControllerSettings: {fileID: 11400000, guid: 1dd99ce80f3e54d03b20bf78d6ff2a40, type: 2} --- !u!1 &9009864878170659705 GameObject: m_ObjectHideFlags: 0 diff --git a/Experimental/DistributedAuthoritySample/Assets/Prefabs/DistributableTransferableObject.prefab b/Experimental/DistributedAuthoritySample/Assets/Prefabs/DistributableTransferableObject.prefab index 0872489ec..66374b06a 100644 --- a/Experimental/DistributedAuthoritySample/Assets/Prefabs/DistributableTransferableObject.prefab +++ b/Experimental/DistributedAuthoritySample/Assets/Prefabs/DistributableTransferableObject.prefab @@ -16,6 +16,7 @@ GameObject: - component: {fileID: -7368379283164171724} - component: {fileID: 6039897070700006726} - component: {fileID: 6689110374311511483} + - component: {fileID: 6961888493949678523} m_Layer: 7 m_Name: DistributableTransferableObject m_TagString: Untagged @@ -107,7 +108,7 @@ BoxCollider: m_Bits: 0 m_LayerOverridePriority: 0 m_IsTrigger: 0 - m_ProvidesContacts: 0 + m_ProvidesContacts: 1 m_Enabled: 1 serializedVersion: 3 m_Size: {x: 1, y: 1, z: 1} @@ -122,8 +123,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: GlobalObjectIdHash: 2069120471 InScenePlacedSourceGlobalObjectIdHash: 0 DeferredDespawnTick: 0 @@ -143,7 +144,7 @@ Rigidbody: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 893974733110552042} serializedVersion: 4 - m_Mass: 1 + m_Mass: 25 m_Drag: 0 m_AngularDrag: 0.05 m_CenterOfMass: {x: 0, y: 0, z: 0} @@ -158,10 +159,10 @@ Rigidbody: m_ImplicitCom: 1 m_ImplicitTensor: 1 m_UseGravity: 1 - m_IsKinematic: 0 + m_IsKinematic: 1 m_Interpolate: 0 m_Constraints: 0 - m_CollisionDetection: 0 + m_CollisionDetection: 2 --- !u!114 &6039897070700006726 MonoBehaviour: m_ObjectHideFlags: 0 @@ -172,8 +173,35 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 54937e94f00ab46c8ae43b81027f342c, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: +--- !u!114 &6689110374311511483 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 893974733110552042} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f6c0be61502bb534f922ebb746851216, type: 3} + m_Name: + m_EditorClassIdentifier: + UseRigidBodyForMotion: 1 + AutoUpdateKinematicState: 1 + AutoSetKinematicOnDespawn: 1 +--- !u!114 &6961888493949678523 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 893974733110552042} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 496a933d305aa4e1c926bd7d19b42f46, type: 3} + m_Name: + m_EditorClassIdentifier: AuthorityMode: 1 UseUnreliableDeltas: 0 SyncPositionX: 1 @@ -194,18 +222,13 @@ MonoBehaviour: InLocalSpace: 0 Interpolate: 1 SlerpPosition: 0 ---- !u!114 &6689110374311511483 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 893974733110552042} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f6c0be61502bb534f922ebb746851216, type: 3} - m_Name: - m_EditorClassIdentifier: - UseRigidBodyForMotion: 1 - AutoUpdateKinematicState: 1 - AutoSetKinematicOnDespawn: 1 + m_CollisionType: 2 + m_CollisionDamage: 5 + m_DebugCollisions: 0 + m_DebugDamage: 0 + m_Colliders: + - {fileID: 7690746213975220858} + m_MaxAngularVelocity: 10 + m_MaxVelocity: 10 + m_StartingHealth: 100 + m_IntangibleDurationAfterDamage: 2 diff --git a/Experimental/DistributedAuthoritySample/Assets/ScriptableObjects.meta b/Experimental/DistributedAuthoritySample/Assets/ScriptableObjects.meta new file mode 100644 index 000000000..d66295912 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/ScriptableObjects.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c4e4c1f7517bb5d4293f1e3ad3e66d18 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Experimental/DistributedAuthoritySample/Assets/ScriptableObjects/AvatarPhysicsPlayerControllerSettings.asset b/Experimental/DistributedAuthoritySample/Assets/ScriptableObjects/AvatarPhysicsPlayerControllerSettings.asset new file mode 100644 index 000000000..fb00037ce --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/ScriptableObjects/AvatarPhysicsPlayerControllerSettings.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 48976c2ba473849eab61b8e1398b8644, type: 3} + m_Name: AvatarPhysicsPlayerControllerSettings + m_EditorClassIdentifier: + WalkSpeed: 6 + SprintSpeed: 10 + Acceleration: 50 + DragCoefficient: 4 + AirControlFactor: 0.5 + JumpImpusle: 80 + CustomGravityMultiplier: 2 + RotationSpeed: 13 + GroundCheckDistance: 1.1 diff --git a/Experimental/DistributedAuthoritySample/Assets/ScriptableObjects/AvatarPhysicsPlayerControllerSettings.asset.meta b/Experimental/DistributedAuthoritySample/Assets/ScriptableObjects/AvatarPhysicsPlayerControllerSettings.asset.meta new file mode 100644 index 000000000..5f6fa669c --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/ScriptableObjects/AvatarPhysicsPlayerControllerSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1dd99ce80f3e54d03b20bf78d6ff2a40 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/AvatarTransformEditor.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/AvatarTransformEditor.cs index 578f9d4ad..05c8f487a 100644 --- a/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/AvatarTransformEditor.cs +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/AvatarTransformEditor.cs @@ -7,7 +7,7 @@ namespace Unity.Multiplayer.Samples.SocialHub.Editor { [CustomEditor(typeof(AvatarTransform))] - class DerivedComponentEditor : UnityEditor.Editor + class AvatarTransformEditor : UnityEditor.Editor { public override VisualElement CreateInspectorGUI() { diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/DestructibleObjectEditor.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/DestructibleObjectEditor.cs new file mode 100644 index 000000000..0070c2487 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/DestructibleObjectEditor.cs @@ -0,0 +1,32 @@ +using Unity.Multiplayer.Samples.SocialHub.Gameplay; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; + +namespace Unity.Multiplayer.Samples.SocialHub.Editor +{ + [CustomEditor(typeof(DestructibleObject))] + class DestructibleObjectEditor : UnityEditor.Editor + { + public override VisualElement CreateInspectorGUI() + { + // Create a new VisualElement to be the root of the inspector UI + var root = new VisualElement(); + + // Generate default inspector for AvatarTransform + serializedObject.Update(); + SerializedProperty property = serializedObject.GetIterator(); + property.NextVisible(true); // Skip the script field + while (property.NextVisible(false)) + { + var propertyField = new PropertyField(property); + root.Add(propertyField); + } + + serializedObject.ApplyModifiedProperties(); + + return root; + } + } +} diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/DestructibleObjectEditor.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/DestructibleObjectEditor.cs.meta new file mode 100644 index 000000000..63681d1be --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/DestructibleObjectEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c4746eccfea8d4378a13a35effd619c1 diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/PhysicsObjectMotionEditor.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/PhysicsObjectMotionEditor.cs new file mode 100644 index 000000000..2393faeec --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/PhysicsObjectMotionEditor.cs @@ -0,0 +1,32 @@ +using Unity.Multiplayer.Samples.SocialHub.Physics; +using UnityEngine; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace Unity.Multiplayer.Samples.SocialHub.Editor +{ + [CustomEditor(typeof(PhysicsObjectMotion))] + class PhysicsObjectMotionEditor : UnityEditor.Editor + { + public override VisualElement CreateInspectorGUI() + { + // Create a new VisualElement to be the root of the inspector UI + var root = new VisualElement(); + + // Generate default inspector for AvatarTransform + serializedObject.Update(); + SerializedProperty property = serializedObject.GetIterator(); + property.NextVisible(true); // Skip the script field + while (property.NextVisible(false)) + { + var propertyField = new PropertyField(property); + root.Add(propertyField); + } + + serializedObject.ApplyModifiedProperties(); + + return root; + } + } +} diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/PhysicsObjectMotionEditor.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/PhysicsObjectMotionEditor.cs.meta new file mode 100644 index 000000000..199590052 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/PhysicsObjectMotionEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6513302c68bd64368883bcb638331607 \ No newline at end of file diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/Unity.Multiplayer.Samples.SocialHub.Editor.asmdef b/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/Unity.Multiplayer.Samples.SocialHub.Editor.asmdef index 763c559da..9bf630ba7 100644 --- a/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/Unity.Multiplayer.Samples.SocialHub.Editor.asmdef +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Editor/Unity.Multiplayer.Samples.SocialHub.Editor.asmdef @@ -2,7 +2,9 @@ "name": "Unity.Multiplayer.Samples.SocialHub.Editor", "rootNamespace": "Unity.Multiplayer.Samples.SocialHub", "references": [ - "GUID:8314b31eee3cc495ca4a4f078575802d" + "GUID:8314b31eee3cc495ca4a4f078575802d", + "GUID:ae1323dabfa6440b3956d4cc5239e361", + "GUID:9ff2150ce7d7c415a8af1fbff3dc3e6c" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/AssemblyInfo.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/AssemblyInfo.cs index 9b00a0c99..fe86031e5 100644 --- a/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/AssemblyInfo.cs +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/AssemblyInfo.cs @@ -1,2 +1,3 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Unity.Multiplayer.Samples.SocialHub.Player")] +[assembly: InternalsVisibleTo("Unity.Multiplayer.Samples.SocialHub.Editor")] diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/DestructibleObject.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/DestructibleObject.cs new file mode 100644 index 000000000..3ae64d0fa --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/DestructibleObject.cs @@ -0,0 +1,114 @@ +using System; +using Unity.Multiplayer.Samples.SocialHub.Physics; +using Unity.Netcode; +using UnityEngine; + +namespace Unity.Multiplayer.Samples.SocialHub.Gameplay +{ + class DestructibleObject : PhysicsObjectMotion + { + [SerializeField] + float m_StartingHealth = 100f; + + [SerializeField] + float m_IntangibleDurationAfterDamage; + + float m_LastDamageTime; + + NetworkVariable m_Initialized = new NetworkVariable(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + + NetworkVariable m_Health = new NetworkVariable(0.0f, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + + public override void OnNetworkSpawn() + { + base.OnNetworkSpawn(); + InitializeDestructible(); + gameObject.name = $"[NetworkObjectId-{NetworkObjectId}]{name}"; + } + + protected override void OnContactEvent(ulong eventId, Vector3 averagedCollisionNormal, Rigidbody collidingBody, Vector3 contactPoint, bool hasCollisionStay = false, Vector3 averagedCollisionStayNormal = default) + { + var collidingBaseObjectMotion = collidingBody.GetComponent(); + var collidingBodyPhys = collidingBaseObjectMotion as PhysicsObjectMotion; + + // overriding this method to catch when a physics collision happens between two non-kinematic objects + if (!Rigidbody.isKinematic && !collidingBody.isKinematic && collidingBodyPhys != null && HasAuthority && collidingBodyPhys.HasAuthority) + { + var collisionMessageInfo = new CollisionMessageInfo(); + collisionMessageInfo.Damage = collidingBodyPhys.CollisionDamage; + collisionMessageInfo.SetFlag(true, (uint)collidingBodyPhys.CollisionType); + + // apply damage to this non-kinematic object + OnHandleCollision(collisionMessageInfo); + + collisionMessageInfo.Damage = CollisionDamage; + collisionMessageInfo.SetFlag(true, (uint)CollisionType); + + // this can be reworked to an interface, but for now this routes damage directly to another destructible + var destructible = collidingBodyPhys.GetComponent(); + // apply damage to other non-kinematic object + destructible.OnHandleCollision(collisionMessageInfo); + } + else + { + base.OnContactEvent(eventId, averagedCollisionNormal, collidingBody, contactPoint, hasCollisionStay, averagedCollisionStayNormal); + } + } + + protected override void OnHandleCollision(CollisionMessageInfo collisionMessage, bool isLocal = false, bool applyImmediately = false) + { + // Avatars don't damage destructible objects + if (m_Health.Value == 0.0f || collisionMessage.GetCollisionType() == CollisionType.Avatar) + { + base.OnHandleCollision(collisionMessage, isLocal, applyImmediately); + return; + } + + if (Time.realtimeSinceStartup - m_LastDamageTime < m_IntangibleDurationAfterDamage) + { + base.OnHandleCollision(collisionMessage, isLocal, applyImmediately); + return; + } + + ApplyCollisionDamage(collisionMessage.Damage); + + base.OnHandleCollision(collisionMessage, isLocal, applyImmediately); + } + + void ApplyCollisionDamage(float damage) + { + var currentHealth = Mathf.Max(0.0f, m_Health.Value - damage); + + if (currentHealth == 0.0f) + { + Rigidbody.isKinematic = true; + EnableColliders(false); + m_Health.Value = currentHealth; + NetworkObject.Despawn(); + // TODO: Spawn VFX locally + send VFX message + } + else + { + m_Health.Value = currentHealth; + m_LastDamageTime = Time.realtimeSinceStartup; + } + } + + void InitializeDestructible() + { + if (IsSessionOwner && !m_Initialized.Value) + { + InitializeDestructible(m_StartingHealth); + m_Initialized.Value = true; + } + } + + void InitializeDestructible(float health) + { + if (IsSpawned) + { + m_Health.Value = health; + } + } + } +} diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/DestructibleObject.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/DestructibleObject.cs.meta new file mode 100644 index 000000000..5975b7b17 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/DestructibleObject.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 496a933d305aa4e1c926bd7d19b42f46 \ No newline at end of file diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/EnvironmentTransform.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/TransferableObject.cs similarity index 88% rename from Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/EnvironmentTransform.cs rename to Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/TransferableObject.cs index 17decbb41..fc62bd8eb 100644 --- a/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/EnvironmentTransform.cs +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/TransferableObject.cs @@ -1,11 +1,10 @@ using System; using Unity.Netcode; -using Unity.Netcode.Components; using UnityEngine; namespace Unity.Multiplayer.Samples.SocialHub.Gameplay { - class EnvironmentTransform : NetworkTransform, IOwnershipRequestable, IGameplayEventInvokable + class TransferableObject : NetworkBehaviour, IOwnershipRequestable, IGameplayEventInvokable { public event Action OnNetworkObjectOwnershipRequestResponse; @@ -21,6 +20,7 @@ public override void OnNetworkSpawn() public override void OnNetworkDespawn() { + base.OnNetworkDespawn(); if (NetworkObject) { NetworkObject.OnOwnershipRequested -= OnOwnershipRequested; @@ -28,6 +28,8 @@ public override void OnNetworkDespawn() } OnGameplayEvent?.Invoke(NetworkObject, GameplayEvent.Despawned); + OnGameplayEvent = null; + OnNetworkObjectOwnershipRequestResponse = null; } protected override void OnOwnershipChanged(ulong previous, ulong current) diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/EnvironmentTransform.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/TransferableObject.cs.meta similarity index 100% rename from Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/EnvironmentTransform.cs.meta rename to Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/TransferableObject.cs.meta diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/Unity.Multiplayer.Samples.SocialHub.Gameplay.asmdef b/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/Unity.Multiplayer.Samples.SocialHub.Gameplay.asmdef index a09e1b725..506fb058a 100644 --- a/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/Unity.Multiplayer.Samples.SocialHub.Gameplay.asmdef +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Gameplay/Unity.Multiplayer.Samples.SocialHub.Gameplay.asmdef @@ -2,7 +2,8 @@ "name": "Unity.Multiplayer.Samples.SocialHub.Gameplay", "rootNamespace": "Unity.Multiplayer.Samples.SocialHub", "references": [ - "GUID:1491147abca9d7d4bb7105af628b223e" + "GUID:1491147abca9d7d4bb7105af628b223e", + "GUID:ae1323dabfa6440b3956d4cc5239e361" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Input/AvatarInputs.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Input/AvatarInputs.cs index c4abc9618..86dea27ee 100644 --- a/Experimental/DistributedAuthoritySample/Assets/Scripts/Input/AvatarInputs.cs +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Input/AvatarInputs.cs @@ -16,8 +16,6 @@ class AvatarInputs : MonoBehaviour [SerializeField] internal Vector2 Look; [SerializeField] - internal bool Jump; - [SerializeField] internal bool Sprint; [Header("Movement Settings")] @@ -30,6 +28,7 @@ class AvatarInputs : MonoBehaviour [SerializeField] internal bool CursorInputForLook = true; + internal event Action Jumped; internal event Action InteractTapped; internal event Action InteractHeld; @@ -100,7 +99,7 @@ void OnLook(InputValue value) void OnJump(InputValue value) { - JumpInput(value.isPressed); + Jumped?.Invoke(); } void OnSprint(InputValue value) @@ -119,11 +118,6 @@ void LookInput(Vector2 newLookDirection) Look = newLookDirection; } - void JumpInput(bool newJumpState) - { - Jump = newJumpState; - } - void SprintInput(bool newSprintState) { Sprint = newSprintState; diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/AssemblyInfo.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/AssemblyInfo.cs new file mode 100644 index 000000000..eea577a98 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Unity.Multiplayer.Samples.SocialHub.Player")] +[assembly: InternalsVisibleTo("Unity.Multiplayer.Samples.SocialHub.Gameplay")] +[assembly: InternalsVisibleTo("Unity.Multiplayer.Samples.SocialHub.Editor")] diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/AssemblyInfo.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/AssemblyInfo.cs.meta new file mode 100644 index 000000000..0e540c0c2 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/AssemblyInfo.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d3be452b8071c4c96bad702f8424c77f diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/BaseObjectMotionHandler.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/BaseObjectMotionHandler.cs new file mode 100644 index 000000000..376188097 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/BaseObjectMotionHandler.cs @@ -0,0 +1,358 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Netcode; +using Unity.Netcode.Components; +using UnityEngine; +using System.Collections.Generic; + +namespace Unity.Multiplayer.Samples.SocialHub.Physics +{ + class BaseObjectMotionHandler : NetworkTransform, ICollisionHandler, IContactEventHandler + { + protected bool m_IsPooled = true; + + [SerializeField] + CollisionType m_CollisionType; + + internal CollisionType CollisionType => m_CollisionType; + + [SerializeField] + ushort m_CollisionDamage; + + internal ushort CollisionDamage => m_CollisionDamage; + + protected CollisionMessageInfo m_CollisionMessage = new CollisionMessageInfo(); + + protected Rigidbody Rigidbody { get; private set; } + + protected NetworkRigidbody NetworkRigidbody { get; private set; } + + [Tooltip("Enables/Disables collision logging (based on per derived type)")] + [SerializeField] + protected bool m_DebugCollisions; + + [Tooltip("Enables/Disables damage logging (based on per derived type)")] + [SerializeField] + protected bool m_DebugDamage; + + [Tooltip("Add all colliders to this list that will be used to detect collisions (exclude triggers).")] + [SerializeField] + List m_Colliders; + + static int s_CollisionId = 0; + + protected ulong LastEventId { get; private set; } + + protected void EnableColliders(bool enable) + { + foreach (var collider in m_Colliders) + { + collider.enabled = enable; + } + } + + public Rigidbody GetRigidbody() + { + return Rigidbody; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected Vector3 GetObjectVelocity(bool getReference = false) + { + return OnGetObjectVelocity(getReference); + } + + protected virtual Vector3 OnGetObjectVelocity(bool getReference = false) + { + if (Rigidbody != null) + { +#if UNITY_2023_3_OR_NEWER + return Rigidbody.linearVelocity; +#else + return m_Rigidbody.velocity; +#endif + } + + return Vector3.zero; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void SetObjectVelocity(Vector3 velocity) + { + if (Rigidbody != null) + { +#if UNITY_2023_3_OR_NEWER + Rigidbody.linearVelocity = velocity; +#else + m_Rigidbody.velocity = velocity; +#endif + } + } + + protected Vector3 GetObjectAngularVelocity() + { + return OnGetObjectAngularVelocity(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual Vector3 OnGetObjectAngularVelocity() + { + if (Rigidbody != null) + { + return Rigidbody.angularVelocity; + } + + return Vector3.zero; + } + + protected void IgnoreCollision(GameObject objectA, GameObject objectB, bool shouldIgnore) + { + if (objectA == null || objectB == null) + { + return; + } + + var rootA = objectA.transform.root.gameObject; + var rootB = objectB.transform.root.gameObject; + + var collidersA = rootA.GetComponentsInChildren(); + var collidersB = rootB.GetComponentsInChildren(); + + foreach (var colliderA in collidersA) + { + foreach (var colliderB in collidersB) + { + UnityEngine.Physics.IgnoreCollision(colliderA, colliderB, shouldIgnore); + } + } + } + + protected override void Awake() + { + Rigidbody = GetComponent(); + NetworkRigidbody = GetComponent(); + + base.Awake(); + } + + protected virtual void Start() + { + m_CollisionMessage.Damage = m_CollisionDamage; + m_CollisionMessage.SetFlag(true, (uint)m_CollisionType); + } + + /// + /// Invoked every network tick if this instance has sent a update. + /// + /// + protected override void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState) + { + base.OnAuthorityPushTransformState(ref networkTransformState); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static GameObject GetRootParent(GameObject parent) + { + return parent.transform.root.gameObject; + } + + /// + /// This method provides the ability to make adjustments to the collision message as well as apply damage locally if needed + /// + /// + /// + protected virtual bool OnPrepareCollisionMessage(Vector3 averagedCollisionNormal, BaseObjectMotionHandler targetBaseObjectMotionHandler) + { + return true; + } + + protected virtual void OnHandleCollision(CollisionMessageInfo collisionMessage, bool isLocal = false, bool applyImmediately = false) + { + + } + + void HandleCollision(CollisionMessageInfo collisionMessage, bool isLocal = false, bool applyImmediately = false) + { + OnHandleCollision(collisionMessage, isLocal, applyImmediately); + + // Handling is invoked before logging so logging can determine the end result. + if (m_DebugCollisions) + { + LogHandleCollision(collisionMessage); + } + } + + /// + /// Used to communicate collisions + /// + /// + /// + [Rpc(SendTo.Authority, RequireOwnership = false)] + public void HandleCollisionRpc(CollisionMessageInfo collisionMessage, RpcParams rpcParams = default) + { + // If authority changes while this message is in flight, forward it to the new authority + if (!HasAuthority) + { + LogMessage($"[HandleCollisionRpc][Not Owner][Routing Collision][{name}] Routing to Client-{OwnerClientId}"); + SendCollisionMessage(m_CollisionMessage); + return; + } + + m_CollisionMessage.SourceOwner = rpcParams.Receive.SenderClientId; + m_CollisionMessage.TargetOwner = OwnerClientId; + HandleCollision(collisionMessage); + } + + /// + /// Invoked by the owner of the object inflicting damage, this will handle the RPC routing + /// of the message to the appropriate targeted owner of the object taking damage + /// + /// + public void SendCollisionMessage(CollisionMessageInfo collisionMessage) + { + LogCollision(collisionMessage); + HandleCollisionRpc(collisionMessage); + } + + /// + /// Override this method if you have registered the instance with and + /// want to customize collision. + /// + /// + /// Only automatically handles collisions. + /// + /// + /// + /// The that collided with this object. + /// + /// + /// + protected virtual void OnContactEvent(ulong eventId, Vector3 averagedCollisionNormal, Rigidbody collidingBody, Vector3 contactPoint, bool hasCollisionStay = false, Vector3 averagedCollisionStayNormal = default) + { + + } + + /// + /// Invoked from when a non-kinematic body collides + /// with another registered . + /// + /// + /// The averaged normal of the collision + /// The this objects collided with + /// + /// + /// + /// To enable this callback to be triggered, make sure you enable the Provides Contacts toggle on your + /// desired + /// + public void ContactEvent(ulong eventId, Vector3 averageNormal, Rigidbody collidingBody, Vector3 contactPoint, bool hasCollisionStay = false, Vector3 averagedCollisionStayNormal = default) + { + if (!IsSpawned) + { + return; + } + + OnContactEvent(eventId, averageNormal, collidingBody, contactPoint, hasCollisionStay, averagedCollisionStayNormal); + LastEventId = eventId; + } + + /// + /// Invoked this to send a collision message to the authoritative instance. + /// + /// + /// + protected void EventCollision(Vector3 averagedCollisionNormal, BaseObjectMotionHandler collidingBodyBaseObject) + { +#if DEBUG || UNITY_EDITOR + if (m_DebugCollisions) + { + LogCollision(ref collidingBodyBaseObject); + } +#endif + + if (OnPrepareCollisionMessage(averagedCollisionNormal, collidingBodyBaseObject)) + { + s_CollisionId++; + m_CollisionMessage.CollisionId = s_CollisionId; + m_CollisionMessage.Time = Time.realtimeSinceStartup; + m_CollisionMessage.Source = OwnerClientId; + m_CollisionMessage.SourceId = NetworkObjectId; + m_CollisionMessage.Destination = collidingBodyBaseObject.OwnerClientId; + m_CollisionMessage.DestinationNetworkObjId = collidingBodyBaseObject.NetworkObjectId; + m_CollisionMessage.DestinationBehaviourId = collidingBodyBaseObject.NetworkBehaviourId; + + // Otherwise, send the collision message to the owner of the object + collidingBodyBaseObject.SendCollisionMessage(m_CollisionMessage); + } + } + + #region DEBUG CONSOLE LOGGING METHODS + + /// + /// Override to handle local collisions generating an outbound message + /// + /// + /// + protected virtual string OnLogCollision(ref BaseObjectMotionHandler objectHit) + { + return "[LocalCollision-End]"; + } + + void LogCollision(ref BaseObjectMotionHandler objectHit) + { + if (!m_DebugCollisions) + { + return; + } + + var distance = Vector3.Distance(transform.position, objectHit.transform.position); + Debug.Log($"[{Time.realtimeSinceStartup}][LocalCollision][{name}][collided with][{objectHit.name}][Collider:{name}][Distance: {distance}]" + + $"{OnLogCollision(ref objectHit)}.", this); + } + + protected virtual string OnLogCollision(CollisionMessageInfo collisionMessage) + { + return string.Empty; + } + + protected void LogCollision(CollisionMessageInfo collisionMessage) + { + if (!m_DebugDamage || collisionMessage.Damage == 0) + { + return; + } + + var additionalInfo = OnLogCollision(collisionMessage); + Debug.Log($"[{name}][++Damaged++][Client-{collisionMessage.TargetOwner}][{collisionMessage.GetCollisionType()}][Dmg:{collisionMessage.Damage}] {additionalInfo}", this); + } + + /// + /// Override to log incoming collision messages + /// + /// + protected virtual string OnLogHandleCollision(ref CollisionMessageInfo collisionMessage) + { + return "[CollisionMessage-End]"; + } + + void LogHandleCollision(CollisionMessageInfo collisionMessage, bool isLocal = false) + { + var distance = -1.0f; + if (NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(collisionMessage.DestinationNetworkObjId)) + { + distance = Vector3.Distance(transform.position, NetworkManager.SpawnManager.SpawnedObjects[collisionMessage.DestinationNetworkObjId].transform.position); + } + + var distStr = distance == -1.0f ? $"{collisionMessage.DestinationNetworkObjId} DNE!!" : $"Distance: {distance}"; + Debug.Log($"[{collisionMessage.CollisionId}][{collisionMessage.Time}][CollisionMessage][IsLocal: {isLocal}][{name}][Src:{collisionMessage.Source}][Dest:{collisionMessage.Destination}]" + + $"[NObjId:{collisionMessage.DestinationNetworkObjId}][NBvrId:{collisionMessage.DestinationBehaviourId}][{distStr}]{OnLogHandleCollision(ref collisionMessage)}.", this); + } + + protected void LogMessage(string msg, bool forceMessage = false, float messageTime = 10.0f) + { + Debug.Log($"[{name}]{msg} {messageTime} {forceMessage}"); + } + + #endregion + } +} diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/BaseObjectMotionHandler.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/BaseObjectMotionHandler.cs.meta new file mode 100644 index 000000000..d016b1d61 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/BaseObjectMotionHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d97f2549555383847a36d4dc0a26a53f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/CollisionMessageInfo.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/CollisionMessageInfo.cs new file mode 100644 index 000000000..348d08551 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/CollisionMessageInfo.cs @@ -0,0 +1,145 @@ +using System.Runtime.CompilerServices; +using Unity.Netcode; +using UnityEngine; + +namespace Unity.Multiplayer.Samples.SocialHub.Physics +{ + /// + /// Only 15 types are allowed. + /// Applied to the first byte of + /// + enum CollisionType + { + Avatar = 0x01, + Destructible = 0x02, + DebugCollision = 0x0F, + + // Maximum value for collision types is 15 - 0x0E + } + + /// + /// Flags to be used with + /// 16, 32, 64, and 128 are used for flags + /// + enum CollisionCategoryFlags + { + // Four flags are reserved + Standard = 0x10, + CollisionForce = 0x20, + CollisionPoint = 0x40, + } + + struct CollisionMessageInfo : INetworkSerializable + { + /// + /// Flags are serialized and determine the collision type + /// + public byte Flags; + + /// + /// Damage is serialized and is set based on the type + /// + public ushort Damage; + + /// + /// Collision force is serialized only if the flags contains the + /// flag. + /// + public Vector3 CollisionForce; + + /// + /// Collision force is serialized only if the flags contains the + /// flag. + /// + public Vector3 CollisionPoint; + + /// + /// Never serialized, only used locally to determine if + /// we are + /// + public bool DebugCollisionEnabled; + + public int CollisionId; + public float Time; + public ulong Source; + public ulong Destination; + public ulong DestinationNetworkObjId; + public ushort DestinationBehaviourId; + + /// + /// Never serialized, only used locally + /// + public ulong SourceId; + + /// + /// Never serialized, only used locally + /// + public ulong SourceOwner; + + /// + /// Never serialized, only used locally + /// + public ulong TargetOwner; + + public CollisionType GetCollisionType() + { + return (CollisionType)(Flags & 0x0F); + } + + public bool HasCollisionForce() + { + return (Flags & (uint)CollisionCategoryFlags.CollisionForce) == (byte)CollisionCategoryFlags.CollisionForce; + } + + private bool DebugCollisionsEnabled + { + get { return GetFlag((uint)CollisionType.DebugCollision); } + + set { SetFlag(value, (uint)CollisionType.DebugCollision); } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetFlag(bool set, uint flag) + { + var flags = (uint)Flags; + if (set) { flags = flags | flag; } + else { flags = flags & ~flag; } + + Flags = (byte)flags; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetFlag(uint flag) + { + var flags = (uint)Flags; + return (flags & flag) != 0; + } + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref Flags); + serializer.SerializeValue(ref Damage); + + // only if we have a collision force should we serialize it (read and write) + if (GetFlag((uint)CollisionCategoryFlags.CollisionForce)) + { + serializer.SerializeValue(ref CollisionForce); + } + + if (GetFlag((uint)CollisionCategoryFlags.CollisionPoint)) + { + serializer.SerializeValue(ref CollisionForce); + } + + if (DebugCollisionsEnabled) + { + serializer.SerializeValue(ref CollisionId); + serializer.SerializeValue(ref Time); + serializer.SerializeValue(ref Source); + serializer.SerializeValue(ref Destination); + serializer.SerializeValue(ref DestinationNetworkObjId); + serializer.SerializeValue(ref DestinationBehaviourId); + } + } + } +} diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/CollisionMessageInfo.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/CollisionMessageInfo.cs.meta new file mode 100644 index 000000000..dcd19bebc --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/CollisionMessageInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c2fd728083c5b4740926d76679851317 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/ICollisionHandler.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/ICollisionHandler.cs new file mode 100644 index 000000000..218f88b9e --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/ICollisionHandler.cs @@ -0,0 +1,22 @@ +using Unity.Netcode; + +namespace Unity.Multiplayer.Samples.SocialHub.Physics +{ + interface ICollisionHandler + { + /// + /// Invoked by non-authority objects detecting a collision, this determines the + /// appropriate targeted authority of the object being collided with. If the local + /// instance is authority it handles collision locally + /// + void SendCollisionMessage(CollisionMessageInfo collisionMessage); + + /// + /// Authority instances receive collision messages from non-authority instances via this implemented method + /// + /// + /// + [Rpc(SendTo.Authority, DeferLocal = true)] + void HandleCollisionRpc(CollisionMessageInfo collisionMessage, RpcParams rpcParams = default); + } +} diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/ICollisionHandler.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/ICollisionHandler.cs.meta new file mode 100644 index 000000000..675c74f2c --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/ICollisionHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e77960601117fe644a32bd9dc9a9a34d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsObjectMotion.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsObjectMotion.cs new file mode 100644 index 000000000..dcfa701db --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsObjectMotion.cs @@ -0,0 +1,394 @@ +using System; +using System.Collections.Generic; +using Unity.Netcode; +using Unity.Netcode.Components; +using UnityEngine; +using Unity.Multiplayer.Samples.SocialHub.Utils; + +namespace Unity.Multiplayer.Samples.SocialHub.Physics +{ + class PhysicsObjectMotion : BaseObjectMotionHandler + { + [SerializeField] + float m_MaxAngularVelocity = 30; + [SerializeField] + float m_MaxVelocity = 30; + + List m_RemoteAppliedForce = new List(); + + protected NetworkVariable m_IsInitialized = new NetworkVariable(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + /// + /// All of the below values keep the physics objects synchronized between clients so when ownership changes the local Rigidbody can be configured to mirror + /// the last known physics related states. + /// + protected NetworkVariable m_Mass = new NetworkVariable(1.0f, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + protected NetworkVariable m_AngularVelocity = new NetworkVariable(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + protected NetworkVariable m_Velocity = new NetworkVariable(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + protected NetworkVariable m_Torque = new NetworkVariable(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + protected NetworkVariable m_Force = new NetworkVariable(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + + protected override Vector3 OnGetObjectVelocity(bool getReference = false) + { + if (getReference) + { + return m_Velocity.Value; + } + + return base.OnGetObjectVelocity(getReference); + } + + protected override Vector3 OnGetObjectAngularVelocity() + { + return m_AngularVelocity.Value; + } + + protected void UpdateVelocity(Vector3 velocity, bool updateObjectVelocity = true) + { + if (HasAuthority) + { + if (updateObjectVelocity) + { + SetObjectVelocity(velocity); + } + + m_Velocity.Value = velocity; + } + } + + protected void UpdateAngularVelocity(Vector3 angularVelocity) + { + if (HasAuthority) + { + Rigidbody.angularVelocity = angularVelocity; + m_AngularVelocity.Value = angularVelocity; + } + } + + protected void UpdateTorque(Vector3 torque) + { + if (HasAuthority) + { + Rigidbody.AddTorque(torque); + m_Torque.Value = torque; + } + } + + protected void UpdateImpulseForce(Vector3 impulseForce) + { + if (HasAuthority) + { + Rigidbody.AddForce(impulseForce, ForceMode.Impulse); + m_Force.Value = impulseForce; + } + } + + /// + /// Invoked when authority pushes state, we keep track whether the most recent state + /// had rotation or position deltas. + /// + /// + /// This keeps track of angular and motion velocities in order to keep objects synchronized + /// when ownership changes. + /// + /// + protected override void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState) + { + // If we haven't already initialized for the first time or haven't initialized previous state values during spawn then exit early + if (!m_IsInitialized.Value) + { + return; + } + + if (networkTransformState.HasRotAngleChange && !Rigidbody.isKinematic) + { + if (Vector3.Distance(GetObjectAngularVelocity(), Rigidbody.angularVelocity) > RotAngleThreshold) + { + UpdateAngularVelocity(Rigidbody.angularVelocity); + } + } + + if (networkTransformState.HasPositionChange && !Rigidbody.isKinematic) + { + var velocity = GetObjectVelocity(); + if (Vector3.Distance(GetObjectVelocity(true), velocity) > PositionThreshold) + { + UpdateVelocity(velocity, false); + } + } + + base.OnAuthorityPushTransformState(ref networkTransformState); + } + + public override void OnNetworkSpawn() + { + // When creating customized NetworkTransform behaviors, you must always invoke the base OnNetworkSpawn + // method if you override it in any child derive generation (i.e. always assure the NetworkTransform.OnNetworkSpawn + // method is invoked) + base.OnNetworkSpawn(); + + // Assure all colliders are enabled (authority and non-authority) + EnableColliders(true); + + // Register for contact events (authority and non-authority) + RigidbodyContactEventManager.Instance.RegisterHandler(this); + + // Clamp the linear and angular velocities + Rigidbody.maxAngularVelocity = m_MaxAngularVelocity; + Rigidbody.maxLinearVelocity = m_MaxVelocity; + if (HasAuthority) + { + // Assure we are not still in kinematic mode + NetworkRigidbody.SetIsKinematic(false); + + // Since state can be preserved during a CMB service connection when there are no clients connected, + // this section determines whether we need to initialize the physics object or just apply the last + // known velocities. +#if SESSION_STORE_ENABLED + if (!BeenInitialized.Value) +#endif + { + m_IsInitialized.Value = true; + } +#if SESSION_STORE_ENABLED + else + { + Rigidbody.angularVelocity = Vector3.ClampMagnitude(GetObjectAngularVelocity(), MaxAngularVelocity); + SetObjectVelocity(Vector3.ClampMagnitude(GetObjectVelocity(), MaxVelocity)); + } +#endif + } + } + + public override void OnNetworkDespawn() + { + RigidbodyContactEventManager.Instance.RegisterHandler(this, false); + + // Invoke the base before applying any additional adjustments + base.OnNetworkDespawn(); + + // If we are pooled and not shutting down, then reset the physics object for re-use later + // ** Important to do this ** + if (m_IsPooled) + { + EnableColliders(false); + if (!Rigidbody.isKinematic) + { + Rigidbody.angularVelocity = Vector3.zero; + SetObjectVelocity(Vector3.zero); + NetworkRigidbody.SetIsKinematic(true); + } + + m_IsInitialized.Reset(); + m_AngularVelocity.Reset(); + m_Velocity.Reset(); + m_Torque.Reset(); + m_Force.Reset(); + m_Mass.Reset(); + } + } + + /// + /// When ownership changes, we apply the last known angular and motion velocities. + /// Otherwise, + /// + protected override void OnOwnershipChanged(ulong previous, ulong current) + { + if (NetworkManager.LocalClientId == current) + { + NetworkRigidbody.SetIsKinematic(false); + if (m_IsInitialized.Value) + { + Rigidbody.angularVelocity = Vector3.ClampMagnitude(GetObjectAngularVelocity(), m_MaxAngularVelocity); + SetObjectVelocity(Vector3.ClampMagnitude(GetObjectVelocity(true), m_MaxVelocity)); + } + else + { + Rigidbody.AddTorque(m_Torque.Value, ForceMode.Impulse); + Rigidbody.AddForce(m_Force.Value, ForceMode.Impulse); + } + } + else + { + NetworkRigidbody.SetIsKinematic(true); + } + + base.OnOwnershipChanged(previous, current); + } + + /// + /// Handles queuing up incoming collisions (remote and local) to be processed + /// + protected override void OnHandleCollision(CollisionMessageInfo collisionMessage, bool isLocal = false, bool applyImmediately = false) + { + if (collisionMessage.HasCollisionForce()) + { + AddForceDirect(collisionMessage.CollisionForce); + } + + base.OnHandleCollision(collisionMessage, isLocal, applyImmediately); + } + + void AddForceDirect(Vector3 force) + { + var remoteForce = new RemoteForce() + { + TargetForce = force, + AppliedForce = Vector3.zero, + }; + + m_RemoteAppliedForce.Add(remoteForce); + } + + protected override void OnContactEvent(ulong eventId, Vector3 averagedCollisionNormal, Rigidbody collidingBody, Vector3 contactPoint, bool hasCollisionStay = false, Vector3 averagedCollisionStayNormal = default) + { + var collidingBaseObjectMotion = collidingBody.GetComponent(); + var collidingBodyPhys = collidingBaseObjectMotion as PhysicsObjectMotion; + + // If we don't have authority over either object or we are doing a second FixedUpdate pass, then exit early + if (eventId == LastEventId || collidingBaseObjectMotion == null || (!HasAuthority && !collidingBaseObjectMotion.HasAuthority)) + { + return; + } + + if (collidingBodyPhys == null) + { + return; + } + + var collisionNormal = hasCollisionStay ? averagedCollisionStayNormal : averagedCollisionNormal; + + var thisVelocity = (!Rigidbody.isKinematic ? Rigidbody.linearVelocity.sqrMagnitude : GetObjectVelocity().sqrMagnitude) * 0.5f; + var otherVelocity = (!collidingBody.isKinematic ? collidingBody.linearVelocity.sqrMagnitude : collidingBodyPhys.GetObjectVelocity().sqrMagnitude) * 0.5f; + var thisKineticForce = (Rigidbody.mass / collidingBody.mass) * -collisionNormal * thisVelocity; + var otherKineticForce = (collidingBody.mass / Rigidbody.mass) * collisionNormal * otherVelocity; + + if (!Rigidbody.isKinematic && collidingBody.isKinematic && thisVelocity > 0.01f) + { + m_CollisionMessage.CollisionForce = thisKineticForce; + m_CollisionMessage.SetFlag(true, (uint)CollisionCategoryFlags.CollisionForce); + if (m_DebugCollisions) + { + Debug.Log($"[{name}][SecondBody][Collision Stay: {hasCollisionStay}] Sending impulse thrust {MathUtils.GetVector3Values(thisKineticForce)} to {collidingBody.name}.", this); + } + + // Send collision to owner of kinematic body + EventCollision(averagedCollisionNormal, collidingBodyPhys); + } + else if (Rigidbody.isKinematic && !collidingBody.isKinematic && otherVelocity > 0.01f) + { + // our kinematic Rigidbody was hit by a non-kinematic physics-moving Rigidbody + + collidingBodyPhys.m_CollisionMessage.CollisionForce = otherKineticForce; + collidingBodyPhys.m_CollisionMessage.SetFlag(true, (uint)CollisionCategoryFlags.CollisionForce); + collidingBodyPhys.EventCollision(averagedCollisionNormal, this); + if (m_DebugCollisions) + { + Debug.Log($"[{collidingBodyPhys.name}][FirstBody][Collision Stay: {hasCollisionStay}] Sending impulse thrust {MathUtils.GetVector3Values(otherKineticForce)} to {name}.", this); + } + } + + base.OnContactEvent(eventId, averagedCollisionNormal, collidingBody, contactPoint, hasCollisionStay, averagedCollisionStayNormal); + } + + /// + /// Accumulative-ly apply the resultant collision force + /// + /// s + void ApplyCollisionForce(Vector3 force) + { + Rigidbody.AddForce(force, ForceMode.Impulse); + Rigidbody.AddTorque(force * 0.25f, ForceMode.Impulse); + } + + /// + /// Processes the queued collisions forces + /// + void ProcessRemoteForces() + { + if (m_RemoteAppliedForce.Count == 0) + { + return; + } + + var accumulativeForce = Vector3.zero; + for (int i = m_RemoteAppliedForce.Count - 1; i >= 0; i--) + { + var remoteForce = m_RemoteAppliedForce[i]; + accumulativeForce += remoteForce.TargetForce; + if (MathUtils.Approximately(remoteForce.TargetForce, Vector3.zero)) + { + m_RemoteAppliedForce.RemoveAt(i); + } + else + { + m_RemoteAppliedForce[i] = remoteForce; + } + } + + ApplyCollisionForce(accumulativeForce); + m_RemoteAppliedForce.Clear(); + } + + /// + /// Hijack the FixedUpdate to assure physics simulation is always + /// taking into consideration the queued collisions to process + /// + /// + /// Override this method to apply additional forces to your physics object + /// + protected virtual void FixedUpdate() + { + if (!IsSpawned || !HasAuthority || Rigidbody != null && Rigidbody.isKinematic) + { + return; + } + + // Process any queued collisions + ProcessRemoteForces(); + } + + /// + /// When is enabled, this will log locally + /// generated collision info for the derived component + /// + /// the hit + /// log string + protected override string OnLogCollision(ref BaseObjectMotionHandler objectHit) + { + return $"[CF: {MathUtils.GetVector3Values(ref m_CollisionMessage.CollisionForce)}]-{base.OnLogCollision(ref objectHit)}"; + } + + /// + /// When is enabled, this will log remotely + /// received collision info for the derived component + /// + /// the message received + /// log string + protected override string OnLogHandleCollision(ref CollisionMessageInfo collisionMessage) + { + var sourceCollider = $"{collisionMessage.SourceOwner}"; + if (NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(collisionMessage.SourceOwner)) + { + sourceCollider = NetworkManager.SpawnManager.SpawnedObjects[collisionMessage.SourceOwner].name; + } + + var resLinearVel = string.Empty; + var resAngularVel = string.Empty; + if (Rigidbody != null) + { + resLinearVel = MathUtils.GetVector3Values(GetObjectVelocity()); + resAngularVel = MathUtils.GetVector3Values(Rigidbody.angularVelocity); + } + + return $"[**Collision-Info**][To: {name}][By:{sourceCollider}][Force:{MathUtils.GetVector3Values(ref collisionMessage.CollisionForce)}]" + + $"[LinVel: {resLinearVel}][AngVel: {resAngularVel}]-{base.OnLogHandleCollision(ref collisionMessage)}"; + } + } + + struct RemoteForce + { + public float EndOfLife; + public Vector3 TargetForce; + public Vector3 AppliedForce; + } +} diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsObjectMotion.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsObjectMotion.cs.meta new file mode 100644 index 000000000..2785f4cca --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsObjectMotion.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d3f561fb09787c42abad1ab0a51da2f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsPlayerController.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsPlayerController.cs new file mode 100644 index 000000000..df68b2b8a --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsPlayerController.cs @@ -0,0 +1,133 @@ +using UnityEngine; + +namespace Unity.Multiplayer.Samples.SocialHub.Physics +{ + [RequireComponent(typeof(Rigidbody))] + class PhysicsPlayerController : MonoBehaviour + { + [SerializeField] + Rigidbody m_Rigidbody; + + [SerializeField] + PhysicsPlayerControllerSettings m_PhysicsPlayerControllerSettings; + + // cached grounded check + bool m_IsGrounded; + RaycastHit[] m_RaycastHits = new RaycastHit[1]; + Ray m_Ray; + + Vector3 m_Movement; + bool m_Jump; + bool m_Sprint; + + internal void OnFixedUpdate() + { + if (m_Rigidbody != null && m_Rigidbody.isKinematic) + { + return; + } + + UpdateGroundedStatus(); + + ApplyMovement(); + + ApplyJump(); + + ApplyDrag(); + + ApplyCustomGravity(); + } + + void UpdateGroundedStatus() + { + m_IsGrounded = IsGrounded(); + } + + bool IsGrounded() + { + // Perform a raycast to check if the character is grounded + m_Ray.origin = m_Rigidbody.worldCenterOfMass; + m_Ray.direction = Vector3.down; + return UnityEngine.Physics.RaycastNonAlloc(m_Ray, m_RaycastHits, m_PhysicsPlayerControllerSettings.GroundCheckDistance) > 0; + } + + void ApplyMovement() + { + if (Mathf.Approximately(m_Movement.magnitude, 0f)) + { + return; + } + + var velocity = m_Rigidbody.linearVelocity; + var desiredVelocity = m_Movement * (m_Sprint ? m_PhysicsPlayerControllerSettings.SprintSpeed : m_PhysicsPlayerControllerSettings.WalkSpeed); + var targetVelocity = new Vector3(desiredVelocity.x, velocity.y, desiredVelocity.z); + var velocityChange = targetVelocity - velocity; + + if (m_IsGrounded) + { + // Apply force proportional to acceleration while grounded + var force = velocityChange * m_PhysicsPlayerControllerSettings.Acceleration; + m_Rigidbody.AddForce(force, ForceMode.Acceleration); + } + else + { + // Apply reduced force in the air for air control + var force = velocityChange * (m_PhysicsPlayerControllerSettings.Acceleration * m_PhysicsPlayerControllerSettings.AirControlFactor); + m_Rigidbody.AddForce(force, ForceMode.Acceleration); + } + + // maybe add magnitude check? + var targetAngle = Mathf.Atan2(m_Movement.x, m_Movement.z) * Mathf.Rad2Deg; + var targetRotation = Quaternion.Euler(0, targetAngle, 0); + var smoothRotation = Quaternion.Lerp(m_Rigidbody.rotation, targetRotation, Time.fixedDeltaTime * m_PhysicsPlayerControllerSettings.RotationSpeed); + m_Rigidbody.MoveRotation(smoothRotation); + + m_Movement = Vector3.zero; + } + + void ApplyJump() + { + if (m_Jump && m_IsGrounded) + { + m_Rigidbody.AddForce(Vector3.up * m_PhysicsPlayerControllerSettings.JumpImpusle, ForceMode.Impulse); + } + m_Jump = false; + } + + void ApplyDrag() + { + var groundVelocity = m_Rigidbody.linearVelocity; + groundVelocity.y = 0f; + if (groundVelocity.magnitude > 0f) + { + // Apply deceleration force to stop movement + var dragForce = -m_PhysicsPlayerControllerSettings.DragCoefficient * groundVelocity.magnitude * groundVelocity; + m_Rigidbody.AddForce(dragForce, ForceMode.Acceleration); + } + } + + void ApplyCustomGravity() + { + var customGravity = UnityEngine.Physics.gravity * (m_PhysicsPlayerControllerSettings.CustomGravityMultiplier - 1); + m_Rigidbody.AddForce(customGravity, ForceMode.Acceleration); + } + + public void SetMovement(Vector3 movement) + { + m_Movement = movement; + } + + public void SetJump(bool jump) + { + if (jump) + { + m_Jump = true; + } + } + + public void SetSprint(bool sprint) + { + m_Sprint = sprint; + } + } +} diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsPlayerController.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsPlayerController.cs.meta new file mode 100644 index 000000000..7598df3a1 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsPlayerController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e222c86e1ae554b699fe6d68d3cf4ba9 \ No newline at end of file diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsPlayerControllerSettings.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsPlayerControllerSettings.cs new file mode 100644 index 000000000..2abc876f3 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsPlayerControllerSettings.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +namespace Unity.Multiplayer.Samples.SocialHub.Physics +{ + [CreateAssetMenu(fileName = "PhysicsPlayerControllerSettings", menuName = "ScriptableObjects/PhysicsPlayerControllerSettings", order = 1)] + class PhysicsPlayerControllerSettings : ScriptableObject + { + [SerializeField] + internal float WalkSpeed; + [SerializeField] + internal float SprintSpeed; + [SerializeField] + internal float Acceleration; + [SerializeField] + internal float DragCoefficient; + [SerializeField] + internal float AirControlFactor; + [SerializeField] + internal float JumpImpusle; + [SerializeField] + internal float CustomGravityMultiplier; + [SerializeField] + internal float RotationSpeed; + [SerializeField] + internal float GroundCheckDistance; + } +} diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsPlayerControllerSettings.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsPlayerControllerSettings.cs.meta new file mode 100644 index 000000000..b10dd2b41 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/PhysicsPlayerControllerSettings.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 48976c2ba473849eab61b8e1398b8644 \ No newline at end of file diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/Unity.Multiplayer.Samples.SocialHub.Physics.asmdef b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/Unity.Multiplayer.Samples.SocialHub.Physics.asmdef new file mode 100644 index 000000000..d3213dc19 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/Unity.Multiplayer.Samples.SocialHub.Physics.asmdef @@ -0,0 +1,17 @@ +{ + "name": "Unity.Multiplayer.Samples.SocialHub.Physics", + "rootNamespace": "Unity.Multiplayer.Samples.SocialHub", + "references": [ + "GUID:1491147abca9d7d4bb7105af628b223e", + "GUID:a469b04c8dd43463084f4a2b513f1284" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/Unity.Multiplayer.Samples.SocialHub.Physics.asmdef.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/Unity.Multiplayer.Samples.SocialHub.Physics.asmdef.meta new file mode 100644 index 000000000..f4481a94b --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Physics/Unity.Multiplayer.Samples.SocialHub.Physics.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ae1323dabfa6440b3956d4cc5239e361 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AssemblyInfo.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AssemblyInfo.cs new file mode 100644 index 000000000..ada318509 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AssemblyInfo.cs @@ -0,0 +1,2 @@ +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Unity.Multiplayer.Samples.SocialHub.Editor")] diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AssemblyInfo.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AssemblyInfo.cs.meta new file mode 100644 index 000000000..973e63c16 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AssemblyInfo.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 47327abb6038742b08c7675b13b1765f diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AvatarInteractions.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AvatarInteractions.cs index e511bc6ca..e5daee9a8 100644 --- a/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AvatarInteractions.cs +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AvatarInteractions.cs @@ -47,6 +47,7 @@ void Awake() public override void OnNetworkSpawn() { + base.OnNetworkSpawn(); if (!HasAuthority) { return; @@ -64,6 +65,7 @@ public override void OnNetworkSpawn() public override void OnNetworkDespawn() { + base.OnNetworkDespawn(); if (m_AvatarInputs) { m_AvatarInputs.InteractTapped -= OnTapPerformed; @@ -93,7 +95,7 @@ void OnHoldReleased(double holdDuration) void PickUp() { - if (Physics.OverlapBoxNonAlloc(m_InteractCollider.transform.position, m_InteractCollider.bounds.extents, m_Results, Quaternion.identity, mask: m_PickupableLayerMask) > 0) + if (UnityEngine.Physics.OverlapBoxNonAlloc(m_InteractCollider.transform.position, m_InteractCollider.bounds.extents, m_Results, Quaternion.identity, mask: m_PickupableLayerMask) > 0) { if (m_Results[0].TryGetComponent(out NetworkObject otherNetworkObject)) { @@ -167,7 +169,7 @@ void AttachToFixedJoint(NetworkObject other) gameplayEventInvokable.OnGameplayEvent += OnGameplayEvent; // prevent collisions from this collider to the picked up object and vice-versa - Physics.IgnoreCollision(m_MainCollider, other.GetComponent(), true); + UnityEngine.Physics.IgnoreCollision(m_MainCollider, other.GetComponent(), true); } } @@ -185,7 +187,7 @@ void OnGameplayEvent(NetworkObject networkObject, GameplayEvent gameplayEvent) // revert collision if (networkObject.TryGetComponent(out Collider otherCollider)) { - Physics.IgnoreCollision(m_MainCollider, otherCollider, false); + UnityEngine.Physics.IgnoreCollision(m_MainCollider, otherCollider, false); } // don't have ownership of the item, thus we can't invoke DetachFromFixedJoint(), but we need to remove created FixedJoint component @@ -210,7 +212,7 @@ void ReleaseHeldObject() m_HoldingRigidbody.DetachFromFixedJoint(); m_HoldingRigidbody.GetComponent().useGravity = true; - Physics.IgnoreCollision(m_MainCollider, m_HoldingRigidbody.GetComponent(), false); + UnityEngine.Physics.IgnoreCollision(m_MainCollider, m_HoldingRigidbody.GetComponent(), false); m_HoldingRigidbody = null; } diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AvatarTransform.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AvatarTransform.cs index 7601e7640..67b9e2b17 100644 --- a/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AvatarTransform.cs +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/AvatarTransform.cs @@ -1,45 +1,23 @@ -using Unity.Netcode.Components; +using System; using UnityEngine; using Unity.Multiplayer.Samples.SocialHub.Input; +using Unity.Multiplayer.Samples.SocialHub.Physics; +using Unity.Netcode; using UnityEngine.InputSystem; namespace Unity.Multiplayer.Samples.SocialHub.Player { [RequireComponent(typeof(Rigidbody))] - public class AvatarTransform : NetworkTransform + class AvatarTransform : PhysicsObjectMotion, INetworkUpdateSystem { - [SerializeField] - Rigidbody m_Rigidbody; [SerializeField] PlayerInput m_PlayerInput; [SerializeField] AvatarInputs m_AvatarInputs; [SerializeField] - float m_WalkSpeed; - [SerializeField] - float m_SprintSpeed; - [SerializeField] - float m_Acceleration; - [SerializeField] - float m_DragCoefficient; - [SerializeField] - float m_AirControlFactor; - [SerializeField] - float m_JumpImpusle; + AvatarInteractions m_AvatarInteractions; [SerializeField] - float m_CustomGravityMultiplier; - [SerializeField] - float m_RotationSpeed; - [SerializeField] - float m_GroundCheckDistance; - - Vector3 m_Movement; - // grab jump state from input and clear after consumed - bool m_Jump; - // cached grounded check - bool m_IsGrounded; - RaycastHit[] m_RaycastHits = new RaycastHit[1]; - Ray m_Ray; + PhysicsPlayerController m_PhysicsPlayerController; public override void OnNetworkSpawn() { @@ -54,127 +32,61 @@ public override void OnNetworkSpawn() m_PlayerInput.enabled = true; m_AvatarInputs.enabled = true; - m_Rigidbody.isKinematic = false; + m_AvatarInputs.Jumped += OnJumped; + m_AvatarInteractions.enabled = true; + m_PhysicsPlayerController.enabled = true; + Rigidbody.isKinematic = false; // Freeze rotation on the x and z axes to prevent toppling - m_Rigidbody.freezeRotation = true; + Rigidbody.freezeRotation = true; var spawnPosition = new Vector3(0f, 1.5f, 0f); transform.SetPositionAndRotation(position: spawnPosition, rotation: Quaternion.identity); - m_Rigidbody.position = spawnPosition; - m_Rigidbody.linearVelocity = Vector3.zero; - } - - void Update() - { - if (!IsSpawned || !HasAuthority) - { - return; - } - - m_Movement = new Vector3(m_AvatarInputs.Move.x, 0, m_AvatarInputs.Move.y).normalized; - - // Handle rotation based on input direction - if (m_Movement.magnitude >= 0.1f) - { - var targetAngle = Mathf.Atan2(m_Movement.x, m_Movement.z) * Mathf.Rad2Deg; - var targetRotation = Quaternion.Euler(0, targetAngle, 0); - transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, Time.deltaTime * m_RotationSpeed); - } - - if (IsGrounded() && m_AvatarInputs.Jump) - { - m_Jump = true; - m_AvatarInputs.Jump = false; - } - } + Rigidbody.position = spawnPosition; + Rigidbody.linearVelocity = Vector3.zero; - void ApplyMovement() - { - if (Mathf.Approximately(m_Movement.magnitude, 0f)) - { - return; - } - - var velocity = m_Rigidbody.linearVelocity; - var desiredVelocity = m_Movement * (m_AvatarInputs.Sprint ? m_SprintSpeed : m_WalkSpeed); - var targetVelocity = new Vector3(desiredVelocity.x, velocity.y, desiredVelocity.z); - var velocityChange = targetVelocity - velocity; - - if (m_IsGrounded) - { - // Apply force proportional to acceleration while grounded - var force = velocityChange * m_Acceleration; - m_Rigidbody.AddForce(force, ForceMode.Acceleration); - } - else - { - // Apply reduced force in the air for air control - var force = velocityChange * (m_Acceleration * m_AirControlFactor); - m_Rigidbody.AddForce(force, ForceMode.Acceleration); - } + this.RegisterNetworkUpdate(updateStage: NetworkUpdateStage.Update); + this.RegisterNetworkUpdate(updateStage: NetworkUpdateStage.FixedUpdate); } - void ApplyJump() + public override void OnNetworkDespawn() { - if (m_IsGrounded && m_Jump) + base.OnNetworkDespawn(); + if (m_AvatarInputs) { - m_Rigidbody.AddForce(Vector3.up * m_JumpImpusle, ForceMode.Impulse); - m_Jump = false; + m_AvatarInputs.Jumped -= OnJumped; } - } - void UpdateGroundedStatus() - { - m_IsGrounded = IsGrounded(); + this.UnregisterAllNetworkUpdates(); } - bool IsGrounded() + void OnJumped() { - // Perform a raycast to check if the character is grounded - m_Ray.origin = m_Rigidbody.worldCenterOfMass; - m_Ray.direction = Vector3.down; - return Physics.RaycastNonAlloc(m_Ray, m_RaycastHits, m_GroundCheckDistance) > 0; + m_PhysicsPlayerController.SetJump(true); } - void FixedUpdate() + void OnTransformUpdate() { - if (!IsSpawned || !HasAuthority || m_Rigidbody != null && m_Rigidbody.isKinematic) - { - return; - } - - UpdateGroundedStatus(); - - ApplyMovement(); - - ApplyJump(); + var movement = new Vector3(m_AvatarInputs.Move.x, 0, m_AvatarInputs.Move.y).normalized; - ApplyDrag(); - - ApplyCustomGravity(); + m_PhysicsPlayerController.SetMovement(movement); + m_PhysicsPlayerController.SetSprint(m_AvatarInputs.Sprint); } - void ApplyDrag() + public void NetworkUpdate(NetworkUpdateStage updateStage) { - var groundVelocity = m_Rigidbody.linearVelocity; - groundVelocity.y = 0f; - if (groundVelocity.magnitude > 0f) + switch (updateStage) { - // Apply deceleration force to stop movement - var dragForce = -m_DragCoefficient * groundVelocity.magnitude * groundVelocity; - m_Rigidbody.AddForce(dragForce, ForceMode.Acceleration); + case NetworkUpdateStage.Update: + OnTransformUpdate(); + break; + case NetworkUpdateStage.FixedUpdate: + m_PhysicsPlayerController.OnFixedUpdate(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(updateStage), updateStage, null); } - } - void ApplyCustomGravity() - { - // custom gravity - if (!m_IsGrounded) - { - var customGravity = Physics.gravity * (m_CustomGravityMultiplier - 1); - m_Rigidbody.AddForce(customGravity, ForceMode.Acceleration); - } } } } diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/Unity.Multiplayer.Samples.SocialHub.Player.asmdef b/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/Unity.Multiplayer.Samples.SocialHub.Player.asmdef index ebff7a9ea..bf25a23fa 100644 --- a/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/Unity.Multiplayer.Samples.SocialHub.Player.asmdef +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Player/Unity.Multiplayer.Samples.SocialHub.Player.asmdef @@ -5,7 +5,8 @@ "GUID:1491147abca9d7d4bb7105af628b223e", "GUID:c15e7f658578345fcb824b0a64d4dbe8", "GUID:9ff2150ce7d7c415a8af1fbff3dc3e6c", - "GUID:75469ad4d38634e559750d17036d5f7c" + "GUID:75469ad4d38634e559750d17036d5f7c", + "GUID:ae1323dabfa6440b3956d4cc5239e361" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils.meta new file mode 100644 index 000000000..398bb59c4 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b817bf16348b84528b3335a013608d3a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/AssemblyInfo.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/AssemblyInfo.cs new file mode 100644 index 000000000..c58534e17 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/AssemblyInfo.cs @@ -0,0 +1,2 @@ +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Unity.Multiplayer.Samples.SocialHub.Physics")] diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/AssemblyInfo.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/AssemblyInfo.cs.meta new file mode 100644 index 000000000..1a9ec85f1 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/AssemblyInfo.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 639b9248aa1cd4519b7e04582490b53b diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/MathUtils.cs b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/MathUtils.cs new file mode 100644 index 000000000..2e8a67d5b --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/MathUtils.cs @@ -0,0 +1,106 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; +using Random = UnityEngine.Random; + +namespace Unity.Multiplayer.Samples.SocialHub.Utils +{ + abstract class MathUtils + { + const float k_DefaultThreshold = 0.0025f; + + /// + /// + /// + /// + /// Enable this to get 6 decimal precision when logging Vector3 values + /// + internal static string GetVector3Values(ref Vector3 vector3, bool highPrecisionDecimals = false) + { + if (highPrecisionDecimals) + { + return $"({vector3.x:F6},{vector3.y:F6},{vector3.z:F6})"; + } + else + { + return $"({vector3.x:F2},{vector3.y:F2},{vector3.z:F2})"; + } + } + + internal static string GetVector3Values(Vector3 vector3, bool highPrecisionDecimals = false) + { + return GetVector3Values(ref vector3, highPrecisionDecimals); + } + + internal static Vector3 GetRandomVector3(float min, float max, Vector3 baseLine, bool randomlyApplySign = false) + { + var retValue = new Vector3(baseLine.x * Random.Range(min, max), baseLine.y * Random.Range(min, max), baseLine.z * Random.Range(min, max)); + if (!randomlyApplySign) + { + return retValue; + } + + retValue.x *= Random.Range(1, 100) >= 50 ? -1 : 1; + retValue.y *= Random.Range(1, 100) >= 50 ? -1 : 1; + retValue.z *= Random.Range(1, 100) >= 50 ? -1 : 1; + return retValue; + } + + internal static Vector3 GetRandomVector3(MinMaxVector2Physics minMax, Vector3 baseLine, bool randomlyApplySign = false) + { + return GetRandomVector3(minMax.Min, minMax.Max, baseLine, randomlyApplySign); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool Approximately(float a, float b, float threshold = k_DefaultThreshold) + { + return Math.Round(Mathf.Abs(a - b), 4) <= threshold; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool Approximately(Vector3 a, Vector3 b, float threshold = k_DefaultThreshold) + { + return Approximately(a.x, b.x) && Approximately(a.y, b.y) && Approximately(a.z, b.z); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool Approximately(Quaternion a, Quaternion b, float threshold = k_DefaultThreshold) + { + return Approximately(a.x, b.x) && Approximately(a.y, b.y) && Approximately(a.z, b.z) && Approximately(a.w, b.w); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static float EulerDelta(float a, float b) + { + return Mathf.DeltaAngle(a, b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool ApproximatelyEuler(float a, float b, float threshold = k_DefaultThreshold) + { + return Mathf.Abs(EulerDelta(a, b)) <= threshold; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool ApproximatelyEuler(Vector3 a, Vector3 b, float threshold = k_DefaultThreshold) + { + return ApproximatelyEuler(a.x, b.x, threshold) && ApproximatelyEuler(a.y, b.y, threshold) && ApproximatelyEuler(a.z, b.z, threshold); + } + + [Serializable] + internal class MinMaxVector2Physics + { + [Range(1.0f, 200.0f)] + public float Min; + [Range(1.0f, 200.0f)] + public float Max; + + MinMaxVector2Physics(float min, float max) + { + Min = min; + Max = max; + } + } + + } +} diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/MathUtils.cs.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/MathUtils.cs.meta new file mode 100644 index 000000000..34742ecac --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/MathUtils.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9889098b4101047349101dd918337bd3 \ No newline at end of file diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/Unity.Multiplayer.Samples.SocialHub.Utils.asmdef b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/Unity.Multiplayer.Samples.SocialHub.Utils.asmdef new file mode 100644 index 000000000..9f56fb7f6 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/Unity.Multiplayer.Samples.SocialHub.Utils.asmdef @@ -0,0 +1,14 @@ +{ + "name": "Unity.Multiplayer.Samples.SocialHub.Utils", + "rootNamespace": "Unity.Multiplayer.Samples.SocialHub", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/Unity.Multiplayer.Samples.SocialHub.Utils.asmdef.meta b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/Unity.Multiplayer.Samples.SocialHub.Utils.asmdef.meta new file mode 100644 index 000000000..78a2858a4 --- /dev/null +++ b/Experimental/DistributedAuthoritySample/Assets/Scripts/Utils/Unity.Multiplayer.Samples.SocialHub.Utils.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a469b04c8dd43463084f4a2b513f1284 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: