Files
survival-game/Assets/Mirror/Examples/_Common/Controllers/TankController/TankTurretBase.cs
2025-06-16 15:14:23 +02:00

469 lines
15 KiB
C#

using System;
using UnityEngine;
namespace Mirror.Examples.Common.Controllers.Tank
{
[AddComponentMenu("")]
[RequireComponent(typeof(NetworkIdentity))]
[DisallowMultipleComponent]
public class TankTurretBase : NetworkBehaviour
{
const float BASE_DPI = 96f;
[Serializable]
public struct OptionsKeys
{
public KeyCode MouseLock;
public KeyCode AutoLevel;
public KeyCode ToggleUI;
}
[Serializable]
public struct MoveKeys
{
public KeyCode PitchUp;
public KeyCode PitchDown;
public KeyCode TurnLeft;
public KeyCode TurnRight;
}
[Serializable]
public struct OtherKeys
{
public KeyCode Shoot;
}
[Flags]
public enum ControlOptions : byte
{
None,
MouseLock = 1 << 0,
AutoLevel = 1 << 1,
ShowUI = 1 << 2
}
// Unity clones the material when GetComponent<Renderer>().material is called
// Cache it here and destroy it in OnDestroy to prevent a memory leak
Material cachedMaterial;
[Header("Prefabs")]
public GameObject turretUIPrefab;
public GameObject projectilePrefab;
[Header("Components")]
public Animator animator;
public Transform turret;
public Transform barrel;
public Transform projectileMount;
public CapsuleCollider barrelCollider;
[Header("Seated Player")]
public GameObject playerObject;
[SyncVar(hook = nameof(OnPlayerColorChanged))]
public Color32 playerColor = Color.black;
[Header("Configuration")]
[SerializeField]
public MoveKeys moveKeys = new MoveKeys
{
PitchUp = KeyCode.UpArrow,
PitchDown = KeyCode.DownArrow,
TurnLeft = KeyCode.LeftArrow,
TurnRight = KeyCode.RightArrow
};
[SerializeField]
public OtherKeys otherKeys = new OtherKeys
{
Shoot = KeyCode.Space
};
[SerializeField]
public OptionsKeys optionsKeys = new OptionsKeys
{
MouseLock = KeyCode.M,
AutoLevel = KeyCode.L,
ToggleUI = KeyCode.U
};
[Space(5)]
public ControlOptions controlOptions = ControlOptions.AutoLevel | ControlOptions.ShowUI;
[Header("Shooting")]
[Tooltip("Cooldown time in seconds")]
[Range(0, 10)]
public byte cooldownTime = 1;
[Header("Turret")]
[Range(0, 300f)]
[Tooltip("Max Rotation in degrees per second")]
public float maxTurretSpeed = 250f;
[Range(0, 30f)]
[Tooltip("Rotation acceleration in degrees per second squared")]
public float turretAcceleration = 10f;
[Header("Barrel")]
[Range(0, 180f)]
[Tooltip("Max Pitch in degrees per second")]
public float maxPitchSpeed = 30f;
[Range(0, 40f)]
[Tooltip("Max Pitch in degrees")]
public float maxPitchUpAngle = 25f;
[Range(0, 20f)]
[Tooltip("Max Pitch in degrees")]
public float maxPitchDownAngle = 0f;
[Range(0, 10f)]
[Tooltip("Pitch acceleration in degrees per second squared")]
public float pitchAcceleration = 3f;
// Runtime data in a struct so it can be folded up in inspector
[Serializable]
public struct RuntimeData
{
[ReadOnly, SerializeField, Range(-300f, 300f)] float _turretSpeed;
[ReadOnly, SerializeField, Range(-180f, 180f)] float _pitchAngle;
[ReadOnly, SerializeField, Range(-180f, 180f)] float _pitchSpeed;
[ReadOnly, SerializeField, Range(-1f, 1f)] float _mouseInputX;
[ReadOnly, SerializeField, Range(0, 30f)] float _mouseSensitivity;
[ReadOnly, SerializeField] double _lastShotTime;
[ReadOnly, SerializeField] GameObject _turretUI;
#region Properties
public float mouseInputX
{
get => _mouseInputX;
internal set => _mouseInputX = value;
}
public float mouseSensitivity
{
get => _mouseSensitivity;
internal set => _mouseSensitivity = value;
}
public float turretSpeed
{
get => _turretSpeed;
internal set => _turretSpeed = value;
}
public float pitchAngle
{
get => _pitchAngle;
internal set => _pitchAngle = value;
}
public float pitchSpeed
{
get => _pitchSpeed;
internal set => _pitchSpeed = value;
}
public double lastShotTime
{
get => _lastShotTime;
internal set => _lastShotTime = value;
}
public GameObject turretUI
{
get => _turretUI;
internal set => _turretUI = value;
}
#endregion
}
[Header("Diagnostics")]
public RuntimeData runtimeData;
#region Network Setup
protected override void OnValidate()
{
// Skip if Editor is in Play mode
if (Application.isPlaying) return;
base.OnValidate();
Reset();
}
// NOTE: Do not put objects in DontDestroyOnLoad (DDOL) in Awake. You can do that in Start instead.
protected virtual void Reset()
{
// Ensure syncDirection is Client to Server
syncDirection = SyncDirection.ClientToServer;
if (animator == null)
animator = GetComponentInChildren<Animator>();
// Set default...this may be modified based on DPI at runtime
runtimeData.mouseSensitivity = turretAcceleration;
// Do a recursive search for a children named "Turret" and "ProjectileMount".
// They will be several levels deep in the hierarchy.
if (turret == null)
turret = FindDeepChild(transform, "Turret");
if (barrel == null)
barrel = FindDeepChild(turret, "Barrel");
if (barrelCollider == null)
barrelCollider = barrel.GetComponent<CapsuleCollider>();
if (projectileMount == null)
projectileMount = FindDeepChild(turret, "ProjectileMount");
if (playerObject == null)
playerObject = FindDeepChild(turret, "SeatedPlayer").gameObject;
// tranform.Find will fail - must do recursive search
Transform FindDeepChild(Transform aParent, string aName)
{
var result = aParent.Find(aName);
if (result != null)
return result;
foreach (Transform child in aParent)
{
result = FindDeepChild(child, aName);
if (result != null)
return result;
}
return null;
}
#if UNITY_EDITOR
// For convenience in the examples, we use the GUID of the Projectile prefab
// to find the correct prefab in the Mirror/Examples/_Common/Controllers folder.
// This avoids conflicts with user-created prefabs that may have the same name
// and avoids polluting the user's project with Resources.
// This is not recommended for production code...use Resources.Load or AssetBundles instead.
if (turretUIPrefab == null)
{
string path = UnityEditor.AssetDatabase.GUIDToAssetPath("4d16730f7a8ba0a419530d1156d25080");
turretUIPrefab = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(path);
}
if (projectilePrefab == null)
{
string path = UnityEditor.AssetDatabase.GUIDToAssetPath("aec853915cd4f4477ba1532b5fe05488");
projectilePrefab = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(path);
}
#endif
this.enabled = false;
}
public override void OnStartLocalPlayer()
{
if (turretUIPrefab != null)
runtimeData.turretUI = Instantiate(turretUIPrefab);
if (runtimeData.turretUI != null)
{
if (runtimeData.turretUI.TryGetComponent(out TurretUI canvasControlPanel))
canvasControlPanel.Refresh(moveKeys, optionsKeys);
runtimeData.turretUI.SetActive(controlOptions.HasFlag(ControlOptions.ShowUI));
}
}
public override void OnStopLocalPlayer()
{
if (runtimeData.turretUI != null)
Destroy(runtimeData.turretUI);
runtimeData.turretUI = null;
}
public override void OnStartAuthority()
{
// Calculate DPI-aware sensitivity
float dpiScale = (Screen.dpi > 0) ? (Screen.dpi / BASE_DPI) : 1f;
runtimeData.mouseSensitivity = turretAcceleration * dpiScale;
SetCursor(controlOptions.HasFlag(ControlOptions.MouseLock));
this.enabled = true;
}
public override void OnStopAuthority()
{
SetCursor(false);
this.enabled = false;
}
#endregion
void Update()
{
float deltaTime = Time.deltaTime;
HandleOptions();
HandlePitch(deltaTime);
if (controlOptions.HasFlag(ControlOptions.MouseLock))
HandleMouseTurret(deltaTime);
else
HandleTurning(deltaTime);
HandleShooting();
}
void OnPlayerColorChanged(Color32 _, Color32 newColor)
{
if (cachedMaterial == null)
cachedMaterial = playerObject.GetComponent<Renderer>().material;
cachedMaterial.color = newColor;
playerObject.SetActive(newColor != Color.black);
}
void SetCursor(bool locked)
{
Cursor.lockState = locked ? CursorLockMode.Locked : CursorLockMode.None;
Cursor.visible = !locked;
}
void HandleOptions()
{
if (optionsKeys.MouseLock != KeyCode.None && Input.GetKeyUp(optionsKeys.MouseLock))
{
controlOptions ^= ControlOptions.MouseLock;
SetCursor(controlOptions.HasFlag(ControlOptions.MouseLock));
}
if (optionsKeys.AutoLevel != KeyCode.None && Input.GetKeyUp(optionsKeys.AutoLevel))
controlOptions ^= ControlOptions.AutoLevel;
if (optionsKeys.ToggleUI != KeyCode.None && Input.GetKeyUp(optionsKeys.ToggleUI))
{
controlOptions ^= ControlOptions.ShowUI;
if (runtimeData.turretUI != null)
runtimeData.turretUI.SetActive(controlOptions.HasFlag(ControlOptions.ShowUI));
}
}
void HandleTurning(float deltaTime)
{
float targetTurnSpeed = 0f;
// TurnLeft and TurnRight cancel each other out, reducing targetTurnSpeed to zero.
if (moveKeys.TurnLeft != KeyCode.None && Input.GetKey(moveKeys.TurnLeft))
targetTurnSpeed -= maxTurretSpeed;
if (moveKeys.TurnRight != KeyCode.None && Input.GetKey(moveKeys.TurnRight))
targetTurnSpeed += maxTurretSpeed;
runtimeData.turretSpeed = Mathf.MoveTowards(runtimeData.turretSpeed, targetTurnSpeed, turretAcceleration * maxTurretSpeed * deltaTime);
turret.Rotate(0f, runtimeData.turretSpeed * deltaTime, 0f);
}
void HandleMouseTurret(float deltaTime)
{
// Accumulate mouse input over time
runtimeData.mouseInputX += Input.GetAxisRaw("Mouse X") * runtimeData.mouseSensitivity;
// Clamp the accumulator to simulate key press behavior
runtimeData.mouseInputX = Mathf.Clamp(runtimeData.mouseInputX, -1f, 1f);
// Calculate target turn speed
float targetTurnSpeed = runtimeData.mouseInputX * maxTurretSpeed;
// Use the same acceleration logic as HandleTurning
runtimeData.turretSpeed = Mathf.MoveTowards(runtimeData.turretSpeed, targetTurnSpeed, runtimeData.mouseSensitivity * maxTurretSpeed * deltaTime);
// Apply rotation
turret.Rotate(0f, runtimeData.turretSpeed * deltaTime, 0f);
runtimeData.mouseInputX = Mathf.MoveTowards(runtimeData.mouseInputX, 0f, runtimeData.mouseSensitivity * deltaTime);
}
void HandlePitch(float deltaTime)
{
float targetPitchSpeed = 0f;
bool inputDetected = false;
// Up and Down arrows for pitch
if (moveKeys.PitchUp != KeyCode.None && Input.GetKey(moveKeys.PitchUp))
{
targetPitchSpeed -= maxPitchSpeed;
inputDetected = true;
}
if (moveKeys.PitchDown != KeyCode.None && Input.GetKey(moveKeys.PitchDown))
{
targetPitchSpeed += maxPitchSpeed;
inputDetected = true;
}
runtimeData.pitchSpeed = Mathf.MoveTowards(runtimeData.pitchSpeed, targetPitchSpeed, pitchAcceleration * maxPitchSpeed * deltaTime);
// Apply pitch rotation
runtimeData.pitchAngle += runtimeData.pitchSpeed * deltaTime;
runtimeData.pitchAngle = Mathf.Clamp(runtimeData.pitchAngle, -maxPitchUpAngle, maxPitchDownAngle);
// Return to -90 when no input
if (!inputDetected && controlOptions.HasFlag(ControlOptions.AutoLevel))
runtimeData.pitchAngle = Mathf.MoveTowards(runtimeData.pitchAngle, 0f, maxPitchSpeed * deltaTime);
// Apply rotation to barrel -- rotation is (-90, 0, 180) in the prefab
// so that's what we have to work towards.
barrel.localRotation = Quaternion.Euler(-90f + runtimeData.pitchAngle, 0f, 180f);
}
#region Shooting
bool CanShoot => NetworkTime.time >= runtimeData.lastShotTime + cooldownTime;
void HandleShooting()
{
if (CanShoot && otherKeys.Shoot != KeyCode.None && Input.GetKeyUp(otherKeys.Shoot))
{
CmdShoot();
if (!isServer) DoShoot();
}
}
[Command]
void CmdShoot()
{
if (!CanShoot) return;
//Debug.Log("CmdShoot");
RpcShoot();
DoShoot();
}
[ClientRpc(includeOwner = false)]
void RpcShoot()
{
//Debug.Log("RpcShoot");
if (!isServer) DoShoot();
}
void DoShoot()
{
//Debug.Log($"DoShoot isServerOnly:{isServerOnly} | isServer:{isServer} | isClientOnly:{isClientOnly}");
// Turret
// - Barrel (with Collider)
// - BarrelEnd
// - ProjectileMount
// Locally instantiate the projectile at the end of the barrel
GameObject go = Instantiate(projectilePrefab, projectileMount.position, projectileMount.rotation);
// Ignore collision between the projectile and the barrel collider
Physics.IgnoreCollision(go.GetComponent<Collider>(), barrelCollider);
// Update the last shot time
runtimeData.lastShotTime = NetworkTime.time;
}
#endregion
}
}