diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index c2974dcb3a..c3c3d9f73e 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -10,6 +10,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
### Added
+- Clicking on the Help icon in the inspector will now redirect to the relevant documentation. (#3663)
### Changed
diff --git a/com.unity.netcode.gameobjects/Runtime/Components/AnticipatedNetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/AnticipatedNetworkTransform.cs
index 193a292136..d2b22c75a1 100644
--- a/com.unity.netcode.gameobjects/Runtime/Components/AnticipatedNetworkTransform.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Components/AnticipatedNetworkTransform.cs
@@ -1,4 +1,5 @@
using Unity.Mathematics;
+using Unity.Netcode.Runtime;
using UnityEngine;
namespace Unity.Netcode.Components
@@ -43,6 +44,7 @@ namespace Unity.Netcode.Components
#pragma warning restore IDE0001
[DisallowMultipleComponent]
[AddComponentMenu("Netcode/Anticipated Network Transform")]
+ [HelpURL(HelpUrls.AnticipatedNetworkTransform)]
public class AnticipatedNetworkTransform : NetworkTransform
{
diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs
index f6209a98b3..2fe642162a 100644
--- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
+using Unity.Netcode.Runtime;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor.Animations;
@@ -186,6 +187,7 @@ internal NetworkAnimatorStateChangeHandler(NetworkAnimator networkAnimator)
/// NetworkAnimator enables remote synchronization of state for on network objects.
///
[AddComponentMenu("Netcode/Network Animator")]
+ [HelpURL(HelpUrls.NetworkAnimator)]
public class NetworkAnimator : NetworkBehaviour, ISerializationCallbackReceiver
{
[Serializable]
diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs
index a157d26c63..590fad77b7 100644
--- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs
@@ -1,4 +1,5 @@
#if COM_UNITY_MODULES_PHYSICS
+using Unity.Netcode.Runtime;
using UnityEngine;
namespace Unity.Netcode.Components
@@ -10,6 +11,7 @@ namespace Unity.Netcode.Components
[RequireComponent(typeof(NetworkTransform))]
[RequireComponent(typeof(Rigidbody))]
[AddComponentMenu("Netcode/Network Rigidbody")]
+ [HelpURL(HelpUrls.NetworkRigidbody)]
public class NetworkRigidbody : NetworkRigidbodyBase
{
diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs
index f7c9e14d1e..dd84d52252 100644
--- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs
@@ -1,4 +1,5 @@
#if COM_UNITY_MODULES_PHYSICS2D
+using Unity.Netcode.Runtime;
using UnityEngine;
namespace Unity.Netcode.Components
@@ -10,6 +11,7 @@ namespace Unity.Netcode.Components
[RequireComponent(typeof(NetworkTransform))]
[RequireComponent(typeof(Rigidbody2D))]
[AddComponentMenu("Netcode/Network Rigidbody 2D")]
+ [HelpURL(HelpUrls.NetworkRigidbody2D)]
public class NetworkRigidbody2D : NetworkRigidbodyBase
{
public Rigidbody2D Rigidbody2D => m_InternalRigidbody2D;
diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs
index 56ab5dcb27..33fd4c523d 100644
--- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs
@@ -3,6 +3,7 @@
using System.Runtime.CompilerServices;
using System.Text;
using Unity.Mathematics;
+using Unity.Netcode.Runtime;
using UnityEngine;
namespace Unity.Netcode.Components
@@ -14,6 +15,7 @@ namespace Unity.Netcode.Components
///
[DisallowMultipleComponent]
[AddComponentMenu("Netcode/Network Transform")]
+ [HelpURL(HelpUrls.NetworkTransform)]
public class NetworkTransform : NetworkBehaviour
{
#if UNITY_EDITOR
diff --git a/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs b/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs
index b455bde3d6..d5dff7e069 100644
--- a/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using Unity.Collections;
using Unity.Jobs;
+using Unity.Netcode.Runtime;
using UnityEngine;
namespace Unity.Netcode.Components
@@ -71,6 +72,7 @@ public interface IContactEventHandlerWithInfo : IContactEventHandler
///
///
[AddComponentMenu("Netcode/Rigidbody Contact Event Manager")]
+ [HelpURL(HelpUrls.RigidbodyContactEventManager)]
public class RigidbodyContactEventManager : MonoBehaviour
{
public static RigidbodyContactEventManager Instance { get; private set; }
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
index 3c6662b7be..5fe071ce0e 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
@@ -10,6 +10,7 @@
using UnityEngine.SceneManagement;
using Debug = UnityEngine.Debug;
using Unity.Netcode.Components;
+using Unity.Netcode.Runtime;
namespace Unity.Netcode
{
@@ -17,6 +18,7 @@ namespace Unity.Netcode
/// The main component of the library
///
[AddComponentMenu("Netcode/Network Manager", -100)]
+ [HelpURL(HelpUrls.NetworkManager)]
public class NetworkManager : MonoBehaviour, INetworkUpdateSystem
{
///
diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
index bd9787ca8a..50101deeb7 100644
--- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
@@ -4,6 +4,7 @@
using System.Runtime.CompilerServices;
using System.Text;
using Unity.Netcode.Components;
+using Unity.Netcode.Runtime;
#if UNITY_EDITOR
using UnityEditor;
#if UNITY_2021_2_OR_NEWER
@@ -24,6 +25,7 @@ namespace Unity.Netcode
///
[AddComponentMenu("Netcode/Network Object", -99)]
[DisallowMultipleComponent]
+ [HelpURL(HelpUrls.NetworkObject)]
public sealed class NetworkObject : MonoBehaviour
{
[HideInInspector]
diff --git a/com.unity.netcode.gameobjects/Runtime/HelpUrls.cs b/com.unity.netcode.gameobjects/Runtime/HelpUrls.cs
new file mode 100644
index 0000000000..91bf160544
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/HelpUrls.cs
@@ -0,0 +1,22 @@
+namespace Unity.Netcode.Runtime
+{
+ internal static class HelpUrls
+ {
+ private const string k_BaseUrl = "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@latest/?subfolder=/";
+ private const string k_BaseManualUrl = k_BaseUrl + "manual/";
+ private const string k_BaseApiUrl = k_BaseUrl + "api/Unity.Netcode";
+
+ // The HelpUrls have to be defined as public for the test to work
+ public const string NetworkManager = k_BaseManualUrl + "components/core/networkmanager.html";
+ public const string NetworkObject = k_BaseManualUrl + "components/core/networkobject.html";
+ public const string NetworkAnimator = k_BaseManualUrl + "components/helper/networkanimator.html";
+ public const string NetworkRigidbody = k_BaseManualUrl + "advanced-topics/physics.html#networkrigidbody";
+ public const string NetworkRigidbody2D = k_BaseManualUrl + "advanced-topics/physics.html#networkrigidbody2d";
+ public const string RigidbodyContactEventManager = k_BaseApiUrl + ".Components.RigidbodyContactEventManager.html";
+ public const string NetworkTransform = k_BaseManualUrl + "components/helper/networktransform.html";
+ public const string AnticipatedNetworkTransform = k_BaseManualUrl + "advanced-topics/client-anticipation.html";
+ public const string UnityTransport = k_BaseApiUrl + ".Transports.UTP.UnityTransport.html";
+ public const string SecretsLoaderHelper = k_BaseApiUrl + ".Transports.UTP.SecretsLoaderHelper.html";
+ public const string SinglePlayerTransport = k_BaseApiUrl + ".Transports.SinglePlayer.SinglePlayerTransport.html";
+ }
+}
diff --git a/com.unity.netcode.gameobjects/Runtime/HelpUrls.cs.meta b/com.unity.netcode.gameobjects/Runtime/HelpUrls.cs.meta
new file mode 100644
index 0000000000..24f9cc0f69
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/HelpUrls.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 5f9653e7d00a4ad0aeb89e72cfddb68a
+timeCreated: 1757450738
diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/SinglePlayer/SinglePlayerTransport.cs b/com.unity.netcode.gameobjects/Runtime/Transports/SinglePlayer/SinglePlayerTransport.cs
index 17a240b676..e7ebde09d8 100644
--- a/com.unity.netcode.gameobjects/Runtime/Transports/SinglePlayer/SinglePlayerTransport.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Transports/SinglePlayer/SinglePlayerTransport.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using Unity.Netcode.Runtime;
+using UnityEngine;
namespace Unity.Netcode.Transports.SinglePlayer
{
@@ -11,6 +13,8 @@ namespace Unity.Netcode.Transports.SinglePlayer
///
/// You can only start as a host when using this transport.
///
+ [AddComponentMenu("Netcode/Single Player Transport")]
+ [HelpURL(HelpUrls.SinglePlayerTransport)]
public class SinglePlayerTransport : NetworkTransport
{
///
diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/SecretsLoaderHelper.cs b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/SecretsLoaderHelper.cs
index 40b8f2ba44..dbbfcade04 100644
--- a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/SecretsLoaderHelper.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/SecretsLoaderHelper.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using Unity.Netcode.Runtime;
using UnityEngine;
namespace Unity.Netcode.Transports.UTP
@@ -13,6 +14,7 @@ namespace Unity.Netcode.Transports.UTP
/// - SetClientSecrets
/// directly, instead of relying on this.
///
+ [HelpURL(HelpUrls.SecretsLoaderHelper)]
public class SecretsLoaderHelper : MonoBehaviour
{
internal struct ServerSecrets
diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs
index 4340568122..e6bc0ca957 100644
--- a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs
+++ b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs
@@ -11,6 +11,7 @@
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
+using Unity.Netcode.Runtime;
using Unity.Networking.Transport;
using Unity.Networking.Transport.Relay;
using Unity.Networking.Transport.TLS;
@@ -28,6 +29,7 @@ namespace Unity.Netcode.Transports.UTP
/// Note: This is highly recommended to use over UNet.
///
[AddComponentMenu("Netcode/Unity Transport")]
+ [HelpURL(HelpUrls.UnityTransport)]
public partial class UnityTransport : NetworkTransport, INetworkStreamDriverConstructor
{
///
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/HelpUrlTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/HelpUrlTests.cs
new file mode 100644
index 0000000000..4d3a6d608f
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/HelpUrlTests.cs
@@ -0,0 +1,165 @@
+
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using Unity.Netcode.Runtime;
+using UnityEngine;
+using UnityEngine.TestTools;
+
+namespace Unity.Netcode.RuntimeTests
+{
+ internal class HelpUrlTests
+ {
+ private const string k_PackageName = "com.unity.netcode.gameobjects";
+ private static readonly HttpClient k_HttpClient = new();
+
+ private bool m_VerboseLogging = false;
+
+ [UnityTest]
+ public IEnumerator ValidateUrlsAreValid()
+ {
+ var names = new List();
+ var allUrls = new List();
+
+ // GetFields() can only see public strings. Ensure each HelpUrl is public.
+ foreach (var constant in typeof(HelpUrls).GetFields())
+ {
+ if (constant.IsLiteral && !constant.IsInitOnly)
+ {
+ names.Add(constant.Name);
+ allUrls.Add((string)constant.GetValue(null));
+ }
+ }
+
+ VerboseLog($"Found {allUrls.Count} URLs");
+
+ var tasks = new List>();
+ foreach (var url in allUrls)
+ {
+ tasks.Add(AreUnityDocsAvailableAt(url));
+ }
+
+ while (tasks.Any(task => !task.IsCompleted))
+ {
+ yield return new WaitForSeconds(0.01f);
+ }
+
+ for (int i = 0; i < allUrls.Count; i++)
+ {
+ Assert.IsTrue(tasks[i].Result, $"HelpUrls.{names[i]} has an invalid path! Path: {allUrls[i]}");
+ }
+ }
+
+ private async Task AreUnityDocsAvailableAt(string url)
+ {
+ try
+ {
+ var split = url.Split('#');
+ url = split[0];
+
+ var stream = await GetContentFromRemoteFile(url);
+
+ var redirectUrl = CalculateRedirectURl(url, stream);
+ VerboseLog($"Calculated Redirect URL: {redirectUrl}");
+
+ var content = await GetContentFromRemoteFile(redirectUrl);
+
+ // If original url had an anchor part (e.g. some/url.html#anchor)
+ if (split.Length > 1)
+ {
+ var anchorString = split[1];
+
+ // All headings will have an id with the anchorstring (e.g. )
+ if (!content.Contains($"id=\"{anchorString}\">"))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ catch (Exception e)
+ {
+ VerboseLog(e.Message);
+ return false;
+ }
+ }
+
+ ///
+ /// Checks if a remote file at the exists, and if access is not restricted.
+ ///
+ /// URL to a remote file.
+ /// True if the file at the is able to be downloaded, false if the file does not exist, or if the file is restricted.
+ private async Task GetContentFromRemoteFile(string url)
+ {
+ //Checking if URI is well formed is optional
+ var uri = new Uri(url);
+ if (!uri.IsWellFormedOriginalString())
+ {
+ throw new Exception($"URL {url} is not well formed");
+ }
+
+ try
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, uri);
+ using var response = await k_HttpClient.SendAsync(request);
+ if (!response.IsSuccessStatusCode || response.Content.Headers.ContentLength <= 0)
+ {
+ throw new Exception($"Failed to get remote file from URL {url}");
+ }
+
+ return await response.Content.ReadAsStringAsync();
+ }
+ catch
+ {
+ throw new Exception($"URL {url} request failed");
+ }
+ }
+
+ private string CalculateRedirectURl(string originalRequest, string content)
+ {
+ var uri = new Uri(originalRequest);
+ var baseRequest = $"{uri.Scheme}://{uri.Host}";
+ foreach (var segment in uri.Segments)
+ {
+ if (segment.Contains(k_PackageName))
+ {
+ break;
+ }
+ baseRequest += segment;
+ }
+
+ var subfolderRegex = new Regex(@"[?&](\w[\w.]*)=([^?&]+)").Match(uri.Query);
+ var subfolder = "";
+ foreach (Group match in subfolderRegex.Groups)
+ {
+ subfolder = match.Value;
+ }
+
+ string pattern = @"com.unity.netcode.gameobjects\@(\d+.\d+)";
+ var targetDestination = "";
+ foreach (Match match in Regex.Matches(content, pattern))
+ {
+ targetDestination = match.Value;
+ break;
+ }
+
+ return baseRequest + targetDestination + subfolder;
+ }
+
+ private void VerboseLog(string message)
+ {
+ if (m_VerboseLogging)
+ {
+ Debug.unityLogger.Log(message);
+ }
+ }
+
+ }
+}
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/HelpUrlTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/HelpUrlTests.cs.meta
new file mode 100644
index 0000000000..009ea8ed40
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/HelpUrlTests.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: a44ef45dca794f618ae195832470a7cd
+timeCreated: 1757519683
\ No newline at end of file