aha
This commit is contained in:
@ -0,0 +1,468 @@
|
||||
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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user