This commit is contained in:
2025-06-16 15:14:23 +02:00
committed by devbeni
parent 60fe4620ff
commit 4ff561284f
3174 changed files with 428263 additions and 0 deletions

View File

@ -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
}
}

View File

@ -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

View 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
}
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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

View 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
}
}
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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

View 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
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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