aha
This commit is contained in:
@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Examples.TopDownShooter
|
||||
{
|
||||
public class CameraTopDown : MonoBehaviour
|
||||
{
|
||||
public Transform playerTransform;
|
||||
public Vector3 offset;
|
||||
public float followSpeed = 5f;
|
||||
|
||||
#if !UNITY_SERVER
|
||||
void LateUpdate()
|
||||
{
|
||||
if (playerTransform != null)
|
||||
{
|
||||
Vector3 targetPosition = playerTransform.position + offset;
|
||||
transform.position = Vector3.Lerp(transform.position, targetPosition, followSpeed * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a07de54c920be49f090362033974a3a9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Examples/TopDownShooter/Scripts/CameraTopDown.cs
|
||||
uploadId: 736421
|
224
Assets/Mirror/Examples/TopDownShooter/Scripts/CanvasHUD.cs
Normal file
224
Assets/Mirror/Examples/TopDownShooter/Scripts/CanvasHUD.cs
Normal file
@ -0,0 +1,224 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Mirror;
|
||||
|
||||
namespace Mirror.Examples.TopDownShooter
|
||||
{
|
||||
// Note: EventSystem is needed in your scene for Unitys UI Canvas
|
||||
public class CanvasHUD : MonoBehaviour
|
||||
{
|
||||
public CanvasTopDown canvasTopDown;
|
||||
|
||||
[SerializeField] private GameObject startButtonsGroup;
|
||||
[SerializeField] private GameObject statusLabelsGroup;
|
||||
|
||||
[SerializeField] private Button startHostButton;
|
||||
[SerializeField] private Button startServerOnlyButton;
|
||||
[SerializeField] private Button startClientButton;
|
||||
|
||||
[SerializeField] private Button mainStopButton;
|
||||
[SerializeField] private Text statusText;
|
||||
|
||||
[SerializeField] private InputField inputNetworkAddress;
|
||||
[SerializeField] private InputField inputPort;
|
||||
|
||||
#if !UNITY_SERVER
|
||||
private void Start()
|
||||
{
|
||||
// Init the input field with Network Manager's network address.
|
||||
inputNetworkAddress.text = NetworkManager.singleton.networkAddress;
|
||||
GetPort();
|
||||
|
||||
RegisterListeners();
|
||||
|
||||
//RegisterClientEvents();
|
||||
|
||||
CheckWebGLPlayer();
|
||||
}
|
||||
|
||||
private void RegisterListeners()
|
||||
{
|
||||
// Add button listeners. These buttons are already added in the inspector.
|
||||
startHostButton.onClick.AddListener(OnClickStartHostButton);
|
||||
startServerOnlyButton.onClick.AddListener(OnClickStartServerButton);
|
||||
startClientButton.onClick.AddListener(OnClickStartClientButton);
|
||||
mainStopButton.onClick.AddListener(OnClickMainStopButton);
|
||||
|
||||
// Add input field listener to update NetworkManager's Network Address
|
||||
// when changed.
|
||||
inputNetworkAddress.onValueChanged.AddListener(delegate { OnNetworkAddressChange(); });
|
||||
inputPort.onValueChanged.AddListener(delegate { OnPortChange(); });
|
||||
}
|
||||
|
||||
// Not working at the moment. Can't register events.
|
||||
/*private void RegisterClientEvents()
|
||||
{
|
||||
NetworkClient.OnConnectedEvent += OnClientConnect;
|
||||
NetworkClient.OnDisconnectedEvent += OnClientDisconnect;
|
||||
}*/
|
||||
|
||||
private void CheckWebGLPlayer()
|
||||
{
|
||||
// WebGL can't be host or server.
|
||||
if (Application.platform == RuntimePlatform.WebGLPlayer)
|
||||
{
|
||||
startHostButton.interactable = false;
|
||||
startServerOnlyButton.interactable = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshHUD()
|
||||
{
|
||||
if (!NetworkServer.active && !NetworkClient.isConnected)
|
||||
{
|
||||
StartButtons();
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusLabelsAndStopButtons();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartButtons()
|
||||
{
|
||||
if (!NetworkClient.active)
|
||||
{
|
||||
statusLabelsGroup.SetActive(false);
|
||||
startButtonsGroup.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowConnectingStatus();
|
||||
}
|
||||
}
|
||||
|
||||
private void StatusLabelsAndStopButtons()
|
||||
{
|
||||
startButtonsGroup.SetActive(false);
|
||||
statusLabelsGroup.SetActive(true);
|
||||
|
||||
// Host
|
||||
if (NetworkServer.active && NetworkClient.active)
|
||||
{
|
||||
statusText.text = $"<b>Host</b>: running via {Transport.active}";
|
||||
}
|
||||
// Server only
|
||||
else if (NetworkServer.active)
|
||||
{
|
||||
statusText.text = $"<b>Server</b>: running via {Transport.active}";
|
||||
}
|
||||
// Client only
|
||||
else if (NetworkClient.isConnected)
|
||||
{
|
||||
statusText.text = $"<b>Client</b>: connected to {NetworkManager.singleton.networkAddress} via {Transport.active}";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void ShowConnectingStatus()
|
||||
{
|
||||
startButtonsGroup.SetActive(false);
|
||||
statusLabelsGroup.SetActive(true);
|
||||
|
||||
statusText.text = "Connecting to " + NetworkManager.singleton.networkAddress + "..";
|
||||
}
|
||||
|
||||
private void OnClickStartHostButton()
|
||||
{
|
||||
canvasTopDown.PlaySoundButtonUI();
|
||||
NetworkManager.singleton.StartHost();
|
||||
}
|
||||
|
||||
private void OnClickStartServerButton()
|
||||
{
|
||||
canvasTopDown.PlaySoundButtonUI();
|
||||
NetworkManager.singleton.StartServer();
|
||||
}
|
||||
|
||||
private void OnClickStartClientButton()
|
||||
{
|
||||
canvasTopDown.PlaySoundButtonUI();
|
||||
NetworkManager.singleton.StartClient();
|
||||
//ShowConnectingStatus();
|
||||
}
|
||||
|
||||
private void OnClickMainStopButton()
|
||||
{
|
||||
canvasTopDown.PlaySoundButtonUI();
|
||||
if (NetworkClient.active && NetworkServer.active)
|
||||
{
|
||||
NetworkManager.singleton.StopHost();
|
||||
}
|
||||
if (NetworkClient.active)
|
||||
{
|
||||
NetworkManager.singleton.StopClient();
|
||||
}
|
||||
else
|
||||
{
|
||||
NetworkManager.singleton.StopServer();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNetworkAddressChange()
|
||||
{
|
||||
NetworkManager.singleton.networkAddress = inputNetworkAddress.text;
|
||||
}
|
||||
|
||||
private void OnPortChange()
|
||||
{
|
||||
SetPort(inputPort.text);
|
||||
}
|
||||
|
||||
private void SetPort(string _port)
|
||||
{
|
||||
// only show a port field if we have a port transport
|
||||
// we can't have "IP:PORT" in the address field since this only
|
||||
// works for IPV4:PORT.
|
||||
// for IPV6:PORT it would be misleading since IPV6 contains ":":
|
||||
// 2001:0db8:0000:0000:0000:ff00:0042:8329
|
||||
if (Transport.active is PortTransport portTransport)
|
||||
{
|
||||
// use TryParse in case someone tries to enter non-numeric characters
|
||||
if (ushort.TryParse(_port, out ushort port))
|
||||
portTransport.Port = port;
|
||||
}
|
||||
}
|
||||
|
||||
private void GetPort()
|
||||
{
|
||||
if (Transport.active is PortTransport portTransport)
|
||||
{
|
||||
inputPort.text = portTransport.Port.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
RefreshHUD();
|
||||
}
|
||||
|
||||
/* This does not work because we can't register the handler.
|
||||
void OnClientConnect() {}
|
||||
|
||||
private void OnClientDisconnect()
|
||||
{
|
||||
RefreshHUD();
|
||||
}
|
||||
*/
|
||||
|
||||
// Do a check for the presence of a Network Manager component when
|
||||
// you first add this script to a gameobject.
|
||||
private void Reset()
|
||||
{
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
if (!FindAnyObjectByType<NetworkManager>())
|
||||
Debug.LogError("This component requires a NetworkManager component to be present in the scene. Please add!");
|
||||
#else
|
||||
// Deprecated in Unity 2023.1
|
||||
if (!FindObjectOfType<NetworkManager>())
|
||||
Debug.LogError("This component requires a NetworkManager component to be present in the scene. Please add!");
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89f872f84342749b9a8f03a4bfa88ed7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Examples/TopDownShooter/Scripts/CanvasHUD.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,97 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Mirror.Examples.TopDownShooter
|
||||
{
|
||||
public class CanvasTopDown : MonoBehaviour
|
||||
{
|
||||
public NetworkTopDown networkTopDown;
|
||||
public PlayerTopDown playerTopDown; // This is automatically set by local players script
|
||||
|
||||
public Button buttonSpawnEnemy, buttonRespawnPlayer;
|
||||
public Text textEnemies, textKills;
|
||||
|
||||
public GameObject shotMarker;
|
||||
public GameObject deathSplatter;
|
||||
public AudioSource soundGameIntro, soundGameLoop, soundButtonUI;
|
||||
|
||||
|
||||
#if !UNITY_SERVER
|
||||
private void Start()
|
||||
{
|
||||
buttonSpawnEnemy.onClick.AddListener(ButtonSpawnEnemy);
|
||||
buttonRespawnPlayer.onClick.AddListener(ButtonRespawnPlayer);
|
||||
|
||||
StartCoroutine(BGSound());
|
||||
}
|
||||
#endif
|
||||
|
||||
private void ButtonSpawnEnemy()
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
PlaySoundButtonUI();
|
||||
networkTopDown.SpawnEnemy();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void ButtonRespawnPlayer()
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
PlaySoundButtonUI();
|
||||
playerTopDown.CmdRespawnPlayer();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void UpdateEnemyUI(int value)
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
textEnemies.text = "Enemies: " + value;
|
||||
#endif
|
||||
}
|
||||
|
||||
public void UpdateKillsUI(int value)
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
textKills.text = "Kills: " + value;
|
||||
#endif
|
||||
}
|
||||
|
||||
public void ResetUI()
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
if (NetworkServer.active)
|
||||
{
|
||||
buttonSpawnEnemy.gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonSpawnEnemy.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
buttonRespawnPlayer.gameObject.SetActive(false);
|
||||
shotMarker.SetActive(false);
|
||||
textEnemies.text = "Enemies: 0";
|
||||
textKills.text = "Kills: 0";
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !UNITY_SERVER
|
||||
IEnumerator BGSound()
|
||||
{
|
||||
soundGameIntro.Play();
|
||||
yield return new WaitForSeconds(4.1f);
|
||||
soundGameLoop.Play();
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
public void PlaySoundButtonUI()
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
soundButtonUI.Play();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c415e1bc259040ac9e72dea41b57c55
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Examples/TopDownShooter/Scripts/CanvasTopDown.cs
|
||||
uploadId: 736421
|
183
Assets/Mirror/Examples/TopDownShooter/Scripts/EnemyTopDown.cs
Normal file
183
Assets/Mirror/Examples/TopDownShooter/Scripts/EnemyTopDown.cs
Normal file
@ -0,0 +1,183 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using Mirror;
|
||||
|
||||
namespace Mirror.Examples.TopDownShooter
|
||||
{
|
||||
public class EnemyTopDown : NetworkBehaviour
|
||||
{
|
||||
private CanvasTopDown canvasTopDown;
|
||||
|
||||
public float followDistance = 8f; // Distance at which the enemy will start following the target
|
||||
public float findPlayersTime = 1.0f; // We want to avoid this being in Update, allow enemies to scan for playes every X time
|
||||
public float distanceToKillAt = 0.5f;
|
||||
|
||||
private NavMeshAgent agent;
|
||||
private Transform closestTarget;
|
||||
public Vector3 previousPosition;
|
||||
|
||||
public GameObject enemyArt;
|
||||
public GameObject idleSprite, aggroSprite;
|
||||
public AudioSource soundDeath, soundAggro;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
//allow all to run this, they may need it for reference
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
canvasTopDown = GameObject.FindAnyObjectByType<CanvasTopDown>();
|
||||
#else
|
||||
canvasTopDown = GameObject.FindObjectOfType<CanvasTopDown>();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
previousPosition = this.transform.position;
|
||||
|
||||
if (isServer)
|
||||
{
|
||||
agent = GetComponent<NavMeshAgent>();
|
||||
InvokeRepeating("FindClosestTarget", findPlayersTime, findPlayersTime);
|
||||
}
|
||||
#if !UNITY_SERVER
|
||||
if (isClient)
|
||||
{
|
||||
InvokeRepeating("SetSprite", 0.1f, 0.1f);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void Update()
|
||||
{
|
||||
FollowTarget();
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void FindClosestTarget()
|
||||
{
|
||||
float closestDistance = Mathf.Infinity;
|
||||
closestTarget = null;
|
||||
|
||||
// This is our static player list, set and updated in players scripts via Start and OnDestroy.
|
||||
foreach (PlayerTopDown target in PlayerTopDown.playerList)
|
||||
{
|
||||
float distanceToTarget = Vector3.Distance(transform.position, target.transform.position);
|
||||
if (target.flashLightStatus == true)
|
||||
{
|
||||
// players with flashlight off, gets lower aggro by enemies
|
||||
distanceToTarget = distanceToTarget / 2;
|
||||
}
|
||||
|
||||
// chase only if alive
|
||||
if (target.playerStatus == 0 && distanceToTarget < closestDistance && distanceToTarget <= followDistance)
|
||||
{
|
||||
closestDistance = distanceToTarget;
|
||||
closestTarget = target.transform;
|
||||
|
||||
float distanceKill = Vector3.Distance(transform.position, target.transform.position);
|
||||
if (distanceKill < distanceToKillAt)
|
||||
{
|
||||
target.Kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Even with no target, Unitys nav agent continues moving to last set position
|
||||
// We do not want this for a respawning enemy, so we manually stop the agent.
|
||||
if (closestTarget == null)
|
||||
{
|
||||
agent.isStopped = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
agent.isStopped = false;
|
||||
}
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void FollowTarget()
|
||||
{
|
||||
if (closestTarget != null)
|
||||
{
|
||||
agent.SetDestination(closestTarget.position);
|
||||
}
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public void Kill()
|
||||
{
|
||||
RpcKill();
|
||||
// Player host will run the RPC, but Server-Only will not, and we need the function to run that the rpc calls, so check and call it.
|
||||
if (isServerOnly)
|
||||
{
|
||||
StartCoroutine(KillCoroutine());
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcKill()
|
||||
{
|
||||
StartCoroutine(KillCoroutine());
|
||||
}
|
||||
|
||||
IEnumerator KillCoroutine()
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
soundDeath.Play();
|
||||
enemyArt.SetActive(false);
|
||||
if (isClient)
|
||||
{
|
||||
GameObject splatter = Instantiate(canvasTopDown.deathSplatter, this.transform.position, this.transform.rotation);
|
||||
Destroy(splatter, 5.0f);
|
||||
}
|
||||
#endif
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
|
||||
if (isServer)
|
||||
{
|
||||
// reset enemy, rather than despawning, makes it look like a new enemy appears, better for performance too
|
||||
closestTarget = null;
|
||||
transform.position = new Vector3(Random.Range(canvasTopDown.networkTopDown.enemySpawnRangeX.x, canvasTopDown.networkTopDown.enemySpawnRangeX.y), 0, Random.Range(canvasTopDown.networkTopDown.enemySpawnRangeZ.x, canvasTopDown.networkTopDown.enemySpawnRangeZ.y));
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
#if !UNITY_SERVER
|
||||
enemyArt.SetActive(true);
|
||||
#endif
|
||||
if (isServer)
|
||||
{
|
||||
// spawn another, this means for every 1 enemy killed, 2 more appear, increasing difficulty
|
||||
canvasTopDown.networkTopDown.SpawnEnemy();
|
||||
}
|
||||
}
|
||||
|
||||
[ClientCallback]
|
||||
void SetSprite()
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
// A simple way to change sprite animation, without networking it
|
||||
// If not moving, be idle sprite, if moving, presume aggrod sprite.
|
||||
if (this.transform.position == previousPosition)
|
||||
{
|
||||
if (idleSprite.activeInHierarchy == false)
|
||||
{
|
||||
idleSprite.SetActive(true);
|
||||
aggroSprite.SetActive(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (aggroSprite.activeInHierarchy == false)
|
||||
{
|
||||
idleSprite.SetActive(false);
|
||||
aggroSprite.SetActive(true);
|
||||
soundAggro.Play();
|
||||
}
|
||||
previousPosition = this.transform.position;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72f6c4ff38c894d72957e061175c6cb9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Examples/TopDownShooter/Scripts/EnemyTopDown.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,66 @@
|
||||
using UnityEngine;
|
||||
using Mirror;
|
||||
|
||||
namespace Mirror.Examples.TopDownShooter
|
||||
{
|
||||
public class NetworkTopDown : NetworkBehaviour
|
||||
{
|
||||
public CanvasTopDown canvasTopDown;
|
||||
|
||||
// Have as many enemy variations as you want, remember to set them in NetworkManagers Registered Spawnable Prefabs array.
|
||||
public GameObject[] enemyPrefabs;
|
||||
// For our square map with no obstacles, we'l just set a range, for your own game, you may have set spawn points
|
||||
public Vector2 enemySpawnRangeX;
|
||||
public Vector2 enemySpawnRangeZ;
|
||||
|
||||
[SyncVar(hook = nameof(OnEnemyCounterChanged))]
|
||||
public int enemyCounter = 0;
|
||||
|
||||
public override void OnStartServer()
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
canvasTopDown.ResetUI();
|
||||
#endif
|
||||
// Spawn one enemy on start of game, then let player host spawn more via button
|
||||
SpawnEnemy();
|
||||
}
|
||||
|
||||
#if !UNITY_SERVER
|
||||
public override void OnStartClient()
|
||||
{
|
||||
canvasTopDown.ResetUI();
|
||||
}
|
||||
#endif
|
||||
|
||||
[ServerCallback]
|
||||
public void SpawnEnemy()
|
||||
{
|
||||
if (isServer == false)
|
||||
{
|
||||
print("Only server can spawn enemies, or clients via cmd request.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Select random enemy prefab if we have more than one
|
||||
GameObject enemy = Instantiate(enemyPrefabs[Random.Range(0, enemyPrefabs.Length)]);
|
||||
// Set random spawn position depending on our ranges set via inspector
|
||||
enemy.transform.position = new Vector3(Random.Range(enemySpawnRangeX.x, enemySpawnRangeX.y), 0, Random.Range(enemySpawnRangeZ.x, enemySpawnRangeZ.y));
|
||||
// Network spawn enemy to current and new players
|
||||
NetworkServer.Spawn(enemy);
|
||||
enemyCounter += 1;
|
||||
#if !UNITY_SERVER
|
||||
// update UI
|
||||
canvasTopDown.UpdateEnemyUI(enemyCounter);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void OnEnemyCounterChanged(int _Old, int _New)
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
canvasTopDown.UpdateEnemyUI(enemyCounter);
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b1dcba379627a482ab84c8a73c92546d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Examples/TopDownShooter/Scripts/NetworkTopDown.cs
|
||||
uploadId: 736421
|
338
Assets/Mirror/Examples/TopDownShooter/Scripts/PlayerTopDown.cs
Normal file
338
Assets/Mirror/Examples/TopDownShooter/Scripts/PlayerTopDown.cs
Normal file
@ -0,0 +1,338 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Mirror;
|
||||
|
||||
namespace Mirror.Examples.TopDownShooter
|
||||
{
|
||||
public class PlayerTopDown : NetworkBehaviour
|
||||
{
|
||||
public readonly static List<PlayerTopDown> playerList = new List<PlayerTopDown>();
|
||||
|
||||
private Camera mainCamera;
|
||||
private CameraTopDown cameraTopDown;
|
||||
private CanvasTopDown canvasTopDown;
|
||||
|
||||
public float moveSpeed = 5f;
|
||||
public CharacterController characterController;
|
||||
|
||||
public GameObject leftFoot, rightFoot;
|
||||
private Vector3 previousPosition;
|
||||
private Quaternion previousRotation;
|
||||
|
||||
[SyncVar(hook = nameof(OnFlashLightChanged))]
|
||||
public bool flashLightStatus = true;
|
||||
public Light flashLight;
|
||||
|
||||
[SyncVar(hook = nameof(OnKillsChanged))]
|
||||
public int kills = 0;
|
||||
|
||||
[SyncVar(hook = nameof(OnPlayerStatusChanged))]
|
||||
public int playerStatus = 0;
|
||||
public GameObject[] objectsToHideOnDeath;
|
||||
|
||||
public float shootDistance = 100f;
|
||||
public LayerMask hitLayers;
|
||||
public GameObject muzzleFlash;
|
||||
public AudioSource soundGunShot, soundDeath, soundFlashLight, soundLeftFoot, soundRightFoot;
|
||||
|
||||
#if !UNITY_SERVER
|
||||
public override void OnStartLocalPlayer()
|
||||
{
|
||||
// Grab and setup camera for local player only
|
||||
mainCamera = Camera.main;
|
||||
cameraTopDown = mainCamera.GetComponent<CameraTopDown>();
|
||||
cameraTopDown.playerTransform = this.transform;
|
||||
cameraTopDown.offset.y = 20.0f; // dramatic zoom out once players setup
|
||||
canvasTopDown.playerTopDown = this;
|
||||
|
||||
// We want 3D audio effects to be around the player, not the camera 50 meters in the air
|
||||
// Otherwise it looks weird in-game, trust me
|
||||
mainCamera.GetComponent<AudioListener>().enabled = false;
|
||||
this.gameObject.AddComponent<AudioListener>();
|
||||
}
|
||||
#endif
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// Allow all players to run this, they may need it for reference
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
canvasTopDown = GameObject.FindAnyObjectByType<CanvasTopDown>();
|
||||
#else
|
||||
canvasTopDown = GameObject.FindObjectOfType<CanvasTopDown>();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
// If only server needs access to a player list, place the Add and Remove in public override void OnStartServer/OnStopServer
|
||||
playerList.Add(this);
|
||||
print("Player joined, total players: " + playerList.Count);
|
||||
|
||||
#if !UNITY_SERVER
|
||||
if (isClient)
|
||||
{
|
||||
InvokeRepeating("AnimatePlayer", 0.2f, 0.2f);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
playerList.Remove(this);
|
||||
print("Player removed, total players: " + playerList.Count);
|
||||
|
||||
if (mainCamera) { mainCamera.GetComponent<AudioListener>().enabled = true; }
|
||||
}
|
||||
|
||||
#if !UNITY_SERVER
|
||||
[ClientCallback]
|
||||
void Update()
|
||||
{
|
||||
if (!Application.isFocused) return;
|
||||
if (isOwned == false) { return; }
|
||||
if (playerStatus != 0) { return; } // make sure we are alive
|
||||
|
||||
// Handle movement
|
||||
float moveHorizontal = Input.GetAxis("Horizontal");
|
||||
float moveVertical = Input.GetAxis("Vertical");
|
||||
|
||||
Vector3 movement = new Vector3(moveHorizontal, 0f, moveVertical);
|
||||
if (movement.magnitude > 1f) movement.Normalize(); // Normalize to prevent faster diagonal movement
|
||||
characterController.Move(movement * moveSpeed * Time.deltaTime);
|
||||
|
||||
RotatePlayerToMouse();
|
||||
|
||||
if (Input.GetKeyUp(KeyCode.F))
|
||||
{
|
||||
// We could optionally call this locally too, to avoid minor delay in the command->sync var hook result
|
||||
CmdFlashLight();
|
||||
}
|
||||
|
||||
// We currently have no shoot limiter, ideally thats a feature you would need to add.
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
{
|
||||
Shoot();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !UNITY_SERVER
|
||||
[ClientCallback]
|
||||
void RotatePlayerToMouse()
|
||||
{
|
||||
Plane playerPlane = new Plane(Vector3.up, transform.position);
|
||||
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
|
||||
|
||||
if (playerPlane.Raycast(ray, out float hitDist))
|
||||
{
|
||||
Vector3 targetPoint = ray.GetPoint(hitDist);
|
||||
Quaternion targetRotation = Quaternion.LookRotation(targetPoint - transform.position);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, moveSpeed * Time.deltaTime);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !UNITY_SERVER
|
||||
[ClientCallback]
|
||||
void Shoot()
|
||||
{
|
||||
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
|
||||
RaycastHit hit;
|
||||
|
||||
if (Physics.Raycast(ray, out hit, shootDistance, hitLayers))
|
||||
{
|
||||
//print("Hit: " + hit.collider.gameObject.name);
|
||||
|
||||
canvasTopDown.shotMarker.transform.position = hit.point;
|
||||
|
||||
if (hit.collider.gameObject.GetComponent<NetworkIdentity>() != null)
|
||||
{
|
||||
CmdShoot(hit.collider.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
CmdShoot(null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//print("Missed");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !UNITY_SERVER
|
||||
IEnumerator GunShotEffect()
|
||||
{
|
||||
soundGunShot.Play();
|
||||
muzzleFlash.SetActive(true);
|
||||
if (isLocalPlayer)
|
||||
{
|
||||
canvasTopDown.shotMarker.SetActive(true);
|
||||
}
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
muzzleFlash.SetActive(false);
|
||||
if (isLocalPlayer)
|
||||
{
|
||||
canvasTopDown.shotMarker.SetActive(false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
[Command]
|
||||
public void CmdFlashLight()
|
||||
{
|
||||
flashLightStatus = !flashLightStatus;
|
||||
}
|
||||
|
||||
// our sync var hook, which sets flashlight status to the same on all clients for this player
|
||||
void OnFlashLightChanged(bool _Old, bool _New)
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
Debug.Log($"OnFlashLightChanged: {_New}");
|
||||
flashLight.enabled = _New;
|
||||
soundFlashLight.Play();
|
||||
#endif
|
||||
}
|
||||
|
||||
[Command]
|
||||
public void CmdShoot(GameObject target)
|
||||
{
|
||||
RpcShoot();
|
||||
if (target)
|
||||
{
|
||||
// you should check for a tag, not name contains
|
||||
// this is a quick workaround to make sure the example works without custom tags that may not be in your project
|
||||
if (target.name.Contains("Enemy"))
|
||||
{
|
||||
target.GetComponent<EnemyTopDown>().Kill();
|
||||
}
|
||||
else if (CompareTag("Player") == true) // Player tag exists in unity by default, so we should be good to use it here
|
||||
{
|
||||
// Make sure they are alive/dont shoot themself
|
||||
if (target.GetComponent<PlayerTopDown>().playerStatus != 0 || target == this.gameObject) { return; }
|
||||
target.GetComponent<PlayerTopDown>().Kill();
|
||||
}
|
||||
kills += 1; // update user kills sync var
|
||||
}
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcShoot()
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
StartCoroutine(GunShotEffect());
|
||||
#endif
|
||||
}
|
||||
|
||||
// hook for sync var kills
|
||||
void OnKillsChanged(int _Old, int _New)
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
// all players get your latest kill data, however only local player updates their UI
|
||||
if (isLocalPlayer)
|
||||
{
|
||||
canvasTopDown.UpdateKillsUI(kills);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
[ClientCallback]
|
||||
void AnimatePlayer()
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
// A simple way to change sprite animation, without networking it
|
||||
// If not moving or rotating, show no feet animation or sound, if moving, flick through footstep animations and sound effects.
|
||||
if (this.transform.position == previousPosition && Quaternion.Angle(this.transform.rotation, previousRotation) < 20.0f)
|
||||
{
|
||||
rightFoot.SetActive(false);
|
||||
leftFoot.SetActive(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rightFoot.activeInHierarchy)
|
||||
{
|
||||
leftFoot.SetActive(true);
|
||||
rightFoot.SetActive(false);
|
||||
soundLeftFoot.Play();
|
||||
}
|
||||
else
|
||||
{
|
||||
leftFoot.SetActive(false);
|
||||
rightFoot.SetActive(true);
|
||||
soundRightFoot.Play();
|
||||
}
|
||||
previousPosition = this.transform.position;
|
||||
previousRotation = this.transform.rotation;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
[Command]
|
||||
public void CmdRespawnPlayer()
|
||||
{
|
||||
// We use a number playerStatus here, rather than bool, as you can use it for other things such as delayed respawn, respawn armour, spectating etc
|
||||
if (playerStatus == 0)
|
||||
{
|
||||
playerStatus = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
playerStatus = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Our sync var hook for death and alive
|
||||
void OnPlayerStatusChanged(int _Old, int _New)
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
if (playerStatus == 0) // default/show
|
||||
{
|
||||
foreach (var obj in objectsToHideOnDeath)
|
||||
{
|
||||
obj.SetActive(true);
|
||||
}
|
||||
characterController.enabled = true;
|
||||
if (isLocalPlayer)
|
||||
{
|
||||
this.transform.position = NetworkManager.startPositions[Random.Range(0, NetworkManager.startPositions.Count)].position;
|
||||
canvasTopDown.buttonRespawnPlayer.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
else if (playerStatus == 1) // death
|
||||
{
|
||||
// have meshes hidden, disable movement and show respawn button
|
||||
foreach (var obj in objectsToHideOnDeath)
|
||||
{
|
||||
obj.SetActive(false);
|
||||
}
|
||||
characterController.enabled = false;
|
||||
if (isLocalPlayer)
|
||||
{
|
||||
canvasTopDown.buttonRespawnPlayer.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
// else if (playerStatus == 2) // can be used for other features, such as spectator, make local camera follow another player
|
||||
#endif
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public void Kill()
|
||||
{
|
||||
//print("Kill Player");
|
||||
playerStatus = 1;
|
||||
RpcKill();
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcKill()
|
||||
{
|
||||
#if !UNITY_SERVER
|
||||
soundDeath.Play();
|
||||
GameObject splatter = Instantiate(canvasTopDown.deathSplatter, this.transform.position, this.transform.rotation);
|
||||
Destroy(splatter, 5.0f);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e600b9ef999b64e7ba94300149e0a96e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Examples/TopDownShooter/Scripts/PlayerTopDown.cs
|
||||
uploadId: 736421
|
@ -0,0 +1,52 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Examples.TopDownShooter
|
||||
{
|
||||
public class RespawnPortal : MonoBehaviour
|
||||
{
|
||||
public float rotationSpeed = 360f; // Degrees per second
|
||||
public float shrinkDuration = 1f; // Time in seconds to shrink to zero
|
||||
public AudioSource soundEffect;
|
||||
|
||||
private Vector3 originalScale;
|
||||
private float shrinkTimer;
|
||||
|
||||
#if !UNITY_SERVER
|
||||
void Awake()
|
||||
{
|
||||
// Store the original setup
|
||||
originalScale = transform.localScale;
|
||||
shrinkTimer = shrinkDuration;
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
// By using OnEnable, it shortcuts the function to be called automatically when gameobject is SetActive false/true.
|
||||
// Here we reset variables, and then call the Portal respawn effect
|
||||
transform.localScale = originalScale;
|
||||
shrinkTimer = shrinkDuration;
|
||||
|
||||
StartCoroutine(StartEffect());
|
||||
}
|
||||
|
||||
IEnumerator StartEffect()
|
||||
{
|
||||
soundEffect.Play();
|
||||
while (shrinkTimer > 0)
|
||||
{
|
||||
transform.Rotate(Vector3.up, rotationSpeed * Time.deltaTime);
|
||||
|
||||
if (shrinkTimer > 0)
|
||||
{
|
||||
shrinkTimer -= Time.deltaTime;
|
||||
float scale = Mathf.Clamp01(shrinkTimer / shrinkDuration);
|
||||
transform.localScale = originalScale * scale;
|
||||
|
||||
yield return new WaitForEndOfFrame();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f8a07eafc4eb42f3a5152fe7d9255fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 129321
|
||||
packageName: Mirror
|
||||
packageVersion: 96.0.1
|
||||
assetPath: Assets/Mirror/Examples/TopDownShooter/Scripts/RespawnPortal.cs
|
||||
uploadId: 736421
|
Reference in New Issue
Block a user