├── AzureFunctions.cs ├── Goodgulf ├── Emerald Integration │ ├── AIScript.cs │ ├── NavMeshAgentImposter.cs │ ├── Readme.md │ └── diff files │ │ ├── 1.2.0 │ │ ├── EmeraldDebugger-patchfile.txt │ │ ├── EmeraldMovement-patchfile.txt │ │ └── EmeraldSystem-patchfile.txt │ │ └── 1.2.5 │ │ ├── EmeraldDebugger.patch │ │ ├── EmeraldMovement.patch │ │ └── EmeraldSystem.patch ├── Instructions.md ├── Linux Server for Fishnet │ ├── Part 1 │ │ ├── Bootstrap.cs │ │ ├── BuildTargets.cs │ │ ├── ConnectClientLite.cs │ │ └── ServerHelper.cs │ ├── Part 2 │ │ └── Linux Setup Instructions.md │ └── Part 3 │ │ ├── AzureFunctions.cs │ │ ├── ServerData.cs │ │ ├── ServerDataRecord.cs │ │ └── UnityGameServer.cs ├── Part 1 │ └── PlayerScript.cs ├── Part 11 │ └── PlayerScript.cs ├── Part 13 │ ├── Bootstrap.cs │ ├── DontDestroy.cs │ └── SteamLobby.cs ├── Part 14 │ ├── HostGame.cs │ ├── PlayerScript.cs │ └── SteamLobby.cs ├── Part 15 │ ├── Cheat.cs │ ├── NetworkInputControl.cs │ ├── PlayerScript.cs │ ├── Simple cheat │ │ └── Cheat.cs │ ├── StartNetwork.cs │ └── ThirdPersonControllerCSP.cs ├── Part 16 │ ├── GamePlayers.cs │ ├── NetworkedDoor.cs │ ├── NetworkedTriggerDoorController.cs │ └── PlayerScript.cs ├── Part 17 │ ├── MatchChat.cs │ └── MatchChatLine.cs ├── Part 19 │ ├── MatchClientManager.cs │ └── MatchServerManager.cs ├── Part 2 │ ├── GameTimer.cs │ ├── NetworkManagerMyGame.cs │ ├── PlayerScript.cs │ ├── SpellStatic.cs │ └── Startup.cs ├── Part 3 │ ├── GameTimer.cs │ ├── Health.cs │ ├── NetworkManagerMyGame.cs │ ├── PlayerScript.cs │ ├── SpellStatic.cs │ └── Startup.cs ├── Part 4 │ ├── NetworkManagerUI.cs │ ├── NetworkRoomPlayerUI.cs │ └── RoomPlayerUI.cs ├── Part 5 │ ├── NetworkManagerUI.cs │ ├── NetworkRoomManagerUI.cs │ ├── NetworkRoomPlayerUI.cs │ ├── PlayerScript.cs │ └── RoomPlayerUI.cs ├── Part 6 │ ├── HostListItemUI.cs │ ├── NetworkListHosts.cs │ ├── NetworkRoomManagerUI_NLS.cs │ ├── NetworkRoomPlayerUI.cs │ ├── NodeListServerCommunicationManager.cs │ ├── PlayerScript.cs │ ├── RoomPlayerUI.cs │ └── Serializables.cs ├── Part 9 │ └── SteamLobby.cs ├── mir5a.png ├── mir5b.png ├── mir5c.png └── mirror-ui-standard-assets.unitypackage ├── LICENSE ├── README.md ├── ServerData.cs ├── ServerDataRecord.cs └── UnityGameServer.cs /Goodgulf/Emerald Integration/AIScript.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using FishNet; 4 | using FishNet.Managing; 5 | using FishNet.Object; 6 | using UnityEngine; 7 | using EmeraldAI; 8 | 9 | namespace Goodgulf.Networking 10 | { 11 | 12 | /* 13 | * This class is added to the networked prefab of your NPC. This acts in a similar manner as the playerscripts I created for earlier videos. 14 | * It basically disables the AI (and this the movement) on the client using a kill switch. Only when the code is run on a server will 15 | * the AI be enabled. 16 | */ 17 | 18 | public class AIScript : NetworkBehaviour 19 | { 20 | private EmeraldSystem _emeraldSystem; 21 | private NetworkManager _networkManager; 22 | 23 | private void Awake() 24 | { 25 | _networkManager = InstanceFinder.NetworkManager; 26 | _emeraldSystem = GetComponent(); 27 | 28 | EnableEmeraldAI(false); 29 | } 30 | 31 | public override void OnStartServer() 32 | { 33 | EnableEmeraldAI(true); 34 | 35 | } 36 | 37 | // This is the kill switch. Ideally this should be a method in the EmeraldSystem class 38 | // since this may change with every asset update. 39 | private void EnableEmeraldAI(bool state = false) 40 | { 41 | if (_emeraldSystem) 42 | { 43 | if(_emeraldSystem.MovementComponent) 44 | _emeraldSystem.MovementComponent.enabled = state; 45 | if(_emeraldSystem.AnimationComponent) 46 | _emeraldSystem.AnimationComponent.enabled = state; 47 | if(_emeraldSystem.SoundComponent) 48 | _emeraldSystem.SoundComponent.enabled = state; 49 | if(_emeraldSystem.DetectionComponent) 50 | _emeraldSystem.DetectionComponent.enabled = state; 51 | if(_emeraldSystem.BehaviorsComponent) 52 | _emeraldSystem.BehaviorsComponent.enabled = state; 53 | if(_emeraldSystem.CombatComponent) 54 | _emeraldSystem.CombatComponent.enabled = state; 55 | if(_emeraldSystem.HealthComponent) 56 | _emeraldSystem.HealthComponent.enabled = state; 57 | if(_emeraldSystem.OptimizationComponent) 58 | _emeraldSystem.OptimizationComponent.enabled = state; 59 | if(_emeraldSystem.EventsComponent) 60 | _emeraldSystem.EventsComponent.enabled = state; 61 | if(_emeraldSystem.DebuggerComponent) 62 | _emeraldSystem.DebuggerComponent.enabled = state; 63 | if(_emeraldSystem.UIComponent) 64 | _emeraldSystem.UIComponent.enabled = state; 65 | if(_emeraldSystem.ItemsComponent) 66 | _emeraldSystem.ItemsComponent.enabled = state; 67 | if(_emeraldSystem.SoundDetectorComponent) 68 | _emeraldSystem.SoundDetectorComponent.enabled = state; 69 | if(_emeraldSystem.InverseKinematicsComponent) 70 | _emeraldSystem.InverseKinematicsComponent.enabled = state; 71 | if(_emeraldSystem.TPMComponent) 72 | _emeraldSystem.TPMComponent.enabled = state; 73 | 74 | _emeraldSystem.enabled = state; 75 | 76 | } 77 | else Debug.LogError("AIScript.EnableEmeraldAI(): _emeraldSystem=null"); 78 | } 79 | 80 | 81 | 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /Goodgulf/Emerald Integration/NavMeshAgentImposter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | using Pathfinding; 6 | using UnityEngine.AI; 7 | using static UnityEngine.GraphicsBuffer; 8 | 9 | namespace Pathfinding 10 | { 11 | 12 | 13 | 14 | public class NavMeshAgentImposter : AIPath 15 | { 16 | private bool _autoBraking; 17 | 18 | // private bool _isOnNavMesh = true; 19 | private bool _isGridGraph = true; 20 | private int _debugLevel = 0; 21 | 22 | public int areaMask; // Todo: assign some meaningful value 23 | 24 | public int debugLevel 25 | { 26 | get => _debugLevel; 27 | } 28 | 29 | public float stoppingDistance 30 | { 31 | get => endReachedDistance; 32 | set 33 | { 34 | if (_debugLevel > 1) 35 | { 36 | Debug.Log($"NavMeshAgentImposter.stoppingDistance={value}"); 37 | } 38 | else if (_debugLevel > 0) 39 | { 40 | if (value < endReachedDistance) 41 | Debug.Log($"NavMeshAgentImposter.stoppingDistance reduced to={value}"); 42 | } 43 | 44 | endReachedDistance = value; 45 | } 46 | } 47 | 48 | public float speed 49 | { 50 | get => maxSpeed; 51 | set 52 | { 53 | if (_debugLevel > 1) 54 | { 55 | Debug.Log($"NavMeshAgentImposter.speed={value}"); 56 | } 57 | else if (_debugLevel > 0) 58 | { 59 | if (value < speed) 60 | Debug.Log($"NavMeshAgentImposter.speed reduced to={value}"); 61 | } 62 | 63 | maxSpeed = value; 64 | } 65 | } 66 | 67 | /* Not tested, comment out this block if you derive NaMeshAgentImposter from RichAI instead of AIPath 68 | 69 | public float maxAcceleration 70 | { 71 | get => acceleration; 72 | set 73 | { 74 | if (_debugLevel > 1) 75 | { 76 | Debug.Log($"NavMeshAgentImposter.acceleration={value}"); 77 | } 78 | else if (_debugLevel > 0) 79 | { 80 | if (value < acceleration) 81 | Debug.Log($"NavMeshAgentImposter.acceleration reduced to={value}"); 82 | else 83 | Debug.Log($"NavMeshAgentImposter.acceleration increased to={value}"); 84 | } 85 | 86 | acceleration = value; 87 | } 88 | } 89 | */ 90 | 91 | public bool autoBraking 92 | { 93 | get => _autoBraking; 94 | set 95 | { 96 | if (_debugLevel > 1) 97 | { 98 | Debug.Log($"NavMeshAgentImposter.autoBraking={value}"); 99 | } 100 | else if (_debugLevel > 0) 101 | { 102 | if (value != _autoBraking) 103 | Debug.Log($"NavMeshAgentImposter.autoBraking changed to={value}"); 104 | } 105 | 106 | _autoBraking = value; 107 | if (value) 108 | { 109 | whenCloseToDestination = CloseToDestinationMode.Stop; 110 | } 111 | else 112 | { 113 | whenCloseToDestination = CloseToDestinationMode.ContinueToExactDestination; 114 | } 115 | } 116 | } 117 | 118 | // https://forum.arongranberg.com/t/unity-navmesh-to-a-conversion/7888 119 | 120 | public bool isOnOffMeshLink 121 | { 122 | get => IsOnOffMeshLink(); 123 | } 124 | 125 | 126 | public bool IsOnOffMeshLink() 127 | { 128 | if (_isGridGraph) 129 | { 130 | return false; 131 | } 132 | else 133 | { 134 | // This can only be used if you derive the imposter from RichAI instead of AIPath: 135 | // return traversingOffMeshLink; 136 | return false; 137 | } 138 | } 139 | 140 | public bool isOnNavMesh 141 | { 142 | get => IsOnNavMesh(); 143 | } 144 | 145 | // https://forum.arongranberg.com/t/how-to-check-the-destination-can-be-reached/5554 146 | 147 | public bool IsOnNavMesh() 148 | { 149 | if (_debugLevel > 1) 150 | Debug.Log($"NavMeshAgentImposter.IsOnNavMesh() called with isGridGraph={_isGridGraph}"); 151 | 152 | if (_isGridGraph) 153 | { 154 | return AstarPath.active.GetNearest(transform.position, NNConstraint.None).node.Walkable; 155 | } 156 | else 157 | { 158 | var node = AstarPath.active.data.recastGraph.PointOnNavmesh(transform.position, NNConstraint.Walkable); 159 | if (node != null) 160 | { 161 | return true; 162 | } 163 | 164 | return false; 165 | } 166 | } 167 | 168 | 169 | public void SetGraphMode(bool isGridGraph = true) 170 | { 171 | if (_debugLevel > 0) 172 | Debug.Log($"NavMeshAgentImposter.SetGraphMode(): set GraphMode to {isGridGraph}"); 173 | 174 | _isGridGraph = isGridGraph; 175 | } 176 | 177 | public void ResetPath() 178 | { 179 | //canSearch = false; 180 | SetPath(null); 181 | if (_debugLevel > 0) 182 | Debug.Log($"NavMeshAgentImposter.ResetPath() called"); 183 | } 184 | 185 | public bool SetDestination(Vector3 target) 186 | { 187 | canSearch = true; 188 | destination = target; 189 | 190 | if (_debugLevel > 0) 191 | Debug.Log($"NavMeshAgentImposter.SetDestination(): target = {target}"); 192 | 193 | return true; 194 | } 195 | 196 | public void SetDebugLevel(int level) 197 | { 198 | Debug.Log($"NavMeshAgentImposter.SetDebugLevel(): set debug level to {level}"); 199 | _debugLevel = level; 200 | } 201 | 202 | public bool Warp(Vector3 newPosition) 203 | { 204 | if (_debugLevel > 0) 205 | Debug.Log($"NavMeshAgentImposter.Warp(): target = {newPosition}"); 206 | 207 | Teleport(newPosition); 208 | return true; 209 | } 210 | 211 | public void Initialize() 212 | { 213 | if (_debugLevel > 0) 214 | Debug.Log($"NavMeshAgentImposter.Initialize(): setting masks"); 215 | 216 | // Change these masks is you use a layer for your ground/navigation grid other than Default 217 | areaMask = LayerMask.GetMask("Default"); 218 | groundMask = LayerMask.GetMask("Default"); 219 | } 220 | 221 | public void SetMasks(int _areaMask, int _groundMask) 222 | { 223 | if (_debugLevel > 0) 224 | Debug.Log( 225 | $"NavMeshAgentImposter.SetMasks(): areaMask={_areaMask}, groundMask{_groundMask}"); 226 | 227 | areaMask = _areaMask; 228 | groundMask = _groundMask; 229 | } 230 | 231 | public void SetDestinationWithWaypoint(Vector3 waypoint) 232 | { 233 | if (_debugLevel > 0) 234 | Debug.Log( 235 | $"NavMeshAgentImposter.SetDestinationWithWaypoint(): waypoint = {waypoint}"); 236 | 237 | // Add a small offset to the Y coord. 238 | Vector3 _destination = new Vector3(waypoint.x, waypoint.y + 0.1f, waypoint.z); 239 | destination = _destination; 240 | } 241 | } 242 | 243 | } -------------------------------------------------------------------------------- /Goodgulf/Emerald Integration/Readme.md: -------------------------------------------------------------------------------- 1 | # Instructions for integrating A Star Pathfinding Project into Emerald 2024 2 | 3 | ## Pre-requisites 4 | 5 | * Import Emerald 2024 into your Unity project. 6 | * Import AStar Pathfining Project into your Unity project. 7 | 8 | 9 | ## Patch the files 10 | 11 | Copy the following files from the project to a temporary folder: 12 | * EmeraldSystem.cs 13 | * EmeraldMovement.cs 14 | * EmeraldDebugger.cs 15 | 16 | Then download the diff files for the right version of Emerald from this Github repo. 17 | If you are running Unity on a Windows machine you can get diff/patch binaries easily (use Google). 18 | Instructions on how to patch the files can be found here [Patch and Diff](https://www.pair.com/support/kb/paircloud-diff-and-patch/) or just Google it. 19 | 20 | Patch the three above list files and copy them back into your project. Also include my NavMeshAgentImposter.cs. 21 | 22 | Create an empty game object in your scene and add the AStarPathfinding component. Add a grid graph and set it up properly. 23 | 24 | Add an ASTAR scripting define symbol to the project settings and all should work fine. 25 | 26 | 27 | ## Important notes 28 | 29 | I have updated the files for two versions of Emerald 2024. These diff files are pretty much readable so it should be possible to patch future versions manually. 30 | I will not be keeping track of updates to Emerald 2024 and update these regularly. I have sent a message to the devs from Black Horizon Studios and offered the integration for free. 31 | If they include it in their code it will be much easier to keep it up to date. To date they have not responded or shown any interest. -------------------------------------------------------------------------------- /Goodgulf/Emerald Integration/diff files/1.2.0/EmeraldDebugger-patchfile.txt: -------------------------------------------------------------------------------- 1 | --- D:\Emerald\original\EmeraldDebugger.cs Sat Jul 20 00:28:59 2024 2 | +++ EmeraldDebugger.cs Thu Oct 17 11:52:18 2024 3 | @@ -1,4 +1,11 @@ 4 | using UnityEngine; 5 | +using UnityEngine.AI; 6 | + 7 | +#if ASTAR 8 | +using Pathfinding; // DKE: Referencing the AStarPathfinding code. If necessary also update your code assemblies. 9 | +using System.Collections.Generic; // DKE: Needed for the List<> class in method DrawNavMeshPathInternal() 10 | +#endif 11 | + 12 | 13 | namespace EmeraldAI 14 | { 15 | @@ -172,11 +179,24 @@ 16 | { 17 | if (EnableDebuggingTools == YesOrNo.No || DrawNavMeshPath == YesOrNo.No) return; 18 | 19 | +#if ASTAR 20 | + // DKE: rewritten for AStarPathfinding, see: https://www.arongranberg.com/astar/documentation/5_2_4_67ce3f038/aipath/getremainingpath.html#GetRemainingPath 21 | + 22 | + List buffer = new List(); 23 | + 24 | + EmeraldComponent.m_NavMeshAgent.GetRemainingPath(buffer, out bool stale); 25 | + for (int i = 0; i < buffer.Count - 1; i++) 26 | + { 27 | + Debug.DrawLine(buffer[i], buffer[i+1], NavMeshPathColor); 28 | + } 29 | + 30 | +#else 31 | for (int i = 0; i < EmeraldComponent.m_NavMeshAgent.path.corners.Length; i++) 32 | { 33 | if (i > 0) Debug.DrawLine(EmeraldComponent.m_NavMeshAgent.path.corners[i - 1] + Vector3.up * 0.5f, EmeraldComponent.m_NavMeshAgent.path.corners[i] + Vector3.up * 0.5f, NavMeshPathColor); 34 | else Debug.DrawLine(EmeraldComponent.m_NavMeshAgent.path.corners[0] + Vector3.up * 0.5f, EmeraldComponent.m_NavMeshAgent.path.corners[i] + Vector3.up * 0.5f, NavMeshPathColor); 35 | } 36 | +#endif 37 | } 38 | 39 | /// 40 | -------------------------------------------------------------------------------- /Goodgulf/Emerald Integration/diff files/1.2.0/EmeraldSystem-patchfile.txt: -------------------------------------------------------------------------------- 1 | --- D:\Emerald\original\EmeraldSystem.cs Sat Jul 20 00:26:54 2024 2 | +++ EmeraldSystem.cs Wed Oct 16 23:25:57 2024 3 | @@ -3,6 +3,9 @@ 4 | using UnityEngine.AI; 5 | using EmeraldAI.Utility; 6 | using EmeraldAI.SoundDetection; 7 | +#if ASTAR 8 | +using Pathfinding; // DKE: Referencing the AStarPathfinding code. If necessary also update your code assemblies. 9 | +#endif 10 | 11 | namespace EmeraldAI 12 | { 13 | @@ -43,12 +46,16 @@ 14 | #region Internal Components 15 | public static GameObject ObjectPool; 16 | public static GameObject CombatTextSystemObject; 17 | +#if ASTAR 18 | + [HideInInspector] public NavMeshAgentImposter m_NavMeshAgent; // DKE 19 | +#else 20 | [HideInInspector] public NavMeshAgent m_NavMeshAgent; 21 | +#endif 22 | [HideInInspector] public BoxCollider AIBoxCollider; 23 | [HideInInspector] public Animator AIAnimator; 24 | [HideInInspector] public float TimeSinceEnabled; 25 | #endregion 26 | - 27 | + 28 | #region AI Components 29 | [HideInInspector] public EmeraldDetection DetectionComponent; 30 | [HideInInspector] public EmeraldBehaviors BehaviorsComponent; 31 | @@ -86,7 +93,19 @@ 32 | SoundDetectorComponent = GetComponent(); 33 | InverseKinematicsComponent = GetComponent(); 34 | TPMComponent = GetComponent(); 35 | +#if ASTAR 36 | + m_NavMeshAgent = GetComponent(); // DKE 37 | + if (m_NavMeshAgent == null) 38 | + { 39 | + Debug.LogWarning($"EmeraldSystem.Awake(): No AIPath(derived) component found on this GameObject {this.name}"); 40 | + m_NavMeshAgent = gameObject.AddComponent(); 41 | + //m_NavMeshAgent.areaMask = LayerMask.GetMask("Default"); 42 | + //m_NavMeshAgent.groundMask = LayerMask.GetMask("Default"); 43 | + m_NavMeshAgent.Initialize(); 44 | + } 45 | +#else 46 | m_NavMeshAgent = GetComponent(); 47 | +#endif 48 | AIBoxCollider = GetComponent(); 49 | AIAnimator = GetComponent(); 50 | InitializeEmeraldObjectPool(); 51 | -------------------------------------------------------------------------------- /Goodgulf/Emerald Integration/diff files/1.2.5/EmeraldDebugger.patch: -------------------------------------------------------------------------------- 1 | --- d:\emerald\original\emeralddebugger.cs Wed Oct 30 21:10:34 2024 2 | +++ emeralddebugger.cs Sun Dec 01 19:27:45 2024 3 | @@ -1,5 +1,10 @@ 4 | using UnityEngine; 5 | 6 | +#if ASTAR 7 | +using Pathfinding; // DKE: Referencing the AStarPathfinding code. If necessary also update your code assemblies. 8 | +using System.Collections.Generic; // DKE: Needed for the List<> class in method DrawNavMeshPathInternal() 9 | +#endif 10 | + 11 | namespace EmeraldAI 12 | { 13 | [HelpURL("https://black-horizon-studios.gitbook.io/emerald-ai-wiki/emerald-components-optional/debugger-component")] 14 | @@ -172,11 +177,24 @@ 15 | { 16 | if (EnableDebuggingTools == YesOrNo.No || DrawNavMeshPath == YesOrNo.No) return; 17 | 18 | +#if ASTAR 19 | + // DKE: rewritten for AStarPathfinding, see: https://www.arongranberg.com/astar/documentation/5_2_4_67ce3f038/aipath/getremainingpath.html#GetRemainingPath 20 | + 21 | + List buffer = new List(); 22 | + 23 | + EmeraldComponent.m_NavMeshAgent.GetRemainingPath(buffer, out bool stale); 24 | + for (int i = 0; i < buffer.Count - 1; i++) 25 | + { 26 | + Debug.DrawLine(buffer[i], buffer[i+1], NavMeshPathColor); 27 | + } 28 | +#else 29 | for (int i = 0; i < EmeraldComponent.m_NavMeshAgent.path.corners.Length; i++) 30 | { 31 | if (i > 0) Debug.DrawLine(EmeraldComponent.m_NavMeshAgent.path.corners[i - 1] + Vector3.up * 0.5f, EmeraldComponent.m_NavMeshAgent.path.corners[i] + Vector3.up * 0.5f, NavMeshPathColor); 32 | else Debug.DrawLine(EmeraldComponent.m_NavMeshAgent.path.corners[0] + Vector3.up * 0.5f, EmeraldComponent.m_NavMeshAgent.path.corners[i] + Vector3.up * 0.5f, NavMeshPathColor); 33 | } 34 | + 35 | +#endif 36 | } 37 | 38 | /// 39 | -------------------------------------------------------------------------------- /Goodgulf/Emerald Integration/diff files/1.2.5/EmeraldSystem.patch: -------------------------------------------------------------------------------- 1 | --- d:\emerald\original\emeraldsystem.cs Wed Oct 30 21:08:22 2024 2 | +++ emeraldsystem.cs Sun Dec 01 19:29:46 2024 3 | @@ -3,6 +3,9 @@ 4 | using UnityEngine.AI; 5 | using EmeraldAI.Utility; 6 | using EmeraldAI.SoundDetection; 7 | +#if ASTAR 8 | +using Pathfinding; // DKE: Referencing the AStarPathfinding code. If necessary also update your code assemblies. 9 | +#endif 10 | 11 | namespace EmeraldAI 12 | { 13 | @@ -43,7 +46,11 @@ 14 | #region Internal Components 15 | public static GameObject ObjectPool; 16 | public static GameObject CombatTextSystemObject; 17 | +#if ASTAR 18 | + [HideInInspector] public NavMeshAgentImposter m_NavMeshAgent; // DKE 19 | +#else 20 | [HideInInspector] public NavMeshAgent m_NavMeshAgent; 21 | +#endif 22 | [HideInInspector] public BoxCollider AIBoxCollider; 23 | [HideInInspector] public Animator AIAnimator; 24 | [HideInInspector] public float TimeSinceEnabled; 25 | @@ -86,7 +93,19 @@ 26 | SoundDetectorComponent = GetComponent(); 27 | InverseKinematicsComponent = GetComponent(); 28 | TPMComponent = GetComponent(); 29 | +#if ASTAR 30 | + m_NavMeshAgent = GetComponent(); // DKE 31 | + if (m_NavMeshAgent == null) 32 | + { 33 | + Debug.LogWarning($"EmeraldSystem.Awake(): No AIPath(derived) component found on this GameObject {this.name}"); 34 | + m_NavMeshAgent = gameObject.AddComponent(); 35 | + //m_NavMeshAgent.areaMask = LayerMask.GetMask("Default"); 36 | + //m_NavMeshAgent.groundMask = LayerMask.GetMask("Default"); 37 | + m_NavMeshAgent.Initialize(); 38 | + } 39 | +#else 40 | m_NavMeshAgent = GetComponent(); 41 | +#endif 42 | AIBoxCollider = GetComponent(); 43 | AIAnimator = GetComponent(); 44 | InitializeEmeraldObjectPool(); 45 | -------------------------------------------------------------------------------- /Goodgulf/Instructions.md: -------------------------------------------------------------------------------- 1 | # Instructions for the Part 5 unitypackage 2 | 3 | ## Basic steps 4 | 5 | If you want to import the mirror-ui-standard-assets package into your project then please follow these steps: 6 | 7 | 1. Create a new project in Unity 2020LTS, import the Mirror package. 8 | 2. Import the [Unity starter asset](https://assetstore.unity.com/packages/essentials/starter-assets-third-person-character-controller-196526) and enable the backends when asked. 9 | 3. Import the package "mirror-ui-standard-assets.unitypackage" from my Github page. 10 | 4. Go to the StarterAssets>ThirdpersonController>Prefabs>PlayerArmature prefab in the starter asset folder and add the PlayerScript to it. 11 | 5. You’ll need to configure client authority on the network transform and animator components and link the prefab to the animator property. 12 | 6. Add the Offline, Room and Playground scenes to the build settings and you’re ready to go. 13 | 14 | 15 | ![Network Transform](https://github.com/Goodgulf281/Unity-Mirror-Helper-Scripts/blob/main/Goodgulf/mir5a.png) 16 | 17 | ![Network Animator](https://github.com/Goodgulf281/Unity-Mirror-Helper-Scripts/blob/main/Goodgulf/mir5b.png) 18 | 19 | 20 | 21 | ## Errors 22 | 23 | If you open the Offline scene before completing step 4 then make sure to add the PlayerArmature prefab again as the Player Prefab in the NetworkRoomManager. 24 | 25 | ![Network Room Manager UI](https://github.com/Goodgulf281/Unity-Mirror-Helper-Scripts/blob/main/Goodgulf/mir5c.png) 26 | -------------------------------------------------------------------------------- /Goodgulf/Linux Server for Fishnet/Part 1/Bootstrap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Goodgulf.Azure; 5 | using TMPro; 6 | using UnityEngine; 7 | using UnityEngine.SceneManagement; 8 | using UnityEngine.Serialization; 9 | 10 | namespace Goodgulf.SceneWorkflow 11 | { 12 | // This script starts the game workflow: 13 | // If it is running as a server, do nothing and let the NetworkManager start the show using the StartOnHeadless switch. 14 | // If it is running as a client, do some actions then move to the connection scene, see Start() 15 | 16 | public class Bootstrap : MonoBehaviour 17 | { 18 | public static Bootstrap Instance { get; private set; } 19 | 20 | public TMP_Text _statusLine; 21 | 22 | public float _waitingTime = 2.0f; 23 | public int _sceneToLoad = 1; 24 | 25 | private float timer = 0f; 26 | private bool timerSwitch = true; 27 | 28 | 29 | private void Awake() 30 | { 31 | Debug.Log("Bootstrap.Awake(): method called"); 32 | 33 | if (Instance != null && Instance != this) 34 | { 35 | Destroy(this); 36 | return; 37 | } 38 | Instance = this; 39 | 40 | } 41 | 42 | public void StartServersCallBack(bool success, string fromAzure) 43 | { 44 | Debug.Log($"Bootstrap:StartServersCallBack(): registered {success} with response {fromAzure}"); 45 | 46 | if (success) 47 | { 48 | timerSwitch = true; 49 | 50 | if (_statusLine) 51 | _statusLine.text = "Portals to worlds are now open"; 52 | 53 | } 54 | else 55 | { 56 | Debug.LogError("Bootstrap:StartServersCallBack(): servers did not start"); 57 | } 58 | } 59 | void Start() 60 | { 61 | #if UNITY_SERVER 62 | timerSwitch = false; 63 | Debug.Log("Bootstrap.Start(): server build detected, switching off bootstrap timer"); 64 | #else 65 | Debug.Log("Bootstrap.Start(): client build"); 66 | 67 | if (_statusLine) 68 | _statusLine.text = "Opening portals to worlds..."; 69 | 70 | // Here I use an Azure Function to do a Text to Speech action: 71 | AzureFunctions az = AzureFunctions.Instance; 72 | az.Speak("Client started").Forget(); 73 | 74 | // Now call an Azure Function to startup all servers hosting the maps (if they are not already running): 75 | 76 | // Make sure the timer is not running to load the connection screen. 77 | // Once this Azure Function reports back in the StartServesCallback we'll start the timer which runs in the Update() method. 78 | timerSwitch = false; 79 | az.StartServersWait(StartServersCallBack).Forget(); 80 | 81 | #endif 82 | } 83 | 84 | void Update() 85 | { 86 | if (!timerSwitch) 87 | return; 88 | 89 | timer += Time.deltaTime; 90 | 91 | if (timer > _waitingTime) 92 | { 93 | timerSwitch = false; 94 | LoadNextScene(); 95 | } 96 | } 97 | 98 | public void LoadNextScene() 99 | { 100 | SceneManager.LoadScene(_sceneToLoad); 101 | } 102 | 103 | 104 | 105 | 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Goodgulf/Linux Server for Fishnet/Part 1/BuildTargets.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | using UnityEditor; 5 | using UnityEditor.Build; 6 | using UnityEditor.Build.Reporting; 7 | using UnityEngine; 8 | 9 | namespace Goodgulf.Editor 10 | { 11 | 12 | 13 | 14 | public class BuildTargets : IActiveBuildTargetChanged 15 | { 16 | 17 | [MenuItem("Build/All Goodgulf Builds")] 18 | public static void MyBuild() 19 | { 20 | CreateWindowsServerBuild(); 21 | CreateLinuxServerBuild(); 22 | CreateWindowsBuild(); 23 | } 24 | 25 | [MenuItem("Build/Goodgulf Linux Server Build")] 26 | public static void MyLinuxServerBuild() 27 | { 28 | CreateLinuxServerBuild(); 29 | } 30 | 31 | [MenuItem("Build/Goodgulf Windows Server Build")] 32 | public static void MyWindowsServerBuild() 33 | { 34 | CreateWindowsServerBuild(); 35 | } 36 | 37 | [MenuItem("Build/Goodgulf Windows Standalone Build")] 38 | public static void MyWindowsBuild() 39 | { 40 | CreateWindowsBuild(); 41 | } 42 | 43 | 44 | private static List RequiredScenes() 45 | { 46 | // I'm using a scriptable object which contains all the scenes we need to put into the builds. 47 | // This way I can update it in one spot instead of each method below. 48 | 49 | ScenesRequiredInBuildSO scenesRequiredInBuildSo = Resources.Load("buildScenesSO"); 50 | 51 | List result = new List(); 52 | 53 | foreach (var requiredScene in scenesRequiredInBuildSo.requiredScenes) 54 | { 55 | result.Add("Assets/Scenes/"+requiredScene.name+".unity"); 56 | Debug.Log("Adding scene "+requiredScene.name+" to build list"); 57 | } 58 | 59 | return result; 60 | } 61 | 62 | // This function was intended to switch back to teh default Windows Standalone client build after each build action. 63 | // Unfortunately that is not possible yet using Unity code so always check the build target after running a series of builds. 64 | 65 | private static void ResetBuildTarget() 66 | { 67 | /* 68 | BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions(); 69 | buildPlayerOptions.scenes = new[] { "Assets/Scenes/Bootstrap.unity", "Assets/Scenes/ServerSelect.unity", "Assets/Scenes/Island.unity" }; 70 | buildPlayerOptions.locationPathName = "D:/Build/Wiba3-startV2/WiBa3-startV2.exe"; 71 | buildPlayerOptions.target = BuildTarget.StandaloneWindows64; 72 | 73 | buildPlayerOptions.options = BuildOptions.DetailedBuildReport; 74 | */ 75 | 76 | bool result = EditorUserBuildSettings.SwitchActiveBuildTargetAsync(BuildTargetGroup.Standalone, 77 | BuildTarget.StandaloneWindows); 78 | 79 | } 80 | 81 | public int callbackOrder { get { return 0; } } 82 | 83 | public void OnActiveBuildTargetChanged(BuildTarget previousTarget, BuildTarget newTarget) 84 | { 85 | Debug.Log("Active platform changed from " + previousTarget + " to " + newTarget); 86 | } 87 | 88 | 89 | 90 | private static void CreateWindowsBuild() 91 | { 92 | Debug.Log("Creating Windows Client Build"); 93 | 94 | BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions(); 95 | //buildPlayerOptions.scenes = new[] 96 | // {"Assets/Scenes/Bootstrap.unity", "Assets/Scenes/ServerSelect.unity", "Assets/Scenes/Island.unity"}; 97 | 98 | buildPlayerOptions.scenes = RequiredScenes().ToArray(); 99 | 100 | buildPlayerOptions.locationPathName = "D:/Build/Wiba4-client/WiBa4-client.exe"; 101 | buildPlayerOptions.target = BuildTarget.StandaloneWindows64; 102 | 103 | buildPlayerOptions.options = BuildOptions.DetailedBuildReport; 104 | buildPlayerOptions.subtarget = (int) StandaloneBuildSubtarget.Player; 105 | 106 | BuildReport buildReport = BuildPipeline.BuildPlayer(buildPlayerOptions); 107 | PostBuild(buildReport); 108 | } 109 | 110 | private static void CreateWindowsServerBuild() 111 | { 112 | BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions(); 113 | //buildPlayerOptions.scenes = new[] 114 | // {"Assets/Scenes/Bootstrap.unity", "Assets/Scenes/ServerSelect.unity", "Assets/Scenes/Island.unity"}; 115 | 116 | buildPlayerOptions.scenes = RequiredScenes().ToArray(); 117 | 118 | buildPlayerOptions.locationPathName = "D:/Build/Wiba4-windows-server/WiBa4-server.exe"; 119 | buildPlayerOptions.target = BuildTarget.StandaloneWindows64; 120 | buildPlayerOptions.subtarget = (int) StandaloneBuildSubtarget.Server; 121 | buildPlayerOptions.options = BuildOptions.Development; 122 | 123 | BuildReport buildReport = BuildPipeline.BuildPlayer(buildPlayerOptions); 124 | PostBuild(buildReport); 125 | //ResetBuildTarget(); 126 | 127 | } 128 | 129 | private static void CreateLinuxServerBuild() 130 | { 131 | BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions(); 132 | //buildPlayerOptions.scenes = new[] 133 | // {"Assets/Scenes/Bootstrap.unity", "Assets/Scenes/ServerSelect.unity", "Assets/Scenes/Island.unity"}; 134 | 135 | buildPlayerOptions.scenes = RequiredScenes().ToArray(); 136 | 137 | 138 | buildPlayerOptions.locationPathName = "D:/Build/Wiba4-linux-server/WiBa4.x86_64"; 139 | buildPlayerOptions.target = BuildTarget.StandaloneLinux64; 140 | buildPlayerOptions.subtarget = (int) StandaloneBuildSubtarget.Server; 141 | buildPlayerOptions.options = BuildOptions.Development; 142 | 143 | BuildReport buildReport = BuildPipeline.BuildPlayer(buildPlayerOptions); 144 | PostBuild(buildReport); 145 | //ResetBuildTarget(); 146 | } 147 | 148 | private static void PostBuild(BuildReport report) 149 | { 150 | BuildSummary summary = report.summary; 151 | 152 | if (summary.result == BuildResult.Succeeded) 153 | { 154 | Debug.Log("Build succeeded"); 155 | } 156 | 157 | if (summary.result == BuildResult.Failed) 158 | { 159 | Debug.Log("Build failed"); 160 | } 161 | } 162 | 163 | } 164 | 165 | 166 | } -------------------------------------------------------------------------------- /Goodgulf/Linux Server for Fishnet/Part 1/ConnectClientLite.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using FishNet; 6 | using FishNet.Managing; 7 | using Goodgulf.Azure; 8 | using TMPro; 9 | 10 | namespace Goodgulf.Networking 11 | { 12 | // This is a (very) simple script in order for us to connect to a server from the client. See Start(). 13 | public class ConnectClientLite : MonoBehaviour 14 | { 15 | 16 | public TMP_InputField _ipAddress; 17 | 18 | public void ServerListCallBack(bool success, ServerDataList listOfServerData) 19 | { 20 | // This is the callback called when the ServeList data is returned from the Azure Function. 21 | // This is a but messy and only shows one of the servers and needs to be updated. 22 | 23 | if (success) 24 | { 25 | foreach (ServerData server in listOfServerData.data) 26 | { 27 | Debug.Log($"Found a server running on {server.ipAddress}:{server.port} with scene {server.scene}"); 28 | _ipAddress.text = server.ipAddress; 29 | } 30 | } 31 | else Debug.LogError("ConnectClientLite.ServerListCallBack(): could not retrieve server list."); 32 | } 33 | 34 | public void Start() 35 | { 36 | // Get a list of all servers which are running. In the previous scene we used an Azure Function to wake up all servers so 37 | // by now they should all be running and have registered themselves (as in ServerHelper.cs). 38 | 39 | AzureFunctions az = AzureFunctions.Instance; 40 | az.ServerList(ServerListCallBack).Forget(); 41 | } 42 | 43 | public void Connect() 44 | { 45 | #if UNITY_SERVER 46 | Debug.LogError("ConnectClientLite.Connect(): client connect from server attempted"); 47 | #else 48 | NetworkManager networkManager = InstanceFinder.NetworkManager; 49 | 50 | Debug.Log("ConnectClientLite.Connect(): attempting connect to ["+_ipAddress.text+"]"); 51 | networkManager.ClientManager.StartConnection(_ipAddress.text); 52 | 53 | #endif 54 | 55 | 56 | } 57 | 58 | public void StopServersCallBack(bool success, string fromAzure) 59 | { 60 | Debug.Log($"ConnectClientLite:StartServersCallBack(): stopped {success} with response {fromAzure}"); 61 | } 62 | 63 | public void ShutdownServers() 64 | { 65 | #if UNITY_SERVER 66 | Debug.LogError("ConnectClientLite.ShutdownServers(): server shutdown from server attempted"); 67 | #else 68 | AzureFunctions az = AzureFunctions.Instance; 69 | az.ForceShutdownServers(StopServersCallBack).Forget(); 70 | 71 | #endif 72 | } 73 | 74 | public void PingServer() 75 | { 76 | #if UNITY_SERVER 77 | Debug.LogError("ConnectClientLite.PingServer(): server ping from server attempted"); 78 | #else 79 | string target = _ipAddress.text; 80 | 81 | StartCoroutine(StartPing(target)); 82 | #endif 83 | } 84 | 85 | 86 | IEnumerator StartPing(string ip) 87 | { 88 | WaitForSeconds f = new WaitForSeconds(0.05f); 89 | Ping p = new Ping(ip); 90 | while (p.isDone == false) 91 | { 92 | yield return f; 93 | } 94 | PingFinished(p); 95 | } 96 | 97 | 98 | public void PingFinished(Ping p) 99 | { 100 | Debug.Log($"Ping: {p.time}"); 101 | } 102 | 103 | 104 | } 105 | 106 | 107 | } -------------------------------------------------------------------------------- /Goodgulf/Linux Server for Fishnet/Part 2/Linux Setup Instructions.md: -------------------------------------------------------------------------------- 1 | # Unity Networking - Using a Linux Server in the Cloud for Hosting a Game - Part 2 2 | 3 | ## Updating Linux and Firewall Setup 4 | 5 | Once you logged on to the Linux server using Putty use these commands to update Ubuntu: 6 | 7 | ``` 8 | sudo apt update 9 | sudo apt upgrade 10 | ``` 11 | 12 | Enable the Firewall using these commands: 13 | 14 | ``` 15 | sudo ufw enable 16 | sudo ufw allow 22 17 | sudo ufw allow 7770/udp 18 | 19 | ``` 20 | 21 | Note that port 22 is used for SSH and 7770 is the default listening port for Fishnet networking. If you change the port in your NetworkManager you also need to change it here. 22 | The same goes for adding multiple game servers on a single VM, add the ports you'll be adding in the startup scripts (below) here too. 23 | 24 | ## Uploading a build to your server 25 | 26 | If you already uploaded a build to your server before then stop the startup service first using: 27 | 28 | ``` 29 | sudo systemctl stop wiba4.service 30 | ``` 31 | 32 | Where "wiba4.service" is the name of the startup service you use (see below). 33 | Then copy the files and make the main game executable using: 34 | 35 | ``` 36 | chmod +x WiBa4.x86_64 37 | ``` 38 | 39 | Where "WiBa4.x86_64" is the filename Unity created for my build. 40 | 41 | 42 | ## Create the startup service file 43 | 44 | First start the Nano editor: 45 | 46 | ``` 47 | sudo nano /etc/systemd/system/wiba4.service 48 | ``` 49 | 50 | You can replace name "wiba4" with your own game's name here. Then write (or copy using right mouse click) the following contents into the file: 51 | 52 | ``` 53 | [Unit] 54 | Description=Wiba4 Auto Start 55 | After=network.target 56 | 57 | [Service] 58 | ExecStart=/home/admindude/run-server.sh Island 7770 2 59 | Restart=always 60 | User=admindude 61 | Group=adm 62 | StandardOutput=append:/var/log/wiba4.log 63 | StandardError=append:/var/log/wiba4error.log 64 | 65 | [Install] 66 | WantedBy=multi-user.target 67 | 68 | ``` 69 | Here you'll want to make another few replacements: change the name of your game and change the name of your admin user in both the User and ExecStart path properties. 70 | 71 | Once the service script is ready you can enable it by using these commands: 72 | ``` 73 | sudo systemctl daemon-reload 74 | sudo systemctl start wiba4.service 75 | sudo systemctl enable wiba4.service 76 | 77 | ``` 78 | 79 | You can check the status of the service with this command: 80 | ``` 81 | sudo systemctl status wiba4.service 82 | ``` 83 | 84 | ...and check the logs using the tail command: 85 | 86 | ``` 87 | tail -f /var/log/wiba4.log 88 | ``` 89 | 90 | ## The run-server script 91 | 92 | The last element to be finished is creating the bash script run-server.sh. It is called from the service file and starts the game executable with some command line parameters: 93 | 94 | ``` 95 | #!/bin/bash 96 | 97 | serverip=$( curl https://ipinfo.io/ip ) 98 | echo Server IP=$serverip 99 | 100 | scene=$1 101 | port=$2 102 | serverid=$3 103 | 104 | echo Scene=$scene 105 | echo Port=$port 106 | echo ServerID=$serverid 107 | 108 | /home/admindude/Wiba4-linux-server/WiBa4.x86_64 -scene $scene -port $port -ipaddress $serverip -serverid $serverid 109 | 110 | ``` 111 | 112 | Again, make the changes based on the name of your game. This script uses a command to get the IP address of the server which is passes to the game executable. The other parameters (a scene name, listening port and a unique server id) are passed to this script by the service we created earlier. So this script rarely needs to change and we can run additional game servers by simply adding a new service file with some new parameters. 113 | 114 | -------------------------------------------------------------------------------- /Goodgulf/Linux Server for Fishnet/Part 3/AzureFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Cysharp.Threading.Tasks; 9 | using UnityEngine; 10 | using UnityEngine.Networking; 11 | using UnityEngine.UI; 12 | using Goodgulf.Utilities; 13 | //using HeathenEngineering.SteamworksIntegration; 14 | 15 | namespace Goodgulf.Azure 16 | { 17 | 18 | [Serializable] 19 | public class ServerData 20 | { 21 | public string id; 22 | public string ipAddress; 23 | public int port; 24 | public string scene; 25 | public int playerCount; 26 | } 27 | 28 | [Serializable] 29 | public class ServerDataList 30 | { 31 | public List data; 32 | } 33 | 34 | 35 | [Serializable] 36 | public class CharacterData 37 | { 38 | public string characterName; 39 | public string characterOwner; 40 | public string characterGuid; 41 | 42 | public string characterRace; 43 | public string characterGender; 44 | public string characterProfession; 45 | 46 | public string characterBlob; 47 | public string characterCreationDate; 48 | } 49 | 50 | [Serializable] 51 | public class CharacterList 52 | { 53 | public List characters; 54 | } 55 | 56 | 57 | 58 | [Serializable] 59 | public class PlayerData 60 | { 61 | public string playerName; 62 | public string playerID; 63 | public string os; 64 | } 65 | 66 | [Serializable] 67 | public class SpeechData 68 | { 69 | public string Voice; 70 | public string Lines; 71 | } 72 | 73 | [Serializable] 74 | public class Voice 75 | { 76 | public string Name; 77 | public string DisplayName; 78 | public string ShortName; 79 | public string Gender; 80 | public string Locale; 81 | public string VoiceType; 82 | public string[] StyleList; 83 | public string Status; 84 | } 85 | 86 | [Serializable] 87 | public class Voices 88 | { 89 | public List azureVoices; 90 | public List azureNeuralVoices; 91 | public List azureNeuralVoicesWithStyles; 92 | public List locales; 93 | } 94 | 95 | #region CallBackDefinitions 96 | 97 | public delegate void StringLoadedCallback(string fromAzure); 98 | 99 | public delegate void GetGameKeyCallBack(); 100 | 101 | public delegate void CharacterCallBack(bool success, string fromAzure); 102 | 103 | public delegate void CharacterListCallBack(bool success, CharacterList characterList); 104 | 105 | public delegate void ServerCallBack(bool success, string fromAzure); 106 | 107 | public delegate void ServerListCallBack(bool success, ServerDataList listOfServerData); 108 | 109 | #endregion 110 | 111 | 112 | public class AzureFunctions : MonoBehaviour 113 | { 114 | // https://gamedevbeginner.com/singletons-in-unity-the-right-way/ 115 | public static AzureFunctions Instance { get; private set; } 116 | 117 | public Voices voices; // Voices loaded from Azure 118 | 119 | public AudioSource audioSource; // AudioSource used for Voice Over 120 | 121 | void Awake() 122 | { 123 | if (Instance != null && Instance != this) 124 | { 125 | Destroy(this); 126 | return; 127 | } 128 | 129 | Instance = this; 130 | 131 | 132 | } 133 | 134 | #region ApplicationQuit 135 | 136 | public void GenericServersCallBack(bool success, string fromAzure) 137 | { 138 | Debug.Log($"AzureFunctions:GenericServersCallBack(): registered {success} with response {fromAzure}"); 139 | } 140 | 141 | private void OnDestroy() 142 | { 143 | #if UNITY_EDITOR 144 | Debug.Log("On Destroy called"); 145 | OnApplicationQuit(); 146 | #endif 147 | } 148 | 149 | void OnApplicationQuit() 150 | { 151 | Debug.Log("Application ending after " + Time.time + " seconds"); 152 | 153 | // Send an azure function request to check if there are more clients, if not, shutdown all servers 154 | AzureFunctions az = AzureFunctions.Instance; 155 | az.ConditionalShutdownServers(GenericServersCallBack).Forget(); 156 | } 157 | #endregion 158 | 159 | #region ServerManagement 160 | 161 | 162 | public async UniTaskVoid RegisterServer(ServerData serverData, ServerCallBack serverCallBack) 163 | { 164 | string json = JsonUtility.ToJson(serverData); 165 | byte[] postData = System.Text.Encoding.UTF8.GetBytes(json); 166 | UnityWebRequest webRequest = UnityWebRequest.PostWwwForm("https://", "POST"); //Removed the Azure function connection string 167 | 168 | webRequest.uploadHandler = (UploadHandler)new UploadHandlerRaw(postData); 169 | 170 | var cts = new CancellationTokenSource(); 171 | cts.CancelAfterSlim(TimeSpan.FromSeconds(10)); // 10sec timeout. 172 | 173 | try 174 | { 175 | var result = await webRequest.SendWebRequest().WithCancellation(cts.Token); 176 | 177 | } 178 | catch (OperationCanceledException ex) 179 | { 180 | if (ex.CancellationToken == cts.Token) 181 | { 182 | Debug.LogError("AzureFunctions.RegisterServer(): Timeout"); 183 | } 184 | } 185 | 186 | webRequest.uploadHandler.Dispose(); 187 | 188 | if (webRequest.result != UnityWebRequest.Result.Success) 189 | { 190 | Debug.LogError("AzureFunctions.RegisterServer():"+ webRequest.error); 191 | serverCallBack(false, webRequest.error); 192 | } 193 | else 194 | { 195 | serverCallBack(true, "Server registered successfully, "+webRequest.downloadHandler.text); 196 | } 197 | 198 | } 199 | 200 | public async UniTaskVoid UpdateServer(ServerData serverData, ServerCallBack serverCallBack) 201 | { 202 | string json = JsonUtility.ToJson(serverData); 203 | byte[] postData = System.Text.Encoding.UTF8.GetBytes(json); 204 | UnityWebRequest webRequest = UnityWebRequest.PostWwwForm("https://", "POST"); //Removed the Azure function connection string 205 | 206 | webRequest.uploadHandler = (UploadHandler)new UploadHandlerRaw(postData); 207 | 208 | var cts = new CancellationTokenSource(); 209 | cts.CancelAfterSlim(TimeSpan.FromSeconds(10)); // 10sec timeout. 210 | 211 | try 212 | { 213 | var result = await webRequest.SendWebRequest().WithCancellation(cts.Token); 214 | 215 | } 216 | catch (OperationCanceledException ex) 217 | { 218 | if (ex.CancellationToken == cts.Token) 219 | { 220 | Debug.LogError("AzureFunctions.UpdateServer(): Timeout"); 221 | } 222 | } 223 | 224 | webRequest.uploadHandler.Dispose(); 225 | 226 | if (webRequest.result != UnityWebRequest.Result.Success) 227 | { 228 | Debug.LogError("AzureFunctions.UpdateServer():"+ webRequest.error); 229 | serverCallBack(false, webRequest.error); 230 | } 231 | else 232 | { 233 | serverCallBack(true, "Server updated successfully, "+webRequest.downloadHandler.text); 234 | } 235 | 236 | } 237 | 238 | } 239 | 240 | } -------------------------------------------------------------------------------- /Goodgulf/Linux Server for Fishnet/Part 3/ServerData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace UnityGameServers 8 | { 9 | // This class is shared between the Unity code and the Azure Function. 10 | // We use this to send data between the client and the Azure Function. 11 | 12 | 13 | class ServerData 14 | { 15 | public string id; 16 | public string ipAddress; 17 | public int port; 18 | public string scene; 19 | public int playerCount; 20 | } 21 | 22 | class ServerDataList 23 | { 24 | public List data; 25 | 26 | public ServerDataList() 27 | { 28 | data = new List(); 29 | } 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Goodgulf/Linux Server for Fishnet/Part 3/ServerDataRecord.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Azure; 7 | using Azure.Data.Tables; 8 | 9 | 10 | 11 | namespace UnityGameServers 12 | { 13 | // This is the record for the Azure Storage Table 14 | 15 | public class ServerDataRecord : ITableEntity 16 | { 17 | public string ID { get; set; } 18 | public string IPAddress { get; set; } 19 | public int Port { get; set; } 20 | public string Scene { get; set; } 21 | public int PlayerCount { get; set; } 22 | 23 | 24 | public DateTime LastUpdateDate { get; set; } 25 | 26 | 27 | public DateTime Date { get; set; } 28 | public string PartitionKey { get; set; } 29 | public string RowKey { get; set; } 30 | public DateTimeOffset? Timestamp { get; set; } 31 | public ETag ETag { get; set; } 32 | 33 | 34 | public ServerDataRecord(string id, string ipaddress, int port, string scene, int playercount, DateTime savedate) 35 | { 36 | ID = id; 37 | IPAddress = ipaddress; 38 | Port = port; 39 | Scene = scene; 40 | PlayerCount = playercount; 41 | LastUpdateDate = savedate; 42 | 43 | Date = savedate; 44 | PartitionKey = "UnityGameServer"; 45 | RowKey = id; 46 | } 47 | 48 | public ServerDataRecord() 49 | { 50 | 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Goodgulf/Part 1/PlayerScript.cs: -------------------------------------------------------------------------------- 1 | using Invector.vCharacterController; 2 | using Mirror; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using UnityEngine; 6 | 7 | namespace Goodgulf.Networking 8 | { 9 | 10 | public class PlayerScript : NetworkBehaviour 11 | { 12 | public AudioSource announce; // This clip will be played when a player enters the game. The audio source needs to be tagged as "Announce". 13 | 14 | public vThirdPersonCamera cameraPrefab; // Assign a reference to the Invector camera prefab to this property. 15 | 16 | private vThirdPersonInput _vThirdPersonInput; // Reference to the Invector Input system which we disable on all players except the Local Player. 17 | private vThirdPersonCamera _vThirdPersonCamera; // Reference to the Invector camera we spawn based on the above prefab. 18 | 19 | void Awake() 20 | { 21 | Debug.Log("PlayerScript.Awake(): event fired"); 22 | 23 | // Find the announcement audio source component in the hierarchy. 24 | announce = GameObject.FindGameObjectWithTag("Announce").GetComponent(); 25 | 26 | _vThirdPersonInput = GetComponent(); 27 | if (_vThirdPersonInput) 28 | { 29 | // Disable input by default, enable later for local players in OnStartLocalPlayer() event. 30 | // If we don't do this the input handler script will mess up the 3rd person camera setup. 31 | _vThirdPersonInput.enabled = false; 32 | } 33 | else Debug.LogError("PlayerScript.Start: could not find vThirdPersonInput."); 34 | } 35 | 36 | 37 | public override void OnStartLocalPlayer() 38 | { 39 | Debug.Log("PlayerScript.OnStartLocalPlayer(): event fired"); 40 | 41 | // Deactivate the main camera (which we see at the start of the scene) since the 3rd person camera needs to take over now. 42 | Camera.main.gameObject.SetActive(false); 43 | 44 | // Debug.Log("PlayerScript.OnStartLocalPlayer(): isLocalPlayer = "+isLocalPlayer); 45 | 46 | // Now we instanatiate the third person camera for the local player only: 47 | _vThirdPersonCamera = Instantiate(cameraPrefab, new Vector3(0, 0, 0), Quaternion.identity); 48 | 49 | if (_vThirdPersonCamera) 50 | { 51 | Debug.Log("PlayerScript.OnStartLocalPlayer(): setting main camera to localPlayer"); 52 | 53 | // Now link the camera to this player: 54 | _vThirdPersonCamera.SetMainTarget(this.transform); 55 | } 56 | else Debug.Log("PlayerScript.OnStartLocalPlayer(): _vThirdPersonCamera = null"); 57 | 58 | if (_vThirdPersonInput) 59 | { 60 | // Since this is the local player we'll need to enable the input handler 61 | _vThirdPersonInput.enabled = true; 62 | } 63 | 64 | if(announce) 65 | { 66 | announce.Play(); 67 | } 68 | 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /Goodgulf/Part 11/PlayerScript.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using FishNet.Object; 4 | using Invector.vCharacterController; 5 | using UnityEngine; 6 | 7 | public class PlayerScript : NetworkBehaviour 8 | { 9 | 10 | public vThirdPersonCamera cameraPrefab; // Assign a reference to the Invector camera prefab to this property. 11 | 12 | private vThirdPersonInput _vThirdPersonInput; // Reference to the Invector Input system which we disable on all players except the Local Player. 13 | private vThirdPersonCamera _vThirdPersonCamera; // Reference to the Invector camera we spawn based on the above prefab. 14 | 15 | 16 | private void Awake() 17 | { 18 | Debug.Log("PlayerScript.Awake(): event fired"); 19 | 20 | _vThirdPersonInput = GetComponent(); 21 | if (_vThirdPersonInput) 22 | { 23 | // Disable input by default, enable later for local players in OnStartLocalPlayer() event. 24 | // If we don't do this the input handler script will mess up the 3rd person camera setup. 25 | 26 | // _vThirdPersonInput.enabled = false; 27 | 28 | // For the Fishnetworking version we actually have to disable the script on the prefab. It will be reenabled in the OnStartClient event. 29 | } 30 | else Debug.LogError("PlayerScript.Awake: could not find vThirdPersonInput."); 31 | } 32 | 33 | // If you are coming from Mirror instead of using OnLocalPlayer use OnStartClient with a base.IsOwner check. 34 | public override void OnStartClient() 35 | { 36 | base.OnStartClient(); 37 | 38 | Debug.Log("PlayerScript.OnStartClient(): event fired"); 39 | 40 | if (base.IsOwner) 41 | { 42 | 43 | // Deactivate the main camera (which we see at the start of the scene) since the 3rd person camera needs to take over now. 44 | 45 | Camera.main.gameObject.SetActive(false); 46 | 47 | Vector3 cameraPosition = new Vector3(this.transform.position.x, this.transform.position.y+this.transform.up.y*3, this.transform.position.z); 48 | 49 | _vThirdPersonCamera = Instantiate(cameraPrefab, cameraPosition, Quaternion.identity); 50 | 51 | if (_vThirdPersonCamera) 52 | { 53 | Debug.Log("PlayerScript.OnStartClient(): setting main camera to localPlayer"); 54 | 55 | // Now link the camera to this player: 56 | _vThirdPersonCamera.SetMainTarget(this.transform); 57 | } 58 | else Debug.LogError("PlayerScript.OnStartClient(): _vThirdPersonCamera = null"); 59 | 60 | if (_vThirdPersonInput) 61 | { 62 | // Since this is the local player we'll need to enable the input handler 63 | // In the original Mirror script I just enabled the input and it worked. 64 | // However the Invector free doesn't play nice with Fish and you'll see some odd behaviour (player falling from the sky). 65 | //_vThirdPersonInput.enabled = true; 66 | 67 | // So instead we'll wait a short time then enable the inputs after events have finished executing: 68 | Invoke(nameof(EnableInput), 0.5f); 69 | 70 | } 71 | else Debug.LogError("PlayerScript.OnStartClient(): _vThirdPersonInput = null"); 72 | 73 | } 74 | 75 | 76 | } 77 | 78 | public void EnableInput() 79 | { 80 | if (_vThirdPersonInput) 81 | { 82 | // Since this is the local player we'll need to enable the input handler 83 | // Simply enabling will cause some errors since Start() has not been called 84 | // So we'll need to do some "initialization" ourselves here. 85 | 86 | _vThirdPersonInput.cc = GetComponent(); 87 | _vThirdPersonInput.cc.Init(); 88 | _vThirdPersonInput.enabled = true; 89 | } 90 | else Debug.LogError("PlayerScript.EnableInput(): _vThirdPersonInput = null"); 91 | } 92 | 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Goodgulf/Part 13/Bootstrap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.SceneManagement; 5 | 6 | namespace Goodgulf.Gamelogic 7 | { 8 | public class Bootstrap : MonoBehaviour 9 | { 10 | public float waitingTime = 2.0f; 11 | 12 | private float timer = 0f; 13 | private bool timerSwitch = true; 14 | 15 | void Update() 16 | { 17 | if (!timerSwitch) 18 | return; 19 | 20 | timer += Time.deltaTime; 21 | 22 | if (timer > waitingTime) 23 | { 24 | timerSwitch = false; 25 | LoadMainMenu(); 26 | } 27 | } 28 | 29 | public void LoadMainMenu() 30 | { 31 | SceneManager.LoadScene(1); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Goodgulf/Part 13/DontDestroy.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace Goodgulf.Utilities 6 | { 7 | public class DontDestroy : MonoBehaviour 8 | { 9 | void Awake() 10 | { 11 | DontDestroyOnLoad(this.gameObject); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Goodgulf/Part 13/SteamLobby.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using HeathenEngineering.SteamworksIntegration; 4 | using Steamworks; 5 | using UnityEngine; 6 | using UnityEngine.UI; 7 | using API = HeathenEngineering.SteamworksIntegration.API; 8 | using UserAPI = HeathenEngineering.SteamworksIntegration.API.User.Client; 9 | using FriendsAPI = HeathenEngineering.SteamworksIntegration.API.Friends.Client; 10 | 11 | public class SteamLobby : MonoBehaviour 12 | { 13 | 14 | public LobbyManager lobbyManager; 15 | 16 | // Store the lobby created by this player / searched & joined by this player: 17 | public Lobby selectedLobby; 18 | 19 | // References to the buttons on the UI: 20 | public Button startGameButton; 21 | public Button browseButton; 22 | public Button readyButton; 23 | 24 | // Start is called before the first frame update 25 | private void Start() 26 | { 27 | lobbyManager.evtCreated.AddListener(LobbyCreated); 28 | lobbyManager.evtUserJoined.AddListener(SomeOneJoinedTheLobby); 29 | } 30 | 31 | // This event gets called whenever a player joins the lobby. It's linked to the LobbyManager in the Start() method. 32 | private void SomeOneJoinedTheLobby(UserData arg0) 33 | { 34 | // The argument contains the user who joined the lobby: 35 | Debug.Log(arg0.Name + " joined the lobby."); 36 | 37 | // Now iterate through all lobby members: 38 | Debug.Log("The lobby members are:"); 39 | var lobbyReference = lobbyManager.Lobby; 40 | foreach(var member in lobbyReference.Members) 41 | { 42 | Debug.Log(member.user.Name); 43 | } 44 | 45 | // Now check if all players are ready so we can enable the startGameButton (linked to in this component) 46 | if (startGameButton) 47 | { 48 | if (selectedLobby.AllPlayersReady && selectedLobby.IsOwner) 49 | { 50 | // All players are ready and I am the Lobby owner so I can now start the game 51 | startGameButton.interactable = true; 52 | } 53 | else 54 | { 55 | startGameButton.interactable = false; 56 | } 57 | } 58 | } 59 | 60 | // This event gets called when you enter the lobby 61 | public void EnterLobby(Lobby lobby) 62 | { 63 | Debug.Log("You entered Lobby: "+lobby.Name); 64 | selectedLobby = lobby; 65 | 66 | // Show the lobby details 67 | ReportLobbyDetails(lobby); 68 | } 69 | 70 | 71 | private void LobbyCreated(Lobby lobby) 72 | { 73 | var id = lobby.id; 74 | Debug.Log("On Handle Lobby Created: a new lobby has been created with CSteamID = " + lobby.ToString() 75 | + "\nThe CSteamID can be broken down into its parts such as :" 76 | + "\nAccount Type = " + id.GetEAccountType() 77 | + "\nAccount Instance = " + id.GetUnAccountInstance() 78 | + "\nUniverse = " + id.GetEUniverse() 79 | + "\nAccount Id = " + id.GetAccountID()); 80 | 81 | // When the lobby is created the owner also starts with a IsReady = false status: 82 | if (lobby.IsOwner) 83 | { 84 | lobby.IsReady = false; 85 | selectedLobby = lobby; 86 | } 87 | } 88 | 89 | // This method is used by each player to flag they are ready. 90 | public void PlayerIsReady() 91 | { 92 | // First check if we have created or joined a lobby 93 | if (selectedLobby != null) 94 | { 95 | // OK, we are ready to indicate this to the lobby: 96 | selectedLobby.IsReady = true; 97 | 98 | if (selectedLobby.AllPlayersReady && selectedLobby.IsOwner) 99 | { 100 | // All players are ready and I am the Lobby owner so I can now start the game 101 | if (startGameButton) 102 | { 103 | startGameButton.interactable = true; 104 | } 105 | } 106 | } 107 | } 108 | 109 | // This event gets called whenever any lobby meta data (incl. IsReady) is changed. 110 | public void MetaDataUpdated() 111 | { 112 | Debug.Log("Lobby Meta Data has been updated"); 113 | if (selectedLobby != null) 114 | { 115 | if (selectedLobby.IsOwner) 116 | { 117 | // Only if you are the owner... 118 | if (startGameButton) 119 | { 120 | // ... enable the startGameButton if all players have reported to be ready 121 | startGameButton.interactable = selectedLobby.AllPlayersReady; 122 | } 123 | } 124 | } 125 | } 126 | 127 | // This event is called when a Lobby Search has been initated on the Lobby Manager using the Lobby Search settings 128 | public void ReportSearchResults(Lobby[] results) 129 | { 130 | // Do we have anyh results? 131 | if (results.Length > 0) 132 | { 133 | // Just pick teh first lobby 134 | Lobby firstLobby = results[0]; 135 | 136 | Debug.Log("Joining Lobby: "+firstLobby.Name); 137 | 138 | // Join the lobby 139 | lobbyManager.Join(firstLobby); 140 | 141 | if (browseButton) 142 | browseButton.interactable = false; 143 | 144 | if (readyButton) 145 | readyButton.interactable = true; 146 | } 147 | else Debug.Log("No lobbies found!"); 148 | 149 | } 150 | 151 | public void ReportLobbyDetails(Lobby lobby) 152 | { 153 | if (lobby != null) 154 | { 155 | // retrieve the Lobby owner from the Lobby: 156 | CSteamID owner = API.Matchmaking.Client.GetLobbyOwner(lobby); 157 | // Then get the owner name: 158 | UserData ownerData = new CSteamID(owner.m_SteamID); 159 | 160 | Debug.Log("Lobby name: " + lobby.Name); 161 | Debug.Log("Lobby owner: " + ownerData.Name); 162 | 163 | // Get all lobby members and show some of their details: 164 | LobbyMember[] members = lobby.Members; 165 | foreach (LobbyMember lobbyMember in members) 166 | { 167 | Debug.Log("Lobby member: "+lobbyMember.user.Name + ", isReady = "+lobbyMember.IsReady); 168 | } 169 | } 170 | } 171 | 172 | 173 | 174 | } 175 | -------------------------------------------------------------------------------- /Goodgulf/Part 14/HostGame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using FishNet; 5 | using FishNet.Connection; 6 | using FishNet.Managing; 7 | using FishNet.Managing.Scened; 8 | using FishNet.Object; 9 | using FishNet.Transporting; 10 | using HeathenEngineering.SteamworksIntegration; 11 | using UnityEngine; 12 | using API = HeathenEngineering.SteamworksIntegration.API; 13 | using UserAPI = HeathenEngineering.SteamworksIntegration.API.User.Client; 14 | public class HostGame : MonoBehaviour 15 | { 16 | [Tooltip("Prefab to spawn for the player.")] 17 | [SerializeField] 18 | private NetworkObject playerPrefab; 19 | 20 | private NetworkManager networkManager; 21 | private Transform[] spawns; 22 | private int spawnIndex = 0; 23 | 24 | private string sceneName; 25 | 26 | void Start() 27 | { 28 | // Get the NetworkManager instance 29 | networkManager = InstanceFinder.NetworkManager; 30 | } 31 | 32 | public void FindSpawns() 33 | { 34 | GameObject[] spawners = GameObject.FindGameObjectsWithTag("spawn"); 35 | spawns = new Transform[spawners.Length]; 36 | 37 | for(int i=0;i lobbyMetaData = lobby.GetMetadata(); 52 | 53 | // string asString = string.Join(Environment.NewLine, lobbyMetaData); 54 | // Debug.Log("Meta data: "+asString); 55 | 56 | sceneName = lobbyMetaData["scene"]; 57 | Debug.Log("Storing Scene name:"+sceneName); 58 | 59 | // Add this event in order to load the scene 60 | networkManager.ServerManager.OnServerConnectionState += ServerManager_OnServerConnectionState; 61 | 62 | // Add this event so we can spawn the player 63 | networkManager.SceneManager.OnClientLoadedStartScenes += SceneManager_OnClientLoadedStartScenes; 64 | 65 | networkManager.ServerManager.StartConnection(); 66 | 67 | // Get the user's Steam Id and send it to the other players so they know where to connect to 68 | var user = UserAPI.Id; 69 | lobby.SetGameServer(user.id); 70 | 71 | } 72 | } 73 | 74 | private void ServerManager_OnServerConnectionState(ServerConnectionStateArgs obj) 75 | { 76 | // When server starts load online scene as global. Since this is a global scene clients will automatically join it when connecting. 77 | if (obj.ConnectionState == LocalConnectionState.Started) 78 | { 79 | Debug.Log("Server Connection state = Started"); 80 | 81 | // Now load the global scene where the game is player. Instead of a fixed name you can also retrieve the 82 | // name of the game scene from the lobby meta data, making it more dynamic 83 | 84 | 85 | Debug.Log("Loading scene: "+sceneName); 86 | SceneLoadData sld = new SceneLoadData(sceneName); 87 | sld.ReplaceScenes = ReplaceOption.All; 88 | 89 | Debug.Log("Now load the global map"); 90 | networkManager.SceneManager.LoadGlobalScenes(sld); 91 | } 92 | } 93 | 94 | private void SceneManager_OnClientLoadedStartScenes(NetworkConnection conn, bool asServer) 95 | { 96 | Debug.Log("SceneManager_OnClientLoadedStartScenes called"); 97 | 98 | if (!asServer) 99 | return; 100 | 101 | // So the scene has loaded and this call is made on the server. 102 | Debug.Log("SceneManager_OnClientLoadedStartScenes running asServer"); 103 | 104 | // Check if we gave a player prefab we want to spawn in the game: 105 | if (playerPrefab == null) 106 | { 107 | Debug.LogWarning($"Player prefab is empty and cannot be spawned for connection {conn.ClientId}."); 108 | return; 109 | } 110 | Debug.Log("PlayerPrefab found"); 111 | 112 | // Now find the spawn positions in the active scene: 113 | FindSpawns(); 114 | 115 | Vector3 position; 116 | Quaternion rotation; 117 | 118 | if (spawns.Length > 0) 119 | { 120 | position = spawns[spawnIndex].position; 121 | rotation = spawns[spawnIndex].rotation; 122 | 123 | spawnIndex++; 124 | if (spawnIndex >= spawns.Length) 125 | spawnIndex = 0; 126 | } 127 | else 128 | { 129 | position = playerPrefab.transform.position; 130 | rotation = playerPrefab.transform.rotation; 131 | } 132 | 133 | NetworkObject nob = networkManager.GetPooledInstantiated(playerPrefab, true); 134 | nob.transform.SetPositionAndRotation(position, rotation); 135 | networkManager.ServerManager.Spawn(nob, conn); 136 | 137 | //If there are no global scenes 138 | //if (_addToDefaultScene) 139 | // _networkManager.SceneManager.AddOwnerToDefaultScene(nob); 140 | 141 | } 142 | 143 | 144 | } 145 | -------------------------------------------------------------------------------- /Goodgulf/Part 14/PlayerScript.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using FishNet.Object; 4 | using UnityEngine; 5 | using Invector; 6 | using Invector.vCamera; 7 | using Invector.vCharacterController; 8 | using Invector.vCharacterController.vActions; 9 | public class PlayerScript : NetworkBehaviour 10 | { 11 | private vThirdPersonController _vThirdPersonController; 12 | private vFootStep _vFootStep; 13 | private vHitDamageParticle _vHitDamageParticle; 14 | private vHeadTrack _vHeadTrack; 15 | private vLadderAction _vLadderAction; 16 | private vGenericAction _vGenericAction; 17 | private vThirdPersonInput _vThirdPersonInput; 18 | private vThirdPersonCamera _vThirdPersonCamera; 19 | 20 | 21 | private void Awake() 22 | { 23 | Debug.Log("PlayerScript.Awake(): event fired"); 24 | 25 | // Start with disabling all the Invector components on the instantiated object 26 | 27 | _vThirdPersonController = GetComponent(); 28 | _vFootStep = GetComponent(); 29 | _vHitDamageParticle = GetComponent(); 30 | _vHeadTrack = GetComponent(); 31 | _vLadderAction = GetComponent(); 32 | _vGenericAction = GetComponent(); 33 | _vThirdPersonCamera = GetComponentInChildren(); 34 | _vThirdPersonInput = GetComponent(); 35 | 36 | _vThirdPersonController.enabled = false; 37 | _vFootStep.enabled = false; 38 | _vHitDamageParticle.enabled = false; 39 | _vHeadTrack.enabled = false; 40 | _vLadderAction.enabled = false; 41 | _vGenericAction.enabled = false; 42 | _vThirdPersonInput.enabled = false; 43 | 44 | _vThirdPersonCamera.enabled = false; 45 | _vThirdPersonCamera.gameObject.SetActive(false); 46 | 47 | } 48 | 49 | 50 | // If you are coming from Mirror instead of using OnLocalPlayer use OnStartClient with a base.IsOwner check. 51 | public override void OnStartClient() 52 | { 53 | base.OnStartClient(); 54 | 55 | Debug.Log("PlayerScript.OnStartClient(): event fired"); 56 | 57 | if (base.IsOwner) 58 | { 59 | // So this is the locally owned player object. Enable all Invector controls for this one: 60 | 61 | _vThirdPersonController.enabled = true; 62 | _vFootStep.enabled = true; 63 | _vHitDamageParticle.enabled = true; 64 | _vHeadTrack.enabled = true; 65 | _vLadderAction.enabled = true; 66 | _vGenericAction.enabled = true; 67 | _vThirdPersonInput.enabled = true; 68 | 69 | _vThirdPersonCamera.gameObject.SetActive(true); 70 | _vThirdPersonCamera.enabled = true; 71 | _vThirdPersonCamera.SetMainTarget(this.transform); 72 | 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Goodgulf/Part 14/SteamLobby.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using FishNet; 4 | using FishNet.Connection; 5 | using FishNet.Managing; 6 | using FishNet.Managing.Scened; 7 | using FishNet.Transporting; 8 | using HeathenEngineering.SteamworksIntegration; 9 | using Steamworks; 10 | using UnityEngine; 11 | using UnityEngine.UI; 12 | using API = HeathenEngineering.SteamworksIntegration.API; 13 | using UserAPI = HeathenEngineering.SteamworksIntegration.API.User.Client; 14 | using FriendsAPI = HeathenEngineering.SteamworksIntegration.API.Friends.Client; 15 | 16 | public class SteamLobby : MonoBehaviour 17 | { 18 | 19 | public LobbyManager lobbyManager; 20 | 21 | // Store the lobby created by this player / searched & joined by this player: 22 | public Lobby selectedLobby; 23 | 24 | // References to the buttons on the UI: 25 | public Button startGameButton; 26 | public Button browseButton; 27 | public Button readyButton; 28 | 29 | private NetworkManager networkManager; 30 | private HostGame hostGame; 31 | 32 | // Start is called before the first frame update 33 | private void Start() 34 | { 35 | lobbyManager.evtCreated.AddListener(LobbyCreated); 36 | lobbyManager.evtUserJoined.AddListener(SomeOneJoinedTheLobby); 37 | 38 | // Get the NetworkManager instance 39 | networkManager = InstanceFinder.NetworkManager; 40 | 41 | hostGame = GameObject.FindObjectOfType(); 42 | if(hostGame == null) 43 | Debug.LogError("Could not find HostGame object!"); 44 | 45 | } 46 | 47 | // This event gets called whenever a player joins the lobby. It's linked to the LobbyManager in the Start() method. 48 | private void SomeOneJoinedTheLobby(UserData arg0) 49 | { 50 | // The argument contains the user who joined the lobby: 51 | Debug.Log(arg0.Name + " joined the lobby."); 52 | 53 | // Now iterate through all lobby members: 54 | Debug.Log("The lobby members are:"); 55 | var lobbyReference = lobbyManager.Lobby; 56 | foreach (var member in lobbyReference.Members) 57 | { 58 | Debug.Log(member.user.Name); 59 | } 60 | 61 | // Now check if all players are ready so we can enable the startGameButton (linked to in this component) 62 | if (startGameButton) 63 | { 64 | if (selectedLobby.AllPlayersReady && selectedLobby.IsOwner) 65 | { 66 | // All players are ready and I am the Lobby owner so I can now start the game 67 | startGameButton.interactable = true; 68 | } 69 | else 70 | { 71 | startGameButton.interactable = false; 72 | } 73 | } 74 | } 75 | 76 | // This event gets called when you enter the lobby 77 | public void EnterLobby(Lobby lobby) 78 | { 79 | Debug.Log("You entered Lobby: " + lobby.Name); 80 | selectedLobby = lobby; 81 | 82 | // Show the lobby details 83 | ReportLobbyDetails(lobby); 84 | } 85 | 86 | 87 | private void LobbyCreated(Lobby lobby) 88 | { 89 | var id = lobby.id; 90 | Debug.Log("On Handle Lobby Created: a new lobby has been created with CSteamID = " + lobby.ToString() 91 | + "\nThe CSteamID can be broken down into its parts such as :" 92 | + "\nAccount Type = " + id.GetEAccountType() 93 | + "\nAccount Instance = " + id.GetUnAccountInstance() 94 | + "\nUniverse = " + id.GetEUniverse() 95 | + "\nAccount Id = " + id.GetAccountID()); 96 | 97 | // When the lobby is created the owner also starts with a IsReady = false status: 98 | if (lobby.IsOwner) 99 | { 100 | lobby.IsReady = false; 101 | selectedLobby = lobby; 102 | } 103 | } 104 | 105 | // This method is used by each player to flag they are ready. 106 | public void PlayerIsReady() 107 | { 108 | // First check if we have created or joined a lobby 109 | if (selectedLobby != null) 110 | { 111 | // OK, we are ready to indicate this to the lobby: 112 | selectedLobby.IsReady = true; 113 | 114 | if (selectedLobby.AllPlayersReady && selectedLobby.IsOwner) 115 | { 116 | // All players are ready and I am the Lobby owner so I can now start the game 117 | if (startGameButton) 118 | { 119 | startGameButton.interactable = true; 120 | } 121 | } 122 | } 123 | } 124 | 125 | // This event gets called whenever any lobby meta data (incl. IsReady) is changed. 126 | public void MetaDataUpdated() 127 | { 128 | Debug.Log("Lobby Meta Data has been updated"); 129 | if (selectedLobby != null) 130 | { 131 | if (selectedLobby.IsOwner) 132 | { 133 | // Only if you are the owner... 134 | if (startGameButton) 135 | { 136 | // ... enable the startGameButton if all players have reported to be ready 137 | startGameButton.interactable = selectedLobby.AllPlayersReady; 138 | } 139 | } 140 | } 141 | } 142 | 143 | // This event is called when a Lobby Search has been initated on the Lobby Manager using the Lobby Search settings 144 | public void ReportSearchResults(Lobby[] results) 145 | { 146 | // Do we have anyh results? 147 | if (results.Length > 0) 148 | { 149 | // Just pick teh first lobby 150 | Lobby firstLobby = results[0]; 151 | 152 | Debug.Log("Joining Lobby: " + firstLobby.Name); 153 | 154 | // Join the lobby 155 | lobbyManager.Join(firstLobby); 156 | 157 | if (browseButton) 158 | browseButton.interactable = false; 159 | 160 | if (readyButton) 161 | readyButton.interactable = true; 162 | } 163 | else Debug.Log("No lobbies found!"); 164 | 165 | } 166 | 167 | public void ReportLobbyDetails(Lobby lobby) 168 | { 169 | if (lobby != null) 170 | { 171 | // retrieve the Lobby owner from the Lobby: 172 | CSteamID owner = API.Matchmaking.Client.GetLobbyOwner(lobby); 173 | // Then get the owner name: 174 | UserData ownerData = new CSteamID(owner.m_SteamID); 175 | 176 | Debug.Log("Lobby name: " + lobby.Name); 177 | Debug.Log("Lobby owner: " + ownerData.Name); 178 | 179 | // Get all lobby members and show some of their details: 180 | LobbyMember[] members = lobby.Members; 181 | foreach (LobbyMember lobbyMember in members) 182 | { 183 | Debug.Log("Lobby member: " + lobbyMember.user.Name + ", isReady = " + lobbyMember.IsReady); 184 | } 185 | } 186 | } 187 | 188 | 189 | public void StartGame() 190 | { 191 | if (selectedLobby != null) 192 | { 193 | // We have an active lobby 194 | 195 | if (selectedLobby.IsOwner) 196 | { 197 | // I am the owner so I can start the game 198 | 199 | if(hostGame) 200 | hostGame.StartHostingGame(selectedLobby); 201 | 202 | } 203 | else Debug.LogWarning("StartGame not by the owner."); 204 | 205 | } 206 | } 207 | 208 | public void LobbyGameServerEvent(LobbyGameServer lobbyGameServer) 209 | { 210 | // Everyone in the lobby gets this message (including the player hosting the game) 211 | // In the LobbyGameServer parameter we can find the steamId to connect to 212 | 213 | if (selectedLobby != null) 214 | { 215 | // Get the SteamId of the player hosting the game and use it as the host address: 216 | string hostAddress = lobbyGameServer.id.ToString(); 217 | 218 | // Now connect to the host. This will kick off the OnRemoteConnectionState event on the server 219 | Debug.Log("Connecting to: "+hostAddress); 220 | networkManager.ClientManager.StartConnection(hostAddress); 221 | 222 | } 223 | } 224 | 225 | 226 | public void CreateMyLobby() 227 | { 228 | // Having your own Lobby creation methods add some flexibility, for example adding custom meta data to the lobby 229 | 230 | var user = UserAPI.Id; 231 | 232 | string lobbyName = user.Name + "'s lobby"; // Use a more dynamic lobby name 233 | lobbyManager.createArguments.name = lobbyName; 234 | lobbyManager.createArguments.type = ELobbyType.k_ELobbyTypePublic; // Example: add a switch to the UI which toggles between public and friends only 235 | 236 | LobbyManager.MetadataTempalate scene = new LobbyManager.MetadataTempalate(); // Spelling error here in Heathen Steamworks code I guess. 237 | 238 | // Now we add a meta data field for the game scene to be loaded 239 | // Each meta data entry is using a key/value pair: 240 | scene.key = "scene"; 241 | scene.value = "test scene 1"; // Example: read the scene from a dropdown box instead 242 | lobbyManager.createArguments.metadata.Add(scene); 243 | 244 | lobbyManager.Create(); 245 | } 246 | 247 | } 248 | -------------------------------------------------------------------------------- /Goodgulf/Part 15/Cheat.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.InputSystem; 5 | public class Cheat : MonoBehaviour 6 | { 7 | /* 8 | * This is the new version of the cheat code script which uses the Unity's Input System. 9 | * Recommended viewing on how to use the system: https://youtu.be/m5WsmlEOFiA 10 | * 11 | * Attach this script to the player prefab. 12 | */ 13 | private PlayerInput playerInput; 14 | private InputAction cheatAction; 15 | 16 | private void Awake() 17 | { 18 | // Get a reference to the Player Input component attached to the player prefab: 19 | playerInput = GetComponent(); 20 | 21 | // Create a reference to the action label "Cheat" as defined StarterAssets (Input Action Asset) 22 | cheatAction = playerInput.actions["Cheat"]; 23 | 24 | // Add a listener to the Key Pressed event for the "Cheat" action (linked to the F2 key) 25 | cheatAction.performed += CheatKeyPressed; 26 | } 27 | 28 | private void OnDisable() 29 | { 30 | // Remove the listener on exit: 31 | cheatAction.performed -= CheatKeyPressed; 32 | } 33 | 34 | 35 | private void CheatKeyPressed(InputAction.CallbackContext context) 36 | { 37 | Debug.Log("Cheat key pressed"); 38 | 39 | // The cheat key is pressed so move the character forwards 40 | transform.position += transform.forward*3; 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Goodgulf/Part 15/NetworkInputControl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using UnityEngine.InputSystem; 6 | 7 | public class NetworkInputControl : MonoBehaviour 8 | { 9 | /* 10 | * This is a simple script which listens to the key bound to the "Network" action (F1 key). 11 | * Since we haven't instantiated a player prefab in the scene yet (the network hasn't started), 12 | * we'll need an alternative object to host the PlayerInput script. This is what the GetInput 13 | * object in the scene is for: temporarily host the PlayerInput. 14 | * Once the network is started this object can be deactivated. 15 | * 16 | * Attach this script to the GetInput object. 17 | * 18 | * This script uses the Unity Input System: 19 | * To see how the New Input System works, check out this video: https://youtu.be/m5WsmlEOFiA 20 | */ 21 | 22 | private PlayerInput playerInput; 23 | private InputAction networkAction; 24 | 25 | private void Awake() 26 | { 27 | // Get a reference to the Player Input component attached to the GetInput object: 28 | playerInput = GetComponent(); 29 | 30 | // Create a reference to the action label "Network" as defined StarterAssets (Input Action Asset) 31 | networkAction = playerInput.actions["Network"]; 32 | 33 | // Add a listener to the Key Pressed event for the "Network" action (linked to the F1 key) 34 | networkAction.performed += NetworkKeyPressed; 35 | } 36 | 37 | private void OnDisable() 38 | { 39 | networkAction.performed -= NetworkKeyPressed; 40 | } 41 | 42 | private void NetworkKeyPressed(InputAction.CallbackContext context) 43 | { 44 | Debug.Log("Network key pressed"); 45 | 46 | // now start Fishnet network 47 | GameObject sn = GameObject.Find("NetworkManager"); 48 | 49 | StartNetwork startNetwork = sn.GetComponent(); 50 | startNetwork.StartNetworking(); 51 | 52 | // Deactivate this object since we want to stop the temporary PlayerInput script on the GetInput object. 53 | this.gameObject.SetActive(false); 54 | 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Goodgulf/Part 15/PlayerScript.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using Cinemachine; 4 | using FishNet.Object; 5 | using StarterAssets; 6 | using UnityEngine; 7 | using UnityEngine.InputSystem; 8 | 9 | public class PlayerScript : NetworkBehaviour 10 | { 11 | /* 12 | * This is an updated version of the PlayerScript from "Unity Networking - Mirror Part 5" which I converted 13 | * to Fishnet Networking. See: https://youtu.be/-6DS9wJH8fw 14 | * 15 | * This script is attached to the Player prefab and makes sure that the inputs and character controller 16 | * are only active on the client which owns the network instantiated player object. 17 | * 18 | */ 19 | 20 | private CharacterController characterController; 21 | private PlayerInput playerInput; 22 | private ThirdPersonControllerCSP thirdPersonController; 23 | private Cheat cheat; 24 | 25 | private void Awake() 26 | { 27 | Debug.Log("PlayerScript.Awake(): event fired"); 28 | 29 | // In the Awake function we disable all third party controller components 30 | 31 | thirdPersonController = GetComponent(); 32 | if (thirdPersonController) 33 | { 34 | thirdPersonController.enabled = false; 35 | } 36 | else Debug.LogError("PlayerScript.Awake(): ThirdPersonControllerCSP not found."); 37 | 38 | characterController = GetComponent(); 39 | if (characterController) 40 | { 41 | characterController.enabled = false; 42 | } 43 | else Debug.LogError("PlayerScript.Awake(): CharacterController not found."); 44 | 45 | playerInput = GetComponent(); 46 | if (playerInput) 47 | { 48 | playerInput.enabled = false; 49 | } 50 | else Debug.LogError("PlayerScript.Awake(): PlayerInput not found."); 51 | 52 | cheat = GetComponent(); 53 | if (cheat) 54 | { 55 | cheat.enabled = false; 56 | } 57 | else Debug.LogError("PlayerScript.Awake(): Cheat not found."); 58 | 59 | } 60 | 61 | 62 | public override void OnStartClient() 63 | { 64 | base.OnStartClient(); 65 | 66 | Debug.Log("PlayerScript.OnStartClient(): event fired"); 67 | 68 | if (base.IsOwner) 69 | { 70 | // Now we know this is the network instantiated object owned by the local client. 71 | // So let's enable the components to give the local client control over this object. 72 | 73 | if (playerInput) 74 | { 75 | Debug.Log("PlayerScript.OnStartClient(): enabling playerInput on local player."); 76 | playerInput.enabled = true; 77 | } 78 | else Debug.LogError("PlayerScript.OnStartClient(): PlayerInput not found."); 79 | 80 | if (characterController) 81 | { 82 | Debug.Log("PlayerScript.OnStartClient(): enabling CharacterController on local player."); 83 | characterController.enabled = true; 84 | } 85 | else Debug.LogError("PlayerScript.OnStartClient(): CharacterController not found."); 86 | 87 | if (thirdPersonController) 88 | { 89 | Debug.Log("PlayerScript.OnStartClient(): enabling ThirdPersonControllerCSP on local player."); 90 | thirdPersonController.enabled = true; 91 | } 92 | else Debug.LogError("PlayerScript.OnStartClient(): ThirdPersonControllerCSP not found."); 93 | 94 | if (cheat) 95 | { 96 | Debug.Log("PlayerScript.OnStartClient(): enabling cheat on local player."); 97 | cheat.enabled = true; 98 | } 99 | 100 | // Now we need to link the Cinemachine camera to this gameobject: 101 | GameObject playerCameraRoot = this.transform.Find("PlayerCameraRoot").gameObject; 102 | 103 | GameObject playerFollowCamera = GameObject.Find("PlayerFollowCamera"); 104 | 105 | if(playerCameraRoot && playerFollowCamera) 106 | { 107 | CinemachineVirtualCamera cinemachineVirtualCamera = playerFollowCamera.GetComponent(); 108 | if (cinemachineVirtualCamera) 109 | { 110 | // Let the camera follow this local player 111 | cinemachineVirtualCamera.Follow = playerCameraRoot.transform; 112 | 113 | Vector3 newCameraPosition = new Vector3(this.transform.position.x+0.2f, 1.375f, this.transform.position.z-4.0f); 114 | 115 | // change the camera position close to the player's position 116 | playerFollowCamera.transform.SetPositionAndRotation(newCameraPosition, this.transform.rotation); 117 | 118 | } 119 | else Debug.LogError("PlayerScript.OnStartClient(): CinemachineVirtualCamera component found."); 120 | 121 | } 122 | else Debug.LogError("PlayerScript.OnStartClient(): PlayerCameraRoot or PlayerFollowCamera not found."); 123 | 124 | } 125 | 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Goodgulf/Part 15/Simple cheat/Cheat.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | public class Cheat : MonoBehaviour 6 | { 7 | /* 8 | * This is a simple cheat script which waits for the 1 key to be pressed then moves the player forward. 9 | * This is done outside of the regular character controller's input/move process and would be a typical 10 | * method to cheat in a game. 11 | * 12 | */ 13 | void Update() 14 | { 15 | if (Input.GetKeyDown(KeyCode.Alpha1)) 16 | { 17 | Debug.LogWarning("Cheating on Client!"); 18 | transform.position += transform.forward*3; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Goodgulf/Part 15/StartNetwork.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using FishNet.Managing; 4 | using UnityEngine; 5 | 6 | public class StartNetwork : MonoBehaviour 7 | { 8 | /* 9 | * This is a simple script to star the Fishnet Networking server and client. The StartNetworking method is 10 | * called from the NetworkInputControl script which is attached to the GetInput game object. 11 | * 12 | * Attach this script to the NetworkManager object. 13 | */ 14 | 15 | private NetworkManager _networkManager; 16 | 17 | // Start is called before the first frame update 18 | void Start() 19 | { 20 | _networkManager = FindObjectOfType(); 21 | } 22 | 23 | public void StartNetworking() 24 | { 25 | if (_networkManager == null) 26 | return; 27 | 28 | Debug.Log("Starting Server"); 29 | 30 | _networkManager.ServerManager.StartConnection(); 31 | 32 | Debug.Log("Starting Client"); 33 | 34 | _networkManager.ClientManager.StartConnection(); 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Goodgulf/Part 16/GamePlayers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Codice.Client.BaseCommands; 5 | using FishNet.Connection; 6 | using HeathenEngineering.SteamworksIntegration; 7 | using Steamworks; 8 | using UnityEngine; 9 | using Sirenix.OdinInspector; 10 | 11 | namespace Goodgulf.Networking 12 | { 13 | [Serializable] 14 | public class GamePlayer 15 | { 16 | /* 17 | * This class is used to collect both NetworkConnection and Steam UserData in order for the server 18 | * to be able to match steam users to their network connections. 19 | * 20 | */ 21 | public NetworkConnection networkConnection; 22 | public UserData userData; 23 | 24 | public GamePlayer(NetworkConnection conn, UserData user) 25 | { 26 | networkConnection = conn; 27 | userData = user; 28 | } 29 | } 30 | 31 | public class GamePlayers : MonoBehaviour 32 | { 33 | /* 34 | * This is the class where we collect all GamePlayer instances into a dictionary. 35 | * You can expand this class to query player and connection data. 36 | * 37 | */ 38 | 39 | private Dictionary AllPlayers = new Dictionary(); 40 | 41 | public static GamePlayers Instance { get; private set; } 42 | 43 | private LobbyManager lobbyManager = null; 44 | 45 | void Awake() 46 | { 47 | if (Instance != null && Instance != this) 48 | { 49 | Destroy(this); 50 | return; 51 | } 52 | Instance = this; 53 | 54 | lobbyManager = GameObject.FindObjectOfType(); 55 | if (lobbyManager == null) 56 | { 57 | Debug.LogError("GamePlayers.Awake(): cannot find LobbyManager"); 58 | return; 59 | } 60 | 61 | } 62 | 63 | /* 64 | * The AddGamePlayer method is called to add a new player to the dictionary. 65 | * Typically it will be called from a ServerRPC, collecting player data on the server. 66 | */ 67 | public void AddGamePlayer(NetworkConnection conn, UserData user) 68 | { 69 | CSteamID key = user.id; 70 | if (AllPlayers.ContainsKey(key)) 71 | { 72 | Debug.LogWarning("GamePlayers.AddGamePlayer(): trying to add a player with the same key "+key.ToString()); 73 | } 74 | else 75 | { 76 | GamePlayer gamePlayer = new GamePlayer(conn, user); 77 | AllPlayers.Add(key, gamePlayer); 78 | Debug.Log("GamePlayers.AddGamePlayer: just added a player to the game list ("+user.Name+",connection client id="+conn.ClientId+")"); 79 | } 80 | } 81 | 82 | public GamePlayer GetGamePlayer(CSteamID key) 83 | { 84 | if (AllPlayers.ContainsKey(key)) 85 | { 86 | return AllPlayers[key]; 87 | } 88 | return null; 89 | } 90 | 91 | /* 92 | * This is an example function to gather player information. It iterates through the dictionary and returns 93 | * a string with all player information including its network connection. 94 | * 95 | * Since I defined a meta data field "character" on Steam Player level in my lobby manager script I also 96 | * can show that data by linking up to the Lobby Manager. 97 | */ 98 | public string GetPlayersDebugLog() 99 | { 100 | string result = ""; 101 | 102 | foreach (KeyValuePair entry in AllPlayers) 103 | { 104 | // do something with entry.Value or entry.Key 105 | 106 | result += "Player ID "+entry.Key.ToString() + " on connection " +entry.Value.networkConnection.ClientId.ToString() 107 | + ": name = "+entry.Value.userData.Name 108 | + Environment.NewLine; 109 | 110 | // Find this player in the lobby manager members 111 | LobbyMember player = Array.Find(lobbyManager.Lobby.Members, element => element.user.id.Equals(entry.Key)); 112 | 113 | result += "* LobbyMember: " + player.user.Name+ Environment.NewLine; 114 | 115 | // If you know the key for a user meta data field you can also retrieve it: 116 | result += "* Member Meta Data: character = " + player["character"]; 117 | 118 | } 119 | 120 | return result; 121 | } 122 | 123 | } 124 | } -------------------------------------------------------------------------------- /Goodgulf/Part 16/NetworkedDoor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using FishNet.Object; 4 | using FishNet.Object.Synchronizing; 5 | using UnityEngine; 6 | 7 | namespace Goodgulf.Networking 8 | { 9 | public class NetworkedDoor : NetworkBehaviour 10 | { 11 | [SerializeField] private Animator Door = null; 12 | 13 | // This variable is synchronized between the server and all the clients and indicates teh state of the door (open or closed) 14 | [SyncVar(OnChange = nameof(On_IsDoorOpenChanged))] 15 | private bool IsDoorOpen = false; 16 | 17 | [ServerRpc(RequireOwnership = false)] 18 | public void RpcRequestDoorChangeState(bool OpenTheDoor) 19 | { 20 | // A client requests the door to be opened (OpenTheDoor==true) or closed (OpenTheDoor==false) 21 | if (IsDoorOpen != OpenTheDoor) 22 | { 23 | // We only need to act if: 24 | // IsDoorOpen == true and OpenTheDoor == false (we want to close the door) 25 | // IsDoorOpen == false and OpenTheDoor == true (we want to open the door) 26 | 27 | IsDoorOpen = OpenTheDoor; 28 | // This should trigger the OnChange event of the SyncVar on the client 29 | } 30 | } 31 | 32 | // This is the OnChange event which is triggered on the client (and the server where we ignore it) 33 | private void On_IsDoorOpenChanged(bool prev, bool next, bool asServer) 34 | { 35 | if (!asServer) 36 | { 37 | if (prev == false && next == true) 38 | { 39 | // We're opening the door 40 | Debug.Log("Opening Door"); 41 | // Now play the animation 42 | Door.Play("DoorOpening", 0, 0.0f); 43 | } 44 | else if (prev == true && next == false) 45 | { 46 | // We're opening the door 47 | Debug.Log("Closing Door"); 48 | Door.Play("DoorClosing", 0, 0.0f); 49 | } 50 | } 51 | } 52 | 53 | public bool GetIsDoorOpen() 54 | { 55 | return IsDoorOpen; 56 | } 57 | 58 | 59 | 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Goodgulf/Part 16/NetworkedTriggerDoorController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using Crest; 4 | using FishNet.Object; 5 | using FishNet.Object.Synchronizing; 6 | using UnityEngine; 7 | 8 | 9 | namespace Goodgulf.Networking 10 | { 11 | 12 | public class NetworkedTriggerDoorController : MonoBehaviour 13 | { 14 | // These triggers indicate the action of the trigger: open or close the door when the player 15 | // enters this trigger 16 | [SerializeField] private bool openTrigger = false; 17 | [SerializeField] private bool closeTrigger = false; 18 | 19 | // Link to the NetworkDoor script which will do the actual opening/closing of the door 20 | [SerializeField] private NetworkedDoor networkedDoor = null; 21 | 22 | private void Awake() 23 | { 24 | if(networkedDoor==null) 25 | Debug.LogError("NetworkedTriggerDoorController.Awake(): networkedDoor not setup."); 26 | } 27 | 28 | 29 | private void OnTriggerEnter(Collider other) 30 | { 31 | Debug.Log("Door trigger entered by "+other.tag); 32 | 33 | if (other.CompareTag("Player")) 34 | { 35 | // We've made sure it's a player triggering the event 36 | bool IsDoorOpen = networkedDoor.GetIsDoorOpen(); 37 | 38 | // Now check if we actually need to do something (so we don't try to open an already open door) 39 | if (openTrigger && !IsDoorOpen) 40 | { 41 | Debug.Log("Request Opening Door from trigger"); 42 | 43 | // Call the server RPC: 44 | networkedDoor.RpcRequestDoorChangeState(true); 45 | } 46 | else if (closeTrigger && IsDoorOpen) 47 | { 48 | Debug.Log("Request Closing Door from trigger"); 49 | networkedDoor.RpcRequestDoorChangeState(false); 50 | 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Goodgulf/Part 16/PlayerScript.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using FishNet.Object; 4 | using UnityEngine; 5 | using Invector; 6 | using Invector.vCamera; 7 | using Invector.vCharacterController; 8 | using Invector.vCharacterController.vActions; 9 | using Crest; 10 | using CompassNavigatorPro; 11 | using FishNet; 12 | using FishNet.Connection; 13 | using FishNet.Managing; 14 | using Goodgulf.Gamelogic; 15 | using Goodgulf.Networking; 16 | using HeathenEngineering.SteamworksIntegration; 17 | using UserAPI = HeathenEngineering.SteamworksIntegration.API.User.Client; 18 | 19 | public class PlayerScript : NetworkBehaviour 20 | { 21 | private vThirdPersonController _vThirdPersonController; 22 | private vFootStep _vFootStep; 23 | private vHitDamageParticle _vHitDamageParticle; 24 | private vHeadTrack _vHeadTrack; 25 | private vLadderAction _vLadderAction; 26 | private vGenericAction _vGenericAction; 27 | private vThirdPersonInput _vThirdPersonInput; 28 | private vThirdPersonCamera _vThirdPersonCamera; 29 | private OceanRenderer oceanRenderer; 30 | private Camera playerCamera; 31 | private CompassPro compassPro; 32 | 33 | private NetworkManager networkManager; 34 | private void Awake() 35 | { 36 | Debug.Log("PlayerScript.Awake(): event fired"); 37 | 38 | networkManager = InstanceFinder.NetworkManager; 39 | 40 | // Start with disabling all the Invector components on the instantiated object 41 | 42 | _vThirdPersonController = GetComponent(); 43 | _vFootStep = GetComponent(); 44 | _vHitDamageParticle = GetComponent(); 45 | _vHeadTrack = GetComponent(); 46 | _vLadderAction = GetComponent(); 47 | _vGenericAction = GetComponent(); 48 | _vThirdPersonCamera = GetComponentInChildren(); 49 | _vThirdPersonInput = GetComponent(); 50 | 51 | _vThirdPersonController.enabled = false; 52 | _vFootStep.enabled = false; 53 | _vHitDamageParticle.enabled = false; 54 | _vHeadTrack.enabled = false; 55 | _vLadderAction.enabled = false; 56 | _vGenericAction.enabled = false; 57 | _vThirdPersonInput.enabled = false; 58 | 59 | _vThirdPersonCamera.enabled = false; 60 | _vThirdPersonCamera.gameObject.SetActive(false); 61 | 62 | oceanRenderer = OceanRenderer.Instance; 63 | } 64 | 65 | 66 | // If you are coming from Mirror instead of using OnLocalPlayer use OnStartClient with a base.IsOwner check. 67 | public override void OnStartClient() 68 | { 69 | base.OnStartClient(); 70 | 71 | Debug.Log("PlayerScript.OnStartClient(): event fired"); 72 | 73 | if (base.IsOwner) 74 | { 75 | // So this is the locally owned player object. Enable all Invector controls for this one: 76 | 77 | _vThirdPersonController.enabled = true; 78 | _vFootStep.enabled = true; 79 | _vHitDamageParticle.enabled = true; 80 | _vHeadTrack.enabled = true; 81 | _vLadderAction.enabled = true; 82 | _vGenericAction.enabled = true; 83 | _vThirdPersonInput.enabled = true; 84 | 85 | _vThirdPersonCamera.gameObject.SetActive(true); 86 | _vThirdPersonCamera.enabled = true; 87 | _vThirdPersonCamera.SetMainTarget(this.transform); 88 | 89 | //Enviro.EnviroManager.instance.AssignAndStart(this.gameObject, playerCamera); 90 | playerCamera = _vThirdPersonCamera.transform.GetChild(0).GetComponent(); 91 | 92 | if (!playerCamera) 93 | { 94 | Debug.LogError("PlayerScript.OnStartClient(): cannot find player Camera."); 95 | } 96 | 97 | if (oceanRenderer) 98 | { 99 | oceanRenderer.ViewCamera = playerCamera; 100 | } 101 | else Debug.LogWarning("PlayerScript.OnStartClient(): cannot find OceanRenderer."); 102 | 103 | compassPro = CompassPro.instance; 104 | compassPro.cameraMain = playerCamera; 105 | compassPro.miniMapFollow = this.transform; 106 | 107 | 108 | // Now send player information to server: Steam user id and network connection (= base.Owner) 109 | var user = UserAPI.Id; 110 | SendPlayerInfo(base.Owner, user); 111 | } 112 | } 113 | 114 | // This is the server RPC which adds the steam userid and network connection to the GamePlayers class. 115 | [ServerRpc] 116 | private void SendPlayerInfo(NetworkConnection conn, UserData steamUser) 117 | { 118 | GamePlayers.Instance.AddGamePlayer(conn, steamUser); 119 | } 120 | 121 | void Update() 122 | { 123 | if (base.IsOwner) 124 | { 125 | if (Input.GetKeyDown(KeyCode.Alpha1)) 126 | { 127 | // Call an RPC when the player presses the 1-key 128 | RpcCastSpell(base.Owner, 0); 129 | } 130 | 131 | if (Input.GetKeyDown(KeyCode.Alpha2)) 132 | { 133 | RpcCastSpell(base.Owner, 1); 134 | } 135 | 136 | if (Input.GetKeyDown(KeyCode.Comma)) 137 | { 138 | string debug = GamePlayers.Instance.GetPlayersDebugLog(); 139 | Debug.Log(debug); 140 | } 141 | } 142 | 143 | } 144 | 145 | [ServerRpc] 146 | public void RpcCastSpell(NetworkConnection conn, int spellIndex) 147 | { 148 | // The Spells class contains a list of spells and their attributes 149 | Spells spells = GetComponent(); 150 | if (spells != null) 151 | { 152 | Spell spell = spells.GetSpell(spellIndex); 153 | 154 | if (spell != null) 155 | { 156 | // some spells need a target to be defined, this target is a prefab also instantiated by the server 157 | // in front of the player 158 | NetworkObject ntarget=null; 159 | 160 | // STEP 1: instantiate the spell target object if we need it 161 | if (spell.usesTarget) 162 | { 163 | Vector3 targetPosition = transform.position + transform.forward * spell.targetOffset.z + 164 | transform.up * spell.targetOffset.y; 165 | 166 | ntarget = networkManager.GetPooledInstantiated(spell.targetPrefab, true); 167 | ntarget.transform.SetPositionAndRotation(targetPosition, Quaternion.identity); 168 | 169 | networkManager.ServerManager.Spawn(ntarget, conn); 170 | } 171 | 172 | // STEP 2: instantiate the spell itself. A prefab with the visual effects is instantiated on the server: 173 | 174 | NetworkObject networkedSpell = networkManager.GetPooledInstantiated(spell.prefab, true); 175 | networkedSpell.transform.SetPositionAndRotation(this.transform.position + spell.sourceOffset, this.transform.rotation); 176 | networkManager.ServerManager.Spawn(networkedSpell, conn); 177 | 178 | 179 | // STEP 3: link the spell to the target (if spell is targeted) 180 | if (spell.usesTarget && ntarget!=null) 181 | { 182 | RFX1_Target target = networkedSpell.GetComponent(); 183 | if (target != null) 184 | { 185 | // link the spell's target on the server side 186 | target.Target = ntarget.gameObject; 187 | 188 | } 189 | else Debug.LogWarning("PlayerScript.RpcCastSpell(): spell is targeted but no target script found."); 190 | 191 | SpellObject spellObject = networkedSpell.GetComponent(); 192 | if (spellObject == null) 193 | { 194 | Debug.LogError("PlayerScript.RpcCastSpell(): spell object not found."); 195 | } 196 | else 197 | { 198 | // link the instantiated networked spell to the networked target object so the client will link up too. 199 | spellObject.targetId = ntarget.ObjectId; 200 | 201 | } 202 | } 203 | 204 | // Set duration on spell and target, destroy it after duration in seconds: 205 | StartCoroutine(DestroySpell(networkedSpell, ntarget, spell.duration)); 206 | 207 | } 208 | } 209 | } 210 | 211 | IEnumerator DestroySpell(NetworkObject nspell, NetworkObject ntarget, float waitTime) 212 | { 213 | yield return new WaitForSeconds(waitTime); 214 | 215 | nspell.Despawn(DespawnType.Pool); 216 | ntarget.Despawn(DespawnType.Pool); 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /Goodgulf/Part 17/MatchChat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Cysharp.Threading.Tasks; 5 | using FishNet.Object; 6 | using Goodgulf.Graphics; 7 | using Invector.vCharacterController; 8 | using Sirenix.OdinInspector; 9 | using TMPro; 10 | using UnityEngine; 11 | using UnityEngine.UI; 12 | 13 | namespace Goodgulf.Networking 14 | { 15 | 16 | public enum ChatType 17 | { 18 | ChatSay, 19 | ChatShout 20 | } 21 | 22 | 23 | public class MatchChat : NetworkBehaviour 24 | { 25 | public static MatchChat Instance { get; private set; } 26 | 27 | [BoxGroup("Input")] 28 | public TMP_InputField inputLine; 29 | [BoxGroup("Input")] public 30 | Button inputShoutButton; 31 | [BoxGroup("Input")] public 32 | Button inputSayButton; 33 | 34 | [BoxGroup("Input")] [SerializeField] 35 | private bool inputFieldEnabled = false; 36 | 37 | [BoxGroup("Output")] 38 | public GameObject chatLinePrefab; 39 | [BoxGroup("Output")] 40 | public Transform parentForChatLines; 41 | [BoxGroup("Output")] 42 | public float duration; 43 | [BoxGroup("Output")] 44 | public float fadeDuration; 45 | 46 | [BoxGroup("Chat Settings")] 47 | public float sayRange = 25f; 48 | 49 | private GameObject localOwner; 50 | private vThirdPersonInput playerInputScriptOwner; 51 | private bool disableInput = false; 52 | 53 | 54 | void Awake() 55 | { 56 | if (Instance != null && Instance != this) 57 | { 58 | Destroy(this); 59 | return; 60 | } 61 | 62 | Instance = this; 63 | 64 | inputLine.gameObject.SetActive(inputFieldEnabled); 65 | inputShoutButton.gameObject.SetActive(inputFieldEnabled); 66 | inputSayButton.gameObject.SetActive(inputFieldEnabled); 67 | } 68 | 69 | 70 | public void AssignLocalOwner(GameObject owner) 71 | { 72 | localOwner = owner; 73 | } 74 | 75 | public void AssignPlayerScript(vThirdPersonInput playerInputScript) 76 | { 77 | playerInputScriptOwner = playerInputScript; 78 | } 79 | 80 | // Use DisableInput() in other scripts checking for pressed keys to pause these until inputs are enabled again 81 | public bool DisableInput() 82 | { 83 | return disableInput; 84 | } 85 | 86 | [Client] 87 | public void Update() 88 | { 89 | if (Input.GetKeyDown(KeyCode.Period)) 90 | { 91 | inputFieldEnabled = !inputFieldEnabled; 92 | inputLine.gameObject.SetActive(inputFieldEnabled); 93 | inputShoutButton.gameObject.SetActive(inputFieldEnabled); 94 | inputSayButton.gameObject.SetActive(inputFieldEnabled); 95 | 96 | // Also disable this player's Input 97 | playerInputScriptOwner.enabled = !inputFieldEnabled; 98 | disableInput = inputFieldEnabled; 99 | } 100 | } 101 | 102 | public void OnInputEnterShout() 103 | { 104 | // This method is called by the Shout button OnClick() event 105 | RpcSendChatLine(inputLine.text, localOwner, ChatType.ChatShout); 106 | inputLine.text = ""; 107 | } 108 | 109 | public void OnInputEnterSay() 110 | { 111 | // This method is called by the Say button OnClick() event 112 | RpcSendChatLine(inputLine.text, localOwner, ChatType.ChatSay); 113 | inputLine.text = ""; 114 | } 115 | 116 | public override void OnStartClient() 117 | { 118 | base.OnStartClient(); 119 | } 120 | 121 | [ServerRpc(RequireOwnership = false)] 122 | private void RpcSendChatLine(string line, GameObject Sender, ChatType chatType) 123 | { 124 | RpcSendChatLineToAllObservers(line, Sender, chatType); 125 | } 126 | 127 | [ObserversRpc] 128 | private void RpcSendChatLineToAllObservers(string line, GameObject Sender, ChatType chatType) 129 | { 130 | // Debug.Log("MatchChat.RpcSendChatLineToAllObservers(): Chat received: "+line); 131 | 132 | if (chatType == ChatType.ChatSay) 133 | { 134 | // Check proximity with owner and receiving network object 135 | if (Vector3.Distance(localOwner.transform.position, Sender.transform.position) > sayRange) 136 | return; 137 | } 138 | 139 | 140 | // Collect all existing chat lines 141 | int childrenCount = parentForChatLines.childCount; 142 | 143 | List children = new List(); 144 | for (int i = 0; i < childrenCount; i++) 145 | { 146 | GameObject child = parentForChatLines.GetChild(i).gameObject; 147 | TMP_Text childText = child.GetComponent(); 148 | if (childText != null) 149 | { 150 | children.Add(child); 151 | } 152 | } 153 | 154 | // Move existing lines up and delete EOL chat lines 155 | for (int i=children.Count - 1; i >= 0;i--) 156 | { 157 | GameObject child = children[i]; 158 | MatchChatLine mLine = child.GetComponent(); 159 | if (mLine.deleteMe) 160 | { 161 | children.RemoveAt(i); 162 | Destroy(child); 163 | } 164 | else 165 | { 166 | // This is a chatline so move it up 167 | RectTransform rectTransform = child.GetComponent(); 168 | Vector2 position = rectTransform.anchoredPosition; 169 | rectTransform.anchoredPosition = new Vector2(position.x, position.y + 35); 170 | } 171 | } 172 | 173 | // Create the new chat line from the prefab chatLinePrefab 174 | GameObject chatLine = Instantiate(chatLinePrefab, parentForChatLines); 175 | TMP_Text tmpText = chatLine.GetComponent(); 176 | tmpText.text = line; 177 | 178 | MatchChatLine newMatchChatLine = chatLine.GetComponent(); 179 | newMatchChatLine.duration = duration; 180 | newMatchChatLine.fadeDuration = fadeDuration; 181 | newMatchChatLine.StartDuration(); 182 | } 183 | 184 | } 185 | 186 | 187 | 188 | } -------------------------------------------------------------------------------- /Goodgulf/Part 17/MatchChatLine.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using TMPro; 4 | using UnityEngine; 5 | 6 | 7 | namespace Goodgulf.Graphics 8 | { 9 | public class MatchChatLine : MonoBehaviour 10 | { 11 | public float duration; 12 | public float fadeDuration; 13 | public bool deleteMe = false; 14 | 15 | private bool startCounting = false; 16 | private bool startFading = false; 17 | private float currentTime = 0f; 18 | private float currentFadeTime = 0f; 19 | private float alpha; 20 | 21 | private TMP_Text matchLine; 22 | 23 | public void StartDuration() 24 | { 25 | startCounting = true; 26 | } 27 | 28 | // Start is called before the first frame update 29 | void Start() 30 | { 31 | matchLine = GetComponent(); 32 | } 33 | 34 | // Update is called once per frame 35 | void Update() 36 | { 37 | if(!startCounting) 38 | return; 39 | 40 | currentTime += Time.deltaTime; 41 | if (currentTime > duration) 42 | startFading = true; 43 | 44 | if (startFading) 45 | { 46 | if (currentFadeTime < fadeDuration) 47 | { 48 | alpha = Mathf.Lerp(1f, 0f, currentFadeTime / fadeDuration); 49 | 50 | matchLine.color = new Color(matchLine.color.r, matchLine.color.g, matchLine.color.b, 51 | alpha); 52 | currentFadeTime += Time.deltaTime; 53 | } 54 | else 55 | { 56 | deleteMe = true; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Goodgulf/Part 19/MatchClientManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using FishNet.Object; 5 | using UnityEngine; 6 | using TMPro; 7 | using UnityEngine.UI; 8 | using ConsoleUtility; 9 | using FishNet; 10 | using FishNet.Connection; 11 | using FishNet.Managing; 12 | using FishNet.Managing.Scened; 13 | using FishNet.Transporting; 14 | using Goodgulf.UI; 15 | using UnityEngine.SceneManagement; 16 | 17 | namespace Goodgulf.Networking 18 | { 19 | /* 20 | * The MatchClientManager is used to manage the match from the client's perspective. Next to the Client Status UI 21 | * it also deals (mostly) with the end of the match. 22 | * 23 | * This script should be paired with the MatchServerManager which deals with the server events. 24 | * 25 | * The match workflow looks like this from a client perspective: 26 | * 27 | * Server - unload scene 28 | * Client - unload scene 29 | * Client - when unload scene completes, disconnect client 30 | * 31 | * Note that this script makes use of the ConsoleUtility script: https://github.com/peeweek/net.peeweek.console 32 | */ 33 | 34 | public class MatchClientManager : MonoBehaviour 35 | { 36 | public static MatchClientManager Instance { get; private set; } 37 | 38 | // These are the references to the UI for the ClientStatus which you can show using the F2 key: 39 | public Canvas clientCanvas; 40 | public Image clientStatus; 41 | public TMP_Text clientStatusText; 42 | public TMP_Text connectionText; 43 | public TMP_Text clientLogLine; 44 | public TMP_Text connectionDetails; 45 | 46 | private NetworkManager networkManager; 47 | 48 | private void Awake() 49 | { 50 | #if MatchServerDebug 51 | Debug.Log("MatchClientManager.Awake(): method called"); 52 | #endif 53 | 54 | if (Instance != null && Instance != this) 55 | { 56 | Destroy(this); 57 | return; 58 | } 59 | 60 | Instance = this; 61 | } 62 | 63 | // Set the color in the ClientStatus UI and update its status field 64 | private void SetClientStatusColor(Color color, string status) 65 | { 66 | if (clientStatus) 67 | clientStatus.color = color; 68 | 69 | if (clientStatusText) 70 | clientStatusText.text = status; 71 | } 72 | 73 | // Show the latest client log entry on the ClientStatus UI and log it to the console 74 | private void ClientLog(string entry) 75 | { 76 | if (clientLogLine) 77 | { 78 | clientLogLine.text = entry; 79 | } 80 | 81 | ConsoleUtility.Console.Log("client", entry); 82 | } 83 | 84 | // Show the connection details in the ClientStatus UI for the local client. 85 | // Note that the clientID always shows as -1 86 | // See: https://firstgeargames.com/FishNet/api/api/FishNet.Connection.NetworkConnection.html#FishNet_Connection_NetworkConnection_UNSET_CLIENTID_VALUE 87 | private void ShowConnectionDetails() 88 | { 89 | NetworkConnection connection = networkManager.ClientManager.Connection; 90 | 91 | connectionText.text = connection.ClientId.ToString(); 92 | 93 | connectionDetails.text = "Started=" + networkManager.ClientManager.Started + "ClientId=" + connection.ClientId.ToString() + Environment.NewLine + 94 | " IsOnHost=" + connection.IsHost; 95 | } 96 | 97 | void Start() 98 | { 99 | // Get the NetworkManager instance 100 | networkManager = InstanceFinder.NetworkManager; 101 | 102 | // Hide the ClientStatus UI since at start the client will not be active 103 | if (clientCanvas) 104 | clientCanvas.enabled = false; 105 | 106 | ClientLog("Initiated Client HUD"); 107 | SetClientStatusColor(Color.red, "Stopped"); 108 | 109 | // Use this event to show the various connection states of the client (Starting, Started, etc,..): 110 | networkManager.ClientManager.OnClientConnectionState += ClientManager_OnClientConnectionState; 111 | 112 | // This event is called when the client has loaded the start scenes: 113 | networkManager.SceneManager.OnClientLoadedStartScenes += SceneManager_OnClientLoadedStartScenes; 114 | 115 | // This event is called when the client starts unloading a scene: 116 | networkManager.SceneManager.OnUnloadStart += SceneManager_OnUnloadStart; 117 | 118 | // This event is called when the client ends unloading a scene. 119 | // We use this event to disconnect from the server: 120 | networkManager.SceneManager.OnUnloadEnd += SceneManager_OnUnloadEnd; 121 | 122 | // This event is called when the client starts loading a scene: 123 | networkManager.SceneManager.OnLoadStart += SceneManager_OnLoadStart; 124 | 125 | // This event is called when the client ends loading a scene: 126 | networkManager.SceneManager.OnLoadEnd += SceneManager_OnLoadEnd; 127 | 128 | } 129 | 130 | 131 | private void OnDisable() 132 | { 133 | networkManager.ClientManager.OnClientConnectionState -= ClientManager_OnClientConnectionState; 134 | networkManager.SceneManager.OnClientLoadedStartScenes -= SceneManager_OnClientLoadedStartScenes; 135 | networkManager.SceneManager.OnUnloadStart -= SceneManager_OnUnloadStart; 136 | networkManager.SceneManager.OnUnloadEnd -= SceneManager_OnUnloadEnd; 137 | 138 | networkManager.SceneManager.OnLoadStart -= SceneManager_OnLoadStart; 139 | networkManager.SceneManager.OnLoadEnd -= SceneManager_OnLoadEnd; 140 | } 141 | 142 | 143 | void Update() 144 | { 145 | if (Input.GetKeyDown(KeyCode.F2) && networkManager.IsClient) 146 | { 147 | // Only enable the ClientStatus UI when the client is running 148 | // (the script may be active before the client is running, in my case since I put it in the bootstrap scene) 149 | if (clientCanvas) 150 | clientCanvas.enabled = !clientCanvas.enabled; 151 | 152 | } 153 | } 154 | 155 | private void SceneManager_OnClientLoadedStartScenes(NetworkConnection conn, bool asServer) 156 | { 157 | if (asServer) 158 | return; 159 | 160 | ClientLog("Loaded Start Scenes"); 161 | } 162 | 163 | 164 | // https://fish-networking.gitbook.io/docs/manual/guides/scene-management/scene-events 165 | 166 | private void SceneManager_OnLoadStart(SceneLoadStartEventArgs obj) 167 | { 168 | bool AsServer = obj.QueueData.AsServer; 169 | 170 | ClientLog("Start Load Scene AsServer="+AsServer); 171 | } 172 | 173 | 174 | private void SceneManager_OnLoadEnd(SceneLoadEndEventArgs obj) 175 | { 176 | bool AsServer = obj.QueueData.AsServer; 177 | 178 | ClientLog("End Load Scene AsServer="+AsServer); 179 | } 180 | 181 | private void SceneManager_OnUnloadStart(SceneUnloadStartEventArgs obj) 182 | { 183 | bool AsServer = obj.QueueData.AsServer; 184 | 185 | ClientLog("Start Unload Scene AsServer="+AsServer); 186 | } 187 | 188 | // This is the key event I'm using in the end of Match workflow. The server has sent the unload scene command 189 | // and when unloading is ready we're disconnecting this client 190 | private void SceneManager_OnUnloadEnd(SceneUnloadEndEventArgs obj) 191 | { 192 | bool AsServer = obj.QueueData.AsServer; 193 | 194 | ClientLog("End Unload Scene AsSever="+AsServer); 195 | 196 | // So now the match scene has been unloaded, 197 | // let's return to the lobby scene 198 | // Step 1: disconnect 199 | 200 | networkManager.ClientManager.StopConnection(); 201 | clientCanvas.enabled = false; 202 | 203 | // Step 2: go to the lobby scene 204 | UnityEngine.SceneManagement.SceneManager.LoadScene(2); // Load the Lobby scene 205 | SceneUIFlowManager.Instance.ShowMenu(Menus.LobbyMenu); // make sure any other menus are invisible 206 | 207 | } 208 | 209 | // Use this event to show the state of the client connection in the Client Status UI. 210 | // Usually you can't see it switching between Starting and Started on the client sitting on the host PC. 211 | // However it can be useful if the client is somehow stuck in the process. 212 | private void ClientManager_OnClientConnectionState(ClientConnectionStateArgs obj) 213 | { 214 | if (obj.ConnectionState == LocalConnectionState.Starting) 215 | { 216 | SetClientStatusColor(Color.blue, "Starting"); 217 | ClientLog("Client starting"); 218 | } 219 | else if (obj.ConnectionState == LocalConnectionState.Started) 220 | { 221 | SetClientStatusColor(Color.green, "Started"); 222 | ShowConnectionDetails(); 223 | ClientLog("Client started"); 224 | } 225 | else if (obj.ConnectionState == LocalConnectionState.Stopping) 226 | { 227 | SetClientStatusColor(Color.yellow, "Stopping"); 228 | ClientLog("Client stopping"); 229 | } 230 | else if (obj.ConnectionState == LocalConnectionState.Stopped) 231 | { 232 | SetClientStatusColor(Color.red, "Stopped"); 233 | ClientLog("Client stopped"); 234 | 235 | } 236 | } 237 | 238 | 239 | 240 | 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /Goodgulf/Part 2/GameTimer.cs: -------------------------------------------------------------------------------- 1 | using Mirror; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using UnityEngine.Events; 6 | using TMPro; 7 | 8 | namespace Goodgulf.Networking 9 | { 10 | 11 | public class GameTimer : NetworkBehaviour 12 | { 13 | [SyncVar] 14 | public float timer = 15 * 60; // 15 minutes countdown timer 15 | 16 | private int minutes; 17 | private int seconds; 18 | public string showTime; // The actual timer countdown showing in a 09:37 format 19 | private bool running = true; // Is the timer stil running? 20 | 21 | private TMP_Text clockText; // The timer text shown in the UI 22 | 23 | public UnityEvent ClockReady; // The event which is called when the timer reaches zero 24 | 25 | 26 | void Awake() 27 | { 28 | Debug.Log("GameTimer.Awake(): start"); 29 | 30 | // Find the UI object representing the time 31 | GameObject texttimer = GameObject.Find("textTimer"); 32 | if (texttimer) 33 | { 34 | clockText = texttimer.GetComponent(); 35 | 36 | if (clockText == null) 37 | Debug.LogError("GameTimer.Awake(): Cannot find TMP_Text."); 38 | } 39 | else Debug.LogError("GameTimer.Awake(): Cannot find textTimer."); 40 | } 41 | 42 | void Update() 43 | { 44 | if (!running) 45 | { 46 | return; 47 | } 48 | 49 | // Decrease the timer value as time ticks by: 50 | if (timer > 0) 51 | timer -= Time.deltaTime; 52 | 53 | // Convert the timer to a string 54 | minutes = Mathf.FloorToInt(timer / 60F); 55 | seconds = Mathf.FloorToInt(timer - minutes * 60); 56 | showTime = string.Format("{0:00}:{1:00}", minutes, seconds); 57 | 58 | if (timer < 0) 59 | { 60 | running = false; 61 | showTime = "00:00"; 62 | // Callback event when the timer reaches zero 63 | ClockReady.Invoke(); 64 | } 65 | 66 | if (clockText) 67 | { 68 | clockText.text = showTime; 69 | } 70 | else Debug.LogError("GameTimer.Update(): timer = null."); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /Goodgulf/Part 2/NetworkManagerMyGame.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.Events; 5 | using Mirror; 6 | 7 | namespace Goodgulf.Networking 8 | { 9 | 10 | public class NetworkManagerMyGame : NetworkManager 11 | { 12 | public GameObject prefabTimer; 13 | private GameObject timer; 14 | 15 | public override void OnServerAddPlayer(NetworkConnection conn) 16 | { 17 | // Call the base method which actually spawns the players on the playe spawn locations 18 | base.OnServerAddPlayer(conn); 19 | 20 | // spawn a game timer as soon as we have two players 21 | if (numPlayers == 2) 22 | { 23 | Debug.Log("NetworkManagerMyGame.OnServerAddPlayer(): spawn timer."); 24 | 25 | // The Timer prefab is picked from the registered spawnable prefabs 26 | timer = Instantiate(spawnPrefabs.Find(prefab => prefab.name == "GameTimer")); 27 | 28 | // Add a callback when the timer reaches 0 29 | GameTimer gameTimer = timer.GetComponent(); 30 | if (gameTimer) 31 | gameTimer.ClockReady.AddListener(EndOfTimer); 32 | 33 | // Now spawn it on all clients 34 | NetworkServer.Spawn(timer); 35 | } 36 | } 37 | 38 | public void EndOfTimer() 39 | { 40 | Debug.Log("NetworkManagerMyGame.EndOfTimer(): timer ready."); 41 | 42 | // End of match code here 43 | } 44 | 45 | public override void OnServerDisconnect(NetworkConnection conn) 46 | { 47 | // destroy time 48 | if (timer != null) 49 | NetworkServer.Destroy(timer); 50 | 51 | // call base functionality (actually destroys the player) 52 | base.OnServerDisconnect(conn); 53 | } 54 | 55 | 56 | } 57 | } -------------------------------------------------------------------------------- /Goodgulf/Part 2/PlayerScript.cs: -------------------------------------------------------------------------------- 1 | using Invector.vCharacterController; 2 | using Mirror; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using UnityEngine; 6 | 7 | namespace Goodgulf.Networking 8 | { 9 | 10 | public class PlayerScript : NetworkBehaviour 11 | { 12 | public TextMesh playerNameText; // The text floating above the player object showing its name 13 | public GameObject floatingInfo; // The placeholder object containing above playerNameText 14 | 15 | // SyncVars are properties of classes that inherit from NetworkBehaviour, 16 | // which are synchronized from the server to clients. 17 | [SyncVar(hook = nameof(OnNameChanged))] // You can consider the hook to be similar to the setter, it gets called when the value changes on the client 18 | public string playerName; // Player name which we'll float over the player object 19 | 20 | [SyncVar(hook = nameof(OnColorChanged))] 21 | public Color playerColor = Color.white; // The color of the floating playerNameText 22 | 23 | public KeyCode spellKey1 = KeyCode.Alpha1; // The keyboard commands we'll use (for now) to trigger spells 24 | public KeyCode spellKey2 = KeyCode.Alpha2; 25 | public KeyCode spellKey3 = KeyCode.Alpha3; 26 | 27 | public GameObject spell1Prefab; // The prefabs to be instantiated when we trigger a spell 28 | public GameObject spell2Prefab; // Note: these prefabs also need to be registered in the NetworkManager as spawnable prefab 29 | public GameObject spell3Prefab; 30 | 31 | public AudioSource announce; // This clip will be played when a player enters the game. The audio source needs to be tagged as "Announce". 32 | 33 | public vThirdPersonCamera cameraPrefab; // Assign a reference to the Invector camera prefab to this property. 34 | 35 | private vThirdPersonInput _vThirdPersonInput; // Reference to the Invector Input system which we disable on all players except the Local Player. 36 | private vThirdPersonCamera _vThirdPersonCamera; // Reference to the Invector camera we spawn based on the above prefab. 37 | 38 | void Awake() 39 | { 40 | Debug.Log("PlayerScript.Awake(): event fired"); 41 | 42 | // Find the announcement audio source component in the hierarchy sicne we can't assign this to the non instantiated prefab. 43 | announce = GameObject.FindGameObjectWithTag("Announce").GetComponent(); 44 | 45 | // Find the Invector Input Manager component 46 | _vThirdPersonInput = GetComponent(); 47 | if (_vThirdPersonInput) 48 | { 49 | // Disable input by default, enable later for local players in OnStartLocalPlayer() event. 50 | // If we don't do this the input handler script will mess up the 3rd person camera setup. 51 | _vThirdPersonInput.enabled = false; 52 | } 53 | else Debug.LogError("PlayerScript.Start: could not find vThirdPersonInput."); 54 | } 55 | 56 | public override void OnStartLocalPlayer() 57 | { 58 | Debug.Log("PlayerScript.OnStartLocalPlayer(): event fired"); 59 | 60 | // Deactivate the main camera (which we see at the start of the scene) since the 3rd person camera needs to take over now. 61 | Camera.main.gameObject.SetActive(false); 62 | 63 | // Debug.Log("PlayerScript.OnStartLocalPlayer(): isLocalPlayer = "+isLocalPlayer); 64 | 65 | // Now we instanatiate the third person camera for the local player only: 66 | _vThirdPersonCamera = Instantiate(cameraPrefab, new Vector3(0, 0, 0), Quaternion.identity); 67 | 68 | if (_vThirdPersonCamera) 69 | { 70 | Debug.Log("PlayerScript.OnStartLocalPlayer(): setting main camera to localPlayer"); 71 | 72 | // Now link the camera to this player: 73 | _vThirdPersonCamera.SetMainTarget(this.transform); 74 | } 75 | else Debug.Log("PlayerScript.OnStartLocalPlayer(): _vThirdPersonCamera = null"); 76 | 77 | if (_vThirdPersonInput) 78 | { 79 | // Since this is the local player we'll need to enable the input handler 80 | _vThirdPersonInput.enabled = true; 81 | } 82 | 83 | if(announce) 84 | { 85 | announce.Play(); 86 | } 87 | 88 | 89 | // For the local player - hide the the placeholder object for the playerNameText (above the player) 90 | floatingInfo.transform.localPosition = new Vector3(0, -10.0f, 0.6f); 91 | floatingInfo.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f); 92 | 93 | // Pick a random name and color for the player 94 | string name = "Player" + Random.Range(100, 999); 95 | Color color = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f)); 96 | CmdSetupPlayer(name, color); 97 | } 98 | 99 | // These two "events" are called by the SyncVar hook when the value changes 100 | void OnNameChanged(string _Old, string _New) 101 | { 102 | playerNameText.text = playerName; 103 | } 104 | 105 | void OnColorChanged(Color _Old, Color _New) 106 | { 107 | playerNameText.color = _New; 108 | } 109 | 110 | [Command] 111 | public void CmdSetupPlayer(string _name, Color _col) 112 | { 113 | // Command: call this from a client to run this function on the server. 114 | // So the player sends info to server, then server updates sync vars which handles it on all clients 115 | playerName = _name; 116 | playerColor = _col; 117 | } 118 | 119 | void Update() 120 | { 121 | if (!isLocalPlayer) 122 | { 123 | // make non-local players run this 124 | floatingInfo.transform.LookAt(Camera.main.transform); 125 | return; 126 | } 127 | else 128 | { 129 | // Check the input from the user. A bit of dirty code to be updated later 130 | if (Input.GetKeyDown(spellKey1)) 131 | { 132 | CmdSpell(1); 133 | } 134 | else 135 | if (Input.GetKeyDown(spellKey2)) 136 | { 137 | CmdSpell(2); 138 | } 139 | if (Input.GetKeyDown(spellKey3)) 140 | { 141 | CmdSpell(3); 142 | } 143 | 144 | } 145 | } 146 | 147 | // This command is run on the server: instantiate one of the spell prefabs then spawn these on all clients 148 | [Command] 149 | void CmdSpell(int s) 150 | { 151 | GameObject spell; 152 | if (s == 1) 153 | { 154 | spell = Instantiate(spell1Prefab, this.transform.position, this.transform.rotation); 155 | } 156 | else if(s == 2) 157 | { 158 | spell = Instantiate(spell2Prefab, this.transform.position+this.transform.forward*2, this.transform.rotation); 159 | } 160 | else spell = Instantiate(spell3Prefab, this.transform.position + this.transform.forward * 3, this.transform.rotation); 161 | 162 | NetworkServer.Spawn(spell); // Spawn on all clients 163 | RpcOnSpell(); // Call any code that needs to run on the clients 164 | } 165 | 166 | // this is called on the player that cast a spell for all observers 167 | [ClientRpc] 168 | void RpcOnSpell() 169 | { 170 | // The server uses a Remote Procedure Call (RPC) to run that function on clients. 171 | // Do something 172 | } 173 | 174 | 175 | } 176 | } -------------------------------------------------------------------------------- /Goodgulf/Part 2/SpellStatic.cs: -------------------------------------------------------------------------------- 1 | using Mirror; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | 7 | namespace Goodgulf.Networking 8 | { 9 | public class SpellStatic : NetworkBehaviour 10 | { 11 | // Use SpellStatic on the spell effect prefabs to give them a limited lifetime 12 | 13 | public float destroyDelaySeconds = 10; 14 | 15 | public override void OnStartServer() 16 | { 17 | // As soon a the object is created on the server we initiate a delayed delete: 18 | Invoke(nameof(DestroySelf), destroyDelaySeconds); 19 | } 20 | 21 | // destroy for everyone on the server 22 | [Server] 23 | void DestroySelf() 24 | { 25 | NetworkServer.Destroy(gameObject); 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Goodgulf/Part 2/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using UnityEngine.Audio; 6 | 7 | namespace Goodgulf.Networking 8 | { 9 | public class Startup : MonoBehaviour 10 | { 11 | public AudioMixer audioMixer; 12 | 13 | public void SetMasterVolume (float mvolume) 14 | { 15 | audioMixer.SetFloat("masterVol", mvolume); 16 | } 17 | 18 | void Awake() 19 | { 20 | // Get all the command line arguments for the executable 21 | // I'm using this target value for the shortcut I created for the game executable: "D:\Build\Mirror Networking.exe" -volume zero 22 | Dictionary args = GetCommandLineArgs(); 23 | 24 | if(args.TryGetValue("-volume",out string myarg)) 25 | { 26 | if(myarg == "zero") 27 | { 28 | // If we use "-volume zero" then the master volume is set to zero. 29 | SetMasterVolume(-80.0f); 30 | } 31 | } 32 | } 33 | 34 | 35 | // Get command line arguments, code from: 36 | // https://pauliom.medium.com/command-line-arguments-in-unity-b30a5815cd88 37 | private Dictionary GetCommandLineArgs() 38 | { 39 | Dictionary argumentDictionary = new Dictionary(); 40 | 41 | var commandLineArgs = System.Environment.GetCommandLineArgs(); 42 | 43 | for (int argumentIndex = 0; argumentIndex < commandLineArgs.Length; ++argumentIndex) 44 | { 45 | var arg = commandLineArgs[argumentIndex].ToLower(); 46 | if (arg.StartsWith("-")) 47 | { 48 | var value = argumentIndex < commandLineArgs.Length - 1 ? 49 | commandLineArgs[argumentIndex + 1].ToLower() : null; 50 | value = (value?.StartsWith("-") ?? false) ? null : value; 51 | 52 | argumentDictionary.Add(arg, value); 53 | } 54 | } 55 | return argumentDictionary; 56 | } 57 | 58 | } 59 | } -------------------------------------------------------------------------------- /Goodgulf/Part 3/GameTimer.cs: -------------------------------------------------------------------------------- 1 | using Mirror; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using UnityEngine.Events; 6 | using TMPro; 7 | 8 | namespace Goodgulf.Networking 9 | { 10 | 11 | public class GameTimer : NetworkBehaviour 12 | { 13 | [SyncVar] 14 | public float timer = 15 * 60; // 15 minutes countdown timer 15 | 16 | private int minutes; 17 | private int seconds; 18 | public string showTime; // The actual timer countdown showing in a 09:37 format 19 | private bool running = true; // Is the timer stil running? 20 | 21 | private TMP_Text clockText; // The timer text shown in the UI 22 | 23 | public UnityEvent ClockReady; // The event which is called when the timer reaches zero 24 | 25 | 26 | void Awake() 27 | { 28 | Debug.Log("GameTimer.Awake(): start"); 29 | 30 | // Find the UI object representing the time 31 | GameObject texttimer = GameObject.Find("textTimer"); 32 | if (texttimer) 33 | { 34 | clockText = texttimer.GetComponent(); 35 | 36 | if (clockText == null) 37 | Debug.LogError("GameTimer.Awake(): Cannot find TMP_Text."); 38 | } 39 | else Debug.LogError("GameTimer.Awake(): Cannot find textTimer."); 40 | } 41 | 42 | void Update() 43 | { 44 | if (!running) 45 | { 46 | return; 47 | } 48 | 49 | // Decrease the timer value as time ticks by: 50 | if (timer > 0) 51 | timer -= Time.deltaTime; 52 | 53 | // Convert the timer to a string 54 | minutes = Mathf.FloorToInt(timer / 60F); 55 | seconds = Mathf.FloorToInt(timer - minutes * 60); 56 | showTime = string.Format("{0:00}:{1:00}", minutes, seconds); 57 | 58 | if (timer < 0) 59 | { 60 | running = false; 61 | showTime = "00:00"; 62 | // Callback event when the timer reaches zero 63 | ClockReady.Invoke(); 64 | } 65 | 66 | if (clockText) 67 | { 68 | clockText.text = showTime; 69 | } 70 | else Debug.LogError("GameTimer.Update(): timer = null."); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /Goodgulf/Part 3/Health.cs: -------------------------------------------------------------------------------- 1 | using Mirror; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using UnityEngine; 6 | using UnityEngine.Events; 7 | using TMPro; 8 | 9 | namespace Goodgulf.Networking 10 | { 11 | 12 | [System.Serializable] 13 | public class MyDeathEvent : UnityEvent// Define a Unity event with a parameter 14 | { 15 | } 16 | 17 | 18 | public class Health : NetworkBehaviour 19 | { 20 | public TextMesh hitPointsText; // The text floating above the player object showing hitpoints 21 | // This text mesh was added to the Player prefab 22 | public TMP_Text myHitpointsText; // The text showing local client's hitpoints in the top rigth of the UI 23 | 24 | public int maxHitPoints = 1000; // The maximum hit points the player gets at the start of the match 25 | 26 | [SyncVar(hook = nameof(OnHitPointsChanged))] // The actual hit points with a hook to make changes on the client when the 27 | public int hitPoints = 0; // hitpoints change 28 | 29 | public MyDeathEvent OnDeath; // The event called when the Health reaches zero 30 | 31 | private PlayerScript playerScript; // A reference to the associated playerScript which we pass as a parameter to the 32 | // OnDeath event 33 | 34 | public override void OnStartServer() 35 | { 36 | base.OnStartServer(); 37 | 38 | // On the server, award the hitpoints at the start of the game 39 | hitPoints = maxHitPoints; 40 | playerScript = GetComponent(); 41 | } 42 | 43 | 44 | void OnHitPointsChanged(int _Old, int _New) 45 | { 46 | // Change Hitspoints on clients, update the hitpoints text floating above the other players 47 | hitPointsText.text = _New.ToString(); 48 | } 49 | 50 | [Server] 51 | public void AddHitPoints (int value) 52 | { 53 | // We only allow the server at add or remove hitpoints 54 | if(value>0) 55 | hitPoints = Math.Max(hitPoints + value, maxHitPoints); 56 | } 57 | 58 | [Server] 59 | public void RemoveHitPoints(int value) 60 | { 61 | if (value > 0) 62 | hitPoints = Math.Max(hitPoints - value, 0); 63 | 64 | 65 | if(hitPoints == 0 && playerScript) 66 | { 67 | // Player Death, invoke the event and pass this player as an argument 68 | OnDeath.Invoke(playerScript); 69 | } 70 | } 71 | 72 | private void Update() 73 | { 74 | if(isLocalPlayer) 75 | { 76 | if(myHitpointsText) 77 | { 78 | // Update the local player's hitpoints in the top right of the screen 79 | myHitpointsText.text = "Hitpoints: " + hitPoints.ToString(); 80 | } 81 | } 82 | } 83 | 84 | public override void OnStartLocalPlayer() 85 | { 86 | // Find the UI object representing the local player's hitpoints 87 | GameObject textHitpoints = GameObject.Find("textHitpoints"); 88 | if (textHitpoints) 89 | { 90 | myHitpointsText = textHitpoints.GetComponent(); 91 | 92 | if (myHitpointsText == null) 93 | Debug.LogError("Health.Awake(): Cannot find TMP_Text."); 94 | } 95 | else Debug.LogError("Health.Awake(): Cannot find textHitpoints."); 96 | } 97 | 98 | } 99 | } -------------------------------------------------------------------------------- /Goodgulf/Part 3/NetworkManagerMyGame.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.Events; 5 | using Mirror; 6 | 7 | namespace Goodgulf.Networking 8 | { 9 | 10 | public class NetworkManagerMyGame : NetworkManager 11 | { 12 | public GameObject prefabTimer; 13 | private GameObject timer; 14 | 15 | public override void OnServerAddPlayer(NetworkConnection conn) 16 | { 17 | // Call the base method which actually spawns the players on the playe spawn locations 18 | base.OnServerAddPlayer(conn); 19 | 20 | // Now add the Death Event to the Health script 21 | GameObject player = conn.identity.gameObject; // First get the player object linked to the newly added connection 22 | Health health = player.GetComponent(); // Get its Health Component 23 | if (health) 24 | { 25 | PlayerScript playerScript = player.GetComponent(); 26 | health.OnDeath.AddListener(playerScript.PlayerDied); // Link the event to the PlayerDied event (see below) 27 | } 28 | else Debug.Log("NetworkManagerMyGame.OnServerAddPlayer(): cannot find Health on object"); 29 | 30 | 31 | 32 | // spawn a game timer as soon as we have two players 33 | if (numPlayers == 2) 34 | { 35 | Debug.Log("NetworkManagerMyGame.OnServerAddPlayer(): spawn timer."); 36 | 37 | // The Timer prefab is picked from the registered spawnable prefabs 38 | timer = Instantiate(spawnPrefabs.Find(prefab => prefab.name == "GameTimer")); 39 | 40 | // Add a callback when the timer reaches 0 41 | GameTimer gameTimer = timer.GetComponent(); 42 | if (gameTimer) 43 | gameTimer.ClockReady.AddListener(EndOfTimer); 44 | 45 | // Now spawn it on all clients 46 | NetworkServer.Spawn(timer); 47 | } 48 | } 49 | 50 | public void EndOfTimer() 51 | { 52 | Debug.Log("NetworkManagerMyGame.EndOfTimer(): timer ready."); 53 | 54 | // End of match code here 55 | } 56 | 57 | public override void OnServerDisconnect(NetworkConnection conn) 58 | { 59 | // destroy time 60 | if (timer != null) 61 | NetworkServer.Destroy(timer); 62 | 63 | // call base functionality (actually destroys the player) 64 | base.OnServerDisconnect(conn); 65 | } 66 | 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /Goodgulf/Part 3/PlayerScript.cs: -------------------------------------------------------------------------------- 1 | using Invector.vCharacterController; 2 | using Mirror; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using UnityEngine; 6 | using TMPro; 7 | 8 | namespace Goodgulf.Networking 9 | { 10 | 11 | public class PlayerScript : NetworkBehaviour 12 | { 13 | private TMP_Text messageText; // Text at the top right of the screen used for messages 14 | 15 | public TextMesh playerNameText; // The text floating above the player object showing its name 16 | public GameObject floatingInfo; // The placeholder object containing above playerNameText 17 | 18 | // SyncVars are properties of classes that inherit from NetworkBehaviour, 19 | // which are synchronized from the server to clients. 20 | [SyncVar(hook = nameof(OnNameChanged))] // You can consider the hook to be similar to the setter, it gets called when the value changes on the client 21 | public string playerName; // Player name which we'll float over the player object 22 | 23 | [SyncVar(hook = nameof(OnColorChanged))] 24 | public Color playerColor = Color.white; // The color of the floating playerNameText 25 | 26 | public KeyCode spellKey1 = KeyCode.Alpha1; // The keyboard commands we'll use (for now) to trigger spells 27 | public KeyCode spellKey2 = KeyCode.Alpha2; 28 | public KeyCode spellKey3 = KeyCode.Alpha3; 29 | 30 | public GameObject spell1Prefab; // The prefabs to be instantiated when we trigger a spell 31 | public GameObject spell2Prefab; // Note: these prefabs also need to be registered in the NetworkManager as spawnable prefab 32 | public GameObject spell3Prefab; 33 | 34 | public AudioSource announce; // This clip will be played when a player enters the game. The audio source needs to be tagged as "Announce". 35 | 36 | public vThirdPersonCamera cameraPrefab; // Assign a reference to the Invector camera prefab to this property. 37 | 38 | private vThirdPersonInput _vThirdPersonInput; // Reference to the Invector Input system which we disable on all players except the Local Player. 39 | private vThirdPersonCamera _vThirdPersonCamera; // Reference to the Invector camera we spawn based on the above prefab. 40 | 41 | void Awake() 42 | { 43 | Debug.Log("PlayerScript.Awake(): event fired"); 44 | 45 | // Find the announcement audio source component in the hierarchy sicne we can't assign this to the non instantiated prefab. 46 | announce = GameObject.FindGameObjectWithTag("Announce").GetComponent(); 47 | 48 | // Find the UI object used to display game announcement message (currently only Player Death) 49 | GameObject messageTextObject = GameObject.Find("textMessage"); 50 | if(messageTextObject) 51 | { 52 | messageText = messageTextObject.GetComponent(); 53 | } 54 | 55 | // Find the Invector Input Manager component 56 | _vThirdPersonInput = GetComponent(); 57 | if (_vThirdPersonInput) 58 | { 59 | // Disable input by default, enable later for local players in OnStartLocalPlayer() event. 60 | // If we don't do this the input handler script will mess up the 3rd person camera setup. 61 | _vThirdPersonInput.enabled = false; 62 | } 63 | else Debug.LogError("PlayerScript.Start: could not find vThirdPersonInput."); 64 | } 65 | 66 | public override void OnStartLocalPlayer() 67 | { 68 | Debug.Log("PlayerScript.OnStartLocalPlayer(): event fired"); 69 | 70 | // Deactivate the main camera (which we see at the start of the scene) since the 3rd person camera needs to take over now. 71 | Camera.main.gameObject.SetActive(false); 72 | 73 | // Debug.Log("PlayerScript.OnStartLocalPlayer(): isLocalPlayer = "+isLocalPlayer); 74 | 75 | // Now we instanatiate the third person camera for the local player only: 76 | _vThirdPersonCamera = Instantiate(cameraPrefab, new Vector3(0, 0, 0), Quaternion.identity); 77 | 78 | if (_vThirdPersonCamera) 79 | { 80 | Debug.Log("PlayerScript.OnStartLocalPlayer(): setting main camera to localPlayer"); 81 | 82 | // Now link the camera to this player: 83 | _vThirdPersonCamera.SetMainTarget(this.transform); 84 | } 85 | else Debug.Log("PlayerScript.OnStartLocalPlayer(): _vThirdPersonCamera = null"); 86 | 87 | if (_vThirdPersonInput) 88 | { 89 | // Since this is the local player we'll need to enable the input handler 90 | _vThirdPersonInput.enabled = true; 91 | } 92 | 93 | if(announce) 94 | { 95 | announce.Play(); 96 | } 97 | 98 | 99 | // For the local player - hide the the placeholder object for the playerNameText (above the player) 100 | floatingInfo.transform.localPosition = new Vector3(0, -10.0f, 0.6f); 101 | floatingInfo.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f); 102 | 103 | // Pick a random name and color for the player 104 | string name = "Player" + Random.Range(100, 999); 105 | Color color = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f)); 106 | CmdSetupPlayer(name, color); 107 | } 108 | 109 | // These two "events" are called by the SyncVar hook when the value changes 110 | void OnNameChanged(string _Old, string _New) 111 | { 112 | playerNameText.text = playerName; 113 | } 114 | 115 | void OnColorChanged(Color _Old, Color _New) 116 | { 117 | playerNameText.color = _New; 118 | } 119 | 120 | [Command] 121 | public void CmdSetupPlayer(string _name, Color _col) 122 | { 123 | // Command: call this from a client to run this function on the server. 124 | // So the player sends info to server, then server updates sync vars which handles it on all clients 125 | playerName = _name; 126 | playerColor = _col; 127 | } 128 | 129 | void Update() 130 | { 131 | if (!isLocalPlayer) 132 | { 133 | // make non-local players run this 134 | floatingInfo.transform.LookAt(Camera.main.transform); 135 | return; 136 | } 137 | else 138 | { 139 | // Check the input from the user. A bit of dirty code to be updated later 140 | if (Input.GetKeyDown(spellKey1)) 141 | { 142 | CmdSpell(1); 143 | } 144 | else 145 | if (Input.GetKeyDown(spellKey2)) 146 | { 147 | CmdSpell(2); 148 | } 149 | if (Input.GetKeyDown(spellKey3)) 150 | { 151 | CmdSpell(3); 152 | } 153 | 154 | } 155 | } 156 | 157 | // This command is run on the server: instantiate one of the spell prefabs then spawn these on all clients 158 | [Command] 159 | void CmdSpell(int s) 160 | { 161 | GameObject spell; 162 | if (s == 1) 163 | { 164 | spell = Instantiate(spell1Prefab, this.transform.position, this.transform.rotation); 165 | } 166 | else if(s == 2) 167 | { 168 | spell = Instantiate(spell2Prefab, this.transform.position+this.transform.forward*2, this.transform.rotation); 169 | } 170 | else spell = Instantiate(spell3Prefab, this.transform.position + this.transform.forward * 3, this.transform.rotation); 171 | 172 | NetworkServer.Spawn(spell); // Spawn on all clients 173 | RpcOnSpell(); // Call any code that needs to run on the clients 174 | } 175 | 176 | // this is called on the player that cast a spell for all observers 177 | [ClientRpc] 178 | void RpcOnSpell() 179 | { 180 | // The server uses a Remote Procedure Call (RPC) to run that function on clients. 181 | // Do something 182 | } 183 | 184 | 185 | // This even is triggered by the Health script OnDeath event 186 | public void PlayerDied(PlayerScript playerScript) 187 | { 188 | // Find the unique identity for this player 189 | NetworkIdentity networkIdentity = playerScript.gameObject.GetComponent(); 190 | 191 | // Send a remote procedure call to all clients (below method), pass the Network ID of the player who died 192 | PlayerDeath(networkIdentity.netId); 193 | } 194 | 195 | [ClientRpc] 196 | void PlayerDeath(uint netID) 197 | { 198 | // The server uses a Remote Procedure Call (RPC) to run that function on clients. 199 | 200 | // Find all players 201 | PlayerScript[] playerScripts = GameObject.FindObjectsOfType(); 202 | foreach (PlayerScript playerScript in playerScripts) 203 | { 204 | if (playerScript.netId == netID && playerScript.isLocalPlayer) 205 | { 206 | // We found the player who died and it's the local player 207 | if(messageText) 208 | messageText.text="You died!"; 209 | } 210 | else if (playerScript.netId == netID) 211 | { 212 | // We found the player who died and it's another player 213 | if (messageText) 214 | messageText.text = "Player " + playerScript.playerName + " died."; 215 | 216 | } 217 | } 218 | } 219 | 220 | 221 | } 222 | } -------------------------------------------------------------------------------- /Goodgulf/Part 3/SpellStatic.cs: -------------------------------------------------------------------------------- 1 | using Mirror; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | 7 | namespace Goodgulf.Networking 8 | { 9 | public class SpellStatic : NetworkBehaviour 10 | { 11 | 12 | public float destroyDelaySeconds = 10; // Use SpellStatic on the spell effect prefabs to give them a limited lifetime 13 | 14 | public float radius = 6.0f; // All within this radius will be damaged 15 | public float damagePeriodStartSeconds = 2.0f; // Start handing out damage after this time 16 | public float damagePeriodInSeconds = 1.0f; // Do damage at the start of every period 17 | public int damagePerCycle = 100; // The amount of damage every period 18 | 19 | 20 | public override void OnStartServer() 21 | { 22 | // As soon a the object is created on the server we initiate a delayed delete: 23 | Invoke(nameof(DestroySelf), destroyDelaySeconds); 24 | 25 | // Now invoke this damage dealing method on the server only 26 | InvokeRepeating(nameof(DoDamage), damagePeriodStartSeconds, damagePeriodInSeconds); 27 | } 28 | 29 | [Server] 30 | void DoDamage() 31 | { 32 | // Find all colliders withing the radius of the spell 33 | Collider[] hitColliders = Physics.OverlapSphere(transform.position, radius); 34 | foreach (var hitCollider in hitColliders) 35 | { 36 | // Find the Health script for the collider we found 37 | Health health = hitCollider.transform.gameObject.GetComponent(); 38 | 39 | if(health) 40 | { 41 | // The collider's object has a Health script so someone is inside the radius 42 | // Do damage: 43 | health.RemoveHitPoints(damagePerCycle); 44 | } 45 | } 46 | } 47 | 48 | // destroy for everyone on the server 49 | [Server] 50 | void DestroySelf() 51 | { 52 | NetworkServer.Destroy(gameObject); 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Goodgulf/Part 3/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using UnityEngine.Audio; 6 | 7 | namespace Goodgulf.Networking 8 | { 9 | public class Startup : MonoBehaviour 10 | { 11 | public AudioMixer audioMixer; 12 | 13 | public void SetMasterVolume (float mvolume) 14 | { 15 | audioMixer.SetFloat("masterVol", mvolume); 16 | } 17 | 18 | void Awake() 19 | { 20 | // Get all the command line arguments for the executable 21 | // I'm using this target value for the shortcut I created for the game executable: "D:\Build\Mirror Networking.exe" -volume zero 22 | Dictionary args = GetCommandLineArgs(); 23 | 24 | if(args.TryGetValue("-volume",out string myarg)) 25 | { 26 | if(myarg == "zero") 27 | { 28 | // If we use "-volume zero" then the master volume is set to zero. 29 | SetMasterVolume(-80.0f); 30 | } 31 | } 32 | } 33 | 34 | 35 | // Get command line arguments, code from: 36 | // https://pauliom.medium.com/command-line-arguments-in-unity-b30a5815cd88 37 | private Dictionary GetCommandLineArgs() 38 | { 39 | Dictionary argumentDictionary = new Dictionary(); 40 | 41 | var commandLineArgs = System.Environment.GetCommandLineArgs(); 42 | 43 | for (int argumentIndex = 0; argumentIndex < commandLineArgs.Length; ++argumentIndex) 44 | { 45 | var arg = commandLineArgs[argumentIndex].ToLower(); 46 | if (arg.StartsWith("-")) 47 | { 48 | var value = argumentIndex < commandLineArgs.Length - 1 ? 49 | commandLineArgs[argumentIndex + 1].ToLower() : null; 50 | value = (value?.StartsWith("-") ?? false) ? null : value; 51 | 52 | argumentDictionary.Add(arg, value); 53 | } 54 | } 55 | return argumentDictionary; 56 | } 57 | 58 | } 59 | } -------------------------------------------------------------------------------- /Goodgulf/Part 4/NetworkManagerUI.cs: -------------------------------------------------------------------------------- 1 | using Mirror; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using UnityEngine.UI; 6 | 7 | namespace Goodgulf.Networking 8 | { 9 | public class NetworkManagerUI : MonoBehaviour 10 | { 11 | 12 | [SerializeField] 13 | private Button btnHost; // Host button 14 | 15 | [SerializeField] 16 | private Button btnClient; // Client button 17 | 18 | [SerializeField] 19 | private InputField inputAddress; // Address field (defaults to localhost) 20 | 21 | NetworkManager manager; 22 | 23 | void Awake() 24 | { 25 | // This component is attached to the NetworkManager (or in this case: the NetworkRoomManagerMyGame of which NetworkManager is the base class 26 | // The Networkmanager is used in events when a button is clicked so store a reference during Awake. 27 | manager = GetComponent(); 28 | } 29 | 30 | public void DisableUI () 31 | { 32 | // Disable the GUI in one go 33 | btnClient.interactable = false; 34 | btnHost.interactable = false; 35 | inputAddress.interactable = false; 36 | } 37 | 38 | public void StartHost() 39 | { 40 | // This is -more or less- the same code which Mirror used in the OnGUI() calls 41 | if (!NetworkClient.active && !NetworkServer.active) 42 | { 43 | DisableUI(); 44 | manager.StartHost(); 45 | } 46 | else Debug.LogWarning("NetworkManagerUI.StartHost(): client already active."); 47 | } 48 | 49 | public void StartClient() 50 | { 51 | if (!NetworkClient.active && !NetworkServer.active) 52 | { 53 | manager.networkAddress = inputAddress.text; 54 | DisableUI(); 55 | manager.StartClient(); 56 | } 57 | else Debug.LogWarning("NetworkManagerUI.StartClient(): client already active."); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Goodgulf/Part 4/RoomPlayerUI.cs: -------------------------------------------------------------------------------- 1 | using Mirror; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using UnityEngine.UI; 6 | using System; 7 | 8 | namespace Goodgulf.Networking 9 | { 10 | 11 | public class RoomPlayerUI : MonoBehaviour 12 | { 13 | /* 14 | * Attach this script to a Canvas UI prefab which is: 15 | * 16 | * an Image and underneath it 17 | * a Text (acts as a label "Player [1]") 18 | * an InputField where the player can enter a player name 19 | * a button for Player Ready 20 | * a button for Player Cancel (ready) 21 | */ 22 | 23 | private Text playerLabel; // These are the references to each of the Canvas UI sub elements 24 | private InputField playerNameInput; 25 | private Button playerReadyButton; 26 | private Button playerCancelButton; 27 | 28 | private GameObject panelContainer; // This is where a reference is stored to the parent (panel) to which the list items are parented 29 | 30 | [SerializeField] 31 | private uint _ownerID; // Store the NetID of the owner (NetworkRoomPplayerUI) of this list item 32 | public uint ownerID 33 | { 34 | get { return _ownerID; } 35 | set { _ownerID = value; } 36 | } 37 | 38 | public void Awake() 39 | { 40 | // Attach the list item to the parent panel: 41 | panelContainer = GameObject.Find("ListOfPlayers"); 42 | if (panelContainer) 43 | { 44 | transform.SetParent(panelContainer.transform); 45 | } 46 | else Debug.LogError("RoomPlayerUI.Start(): UI parent container not found"); 47 | 48 | // Now try and find the references to the Canvas UI list item's sub elements 49 | try 50 | { 51 | playerLabel = transform.GetChild(0).GetComponent(); 52 | playerNameInput = transform.GetChild(1).GetComponent(); 53 | playerReadyButton = transform.GetChild(2).GetComponent