Files
survival-game/Assets/Mirror/Examples/TopDownShooter/Scripts/PlayerTopDown.cs
2025-06-16 13:15:42 +00:00

338 lines
11 KiB
C#

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