RPCs vs NetworkVariables Examples
This page has examples of how the Small Coop Sample (Boss Room) uses RPC
s and NetworkVariable
s. It gives guidance on when to use RPC
s versus NetworkVariable
s in your own projects.
See the RPC vs NetworkVariable tutorial for more information.
RPCs for movement
Boss Room uses RPCs to send movement inputs.
using System;
using Unity.BossRoom.Gameplay.Actions;
using Unity.BossRoom.Gameplay.Configuration;
using Unity.BossRoom.Gameplay.GameplayObjects;
using Unity.BossRoom.Gameplay.GameplayObjects.Character;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Assertions;
using UnityEngine.EventSystems;
namespace Unity.BossRoom.Gameplay.UserInput
{
/// <summary>
/// Captures inputs for a character on a client and sends them to the server.
/// </summary>
[RequireComponent(typeof(ServerCharacter))]
public class ClientInputSender : NetworkBehaviour
{
const float k_MouseInputRaycastDistance = 100f;
//The movement input rate is capped at 40ms (or 25 fps). This provides a nice balance between responsiveness and
//upstream network conservation. This matters when holding down your mouse button to move.
const float k_MoveSendRateSeconds = 0.04f; //25 fps.
const float k_TargetMoveTimeout = 0.45f; //prevent moves for this long after targeting someone (helps prevent walking to the guy you clicked).
float m_LastSentMove;
// Cache raycast hit array so that we can use non alloc raycasts
readonly RaycastHit[] k_CachedHit = new RaycastHit[4];
// This is basically a constant but layer masks cannot be created in the constructor, that's why it's assigned int Awake.
LayerMask m_GroundLayerMask;
LayerMask m_ActionLayerMask;
const float k_MaxNavMeshDistance = 1f;
RaycastHitComparer m_RaycastHitComparer;
[SerializeField]
ServerCharacter m_ServerCharacter;
/// <summary>
/// This event fires at the time when an action request is sent to the server.
/// </summary>
public event Action<ActionRequestData> ActionInputEvent;
/// <summary>
/// This describes how a skill was requested. Skills requested via mouse click will do raycasts to determine their target; skills requested
/// in other matters will use the stateful target stored in NetworkCharacterState.
/// </summary>
public enum SkillTriggerStyle
{
None, //no skill was triggered.
MouseClick, //skill was triggered via mouse-click implying you should do a raycast from the mouse position to find a target.
Keyboard, //skill was triggered via a Keyboard press, implying target should be taken from the active target.
KeyboardRelease, //represents a released key.
UI, //skill was triggered from the UI, and similar to Keyboard, target should be inferred from the active target.
UIRelease, //represents letting go of the mouse-button on a UI button
}
bool IsReleaseStyle(SkillTriggerStyle style)
{
return style == SkillTriggerStyle.KeyboardRelease || style == SkillTriggerStyle.UIRelease;
}
/// <summary>
/// This struct essentially relays the call params of RequestAction to FixedUpdate. Recall that we may need to do raycasts
/// as part of doing the action, and raycasts done outside of FixedUpdate can give inconsistent results (otherwise we would
/// just expose PerformAction as a public method, and let it be called in whatever scoped it liked.
/// </summary>
/// <remarks>
/// Reference: https://answers.unity.com/questions/1141633/why-does-fixedupdate-work-when-update-doesnt.html
/// </remarks>
struct ActionRequest
{
public SkillTriggerStyle TriggerStyle;
public ActionID RequestedActionID;
public ulong TargetId;
}
/// <summary>
/// List of ActionRequests that have been received since the last FixedUpdate ran. This is a static array, to avoid allocs, and
/// because we don't really want to let this list grow indefinitely.
/// </summary>
readonly ActionRequest[] m_ActionRequests = new ActionRequest[5];
/// <summary>
/// Number of ActionRequests that have been queued since the last FixedUpdate.
/// </summary>
int m_ActionRequestCount;
BaseActionInput m_CurrentSkillInput;
bool m_MoveRequest;
Camera m_MainCamera;
public event Action<Vector3> ClientMoveEvent;
/// <summary>
/// Convenience getter that returns our CharacterData
/// </summary>
CharacterClass CharacterClass => m_ServerCharacter.CharacterClass;
[SerializeField]
PhysicsWrapper m_PhysicsWrapper;
public ActionState actionState1 { get; private set; }
public ActionState actionState2 { get; private set; }
public ActionState actionState3 { get; private set; }
public System.Action action1ModifiedCallback;
ServerCharacter m_TargetServerCharacter;
void Awake()
{
m_MainCamera = Camera.main;
}
public override void OnNetworkSpawn()
{
if (!IsClient || !IsOwner)
{
enabled = false;
// dont need to do anything else if not the owner
return;
}
m_ServerCharacter.TargetId.OnValueChanged += OnTargetChanged;
m_ServerCharacter.HeldNetworkObject.OnValueChanged += OnHeldNetworkObjectChanged;
if (CharacterClass.Skill1 &&
GameDataSource.Instance.TryGetActionPrototypeByID(CharacterClass.Skill1.ActionID, out var action1))
{
actionState1 = new ActionState() { actionID = action1.ActionID, selectable = true };
}
if (CharacterClass.Skill2 &&
GameDataSource.Instance.TryGetActionPrototypeByID(CharacterClass.Skill2.ActionID, out var action2))
{
actionState2 = new ActionState() { actionID = action2.ActionID, selectable = true };
}
if (CharacterClass.Skill3 &&
GameDataSource.Instance.TryGetActionPrototypeByID(CharacterClass.Skill3.ActionID, out var action3))
{
actionState3 = new ActionState() { actionID = action3.ActionID, selectable = true };
}
m_GroundLayerMask = LayerMask.GetMask(new[] { "Ground" });
m_ActionLayerMask = LayerMask.GetMask(new[] { "PCs", "NPCs", "Ground" });
m_RaycastHitComparer = new RaycastHitComparer();
}
public override void OnNetworkDespawn()
{
if (m_ServerCharacter)
{
m_ServerCharacter.TargetId.OnValueChanged -= OnTargetChanged;
m_ServerCharacter.HeldNetworkObject.OnValueChanged -= OnHeldNetworkObjectChanged;
}
if (m_TargetServerCharacter)
{
m_TargetServerCharacter.NetLifeState.LifeState.OnValueChanged -= OnTargetLifeStateChanged;
}
}
void OnTargetChanged(ulong previousValue, ulong newValue)
{
if (m_TargetServerCharacter)
{
m_TargetServerCharacter.NetLifeState.LifeState.OnValueChanged -= OnTargetLifeStateChanged;
}
m_TargetServerCharacter = null;
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(newValue, out var selection) &&
selection.TryGetComponent(out m_TargetServerCharacter))
{
m_TargetServerCharacter.NetLifeState.LifeState.OnValueChanged += OnTargetLifeStateChanged;
}
UpdateAction1();
}
void OnHeldNetworkObjectChanged(ulong previousValue, ulong newValue)
{
UpdateAction1();
}
void OnTargetLifeStateChanged(LifeState previousValue, LifeState newValue)
{
UpdateAction1();
}
void FinishSkill()
{
m_CurrentSkillInput = null;
}
void SendInput(ActionRequestData action)
{
ActionInputEvent?.Invoke(action);
m_ServerCharacter.RecvDoActionServerRPC(action);
}
void FixedUpdate()
{
//play all ActionRequests, in FIFO order.
for (int i = 0; i < m_ActionRequestCount; ++i)
{
if (m_CurrentSkillInput != null)
{
//actions requested while input is active are discarded, except for "Release" requests, which go through.
if (IsReleaseStyle(m_ActionRequests[i].TriggerStyle))
{
m_CurrentSkillInput.OnReleaseKey();
}
}
else if (!IsReleaseStyle(m_ActionRequests[i].TriggerStyle))
{
var actionPrototype = GameDataSource.Instance.GetActionPrototypeByID(m_ActionRequests[i].RequestedActionID);
if (actionPrototype.Config.ActionInput != null)
{
var skillPlayer = Instantiate(actionPrototype.Config.ActionInput);
skillPlayer.Initiate(m_ServerCharacter, m_PhysicsWrapper.Transform.position, actionPrototype.ActionID, SendInput, FinishSkill);
m_CurrentSkillInput = skillPlayer;
}
else
{
PerformSkill(actionPrototype.ActionID, m_ActionRequests[i].TriggerStyle, m_ActionRequests[i].TargetId);
}
}
}
m_ActionRequestCount = 0;
if (EventSystem.current.currentSelectedGameObject != null)
{
return;
}
if (m_MoveRequest)
{
m_MoveRequest = false;
if ((Time.time - m_LastSentMove) > k_MoveSendRateSeconds)
{
m_LastSentMove = Time.time;
var ray = m_MainCamera.ScreenPointToRay(UnityEngine.Input.mousePosition);
var groundHits = Physics.RaycastNonAlloc(ray,
k_CachedHit,
k_MouseInputRaycastDistance,
m_GroundLayerMask);
if (groundHits > 0)
{
if (groundHits > 1)
{
// sort hits by distance
Array.Sort(k_CachedHit, 0, groundHits, m_RaycastHitComparer);
}
// verify point is indeed on navmesh surface
if (NavMesh.SamplePosition(k_CachedHit[0].point,
out var hit,
k_MaxNavMeshDistance,
NavMesh.AllAreas))
{
m_ServerCharacter.SendCharacterInputServerRpc(hit.position);
//Send our client only click request
ClientMoveEvent?.Invoke(hit.position);
}
}
}
}
}
/// <summary>
/// Perform a skill in response to some input trigger. This is the common method to which all input-driven skill plays funnel.
/// </summary>
/// <param name="actionID">The action you want to play. Note that "Skill1" may be overriden contextually depending on the target.</param>
/// <param name="triggerStyle">What sort of input triggered this skill?</param>
/// <param name="targetId">(optional) Pass in a specific networkID to target for this action</param>
void PerformSkill(ActionID actionID, SkillTriggerStyle triggerStyle, ulong targetId = 0)
{
Transform hitTransform = null;
if (targetId != 0)
{
// if a targetId is given, try to find the object
NetworkObject targetNetObj;
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out targetNetObj))
{
hitTransform = targetNetObj.transform;
}
}
else
{
// otherwise try to find an object under the input position
int numHits = 0;
if (triggerStyle == SkillTriggerStyle.MouseClick)
{
var ray = m_MainCamera.ScreenPointToRay(UnityEngine.Input.mousePosition);
numHits = Physics.RaycastNonAlloc(ray, k_CachedHit, k_MouseInputRaycastDistance, m_ActionLayerMask);
}
int networkedHitIndex = -1;
for (int i = 0; i < numHits; i++)
{
if (k_CachedHit[i].transform.GetComponentInParent<NetworkObject>())
{
networkedHitIndex = i;
break;
}
}
hitTransform = networkedHitIndex >= 0 ? k_CachedHit[networkedHitIndex].transform : null;
}
if (GetActionRequestForTarget(hitTransform, actionID, triggerStyle, out ActionRequestData playerAction))
{
//Don't trigger our move logic for a while. This protects us from moving just because we clicked on them to target them.
m_LastSentMove = Time.time + k_TargetMoveTimeout;
SendInput(playerAction);
}
else if (!GameDataSource.Instance.GetActionPrototypeByID(actionID).IsGeneralTargetAction)
{
// clicked on nothing... perform an "untargeted" attack on the spot they clicked on.
// (Different Actions will deal with this differently. For some, like archer arrows, this will fire an arrow
// in the desired direction. For others, like mage's bolts, this will fire a "miss" projectile at the spot clicked on.)
var data = new ActionRequestData();
PopulateSkillRequest(k_CachedHit[0].point, actionID, ref data);
SendInput(data);
}
}
/// <summary>
/// When you right-click on something you will want to do contextually different things. For example you might attack an enemy,
/// but revive a friend. You might also decide to do nothing (e.g. right-clicking on a friend who hasn't FAINTED).
/// </summary>
/// <param name="hit">The Transform of the entity we clicked on, or null if none.</param>
/// <param name="actionID">The Action to build for</param>
/// <param name="triggerStyle">How did this skill play get triggered? Mouse, Keyboard, UI etc.</param>
/// <param name="resultData">Out parameter that will be filled with the resulting action, if any.</param>
/// <returns>true if we should play an action, false otherwise. </returns>
bool GetActionRequestForTarget(Transform hit, ActionID actionID, SkillTriggerStyle triggerStyle, out ActionRequestData resultData)
{
resultData = new ActionRequestData();
var targetNetObj = hit != null ? hit.GetComponentInParent<NetworkObject>() : null;
//if we can't get our target from the submitted hit transform, get it from our stateful target in our ServerCharacter.
if (!targetNetObj && !GameDataSource.Instance.GetActionPrototypeByID(actionID).IsGeneralTargetAction)
{
ulong targetId = m_ServerCharacter.TargetId.Value;
NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out targetNetObj);
}
//sanity check that this is indeed a valid target.
if (targetNetObj == null || !ActionUtils.IsValidTarget(targetNetObj.NetworkObjectId))
{
return false;
}
if (targetNetObj.TryGetComponent<ServerCharacter>(out var serverCharacter))
{
//Skill1 may be contextually overridden if it was generated from a mouse-click.
if (actionID == CharacterClass.Skill1.ActionID && triggerStyle == SkillTriggerStyle.MouseClick)
{
if (!serverCharacter.IsNpc && serverCharacter.LifeState == LifeState.Fainted)
{
//right-clicked on a downed ally--change the skill play to Revive.
actionID = GameDataSource.Instance.ReviveActionPrototype.ActionID;
}
}
}
Vector3 targetHitPoint;
if (PhysicsWrapper.TryGetPhysicsWrapper(targetNetObj.NetworkObjectId, out var movementContainer))
{
targetHitPoint = movementContainer.Transform.position;
}
else
{
targetHitPoint = targetNetObj.transform.position;
}
// record our target in case this action uses that info (non-targeted attacks will ignore this)
resultData.ActionID = actionID;
resultData.TargetIds = new ulong[] { targetNetObj.NetworkObjectId };
PopulateSkillRequest(targetHitPoint, actionID, ref resultData);
return true;
}
/// <summary>
/// Populates the ActionRequestData with additional information. The TargetIds of the action should already be set before calling this.
/// </summary>
/// <param name="hitPoint">The point in world space where the click ray hit the target.</param>
/// <param name="actionID">The action to perform (will be stamped on the resultData)</param>
/// <param name="resultData">The ActionRequestData to be filled out with additional information.</param>
void PopulateSkillRequest(Vector3 hitPoint, ActionID actionID, ref ActionRequestData resultData)
{
resultData.ActionID = actionID;
var actionConfig = GameDataSource.Instance.GetActionPrototypeByID(actionID).Config;
//most skill types should implicitly close distance. The ones that don't are explicitly set to false in the following switch.
resultData.ShouldClose = true;
// figure out the Direction in case we want to send it
Vector3 offset = hitPoint - m_PhysicsWrapper.Transform.position;
offset.y = 0;
Vector3 direction = offset.normalized;
switch (actionConfig.Logic)
{
//for projectile logic, infer the direction from the click position.
case ActionLogic.LaunchProjectile:
resultData.Direction = direction;
resultData.ShouldClose = false; //why? Because you could be lining up a shot, hoping to hit other people between you and your target. Moving you would be quite invasive.
return;
case ActionLogic.Melee:
resultData.Direction = direction;
return;
case ActionLogic.Target:
resultData.ShouldClose = false;
return;
case ActionLogic.Emote:
resultData.CancelMovement = true;
return;
case ActionLogic.RangedFXTargeted:
resultData.Position = hitPoint;
return;
case ActionLogic.DashAttack:
resultData.Position = hitPoint;
return;
case ActionLogic.PickUp:
resultData.CancelMovement = true;
resultData.ShouldQueue = false;
return;
}
}
/// <summary>
/// Request an action be performed. This will occur on the next FixedUpdate.
/// </summary>
/// <param name="actionID"> The action you'd like to perform. </param>
/// <param name="triggerStyle"> What input style triggered this action. </param>
/// <param name="targetId"> NetworkObjectId of target. </param>
public void RequestAction(ActionID actionID, SkillTriggerStyle triggerStyle, ulong targetId = 0)
{
Assert.IsNotNull(GameDataSource.Instance.GetActionPrototypeByID(actionID),
$"Action with actionID {actionID} must be contained in the Action prototypes of GameDataSource!");
if (m_ActionRequestCount < m_ActionRequests.Length)
{
m_ActionRequests[m_ActionRequestCount].RequestedActionID = actionID;
m_ActionRequests[m_ActionRequestCount].TriggerStyle = triggerStyle;
m_ActionRequests[m_ActionRequestCount].TargetId = targetId;
m_ActionRequestCount++;
}
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Alpha1))
{
RequestAction(actionState1.actionID, SkillTriggerStyle.Keyboard);
}
else if (Input.GetKeyUp(KeyCode.Alpha1))
{
RequestAction(actionState1.actionID, SkillTriggerStyle.KeyboardRelease);
}
if (Input.GetKeyDown(KeyCode.Alpha2))
{
RequestAction(actionState2.actionID, SkillTriggerStyle.Keyboard);
}
else if (Input.GetKeyUp(KeyCode.Alpha2))
{
RequestAction(actionState2.actionID, SkillTriggerStyle.KeyboardRelease);
}
if (Input.GetKeyDown(KeyCode.Alpha3))
{
RequestAction(actionState3.actionID, SkillTriggerStyle.Keyboard);
}
else if (Input.GetKeyUp(KeyCode.Alpha3))
{
RequestAction(actionState3.actionID, SkillTriggerStyle.KeyboardRelease);
}
if (Input.GetKeyDown(KeyCode.Alpha5))
{
RequestAction(GameDataSource.Instance.Emote1ActionPrototype.ActionID, SkillTriggerStyle.Keyboard);
}
if (Input.GetKeyDown(KeyCode.Alpha6))
{
RequestAction(GameDataSource.Instance.Emote2ActionPrototype.ActionID, SkillTriggerStyle.Keyboard);
}
if (Input.GetKeyDown(KeyCode.Alpha7))
{
RequestAction(GameDataSource.Instance.Emote3ActionPrototype.ActionID, SkillTriggerStyle.Keyboard);
}
if (Input.GetKeyDown(KeyCode.Alpha8))
{
RequestAction(GameDataSource.Instance.Emote4ActionPrototype.ActionID, SkillTriggerStyle.Keyboard);
}
if (!EventSystem.current.IsPointerOverGameObject() && m_CurrentSkillInput == null)
{
//IsPointerOverGameObject() is a simple way to determine if the mouse is over a UI element. If it is, we don't perform mouse input logic,
//to model the button "blocking" mouse clicks from falling through and interacting with the world.
if (Input.GetMouseButtonDown(1))
{
RequestAction(CharacterClass.Skill1.ActionID, SkillTriggerStyle.MouseClick);
}
if (Input.GetMouseButtonDown(0))
{
RequestAction(GameDataSource.Instance.GeneralTargetActionPrototype.ActionID, SkillTriggerStyle.MouseClick);
}
else if (Input.GetMouseButton(0))
{
m_MoveRequest = true;
}
}
}
void UpdateAction1()
{
var isHoldingNetworkObject =
NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(m_ServerCharacter.HeldNetworkObject.Value,
out var heldNetworkObject);
NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(m_ServerCharacter.TargetId.Value,
out var selection);
var isSelectable = true;
if (isHoldingNetworkObject)
{
// show drop!
actionState1.actionID = GameDataSource.Instance.DropActionPrototype.ActionID;
}
else if ((m_ServerCharacter.TargetId.Value != 0
&& selection != null
&& selection.TryGetComponent(out PickUpState pickUpState))
)
{
// special case: targeting a pickup-able item or holding a pickup object
actionState1.actionID = GameDataSource.Instance.PickUpActionPrototype.ActionID;
}
else if (m_ServerCharacter.TargetId.Value != 0
&& selection != null
&& selection.NetworkObjectId != m_ServerCharacter.NetworkObjectId
&& selection.TryGetComponent(out ServerCharacter charState)
&& !charState.IsNpc)
{
// special case: when we have a player selected, we change the meaning of the basic action
// we have another player selected! In that case we want to reflect that our basic Action is a Revive, not an attack!
// But we need to know if the player is alive... if so, the button should be disabled (for better player communication)
actionState1.actionID = GameDataSource.Instance.ReviveActionPrototype.ActionID;
isSelectable = charState.NetLifeState.LifeState.Value != LifeState.Alive;
}
else
{
actionState1.SetActionState(CharacterClass.Skill1.ActionID);
}
actionState1.selectable = isSelectable;
action1ModifiedCallback?.Invoke();
}
public class ActionState
{
public ActionID actionID { get; internal set; }
public bool selectable { get; internal set; }
internal void SetActionState(ActionID newActionID, bool isSelectable = true)
{
actionID = newActionID;
selectable = isSelectable;
}
}
}
}
Boss Room wants the full history of inputs sent, not just the latest value. There is no need for NetworkVariable
s, you just want to blast your inputs to the server. Since Boss Room isn't a twitch shooter, it sends inputs as reliable RPC
s without worrying about the latency an input loss would add.
Arrow's GameObject vs Fireball's VFX
The archer's arrows use a standalone GameObject
that's replicated over time. Since this object's movements are slow, the Boss Room development team decided to use state (via the NetworkTransform
) to replicate the ability's status (in case a client connected while the arrow was flying).
loading...
Boss Room might have used an RPC
instead (for the Mage's projectile attack). Since the Mage's projectile fires quickly, the player experience isn't affected by the few milliseconds where a newly connected client might miss the projectile. In fact, it helps Boss Room save on bandwidth when managing a replicated object. Instead, Boss Room sends a single RPC to trigger the FX client side.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Unity.BossRoom.Gameplay.GameplayObjects
{
/// <summary>
/// Logic that handles an FX-based pretend-missile.
/// </summary>
public class FXProjectile : MonoBehaviour
{
[SerializeField]
private List<GameObject> m_ProjectileGraphics;
[SerializeField]
private List<GameObject> m_TargetHitGraphics;
[SerializeField]
private List<GameObject> m_TargetMissedGraphics;
[SerializeField]
[Tooltip("If this projectile plays an impact particle, how long should we stay alive for it to keep playing?")]
private float m_PostImpactDurationSeconds = 1;
private Vector3 m_StartPoint;
private Transform m_TargetDestination; // null if we're a "miss" projectile (i.e. we hit nothing)
private Vector3 m_MissDestination; // only used if m_TargetDestination is null
private float m_FlightDuration;
private float m_Age;
private bool m_HasImpacted;
public void Initialize(Vector3 startPoint, Transform target, Vector3 missPos, float flightTime)
{
m_StartPoint = startPoint;
m_TargetDestination = target;
m_MissDestination = missPos;
m_FlightDuration = flightTime;
m_HasImpacted = false;
// the projectile graphics are actually already enabled in the prefab, but just in case, turn them on
foreach (var projectileGO in m_ProjectileGraphics)
{
projectileGO.SetActive(true);
}
}
public void Cancel()
{
// we could play a "poof" particle... but for now we just instantly disappear
Destroy(gameObject);
}
private void Update()
{
m_Age += Time.deltaTime;
if (!m_HasImpacted)
{
if (m_Age >= m_FlightDuration)
{
Impact();
}
else
{
// we're flying through the air. Reposition ourselves to be closer to the destination
float progress = m_Age / m_FlightDuration;
transform.position = Vector3.Lerp(m_StartPoint, m_TargetDestination ? m_TargetDestination.position : m_MissDestination, progress);
}
}
else if (m_Age >= m_FlightDuration + m_PostImpactDurationSeconds)
{
Destroy(gameObject);
}
}
private void Impact()
{
m_HasImpacted = true;
foreach (var projectileGO in m_ProjectileGraphics)
{
projectileGO.SetActive(false);
}
// is it impacting an actual enemy? We allow different graphics for the "miss" case
if (m_TargetDestination)
{
foreach (var hitGraphicGO in m_TargetHitGraphics)
{
hitGraphicGO.SetActive(true);
}
}
else
{
foreach (var missGraphicGO in m_TargetMissedGraphics)
{
missGraphicGO.SetActive(true);
}
}
}
}
}
Breakable state
Boss Room might have used a "break" RPC
to set a breakable object as broken and play the appropriate visual effects. Applying the "replicate information when a player joins the game mid-game" rule of thumb, the Boss Room development team used NetworkVariable
s instead. Boss Room uses the OnValueChanged
callback on those values to play the visual effects (and an initial check when spawning the NetworkBehaviour
).
public override void OnNetworkSpawn()
{
if (IsServer)
{
if (m_MaxHealth && m_NetworkHealthState)
{
m_NetworkHealthState.HitPoints.Value = m_MaxHealth.Value;
}
}
if (IsClient)
{
IsBroken.OnValueChanged += OnBreakableStateChanged;
if (IsBroken.Value == true)
{
PerformBreakVisualization(true);
}
}
}
The visual changes:
private void OnBreakableStateChanged(bool wasBroken, bool isBroken)
{
if (!wasBroken && isBroken)
{
PerformBreakVisualization(false);
}
else if (wasBroken && !isBroken)
{
PerformUnbreakVisualization();
}
}
Error when connecting after imps have died: The following is a small gotcha the Boss Room development team encountered while developing Boss Room. Using NetworkVariable
s isn't magical. If you use OnValueChanged
, you still need to make sure you initialize your values when spawning for the first time. OnValueChanged
isn't called when connecting for the first time, only for the next value changes.
loading...
Hit points
Boss Room syncs all character and object hit points through NetworkVariable
s, making it easy to collect data.
If Boss Room synced this data through RPC
s, Boss Room would need to keep a list of RPC
s to send to connecting players to ensure they get the latest hit point values for each object. Keeping a list of RPC
s for each object to send to those RPC
s on connecting would be a maintainability nightmare. By using NetworkVariable
s, Boss Room lets the SDK do the work.