Skip to content

feat: tool to enable monobehaviours only on server or client #477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Assets/BossRoom/Prefabs/Game/InteractiveBossDoor.prefab
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ GameObject:
- component: {fileID: 7788790407431586434}
- component: {fileID: 7788790407431586435}
- component: {fileID: 7788790407431586432}
- component: {fileID: 7487805034968397558}
m_Layer: 0
m_Name: InteractiveBossDoor
m_TagString: Untagged
Expand Down Expand Up @@ -308,6 +309,19 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_PhysicsObject: {fileID: 9183722010770990113}
m_DoorState: {fileID: 7788790407431586434}
--- !u!114 &7487805034968397558
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7788790407431586438}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d03f654818b6ab34c95c30101be45638, type: 3}
m_Name:
m_EditorClassIdentifier:
m_ServerOnlyBehaviours: []
--- !u!1 &9183722010770990113
GameObject:
m_ObjectHideFlags: 0
Expand Down
29 changes: 12 additions & 17 deletions Assets/BossRoom/Scripts/Client/ClientDoorVisualization.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Unity.Multiplayer.Samples.Utilities;
using Unity.Netcode;
using UnityEngine;

Expand All @@ -10,7 +11,8 @@ namespace Unity.Multiplayer.Samples.BossRoom.Client
/// and vice versa.
/// </summary>
[RequireComponent(typeof(NetworkDoorState))]
public class ClientDoorVisualization : NetworkBehaviour
[RequireComponent(typeof(OnSpawnBehaviourEnabler))]
public class ClientDoorVisualization : MonoBehaviour, IClientOnlyMonoBehaviour
{
[SerializeField]
[Tooltip("This physics and navmesh obstacle is enabled when the door is closed.")]
Expand All @@ -19,32 +21,25 @@ public class ClientDoorVisualization : NetworkBehaviour
[SerializeField]
NetworkDoorState m_DoorState;

public override void OnNetworkSpawn()
void OnDoorStateChanged(bool wasDoorOpen, bool isDoorOpen)
{
if (!IsClient)
{
enabled = false;
}
else
m_PhysicsObject.SetActive(!isDoorOpen);
}

public void SetEnabled(bool enable)
{
enabled = enable;
if (enable)
{
m_DoorState.IsOpen.OnValueChanged += OnDoorStateChanged;

// initialize visuals based on current server state (or else we default to "closed")
OnDoorStateChanged(false, m_DoorState.IsOpen.Value);
}
}

public override void OnNetworkDespawn()
{
if (m_DoorState)
else
{
m_DoorState.IsOpen.OnValueChanged -= OnDoorStateChanged;
}
}

void OnDoorStateChanged(bool wasDoorOpen, bool isDoorOpen)
{
m_PhysicsObject.SetActive(!isDoorOpen);
}
}
}
28 changes: 18 additions & 10 deletions Assets/BossRoom/Scripts/Server/ServerSwitchedDoor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Unity.Multiplayer.Samples.Utilities;
using Unity.Netcode;
using UnityEngine;

Expand All @@ -10,7 +11,8 @@ namespace Unity.Multiplayer.Samples.BossRoom.Server
/// (Assign the floor switches for this door in the editor.)
/// </summary>
[RequireComponent(typeof(NetworkDoorState))]
public class ServerSwitchedDoor : NetworkBehaviour
[RequireComponent(typeof(OnSpawnBehaviourEnabler))]
public class ServerSwitchedDoor : MonoBehaviour, IServerOnlyMonoBehaviour
{
[SerializeField]
NetworkFloorSwitchState[] m_SwitchesThatOpenThisDoor;
Expand Down Expand Up @@ -39,15 +41,6 @@ void Awake()
Debug.LogError("Door has no switches and can never be opened!", gameObject);
}

public override void OnNetworkSpawn()
{
enabled = IsServer;

DoorStateChanged(false, m_NetworkDoorState.IsOpen.Value);

m_NetworkDoorState.IsOpen.OnValueChanged += DoorStateChanged;
}

void Update()
{
var isAnySwitchOn = false;
Expand Down Expand Up @@ -76,5 +69,20 @@ void OnValidate()
{
m_AnimatorDoorOpenBoolID = Animator.StringToHash(k_AnimatorDoorOpenBoolVarName);
}

public void SetEnabled(bool enable)
{
enabled = enable;
if (enable)
{
DoorStateChanged(false, m_NetworkDoorState.IsOpen.Value);

m_NetworkDoorState.IsOpen.OnValueChanged += DoorStateChanged;
}
else
{
m_NetworkDoorState.IsOpen.OnValueChanged -= DoorStateChanged;
}
}
}
}
96 changes: 96 additions & 0 deletions Assets/BossRoom/Scripts/Shared/OnSpawnBehaviourEnabler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;

namespace Unity.Multiplayer.Samples.Utilities
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file will be moved to the Utilities package when the PR will leave the draft status. I kept it here to make testing and iterating easier since ParrelSync doesn't sync the packages folder automatically

{
/// <summary>
/// Classes implementing this interface get disabled on Awake by the OnSpawnBehaviorEnabler, then only enabled when
/// it is spawned on a client.
///<remarks>
/// Any class implementing this should add the "[RequireComponent(typeof(OnSpawnBehaviourEnabler))]" attribute
/// </remarks>
/// </summary>
public interface IClientOnlyMonoBehaviour
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if those interfaces are tightly coupled to monobehaviour and netcode, any reason we wouldn't just have an abstract class that inherits from NetworkBehaviour and has SetEnabled as abstract method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An earlier version of this used inheritance, with a single class inheriting NetworkBehavior with a protected field telling if it was meant for a server or a client. The disabling/enabling was handled by that abstract class in its Awake, OnSpawn and OnDespwn events.
The advantage was that we did not need a second script to enable/disable it, but it came with more restrictions that I wasn't really happy with. First, we would need to make sure to call the base methods inside those three methods if we wanted to override it in daughter classes. Also, it would mean that if a specific class needs that behaviour, all its parents would have to have it too, which might not make sense. i.e. class A is a client-only netbehaviour inheriting from class B that doesn't need to know if it is on a client or on a server. In this case, if we want to restrict A to only clients, we would have to have B inherit this abstract class, even though B doesn't require it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Yeah makes sense. I wonder if there could be a way to dynamically add the enabler without requiring us to remember to add it. It's too bad "RequireComponent" doesn't work on interfaces.
Or we could have some editor script magic with some reflection...?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LukeStampfli @pdeschain @fernando-cortez what do you guys think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add "RequireComponent" to the classes implementing these interfaces themselves I suppose, but it's still not as plug-and-play as I hoped it could be

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking back on it, would adding this recommendation as remarks in the interface descriptions be sufficient?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using an interface here is better than using inheritance. With the updated xml docs on the interfaces I think it is easy enough to understand how to use this.

{
public void SetEnabled(bool enable);
}

/// <summary>
/// Classes implementing this interface get disabled on Awake by the OnSpawnBehaviorEnabler, then only enabled when
/// it is spawned on a server.
///<remarks>
/// Any class implementing this should add the "[RequireComponent(typeof(OnSpawnBehaviourEnabler))]" attribute
/// </remarks>
/// </summary>
public interface IServerOnlyMonoBehaviour
{
public void SetEnabled(bool enable);
}


/// <summary>
/// Disables a list of MonoBehaviours on Awake, then only enables them when spawning based on whether this game instance
/// is a client or a server (or both in case of a client-hosted session).
/// </summary>
public class OnSpawnBehaviourEnabler : NetworkBehaviour
{
List<IClientOnlyMonoBehaviour> m_ClientOnlyMonoBehaviours;
List<IServerOnlyMonoBehaviour> m_ServerOnlyMonoBehaviours;

void Awake()
{
m_ClientOnlyMonoBehaviours = new List<IClientOnlyMonoBehaviour>();
m_ServerOnlyMonoBehaviours = new List<IServerOnlyMonoBehaviour>();
foreach (var behaviour in gameObject.GetComponents<IClientOnlyMonoBehaviour>())
{
m_ClientOnlyMonoBehaviours.Add(behaviour);
}
foreach (var behaviour in gameObject.GetComponents<IServerOnlyMonoBehaviour>())
{
m_ServerOnlyMonoBehaviours.Add(behaviour);
}
// Disable everything here to prevent those MonoBehaviours to be updated before this one is spawned.
DisableAll();
}

public override void OnNetworkSpawn()
{
if (IsClient)
{
foreach (var behaviour in m_ClientOnlyMonoBehaviours)
{
behaviour.SetEnabled(true);
}
}

if (IsServer)
{
foreach (var behaviour in m_ServerOnlyMonoBehaviours)
{
behaviour.SetEnabled(true);
}
}
}

public override void OnNetworkDespawn()
{
// Disable everything and wait until it is spawned again to enable them again
DisableAll();
}

void DisableAll()
{
foreach (var behaviour in m_ClientOnlyMonoBehaviours)
{
behaviour.SetEnabled(false);
}

foreach (var behaviour in m_ServerOnlyMonoBehaviours)
{
behaviour.SetEnabled(false);
}
}
}
}
11 changes: 11 additions & 0 deletions Assets/BossRoom/Scripts/Shared/OnSpawnBehaviourEnabler.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.