diff --git a/docs/learn/getting-started-boss-room.md b/docs/learn/getting-started-boss-room.md
index 296b34aec..cc5995c28 100644
--- a/docs/learn/getting-started-boss-room.md
+++ b/docs/learn/getting-started-boss-room.md
@@ -29,7 +29,7 @@ Using Windows' built-in extracting tool may generate a "Error 0x80010135: Path t
:::important Compatibility
- Boss Room supports all major Unity platforms. To use the WebGL platform a custom WebGL transport based on web sockets is needed.
-- Boss Room is compatible with Unity 2020.3.0f1.
+- Boss Room is compatible with Unity 2020.3 LTS and later.
- Make sure to include standalone support for Windows/Mac in your installation.
:::
diff --git a/docs/learn/rpcnetvarexamples.md b/docs/learn/rpcnetvarexamples.md
index ea269a7ab..72ce2debc 100644
--- a/docs/learn/rpcnetvarexamples.md
+++ b/docs/learn/rpcnetvarexamples.md
@@ -10,401 +10,8 @@ See the [RPC vs NetworkVariable](rpcvnetvar.md) tutorial for more information.
## RPCs for movement
Boss Room uses RPCs to send movement inputs.
-
-```csharp
-using MLAPI;
-using System;
-using System.Collections.Generic;
-using MLAPI.Spawning;
-using UnityEngine;
-using UnityEngine.Assertions;
-using UnityEngine.EventSystems;
-
-namespace BossRoom.Client
-{
- ///
- /// Captures inputs for a character on a client and sends them to the server.
- ///
- [RequireComponent(typeof(NetworkCharacterState))]
- public class ClientInputSender : NetworkBehaviour
- {
- private const float k_MouseInputRaycastDistance = 100f;
-
- private const float k_MoveSendRateSeconds = 0.5f;
-
- private float m_LastSentMove;
-
- // Cache raycast hit array so that we can use non alloc raycasts
- private 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.
- private LayerMask k_GroundLayerMask;
- private LayerMask k_ActionLayerMask;
-
- private NetworkCharacterState m_NetworkCharacter;
-
- ///
- /// 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.
- ///
- 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.
- }
-
- ///
- /// 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.
- ///
- ///
- /// Reference: https://answers.unity.com/questions/1141633/why-does-fixedupdate-work-when-update-doesnt.html
- ///
- private struct ActionRequest
- {
- public SkillTriggerStyle TriggerStyle;
- public ActionType RequestedAction;
- public ulong TargetId;
- }
-
- ///
- /// 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.
- ///
- private readonly ActionRequest[] m_ActionRequests = new ActionRequest[5];
-
- ///
- /// Number of ActionRequests that have been queued since the last FixedUpdate.
- ///
- private int m_ActionRequestCount;
-
- private BaseActionInput m_CurrentSkillInput = null;
- private bool m_MoveRequest = false;
-
-
- Camera m_MainCamera;
-
- public event Action OnClientClick;
-
- ///
- /// Convenience getter that returns our CharacterData
- ///
- CharacterClass CharacterData => GameDataSource.Instance.CharacterDataByType[m_NetworkCharacter.CharacterType];
-
- public override void NetworkStart()
- {
- // TODO Don't use NetworkBehaviour for just NetworkStart [GOMPS-81]
- if (!IsClient || !IsOwner)
- {
- enabled = false;
- // dont need to do anything else if not the owner
- return;
- }
-
- k_GroundLayerMask = LayerMask.GetMask(new[] { "Ground" });
- k_ActionLayerMask = LayerMask.GetMask(new[] { "PCs", "NPCs", "Ground" });
-
- // find the hero action UI bar
- GameObject actionUIobj = GameObject.FindGameObjectWithTag("HeroActionBar");
- actionUIobj.GetComponent().RegisterInputSender(this);
-
- // find the emote bar to track its buttons
- GameObject emoteUIobj = GameObject.FindGameObjectWithTag("HeroEmoteBar");
- emoteUIobj.GetComponent().RegisterInputSender(this);
- // once connected to the emote bar, hide it
- emoteUIobj.SetActive(false);
- }
-
- void Awake()
- {
- m_NetworkCharacter = GetComponent();
- m_MainCamera = Camera.main;
- }
-
- public void FinishSkill()
- {
- m_CurrentSkillInput = null;
- }
-
- 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 (m_ActionRequests[i].TriggerStyle == SkillTriggerStyle.KeyboardRelease )
- {
- m_CurrentSkillInput.OnReleaseKey();
- }
- }
- else
- {
- var actionData = GameDataSource.Instance.ActionDataByType[m_ActionRequests[i].RequestedAction];
- if (actionData.ActionInput != null)
- {
- var skillPlayer = Instantiate(actionData.ActionInput);
- skillPlayer.Initiate(m_NetworkCharacter, actionData.ActionTypeEnum, FinishSkill);
- m_CurrentSkillInput = skillPlayer;
- }
- else
- {
- PerformSkill(actionData.ActionTypeEnum, m_ActionRequests[i].TriggerStyle, m_ActionRequests[i].TargetId);
- }
- }
- }
-
- m_ActionRequestCount = 0;
-
- if( m_MoveRequest )
- {
- m_MoveRequest = false;
- if ( (Time.time - m_LastSentMove) > k_MoveSendRateSeconds)
- {
- m_LastSentMove = Time.time;
- var ray = m_MainCamera.ScreenPointToRay(Input.mousePosition);
- if (Physics.RaycastNonAlloc(ray, k_CachedHit, k_MouseInputRaycastDistance, k_GroundLayerMask) > 0)
- {
- // The MLAPI_INTERNAL channel is a reliable sequenced channel. Inputs should always arrive and be in order that's why this channel is used.
- m_NetworkCharacter.SendCharacterInputServerRpc(k_CachedHit[0].point);
- //Send our client only click request
- OnClientClick?.Invoke(k_CachedHit[0].point);
- }
- }
- }
- }
-
- ///
- /// Perform a skill in response to some input trigger. This is the common method to which all input-driven skill plays funnel.
- ///
- /// The action you want to play. Note that "Skill1" may be overriden contextually depending on the target.
- /// What sort of input triggered this skill?
- /// (optional) Pass in a specific networkID to target for this action
- private void PerformSkill(ActionType actionType, SkillTriggerStyle triggerStyle, ulong targetId = 0)
- {
- Transform hitTransform = null;
-
- if (targetId != 0)
- {
- // if a targetId is given, try to find the object
- NetworkObject targetNetObj;
- if (NetworkSpawnManager.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(Input.mousePosition);
- numHits = Physics.RaycastNonAlloc(ray, k_CachedHit, k_MouseInputRaycastDistance, k_ActionLayerMask);
- }
-
- int networkedHitIndex = -1;
- for (int i = 0; i < numHits; i++)
- {
- if (k_CachedHit[i].transform.GetComponent())
- {
- networkedHitIndex = i;
- break;
- }
- }
-
- hitTransform = networkedHitIndex >= 0 ? k_CachedHit[networkedHitIndex].transform : null;
- }
-
- if (GetActionRequestForTarget(hitTransform, actionType, triggerStyle, out ActionRequestData playerAction))
- {
- //Don't trigger our move logic for another 500ms. This protects us from moving just because we clicked on them to target them.
- m_LastSentMove = Time.time;
- m_NetworkCharacter.RecvDoActionServerRPC(playerAction);
- }
- else if(actionType != ActionType.GeneralTarget )
- {
- // clicked on nothing... perform a "miss" attack on the spot they clicked on
- var data = new ActionRequestData();
- PopulateSkillRequest(k_CachedHit[0].point, actionType, ref data);
- m_NetworkCharacter.RecvDoActionServerRPC(data);
- }
- }
-
- ///
- /// 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).
- ///
- /// The Transform of the entity we clicked on, or null if none.
- /// The Action to build for
- /// How did this skill play get triggered? Mouse, Keyboard, UI etc.
- /// Out parameter that will be filled with the resulting action, if any.
- /// true if we should play an action, false otherwise.
- private bool GetActionRequestForTarget(Transform hit, ActionType actionType, SkillTriggerStyle triggerStyle, out ActionRequestData resultData)
- {
- resultData = new ActionRequestData();
-
- var targetNetObj = hit != null ? hit.GetComponent() : null;
-
- //if we can't get our target from the submitted hit transform, get it from our stateful target in our NetworkCharacterState.
- if (!targetNetObj && actionType != ActionType.GeneralTarget)
- {
- ulong targetId = m_NetworkCharacter.TargetId.Value;
- NetworkSpawnManager.SpawnedObjects.TryGetValue(targetId, out targetNetObj);
- }
-
- //sanity check that this is indeed a valid target.
- if(targetNetObj==null || !ActionUtils.IsValidTarget(targetNetObj.NetworkObjectId))
- {
- return false;
- }
-
- var targetNetState = targetNetObj.GetComponent();
- if (targetNetState != null)
- {
- //Skill1 may be contextually overridden if it was generated from a mouse-click.
- if (actionType == CharacterData.Skill1 && triggerStyle == SkillTriggerStyle.MouseClick)
- {
- if (!targetNetState.IsNpc && targetNetState.NetworkLifeState.Value == LifeState.Fainted)
- {
- //right-clicked on a downed ally--change the skill play to Revive.
- actionType = ActionType.GeneralRevive;
- }
- }
- }
-
- // record our target in case this action uses that info (non-targeted attacks will ignore this)
- resultData.ActionTypeEnum = actionType;
- resultData.TargetIds = new ulong[] { targetNetObj.NetworkObjectId };
- PopulateSkillRequest(targetNetObj.transform.position, actionType, ref resultData);
- return true;
- }
-
- ///
- /// Populates the ActionRequestData with additional information. The TargetIds of the action should already be set before calling this.
- ///
- /// The point in world space where the click ray hit the target.
- /// The action to perform (will be stamped on the resultData)
- /// The ActionRequestData to be filled out with additional information.
- private void PopulateSkillRequest(Vector3 hitPoint, ActionType action, ref ActionRequestData resultData)
- {
- resultData.ActionTypeEnum = action;
- var actionInfo = GameDataSource.Instance.ActionDataByType[action];
-
- //most skill types should implicitly close distance. The ones that don't are explicitly set to false in the following switch.
- resultData.ShouldClose = true;
-
- switch (actionInfo.Logic)
- {
- //for projectile logic, infer the direction from the click position.
- case ActionLogic.LaunchProjectile:
- Vector3 offset = hitPoint - transform.position;
- offset.y = 0;
- resultData.Direction = offset.normalized;
- 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.Target:
- resultData.ShouldClose = false;
- return;
- case ActionLogic.Emote:
- resultData.CancelMovement = true;
- return;
- case ActionLogic.RangedFXTargeted:
- if (resultData.TargetIds == null) { resultData.Position = hitPoint; }
- return;
- }
- }
-
- ///
- /// Request an action be performed. This will occur on the next FixedUpdate.
- ///
- /// the action you'd like to perform.
- /// What input style triggered this action.
- public void RequestAction(ActionType action, SkillTriggerStyle triggerStyle, ulong targetId = 0)
- {
- // do not populate an action request unless said action is valid
- if (action == ActionType.None)
- {
- return;
- }
-
- Assert.IsTrue(GameDataSource.Instance.ActionDataByType.ContainsKey(action),
- $"Action {action} must be part of ActionData dictionary!");
-
- if( m_ActionRequestCount < m_ActionRequests.Length )
- {
- m_ActionRequests[m_ActionRequestCount].RequestedAction = action;
- m_ActionRequests[m_ActionRequestCount].TriggerStyle = triggerStyle;
- m_ActionRequests[m_ActionRequestCount].TargetId = targetId;
- m_ActionRequestCount++;
- }
- }
-
- void Update()
- {
- if (Input.GetKeyDown(KeyCode.Alpha1))
- {
- RequestAction(CharacterData.Skill2, SkillTriggerStyle.Keyboard);
- }
- else if (Input.GetKeyUp(KeyCode.Alpha1))
- {
- RequestAction(CharacterData.Skill2, SkillTriggerStyle.KeyboardRelease);
- }
- if (Input.GetKeyDown(KeyCode.Alpha2))
- {
- RequestAction(CharacterData.Skill3, SkillTriggerStyle.Keyboard);
- }
- else if (Input.GetKeyUp(KeyCode.Alpha2))
- {
- RequestAction(CharacterData.Skill3, SkillTriggerStyle.KeyboardRelease);
- }
-
- if (Input.GetKeyDown(KeyCode.Alpha4))
- {
- RequestAction(ActionType.Emote1, SkillTriggerStyle.Keyboard);
- }
- if (Input.GetKeyDown(KeyCode.Alpha5))
- {
- RequestAction(ActionType.Emote2, SkillTriggerStyle.Keyboard);
- }
- if (Input.GetKeyDown(KeyCode.Alpha6))
- {
- RequestAction(ActionType.Emote3, SkillTriggerStyle.Keyboard);
- }
- if (Input.GetKeyDown(KeyCode.Alpha7))
- {
- RequestAction(ActionType.Emote4, 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(CharacterData.Skill1, SkillTriggerStyle.MouseClick);
- }
-
- if (Input.GetMouseButtonDown(0))
- {
- RequestAction(ActionType.GeneralTarget, SkillTriggerStyle.MouseClick);
- }
- else if (Input.GetMouseButton(0))
- {
- m_MoveRequest = true;
- }
- }
- }
- }
-}
```
@@ -415,11 +22,8 @@ We want the full history of inputs sent, not just the latest value. There is no
Sending from server to client `RecvPerformHitReactionClient`
-
-```csharp
-InvokeClientRpcOnEveryone(RecvPerformHitReactionClient);
```
For example, the Boss Room project "ouch" action `RPC` mentioned for `NetworkCharacterState` is interesting for optimization purposes. You would normally want to have only one `RPC` for an action and let the client decide who should play the associated animation. Due to "ouch" being a long running action over multiple frames, you do not know yet when sending the initial `RPC` which characters will be affected by that action. You want this to be dynamic as the boss is hitting targets. As a result, multiple `RPC`s will be sent for each hit character.
@@ -427,310 +31,32 @@ For example, the Boss Room project "ouch" action `RPC` mentioned for `NetworkCha
## Arrow's GameObject vs Fireball's VFX
The archer's arrows uses a standalone `GameObject` that is replicated over time. Since this object's movements are slow moving, we made the choice to use state to replicate this ability's status, in case a client connected while the arrow was flying.
-
-```csharp
-using MLAPI;
-using System.Collections.Generic;
-using System.IO;
-using MLAPI.Spawning;
-using UnityEngine;
-
-namespace BossRoom.Server
-{
-
- public class ServerProjectileLogic : MLAPI.NetworkBehaviour
- {
- private bool m_Started = false;
-
- [SerializeField]
- private NetworkProjectileState m_NetState;
-
- [SerializeField]
- private SphereCollider m_OurCollider;
-
- ///
- /// The character that created us. Can be 0 to signal that we were created generically by the server.
- ///
- private ulong m_SpawnerId;
-
- ///
- /// The data for our projectile. Indicates speed, damage, etc.
- ///
- private ActionDescription.ProjectileInfo m_ProjectileInfo;
-
- private const int k_MaxCollisions = 4;
- private const float k_WallLingerSec = 2f; //time in seconds that arrows linger after hitting a target.
- private const float k_EnemyLingerSec = 0.2f; //time after hitting an enemy that we persist.
- private Collider[] m_CollisionCache = new Collider[k_MaxCollisions];
-
- ///
- /// Time when we should destroy this arrow, in Time.time seconds.
- ///
- private float m_DestroyAtSec;
-
- private int m_CollisionMask; //mask containing everything we test for while moving
- private int m_BlockerMask; //physics mask for things that block the arrow's flight.
- private int m_NPCLayer;
-
- ///
- /// List of everyone we've hit and dealt damage to.
- ///
- ///
- /// Note that it's possible for entries in this list to become null if they're Destroyed post-impact.
- /// But that's fine by us! We use m_HitTargets.Count to tell us how many total enemies we've hit,
- /// so those nulls still count as hits.
- ///
- private List m_HitTargets = new List();
-
- ///
- /// Are we done moving?
- ///
- private bool m_IsDead;
-
- ///
- /// Set everything up based on provided projectile information.
- /// (Note that this is called before NetworkStart(), so don't try to do any network stuff here.)
- ///
- public void Initialize(ulong creatorsNetworkObjectId, in ActionDescription.ProjectileInfo projectileInfo)
- {
- m_SpawnerId = creatorsNetworkObjectId;
- m_ProjectileInfo = projectileInfo;
- }
-
- public override void NetworkStart(Stream stream)
- {
- if (!IsServer)
- {
- enabled = false;
- return;
- }
-
- m_Started = true;
-
- m_DestroyAtSec = Time.fixedTime + (m_ProjectileInfo.Range / m_ProjectileInfo.Speed_m_s);
-
- m_CollisionMask = LayerMask.GetMask(new[] { "NPCs", "Default", "Ground" });
- m_BlockerMask = LayerMask.GetMask(new[] { "Default", "Ground" });
- m_NPCLayer = LayerMask.NameToLayer("NPCs");
-
- RefreshNetworkState();
- }
-
- private void FixedUpdate()
- {
- if (!m_Started) { return; } //don't do anything before NetworkStart has run.
-
- Vector3 displacement = transform.forward * (m_ProjectileInfo.Speed_m_s * Time.fixedDeltaTime);
- transform.position += displacement;
-
- if (m_DestroyAtSec < Time.fixedTime)
- {
- // Time to go away.
- Destroy(gameObject);
- }
-
- if (!m_IsDead)
- {
- DetectCollisions();
- }
-
- RefreshNetworkState();
- }
-
- private void RefreshNetworkState()
- {
- m_NetState.NetworkPosition.Value = transform.position;
- m_NetState.NetworkRotationY.Value = transform.eulerAngles.y;
- m_NetState.NetworkMovementSpeed.Value = m_ProjectileInfo.Speed_m_s;
- }
-
- private void DetectCollisions()
- {
- Vector3 position = transform.localToWorldMatrix.MultiplyPoint(m_OurCollider.center);
- int numCollisions = Physics.OverlapSphereNonAlloc(position, m_OurCollider.radius, m_CollisionCache, m_CollisionMask);
- for (int i = 0; i < numCollisions; i++)
- {
- int layerTest = 1 << m_CollisionCache[i].gameObject.layer;
-
- if ((layerTest & m_BlockerMask) != 0)
- {
- //hit a wall; leave it for a couple of seconds.
- m_ProjectileInfo.Speed_m_s = 0;
- m_IsDead = true;
- m_DestroyAtSec = Time.fixedTime + k_WallLingerSec;
- return;
- }
-
- if (m_CollisionCache[i].gameObject.layer == m_NPCLayer && !m_HitTargets.Contains(m_CollisionCache[i].gameObject))
- {
- m_HitTargets.Add(m_CollisionCache[i].gameObject);
-
- if (m_HitTargets.Count >= m_ProjectileInfo.MaxVictims)
- {
- // we've hit all the enemies we're allowed to! So we're done
- m_DestroyAtSec = Time.fixedTime + k_EnemyLingerSec;
- m_IsDead = true;
- }
-
- //all NPC layer entities should have one of these.
- var targetNetObj = m_CollisionCache[i].GetComponent();
- if (targetNetObj)
- {
- m_NetState.RecvHitEnemyClientRPC(targetNetObj.NetworkObjectId);
-
- //retrieve the person that created us, if he's still around.
- NetworkObject spawnerNet;
- NetworkSpawnManager.SpawnedObjects.TryGetValue(m_SpawnerId, out spawnerNet);
- ServerCharacter spawnerObj = spawnerNet != null ? spawnerNet.GetComponent() : null;
-
- targetNetObj.GetComponent().ReceiveHP(spawnerObj, -m_ProjectileInfo.Damage);
- }
-
- if (m_IsDead)
- return; // don't keep examining collisions since we can't damage anybody else
- }
- }
- }
- }
-}
-
+```csharp reference
+https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop/blob/develop/Assets/BossRoom/Scripts/Server/Game/Entity/ServerProjectileLogic.cs
```
-
We could have used an `RPC` instead, for example the Mage's projectile attack. Since it is expected for that projectile to be quick, we are not affected by the few milliseconds where a newly connected client could miss the projectile and we save on bandwidth having to manage a replicated object. Instead a single RPC is sent to trigger the FX client side.
-
-```csharp
-using MLAPI;
-using MLAPI.Spawning;
-using UnityEngine;
-
-namespace BossRoom.Server
-{
- ///
- /// Action that represents an always-hit raybeam-style ranged attack. A particle is shown from caster to target, and then the
- /// target takes damage. (It is not possible to escape the hit; the target ALWAYS takes damage.) This is intended for fairly
- /// swift particles; the time before it's applied is based on a simple distance-check at the attack's start.
- /// (If no target is provided, it means the user clicked on an empty spot on the map. In that case we still perform an action,
- /// it just hits nothing.)
- ///
- public class FXProjectileTargetedAction : Action
- {
- private bool m_ImpactedTarget;
- private float m_TimeUntilImpact;
- private IDamageable m_Target;
-
- public FXProjectileTargetedAction(ServerCharacter parent, ref ActionRequestData data) : base(parent, ref data) { }
-
- public override bool Start()
- {
- m_Target = GetTarget();
- if (m_Target == null && HasTarget())
- {
- // target has disappeared! Abort.
- return false;
- }
-
- Vector3 targetPos = HasTarget() ? m_Target.transform.position : m_Data.Position;
-
- // turn to face our target!
- m_Parent.transform.LookAt(targetPos);
-
- // figure out how long the pretend-projectile will be flying to the target
- float distanceToTargetPos = Vector3.Distance(targetPos, m_Parent.transform.position);
- m_TimeUntilImpact = Description.ExecTimeSeconds + (distanceToTargetPos / Description.Projectiles[0].Speed_m_s);
- m_Parent.NetState.RecvDoActionClientRPC(Data);
- return true;
- }
-
- public override bool Update()
- {
- if (!m_ImpactedTarget && m_TimeUntilImpact <= (Time.time - TimeStarted))
- {
- m_ImpactedTarget = true;
- if (m_Target != null )
- {
- m_Target.ReceiveHP(m_Parent, -Description.Projectiles[0].Damage);
- }
- }
- return true;
- }
-
- public override void Cancel()
- {
- // TODO: somehow tell the corresponding FX to abort!
- }
-
- ///
- /// Are we even supposed to have a target? (If not, we're representing a "missed" bolt that just hits nothing.)
- ///
- private bool HasTarget()
- {
- return Data.TargetIds != null && Data.TargetIds.Length > 0;
- }
-
- ///
- /// Returns our intended target, or null if not found/no target.
- ///
- private IDamageable GetTarget()
- {
- if (Data.TargetIds == null || Data.TargetIds.Length == 0)
- {
- return null;
- }
-
- NetworkObject obj;
- if (NetworkSpawnManager.SpawnedObjects.TryGetValue(Data.TargetIds[0], out obj) && obj != null)
- {
- return obj.GetComponent();
- }
- else
- {
- // target could have legitimately disappeared in the time it took to queue this action... but that's pretty unlikely, so we'll log about it to ease debugging
- Debug.Log($"FXProjectileTargetedAction was targeted at ID {Data.TargetIds[0]}, but that target can't be found in spawned object list! (May have just been deleted?)");
- return null;
- }
- }
- }
-}
-
```
-
-
## Character life state
We could have used a "kill" `RPC` to set a character as dead and play the appropriate animations. Applying our "should that information be replicated when a player joins the game mid-game" rule of thumb, we used `NetworkVariable`s instead. We used the `OnValueChanged` callback on those values to play our state changes animation.
-
-```csharp
-public NetworkedVar NetworkLifeState { get; } = new NetworkedVar(LifeState.Alive);
```
The animation change:
-
-```csharp
-m_NetState.NetworkLifeState.OnValueChanged += OnLifeStateChanged;
+
```
:::tip Lesson Learned
@@ -739,286 +65,8 @@ m_NetState.NetworkLifeState.OnValueChanged += OnLifeStateChanged;

-
-```csharp
-using BossRoom.Client;
-using Cinemachine;
-using MLAPI;
-using System;
-using System.Collections;
-using System.ComponentModel;
-using UnityEngine;
-
-namespace BossRoom.Visual
-{
- ///
- /// is responsible for displaying a character on the client's screen based on state information sent by the server.
- ///
- public class ClientCharacterVisualization : NetworkBehaviour
- {
- private NetworkCharacterState m_NetState;
-
- [SerializeField]
- private Animator m_ClientVisualsAnimator;
-
- [SerializeField]
- private CharacterSwap m_CharacterSwapper;
-
- [Tooltip("Prefab for the Target Reticule used by this Character")]
- public GameObject TargetReticule;
-
- [Tooltip("Material to use when displaying a friendly target reticule (e.g. green color)")]
- public Material ReticuleFriendlyMat;
-
- [Tooltip("Material to use when displaying a hostile target reticule (e.g. red color)")]
- public Material ReticuleHostileMat;
-
- public Animator OurAnimator { get { return m_ClientVisualsAnimator; } }
-
- private ActionVisualization m_ActionViz;
-
- public Transform Parent { get; private set; }
-
- private const float k_MaxRotSpeed = 280; //max angular speed at which we will rotate, in degrees/second.
-
- /// Player characters need to report health changes and chracter info to the PartyHUD
- private Visual.PartyHUD m_PartyHUD;
-
- private float m_SmoothedSpeed;
-
- int m_AliveStateTriggerID;
- int m_FaintedStateTriggerID;
- int m_DeadStateTriggerID;
- int m_HitStateTriggerID;
-
- ///
- public override void NetworkStart()
- {
- if (!IsClient || transform.parent == null)
- {
- enabled = false;
- return;
- }
-
- m_AliveStateTriggerID = Animator.StringToHash("StandUp");
- m_FaintedStateTriggerID = Animator.StringToHash("FallDown");
- m_DeadStateTriggerID = Animator.StringToHash("Dead");
- m_HitStateTriggerID = Animator.StringToHash(ActionFX.k_DefaultHitReact);
-
- m_ActionViz = new ActionVisualization(this);
-
- Parent = transform.parent;
-
- m_NetState = Parent.gameObject.GetComponent();
- m_NetState.DoActionEventClient += PerformActionFX;
- m_NetState.CancelAllActionsEventClient += CancelAllActionFXs;
- m_NetState.CancelActionsByTypeEventClient += CancelActionFXByType;
- m_NetState.NetworkLifeState.OnValueChanged += OnLifeStateChanged;
- m_NetState.OnPerformHitReaction += OnPerformHitReaction;
- m_NetState.OnStopChargingUpClient += OnStoppedChargingUp;
- m_NetState.IsStealthy.OnValueChanged += OnStealthyChanged;
- // With this call, players connecting to a game with down imps will see all of them do the "dying" animation.
- // we should investigate for a way to have the imps already appear as down when connecting.
- // todo gomps-220
- OnLifeStateChanged(m_NetState.NetworkLifeState.Value, m_NetState.NetworkLifeState.Value);
-
- //we want to follow our parent on a spring, which means it can't be directly in the transform hierarchy.
- Parent.GetComponent().ChildVizObject = this;
- transform.SetParent(null);
-
- // sync our visualization position & rotation to the most up to date version received from server
- var parentMovement = Parent.GetComponent();
- transform.position = parentMovement.NetworkPosition.Value;
- transform.rotation = Quaternion.Euler(0, parentMovement.NetworkRotationY.Value, 0);
-
- // sync our animator to the most up to date version received from server
- SyncEntryAnimation(m_NetState.NetworkLifeState.Value);
-
- // listen for char-select info to change (in practice, this info doesn't
- // change, but we may not have the values set yet) ...
- m_NetState.CharacterAppearance.OnValueChanged += OnCharacterAppearanceChanged;
-
- // ...and visualize the current char-select value that we know about
- OnCharacterAppearanceChanged(0, m_NetState.CharacterAppearance.Value);
-
- // ...and visualize the current char-select value that we know about
- SetAppearanceSwap();
-
- if (!m_NetState.IsNpc)
- {
- // track health for heroes
- m_NetState.HealthState.HitPoints.OnValueChanged += OnHealthChanged;
-
- // find the emote bar to track its buttons
- GameObject partyHUDobj = GameObject.FindGameObjectWithTag("PartyHUD");
- m_PartyHUD = partyHUDobj.GetComponent();
-
- if (IsLocalPlayer)
- {
- ActionRequestData data = new ActionRequestData { ActionTypeEnum = ActionType.GeneralTarget };
- m_ActionViz.PlayAction(ref data);
- gameObject.AddComponent();
- m_PartyHUD.SetHeroData(m_NetState);
- }
- else
- {
- m_PartyHUD.SetAllyData(m_NetState);
- }
- }
- }
-
- ///
- /// The switch to certain LifeStates fires an animation on an NPC/PC. This bypasses that initial animation
- /// and sends an NPC/PC to their eventual looping animation. This is necessary for mid-game player connections.
- ///
- /// The last LifeState received by server.
- void SyncEntryAnimation(LifeState lifeState)
- {
- switch (lifeState)
- {
- case LifeState.Dead: // ie. NPCs already dead
- m_ClientVisualsAnimator.SetTrigger(Animator.StringToHash("EntryDeath"));
- break;
- case LifeState.Fainted: // ie. PCs already fainted
- m_ClientVisualsAnimator.SetTrigger(Animator.StringToHash("EntryFainted"));
- break;
- }
- }
- private void OnDestroy()
- {
- if (m_NetState)
- {
- m_NetState.DoActionEventClient -= PerformActionFX;
- m_NetState.CancelAllActionsEventClient -= CancelAllActionFXs;
- m_NetState.CancelActionsByTypeEventClient -= CancelActionFXByType;
- m_NetState.NetworkLifeState.OnValueChanged -= OnLifeStateChanged;
- m_NetState.OnPerformHitReaction -= OnPerformHitReaction;
- m_NetState.OnStopChargingUpClient -= OnStoppedChargingUp;
- m_NetState.IsStealthy.OnValueChanged -= OnStealthyChanged;
- }
- }
-
- private void OnPerformHitReaction()
- {
- m_ClientVisualsAnimator.SetTrigger(m_HitStateTriggerID);
- }
-
- private void PerformActionFX(ActionRequestData data)
- {
- m_ActionViz.PlayAction(ref data);
- }
-
- private void CancelAllActionFXs()
- {
- m_ActionViz.CancelAllActions();
- }
-
- private void CancelActionFXByType(ActionType actionType)
- {
- m_ActionViz.CancelAllActionsOfType(actionType);
- }
-
- private void OnStoppedChargingUp()
- {
- m_ActionViz.OnStoppedChargingUp();
- }
-
- private void OnLifeStateChanged(LifeState previousValue, LifeState newValue)
- {
- switch (newValue)
- {
- case LifeState.Alive:
- m_ClientVisualsAnimator.SetTrigger(m_AliveStateTriggerID);
- break;
- case LifeState.Fainted:
- m_ClientVisualsAnimator.SetTrigger(m_FaintedStateTriggerID);
- break;
- case LifeState.Dead:
- m_ClientVisualsAnimator.SetTrigger(m_DeadStateTriggerID);
- break;
- default:
- throw new ArgumentOutOfRangeException(nameof(newValue), newValue, null);
- }
- }
-
- private void OnHealthChanged(int previousValue, int newValue)
- {
- // don't do anything if party HUD goes away - can happen as Dungeon scene is destroyed
- if (m_PartyHUD == null) { return; }
-
- if (IsLocalPlayer)
- {
- this.m_PartyHUD.SetHeroHealth(newValue);
- }
- else
- {
- this.m_PartyHUD.SetAllyHealth(m_NetState.NetworkObjectId, newValue);
- }
- }
-
- private void OnCharacterAppearanceChanged(int oldValue, int newValue)
- {
- SetAppearanceSwap();
- }
-
- private void OnStealthyChanged(byte oldValue, byte newValue)
- {
- SetAppearanceSwap();
- }
-
- private void SetAppearanceSwap()
- {
- if (m_CharacterSwapper)
- {
- if (m_NetState.IsStealthy.Value != 0 && !m_NetState.IsOwner)
- {
- // this character is in "stealth mode", so other players can't see them!
- m_CharacterSwapper.SwapAllOff();
- }
- else
- {
- m_CharacterSwapper.SwapToModel(m_NetState.CharacterAppearance.Value);
- }
- }
- }
-
- void Update()
- {
- if (Parent == null)
- {
- // since we aren't in the transform hierarchy, we have to explicitly die when our parent dies.
- Destroy(gameObject);
- return;
- }
-
- VisualUtils.SmoothMove(transform, Parent.transform, Time.deltaTime, ref m_SmoothedSpeed, k_MaxRotSpeed);
-
- if (m_ClientVisualsAnimator)
- {
- // set Animator variables here
- float visibleSpeed = 0;
- if (m_NetState.NetworkLifeState.Value == LifeState.Alive)
- {
- visibleSpeed = m_NetState.VisualMovementSpeed.Value;
- }
- m_ClientVisualsAnimator.SetFloat("Speed", visibleSpeed);
- }
-
- m_ActionViz.Update();
- }
-
- public void OnAnimEvent(string id)
- {
- //if you are trying to figure out who calls this method, it's "magic". The Unity Animation Event system takes method names as strings,
- //and calls a method of the same name on a component on the same GameObject as the Animator. See the "attack1" Animation Clip as one
- //example of where this is configured.
-
- m_ActionViz.OnAnimEvent(id);
- }
- }
-}
```
:::
diff --git a/docs/learn/rpcvnetvar.md b/docs/learn/rpcvnetvar.md
index 89ac16e80..34d19b5bc 100644
--- a/docs/learn/rpcvnetvar.md
+++ b/docs/learn/rpcvnetvar.md
@@ -37,25 +37,8 @@ If we sent an `RPC` to all clients, then all players connecting mid game after t
In that case, it is preferable to use `NetworkVariable`s like shown here.
-
-```csharp
-using MLAPI;
-using MLAPI.NetworkVariable;
-using System.Collections;
-using UnityEngine;
-
-///
-/// Network state for a door which can be opened by pressing on a floor switch.
-///
-public class NetworkDoorState : NetworkBehaviour
-{
- public NetworkVariableBool IsOpen { get; } = new NetworkVariableBool();
-}
-
```
It uses a `BoolNetworkVariable` to represent the "IsOpen" state. If I open the door and a player connects after this, the host will replicate all the world's information to that new player, including the door's state.
@@ -73,203 +56,22 @@ Actions in Boss Room are a great example for this. The area of effect action (`A
`AoeActionInput.cs` Shows the input being updated client side and not waiting for the server. It then calls an `RPC` when clicking on the area to affect.
-
-```csharp
-
-using UnityEngine;
-
-namespace BossRoom.Visual
-{
- ///
- /// This class is the first step in AoE ability. It will update the initial input visuals' position and will be in charge
- /// of tracking the user inputs. Once the ability
- /// is confirmed and the mouse is clicked, it'll send the appropriate RPC to the server, triggering the AoE serer side gameplay logic.
- /// The server side gameplay action will then trigger the client side resulting FX.
- /// This action's flow is this: (Client) AoEActionInput --> (Server) AoEAction --> (Client) AoEActionFX
- ///
- public class AoeActionInput : BaseActionInput
- {
- [SerializeField]
- private GameObject m_InRangeVisualization;
-
- [SerializeField]
- private GameObject m_OutOfRangeVisualization;
-
- Camera m_Camera;
- int m_GroundLayerMask;
- Vector3 m_Origin;
-
- RaycastHit[] m_UpdateResult = new RaycastHit[1];
-
- void Start()
- {
- var radius = GameDataSource.Instance.ActionDataByType[m_ActionType].Radius;
- transform.localScale = new Vector3(radius * 2, radius * 2, radius * 2);
- m_Camera = Camera.main;
- m_GroundLayerMask = LayerMask.GetMask("Ground");
- m_Origin = m_PlayerOwner.transform.position;
- }
-
- void Update()
- {
- if (Physics.RaycastNonAlloc(
- ray: m_Camera.ScreenPointToRay(Input.mousePosition),
- results: m_UpdateResult,
- maxDistance: float.PositiveInfinity,
- layerMask: m_GroundLayerMask) > 0)
- {
- transform.position = m_UpdateResult[0].point;
- }
-
- float range = GameDataSource.Instance.ActionDataByType[m_ActionType].Range;
- bool isInRange = (m_Origin - transform.position).sqrMagnitude <= range * range;
- m_InRangeVisualization.SetActive(isInRange);
- m_OutOfRangeVisualization.SetActive(!isInRange);
-
- if (Input.GetMouseButtonUp(0))
- {
- if (isInRange)
- {
- var data = new ActionRequestData
- {
- Position = transform.position,
- ActionTypeEnum = m_ActionType,
- ShouldQueue = false,
- TargetIds = null
- };
- m_PlayerOwner.RecvDoActionServerRPC(data);
- }
- Destroy(gameObject);
- return;
- }
- }
- }
-}
```
`AOEAction.cs` Server side logic detecting enemies inside the area and applying damage. It then broadcasts an `RPC` to tell all clients to play the VFX at the appropriate position. Character's state will automatically update with their respective `NetworkVariable`s update (health and alive status for example).
-
-```csharp
-using BossRoom;
-using BossRoom.Server;
-using MLAPI;
-using MLAPI.Spawning;
-using System.Collections.Generic;
-using UnityEngine;
-
-///
-/// Area-of-effect attack Action. The attack is centered on a point provided by the client.
-///
-public class AoeAction : Action
-{
- ///
- /// Cheat prevention: to ensure that players don't perform AoEs outside of their attack range,
- /// we ensure that the target is less than Range meters away from the player, plus this "fudge
- /// factor" to accomodate miscellaneous minor movement.
- ///
- const float k_MaxDistanceDivergence = 1;
-
- bool m_DidAoE;
-
- public AoeAction(ServerCharacter parent, ref ActionRequestData data)
- : base(parent, ref data) { }
-
- public override bool Start()
- {
- float distanceAway = Vector3.Distance(m_Parent.transform.position, Data.Position);
- if (distanceAway > Description.Range+k_MaxDistanceDivergence)
- {
- Debug.LogError($"Hacking detected?! (Object ID {m_Parent.NetworkObjectId}) {Description.ActionTypeEnum}'s AoE range is {Description.Range} but we were given a point that's {distanceAway} away from us. Canceling AoE");
- return ActionConclusion.Stop;
- }
-
- // broadcasting to all players including myself.
- // We don't know our actual targets for this attack until it triggers, so the client can't use the TargetIds list (and we clear it out for clarity).
- // This means we are responsible for triggering reaction-anims ourselves, which we do in PerformAoe()
- Data.TargetIds = new ulong[0];
- m_Parent.NetState.RecvDoActionClientRPC(Data);
- return ActionConclusion.Continue;
- }
-
- public override bool Update()
- {
- if (TimeRunning >= Description.ExecTimeSeconds && !m_DidAoE)
- {
- // actually perform the AoE attack
- m_DidAoE = true;
- PerformAoE();
- }
- return ActionConclusion.Continue;
- }
-
- private void PerformAoE()
- {
- // Note: could have a non alloc version of this overlap sphere where we statically store our collider array, but since this is a self
- // destroyed object, the complexity added to have a static pool of colliders that could be called by multiplayer players at the same time
- // doesn't seem worth it for now.
- var colliders = Physics.OverlapSphere(m_Data.Position, Description.Radius, LayerMask.GetMask("NPCs"));
- for (var i = 0; i < colliders.Length; i++)
- {
- var enemy = colliders[i].GetComponent();
- if (enemy != null)
- {
- // make the target "flinch", assuming they're a living enemy
- var networkObject = NetworkSpawnManager.SpawnedObjects[enemy.NetworkObjectId];
- if (networkObject)
- {
- var state = networkObject.GetComponent();
- if (state)
- {
- state.RecvPerformHitReactionClientRPC();
- }
- }
-
- // actually deal the damage
- enemy.ReceiveHP(m_Parent, -Description.Amount);
- }
- }
- }
-}
```
`AoeActionFX.cs` is triggered by an `RPC` coming from the server
-
-```csharp
-using System;
-using UnityEngine;
-
-namespace BossRoom.Visual
-{
- /// Final step in the AoE action flow. Please see AoEActionInput for the first step and more details on overall flow
- public class AoeActionFX : ActionFX
- {
- public AoeActionFX(ref ActionRequestData data, ClientCharacterVisualization parent)
- : base(ref data, parent) { }
-
- public override bool Start()
- {
- m_Parent.OurAnimator.SetTrigger(Description.Anim);
- GameObject.Instantiate(Description.Spawns[0], m_Data.Position, Quaternion.identity);
- return ActionConclusion.Stop;
- }
-
- public override bool Update()
- {
- throw new Exception("This should not execute");
- }
- }
-}
-
```
:::tip
diff --git a/docs/tutorials/helloworldparttwo.md b/docs/tutorials/helloworldparttwo.md
index c7d0eb6d0..6065a98d3 100644
--- a/docs/tutorials/helloworldparttwo.md
+++ b/docs/tutorials/helloworldparttwo.md
@@ -23,126 +23,25 @@ You can copy the script from here and paste it into your file.
1. Click **Copy** in the top right corner.
1. Paste it into your code editor.
:::
-
-```csharp
-
-using MLAPI;
-using UnityEngine;
-
-namespace HelloWorld
-{
- public class HelloWorldManager : MonoBehaviour
- {
- void OnGUI()
- {
- GUILayout.BeginArea(new Rect(10, 10, 300, 300));
- if (!NetworkManager.Singleton.IsClient && !NetworkManager.Singleton.IsServer)
- {
- StartButtons();
- }
- else
- {
- StatusLabels();
-
- SubmitNewPosition();
- }
-
- GUILayout.EndArea();
- }
-
- static void StartButtons()
- {
- if (GUILayout.Button("Host")) NetworkManager.Singleton.StartHost();
- if (GUILayout.Button("Client")) NetworkManager.Singleton.StartClient();
- if (GUILayout.Button("Server")) NetworkManager.Singleton.StartServer();
- }
-
- static void StatusLabels()
- {
- var mode = NetworkManager.Singleton.IsHost ?
- "Host" : NetworkManager.Singleton.IsServer ? "Server" : "Client";
-
- GUILayout.Label("Transport: " +
- NetworkManager.Singleton.NetworkConfig.NetworkTransport.GetType().Name);
- GUILayout.Label("Mode: " + mode);
- }
-
- static void SubmitNewPosition()
- {
- if (GUILayout.Button(NetworkManager.Singleton.IsServer ? "Move" : "Request Position Change"))
- {
- if (NetworkManager.Singleton.ConnectedClients.TryGetValue(NetworkManager.Singleton.LocalClientId,
- out var networkedClient))
- {
- var player = networkedClient.PlayerObject.GetComponent();
- if (player)
- {
- player.Move();
- }
- }
- }
- }
- }
-}
+```csharp reference
+https://github.com/Unity-Technologies/com.unity.multiplayer.samples.poc/tree/feature/hello-world/Assets/Scripts/Shared/HelloWorldManager.cs
```
Inside the `HelloWorldManager.cs` script, we will define two methods which mimic the editor buttons inside of **NetworkManager** during Play mode.
-
-```csharp
- static void StartButtons()
- {
- if (GUILayout.Button("Host")) NetworkManager.Singleton.StartHost();
- if (GUILayout.Button("Client")) NetworkManager.Singleton.StartClient();
- if (GUILayout.Button("Server")) NetworkManager.Singleton.StartServer();
- }
-
- static void StatusLabels()
- {
- var mode = NetworkManager.Singleton.IsHost ?
- "Host" : NetworkManager.Singleton.IsServer ? "Server" : "Client";
-
- GUILayout.Label("Transport: " +
- NetworkManager.Singleton.NetworkConfig.NetworkTransport.GetType().Name);
- GUILayout.Label("Mode: " + mode);
- }
-```
-
`NetworkManager` implements the singleton pattern as it declares its singleton named `Singleton`. This is defined when the `MonoBehaviour` is enabled. This component also contains very useful properties, such as `IsClient`, `IsServer`, and `IsLocalClient`. The first two dictate the connection state we have currently established that you will use shortly.
We will call these methods inside of `OnGUI()`.
-
-
-```csharp
-
-void OnGUI()
- {
- GUILayout.BeginArea(new Rect(10, 10, 300, 300));
- if (!NetworkManager.Singleton.IsClient && !NetworkManager.Singleton.IsServer)
- {
- StartButtons();
- }
- else
- {
- StatusLabels();
-
- SubmitNewPosition();
- }
-
- GUILayout.EndArea();
- }
-
```
:::note
You will notice the introduction of a new method, `SubmitNewPosition()`; which we will be using later.
@@ -153,104 +52,38 @@ You will notice the introduction of a new method, `SubmitNewPosition()`; which
1. Open the `HelloWorldPlayer.cs` script.
1. Edit the `HelloWorldPlayer.cs` script to match the following.
-```csharp
-
-using MLAPI;
-using MLAPI.Messaging;
-using MLAPI.NetworkVariable;
-using UnityEngine;
-
-namespace HelloWorld
-{
- public class HelloWorldPlayer : NetworkBehaviour
- {
- public NetworkVariableVector3 Position = new NetworkVariableVector3(new NetworkVariableSettings
- {
- WritePermission = NetworkVariablePermission.ServerOnly,
- ReadPermission = NetworkVariablePermission.Everyone
- });
-
- public override void NetworkStart()
- {
- Move();
- }
-
- public void Move()
- {
- if (NetworkManager.Singleton.IsServer)
- {
- var randomPosition = GetRandomPositionOnPlane();
- transform.position = randomPosition;
- Position.Value = randomPosition;
- }
- else
- {
- SubmitPositionRequestServerRpc();
- }
- }
-
- [ServerRpc]
- void SubmitPositionRequestServerRpc(ServerRpcParams rpcParams = default)
- {
- Position.Value = GetRandomPositionOnPlane();
- }
-
- static Vector3 GetRandomPositionOnPlane()
- {
- return new Vector3(Random.Range(-3f, 3f), 1f, Random.Range(-3f, 3f));
- }
-
- void Update()
- {
- transform.position = Position.Value;
- }
- }
-}
+```csharp reference
+https://github.com/Unity-Technologies/com.unity.multiplayer.samples.poc/tree/feature/hello-world/Assets/Scripts/Shared/HelloWorldPlayer.cs
```
+
11. Select the Player Prefab.
1. Add the script `HelloWorldPlayer` script as a component.

This class will inherit from `NetworkBehaviour` instead of `MonoBehaviour`.
-
-
-```csharp
- public class HelloWorldPlayer : NetworkBehaviour
-
```
+
Inside this class we will define a `NetworkVariable` to represent this player's networked position.
-
-```csharp
- public NetworkVariableVector3 Position = new NetworkVariableVector3(new NetworkVariableSettings
- {
- WritePermission = NetworkVariablePermission.ServerOnly,
- ReadPermission = NetworkVariablePermission.Everyone
- });
-```
+
We introduce the concept of ownership on a `NetworkVariable` (read and write permissions). For the purposes of this demo, the server will be authoritative on the `NetworkVariable` representing position. All clients are able to read the value, however.
`HelloWorldPlayer` overrides `NetworkStart`.
-
-```csharp
- public override void NetworkStart()
- {
- Move();
- }
-```
+
+
Any `MonoBehaviour` implementing `NetworkBehaviour` can override the MLAPI method `NetworkStart()`. This method is fired when message handlers are ready to be registered and the networking is setup. We override `NetworkStart` since a client and a server will run different logic here.
:::note
@@ -258,111 +91,52 @@ This can be overriden on any `NetworkBehaviour`.
:::
On both client and server instances of this player, we call the `Move()` method, which will simply do the following.
-
-
-```csharp
- public void Move()
- {
- if (NetworkManager.Singleton.IsServer)
- {
- var randomPosition = GetRandomPositionOnPlane();
- transform.position = randomPosition;
- Position.Value = randomPosition;
- }
- else
- {
- SubmitPositionRequestServerRpc();
- }
- }
-```
+
+
+
If this player is a server-owned player, at `NetworkStart()` we can immediately move this player, as suggested in the following code.
-
-
-```csharp
- if (NetworkManager.Singleton.IsServer)
- {
- var randomPosition = GetRandomPositionOnPlane();
- transform.position = randomPosition;
- Position.Value = randomPosition;
- }
-```
+
If we are a client, we call a server RPC.
-
-```csharp
- else
- {
- SubmitPositionRequestServerRpc();
- }
-```
This server RPC simply sets the position `NetworkVariable` on the server's instance of this player by just picking a random point on the plane.
-
-```csharp
- [ServerRpc]
- void SubmitPositionRequestServerRpc(ServerRpcParams rpcParams = default)
- {
- Position.Value = GetRandomPositionOnPlane();
- }
-```
+
+
The server instance of this player has just modified the Position NetworkVariable, meaning that if we are a client, we need to apply this position locally inside of our Update loop.
-
-```csharp
- void Update()
- {
- transform.position = Position.Value;
- }
-```
+
+
We can now go back to `HelloWorldManager.cs` and define the contents of `SubmitNewPosition()`.
-
-```csharp
- static void SubmitNewPosition()
- {
- if (GUILayout.Button(NetworkManager.Singleton.IsServer ? "Move" : "Request Position Change"))
- {
- if (NetworkManager.Singleton.ConnectedClients.TryGetValue(NetworkManager.Singleton.LocalClientId,
- out var networkedClient))
- {
- var player = networkedClient.PlayerObject.GetComponent();
- if (player)
- {
- player.Move();
- }
- }
- }
- }
+
```
Whenever you press the GUI button (which is contextual depending on if you are server or a client), you find your local player and simply call `Move()`.