Map mehanic ready to multiplayer
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
using Unity.Netcode;
|
||||
using Unity.Netcode.Components;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
public static class MapRadarNetworkSetup
|
||||
{
|
||||
private const string LeverPrefabPath =
|
||||
"Assets/CONTENT/Game/Prefabs/Interaction/PhysicalLever.prefab";
|
||||
|
||||
[MenuItem("FORT-BO/Map Radar/Configure Network Prototype")]
|
||||
public static void ConfigureNetworkPrototype()
|
||||
{
|
||||
MapController mapController = Object.FindFirstObjectByType<MapController>();
|
||||
if (mapController == null)
|
||||
{
|
||||
Debug.LogError("MapController was not found in the open scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
MapCoordinateSystem coordinateSystem = mapController.CoordinateSystem;
|
||||
if (coordinateSystem == null || !coordinateSystem.HasValidCorners())
|
||||
{
|
||||
Debug.LogError("MapCoordinateSystem is not configured.", mapController);
|
||||
return;
|
||||
}
|
||||
|
||||
Undo.RegisterFullObjectHierarchyUndo(mapController.transform.root.gameObject,
|
||||
"Configure network map prototype");
|
||||
|
||||
GetOrAdd<NetworkObject>(mapController.gameObject);
|
||||
NetworkBaseHealth baseHealth = GetOrAdd<NetworkBaseHealth>(mapController.gameObject);
|
||||
MapControlBinder binder = GetOrAdd<MapControlBinder>(mapController.gameObject);
|
||||
|
||||
Transform controlsRoot = GetOrCreateChild(
|
||||
mapController.transform.root,
|
||||
"Map Controls");
|
||||
|
||||
MapBasis basis = CreateBasis(coordinateSystem);
|
||||
NetworkPhysicalWheelInteractable horizontalWheel = CreateWheel(
|
||||
controlsRoot,
|
||||
"Map Horizontal Wheel",
|
||||
basis.ControlCenter - basis.Horizontal * 1.8f,
|
||||
basis.Rotation,
|
||||
"Aim horizontal");
|
||||
NetworkPhysicalWheelInteractable verticalWheel = CreateWheel(
|
||||
controlsRoot,
|
||||
"Map Vertical Wheel",
|
||||
basis.ControlCenter - basis.Horizontal * 0.6f,
|
||||
basis.Rotation,
|
||||
"Aim vertical");
|
||||
NetworkPhysicalLeverInteractable fireLever = CreateFireLever(
|
||||
controlsRoot,
|
||||
basis.ControlCenter + basis.Horizontal * 0.8f,
|
||||
basis.Rotation);
|
||||
NetworkButtonInteractable scanButton = CreateScanButton(
|
||||
controlsRoot,
|
||||
basis.ControlCenter + basis.Horizontal * 2f,
|
||||
basis.Rotation);
|
||||
|
||||
SerializedObject map = new(mapController);
|
||||
SetObject(map, "roundGameLoop",
|
||||
Object.FindFirstObjectByType<RoundGameLoopController>());
|
||||
SetObject(map, "baseHealth", baseHealth);
|
||||
map.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
SerializedObject binding = new(binder);
|
||||
SetObject(binding, "mapController", mapController);
|
||||
SetObject(binding, "horizontalWheel", horizontalWheel);
|
||||
SetObject(binding, "verticalWheel", verticalWheel);
|
||||
SetObject(binding, "fireLever", fireLever);
|
||||
SetObject(binding, "scanButton", scanButton);
|
||||
binding.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
EditorUtility.SetDirty(mapController);
|
||||
EditorUtility.SetDirty(binder);
|
||||
EditorUtility.SetDirty(baseHealth);
|
||||
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(
|
||||
mapController.gameObject.scene);
|
||||
UnityEditor.SceneManagement.EditorSceneManager.SaveOpenScenes();
|
||||
Selection.activeObject = controlsRoot.gameObject;
|
||||
|
||||
Debug.Log(
|
||||
"[Map] Network prototype configured: X/Y wheels, fire lever, " +
|
||||
"scan button and base health are connected.",
|
||||
mapController);
|
||||
}
|
||||
|
||||
private static NetworkPhysicalWheelInteractable CreateWheel(
|
||||
Transform parent,
|
||||
string name,
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
string interactionLabel)
|
||||
{
|
||||
Transform root = GetOrCreateChild(parent, name);
|
||||
root.SetPositionAndRotation(position, rotation);
|
||||
root.localScale = Vector3.one;
|
||||
|
||||
GetOrAdd<NetworkObject>(root.gameObject);
|
||||
NetworkTransform networkTransform = GetOrAdd<NetworkTransform>(root.gameObject);
|
||||
networkTransform.AuthorityMode = NetworkTransform.AuthorityModes.Server;
|
||||
networkTransform.UseUnreliableDeltas = true;
|
||||
|
||||
Transform legacyVisual = root.Find("Wheel Visual");
|
||||
Transform visual = root.Find("Wheel Pivot");
|
||||
if (visual == null)
|
||||
{
|
||||
if (legacyVisual != null)
|
||||
Object.DestroyImmediate(legacyVisual.gameObject);
|
||||
visual = new GameObject("Wheel Pivot").transform;
|
||||
visual.SetParent(root, false);
|
||||
}
|
||||
|
||||
visual.localPosition = Vector3.zero;
|
||||
visual.localRotation = Quaternion.identity;
|
||||
visual.localScale = Vector3.one;
|
||||
|
||||
Transform disk = visual.Find("Wheel Disk");
|
||||
if (disk == null)
|
||||
{
|
||||
GameObject cylinder = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
|
||||
cylinder.name = "Wheel Disk";
|
||||
disk = cylinder.transform;
|
||||
disk.SetParent(visual, false);
|
||||
}
|
||||
|
||||
disk.localPosition = Vector3.zero;
|
||||
disk.localRotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
disk.localScale = new Vector3(0.48f, 0.08f, 0.48f);
|
||||
|
||||
Transform handle = visual.Find("Handle");
|
||||
if (handle == null)
|
||||
{
|
||||
GameObject handleObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
handleObject.name = "Handle";
|
||||
handle = handleObject.transform;
|
||||
handle.SetParent(visual, false);
|
||||
Object.DestroyImmediate(handleObject.GetComponent<Collider>());
|
||||
}
|
||||
|
||||
handle.localPosition = new Vector3(0f, 0.65f, 0f);
|
||||
handle.localRotation = Quaternion.identity;
|
||||
handle.localScale = new Vector3(0.14f, 0.4f, 0.14f);
|
||||
|
||||
NetworkPhysicalWheelInteractable wheel =
|
||||
GetOrAdd<NetworkPhysicalWheelInteractable>(root.gameObject);
|
||||
SerializedObject serializedWheel = new(wheel);
|
||||
SetObject(serializedWheel, "wheelVisual", visual);
|
||||
SetVector3(serializedWheel, "neutralLocalEulerAngles", Vector3.zero);
|
||||
SetString(serializedWheel, "interactionLabel", interactionLabel);
|
||||
SetVector3(serializedWheel, "localRotationAxis", Vector3.forward);
|
||||
SetVector3(serializedWheel, "localReferenceDirection", Vector3.up);
|
||||
SetFloat(serializedWheel, "minimumAngle", -180f);
|
||||
SetFloat(serializedWheel, "maximumAngle", 180f);
|
||||
SetFloat(serializedWheel, "initialNormalizedValue", 0.5f);
|
||||
SetFloat(serializedWheel, "maximumInteractionDistance", 3.5f);
|
||||
SetFloat(serializedWheel, "maximumControlDistance", 4f);
|
||||
serializedWheel.ApplyModifiedPropertiesWithoutUndo();
|
||||
return wheel;
|
||||
}
|
||||
|
||||
private static NetworkPhysicalLeverInteractable CreateFireLever(
|
||||
Transform parent,
|
||||
Vector3 position,
|
||||
Quaternion rotation)
|
||||
{
|
||||
Transform existing = parent.Find("Map Fire Lever");
|
||||
GameObject root;
|
||||
if (existing != null)
|
||||
{
|
||||
root = existing.gameObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(LeverPrefabPath);
|
||||
if (prefab == null)
|
||||
throw new MissingReferenceException($"Lever prefab not found: {LeverPrefabPath}");
|
||||
root = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
|
||||
root.name = "Map Fire Lever";
|
||||
}
|
||||
|
||||
root.transform.SetPositionAndRotation(position, rotation);
|
||||
root.transform.localScale = Vector3.one * 0.55f;
|
||||
NetworkPhysicalLeverInteractable lever =
|
||||
root.GetComponent<NetworkPhysicalLeverInteractable>();
|
||||
SerializedObject serializedLever = new(lever);
|
||||
SetString(serializedLever, "interactionLabel", "Fire");
|
||||
SetBool(serializedLever, "initialPositionIsMaximum", false);
|
||||
SetBool(serializedLever, "returnToInitialPositionAfterSwitch", true);
|
||||
SetFloat(serializedLever, "automaticReturnDelay", 0.2f);
|
||||
SetFloat(serializedLever, "maximumInteractionDistance", 3.5f);
|
||||
SetFloat(serializedLever, "maximumControlDistance", 4f);
|
||||
serializedLever.ApplyModifiedPropertiesWithoutUndo();
|
||||
return lever;
|
||||
}
|
||||
|
||||
private static NetworkButtonInteractable CreateScanButton(
|
||||
Transform parent,
|
||||
Vector3 position,
|
||||
Quaternion rotation)
|
||||
{
|
||||
Transform root = GetOrCreateChild(parent, "Map Scan Button");
|
||||
root.SetPositionAndRotation(position, rotation);
|
||||
root.localScale = Vector3.one;
|
||||
|
||||
GetOrAdd<NetworkObject>(root.gameObject);
|
||||
NetworkTransform networkTransform = GetOrAdd<NetworkTransform>(root.gameObject);
|
||||
networkTransform.AuthorityMode = NetworkTransform.AuthorityModes.Server;
|
||||
|
||||
Transform visual = root.Find("Button Visual");
|
||||
if (visual == null)
|
||||
{
|
||||
GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
cube.name = "Button Visual";
|
||||
visual = cube.transform;
|
||||
visual.SetParent(root, false);
|
||||
}
|
||||
|
||||
visual.localPosition = Vector3.zero;
|
||||
visual.localRotation = Quaternion.identity;
|
||||
visual.localScale = new Vector3(0.55f, 0.55f, 0.18f);
|
||||
|
||||
NetworkButtonInteractable button = GetOrAdd<NetworkButtonInteractable>(root.gameObject);
|
||||
SerializedObject serializedButton = new(button);
|
||||
SetString(serializedButton, "interactionLabel", "Scan map");
|
||||
SetFloat(serializedButton, "maximumInteractionDistance", 3.5f);
|
||||
serializedButton.ApplyModifiedPropertiesWithoutUndo();
|
||||
return button;
|
||||
}
|
||||
|
||||
private static MapBasis CreateBasis(MapCoordinateSystem coordinates)
|
||||
{
|
||||
Vector3 bottomLeft = coordinates.BottomLeft.position;
|
||||
Vector3 horizontalEdge = coordinates.BottomRight.position - bottomLeft;
|
||||
Vector3 verticalEdge = coordinates.TopLeft.position - bottomLeft;
|
||||
Vector3 horizontal = horizontalEdge.normalized;
|
||||
Vector3 vertical = verticalEdge.normalized;
|
||||
Vector3 normal = Vector3.Cross(horizontal, vertical).normalized;
|
||||
Vector3 bottomCenter = bottomLeft + horizontalEdge * 0.5f;
|
||||
Vector3 controlCenter = bottomCenter - vertical * 0.85f + normal * 0.55f;
|
||||
return new MapBasis(
|
||||
horizontal,
|
||||
controlCenter,
|
||||
Quaternion.LookRotation(normal, vertical));
|
||||
}
|
||||
|
||||
private static Transform GetOrCreateChild(Transform parent, string name)
|
||||
{
|
||||
Transform existing = parent.Find(name);
|
||||
if (existing != null)
|
||||
return existing;
|
||||
GameObject child = new(name);
|
||||
child.transform.SetParent(parent, false);
|
||||
Undo.RegisterCreatedObjectUndo(child, $"Create {name}");
|
||||
return child.transform;
|
||||
}
|
||||
|
||||
private static T GetOrAdd<T>(GameObject target) where T : Component
|
||||
{
|
||||
T component = target.GetComponent<T>();
|
||||
return component != null ? component : Undo.AddComponent<T>(target);
|
||||
}
|
||||
|
||||
private static void SetObject(SerializedObject target, string property, Object value) =>
|
||||
target.FindProperty(property).objectReferenceValue = value;
|
||||
private static void SetString(SerializedObject target, string property, string value) =>
|
||||
target.FindProperty(property).stringValue = value;
|
||||
private static void SetFloat(SerializedObject target, string property, float value) =>
|
||||
target.FindProperty(property).floatValue = value;
|
||||
private static void SetBool(SerializedObject target, string property, bool value) =>
|
||||
target.FindProperty(property).boolValue = value;
|
||||
private static void SetVector3(SerializedObject target, string property, Vector3 value) =>
|
||||
target.FindProperty(property).vector3Value = value;
|
||||
|
||||
private readonly struct MapBasis
|
||||
{
|
||||
public readonly Vector3 Horizontal;
|
||||
public readonly Vector3 ControlCenter;
|
||||
public readonly Quaternion Rotation;
|
||||
|
||||
public MapBasis(Vector3 horizontal, Vector3 controlCenter, Quaternion rotation)
|
||||
{
|
||||
Horizontal = horizontal;
|
||||
ControlCenter = controlCenter;
|
||||
Rotation = rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3807965ea93055647ad20aa098cd2032
|
||||
@@ -0,0 +1,73 @@
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class MapControlBinder : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private MapController mapController;
|
||||
[SerializeField] private NetworkPhysicalWheelInteractable horizontalWheel;
|
||||
[SerializeField] private NetworkPhysicalWheelInteractable verticalWheel;
|
||||
[SerializeField] private NetworkPhysicalLeverInteractable fireLever;
|
||||
[SerializeField] private NetworkButtonInteractable scanButton;
|
||||
|
||||
private bool initialValuesApplied;
|
||||
|
||||
private void Reset() => mapController = GetComponent<MapController>();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (horizontalWheel != null)
|
||||
horizontalWheel.ValueChanged += HandleHorizontalChanged;
|
||||
if (verticalWheel != null)
|
||||
verticalWheel.ValueChanged += HandleVerticalChanged;
|
||||
if (fireLever != null)
|
||||
fireLever.StateChanged += HandleFireChanged;
|
||||
if (scanButton != null)
|
||||
scanButton.Pressed += HandleScanPressed;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (horizontalWheel != null)
|
||||
horizontalWheel.ValueChanged -= HandleHorizontalChanged;
|
||||
if (verticalWheel != null)
|
||||
verticalWheel.ValueChanged -= HandleVerticalChanged;
|
||||
if (fireLever != null)
|
||||
fireLever.StateChanged -= HandleFireChanged;
|
||||
if (scanButton != null)
|
||||
scanButton.Pressed -= HandleScanPressed;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (initialValuesApplied || mapController == null || !mapController.IsSpawned ||
|
||||
horizontalWheel == null || verticalWheel == null ||
|
||||
!horizontalWheel.IsSpawned || !verticalWheel.IsSpawned)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
initialValuesApplied = true;
|
||||
HandleHorizontalChanged(horizontalWheel.NormalizedValue);
|
||||
HandleVerticalChanged(verticalWheel.NormalizedValue);
|
||||
}
|
||||
|
||||
private void HandleHorizontalChanged(float value) =>
|
||||
ApplyOnServer(() => mapController.SetHorizontalNormalized(value));
|
||||
|
||||
private void HandleVerticalChanged(float value) =>
|
||||
ApplyOnServer(() => mapController.SetVerticalNormalized(value));
|
||||
|
||||
private void HandleFireChanged(bool active)
|
||||
{
|
||||
if (active)
|
||||
ApplyOnServer(mapController.RequestPseudoFire);
|
||||
}
|
||||
|
||||
private void HandleScanPressed() => ApplyOnServer(mapController.RequestRadarScan);
|
||||
|
||||
private void ApplyOnServer(System.Action action)
|
||||
{
|
||||
if (mapController != null && mapController.IsSpawned && mapController.IsServer)
|
||||
action?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a23aadbde3d31042abf625ff22e6fd8
|
||||
@@ -1,100 +1,534 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class MapController : MonoBehaviour
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public sealed class MapController : NetworkBehaviour
|
||||
{
|
||||
[Header("Main References")]
|
||||
[SerializeField] private MapCrosshairController crosshair = null;
|
||||
[SerializeField] private MapCoordinateSystem coordinateSystem = null;
|
||||
[SerializeField] private MapPlayer mapPlayer = null;
|
||||
[SerializeField] private MapCrosshairController crosshair;
|
||||
[SerializeField] private MapCoordinateSystem coordinateSystem;
|
||||
[SerializeField] private MapPlayer mapPlayer;
|
||||
[SerializeField] private RoundGameLoopController roundGameLoop;
|
||||
[SerializeField] private NetworkBaseHealth baseHealth;
|
||||
|
||||
[Header("Target System")]
|
||||
[SerializeField] private MapTargetController targetController = null;
|
||||
[SerializeField] private Transform targetParent = null;
|
||||
[SerializeField] private MapTargetController targetController;
|
||||
[SerializeField] private Transform targetParent;
|
||||
[SerializeField, Min(0.02f)] private float simulationStep = 0.1f;
|
||||
|
||||
[Header("Radar System")]
|
||||
[SerializeField] private MapRadarController radarController = null;
|
||||
[SerializeField] private MapRadarController radarController;
|
||||
[FormerlySerializedAs("targetPrefab")]
|
||||
[SerializeField] private MapTarget radarMarkPrefab = null;
|
||||
[SerializeField] private MapTarget radarMarkPrefab;
|
||||
|
||||
[Header("Crosshair Objects")]
|
||||
[SerializeField] private Transform relayX = null;
|
||||
[SerializeField] private Transform relayY = null;
|
||||
[SerializeField] private Transform crosshairVisual = null;
|
||||
[SerializeField] private Transform relayX;
|
||||
[SerializeField] private Transform relayY;
|
||||
[SerializeField] private Transform crosshairVisual;
|
||||
|
||||
[Header("Input")]
|
||||
[SerializeField, Min(0f)] private float movementSpeed = 1f;
|
||||
[SerializeField] private bool useKeyboardInput = true;
|
||||
[Header("Pseudo Shot")]
|
||||
[SerializeField, Min(0.01f)] private float hitRadius = 0.65f;
|
||||
[SerializeField, Min(1)] private int shotDamage = 1;
|
||||
[SerializeField] private MissileType pseudoMissileType = MissileType.Standard;
|
||||
[SerializeField] private bool ignoreMissileTypeForPrototype = true;
|
||||
|
||||
[Header("Network Events")]
|
||||
[SerializeField] private UnityEvent<int> waveStarted;
|
||||
[SerializeField] private UnityEvent<bool> shotResolved;
|
||||
[SerializeField] private UnityEvent allTargetsResolved;
|
||||
[SerializeField] private UnityEvent allTargetsDestroyed;
|
||||
|
||||
private readonly NetworkVariable<Vector2> crosshairCoordinates = new(
|
||||
new Vector2(5f, 5f), NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
private readonly NetworkVariable<int> currentWave = new(
|
||||
0, NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
private readonly NetworkVariable<double> radarReadyAt = new(
|
||||
0d, NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
private readonly NetworkVariable<MapShotResultState> lastShotResult = new(
|
||||
default, NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
private readonly NetworkVariable<uint> resolvedSequence = new(
|
||||
0, NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
private readonly NetworkVariable<uint> destroyedSequence = new(
|
||||
0, NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private readonly NetworkList<MapTargetNetworkState> activeTargets = new();
|
||||
private readonly NetworkList<Vector2> radarMarks = new();
|
||||
|
||||
private float simulationAccumulator;
|
||||
private int nextTargetId = 1;
|
||||
private int initialTargetCount;
|
||||
private int escapedTargetCount;
|
||||
private bool waveIsActive;
|
||||
private bool roundEventsBound;
|
||||
|
||||
public MapCoordinateSystem CoordinateSystem => coordinateSystem;
|
||||
public Transform CrosshairVisual => crosshairVisual;
|
||||
public Vector2 CrosshairCoordinates => crosshairCoordinates.Value;
|
||||
public int CurrentWave => currentWave.Value;
|
||||
public int ActiveTargetCount => activeTargets.Count;
|
||||
public bool WaveIsActive => waveIsActive;
|
||||
public double RadarCooldownRemaining => IsSpawned && NetworkManager != null
|
||||
? Math.Max(0d, radarReadyAt.Value - NetworkManager.ServerTime.Time)
|
||||
: 0d;
|
||||
|
||||
private void Awake()
|
||||
public event Action<int> WaveStarted;
|
||||
public event Action<MapShotResultState> ShotResolved;
|
||||
public event Action AllTargetsResolved;
|
||||
public event Action AllTargetsDestroyed;
|
||||
|
||||
private void Awake() => ConfigurePresentation();
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (mapPlayer != null)
|
||||
mapPlayer.Configure(coordinateSystem);
|
||||
simulationStep = Mathf.Max(0.02f, simulationStep);
|
||||
hitRadius = Mathf.Max(0.01f, hitRadius);
|
||||
shotDamage = Mathf.Max(1, shotDamage);
|
||||
}
|
||||
|
||||
if (targetController != null)
|
||||
targetController.Configure(coordinateSystem, targetParent, mapPlayer);
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
activeTargets.OnListChanged += HandleTargetListChanged;
|
||||
radarMarks.OnListChanged += HandleRadarListChanged;
|
||||
crosshairCoordinates.OnValueChanged += HandleCrosshairChanged;
|
||||
currentWave.OnValueChanged += HandleWaveChanged;
|
||||
radarReadyAt.OnValueChanged += HandleRadarReadyAtChanged;
|
||||
lastShotResult.OnValueChanged += HandleShotResultChanged;
|
||||
resolvedSequence.OnValueChanged += HandleResolvedSequenceChanged;
|
||||
destroyedSequence.OnValueChanged += HandleDestroyedSequenceChanged;
|
||||
|
||||
ConfigurePresentation();
|
||||
BindRoundEvents();
|
||||
ApplyPresentation();
|
||||
|
||||
if (IsServer && roundGameLoop != null &&
|
||||
roundGameLoop.Phase == RoundPhase.WorkInProgress && currentWave.Value == 0)
|
||||
StartNextWaveOnServer();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
activeTargets.OnListChanged -= HandleTargetListChanged;
|
||||
radarMarks.OnListChanged -= HandleRadarListChanged;
|
||||
crosshairCoordinates.OnValueChanged -= HandleCrosshairChanged;
|
||||
currentWave.OnValueChanged -= HandleWaveChanged;
|
||||
radarReadyAt.OnValueChanged -= HandleRadarReadyAtChanged;
|
||||
lastShotResult.OnValueChanged -= HandleShotResultChanged;
|
||||
resolvedSequence.OnValueChanged -= HandleResolvedSequenceChanged;
|
||||
destroyedSequence.OnValueChanged -= HandleDestroyedSequenceChanged;
|
||||
UnbindRoundEvents();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
activeTargets.Dispose();
|
||||
radarMarks.Dispose();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!roundEventsBound)
|
||||
BindRoundEvents();
|
||||
if (radarController != null)
|
||||
radarController.Configure(coordinateSystem, targetController, radarMarkPrefab);
|
||||
|
||||
if (crosshair == null)
|
||||
radarController.SetCooldownRemaining((float)RadarCooldownRemaining);
|
||||
if (!IsServer || !IsSpawned || !waveIsActive)
|
||||
return;
|
||||
|
||||
crosshair.Configure(
|
||||
coordinateSystem,
|
||||
relayX,
|
||||
relayY,
|
||||
crosshairVisual,
|
||||
movementSpeed,
|
||||
useKeyboardInput,
|
||||
targetController);
|
||||
|
||||
simulationAccumulator += Time.deltaTime;
|
||||
while (simulationAccumulator >= simulationStep)
|
||||
{
|
||||
simulationAccumulator -= simulationStep;
|
||||
SimulateTargets(simulationStep);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetHorizontalAxis(float value)
|
||||
public void SetHorizontalNormalized(float value)
|
||||
{
|
||||
if (crosshair != null)
|
||||
crosshair.SetHorizontalAxis(value);
|
||||
Vector2 next = crosshairCoordinates.Value;
|
||||
next.x = Mathf.Clamp01(value) * MapCoordinateSystem.CoordinateMaximum;
|
||||
RequestCrosshairCoordinates(next);
|
||||
}
|
||||
|
||||
public void SetVerticalAxis(float value)
|
||||
public void SetVerticalNormalized(float value)
|
||||
{
|
||||
if (crosshair != null)
|
||||
crosshair.SetVerticalAxis(value);
|
||||
Vector2 next = crosshairCoordinates.Value;
|
||||
next.y = Mathf.Clamp01(value) * MapCoordinateSystem.CoordinateMaximum;
|
||||
RequestCrosshairCoordinates(next);
|
||||
}
|
||||
|
||||
public Vector2 GetCrosshairCoordinates()
|
||||
public void SetHorizontalAxis(float value) =>
|
||||
SetHorizontalNormalized(Mathf.InverseLerp(-1f, 1f, value));
|
||||
public void SetVerticalAxis(float value) =>
|
||||
SetVerticalNormalized(Mathf.InverseLerp(-1f, 1f, value));
|
||||
public Vector2 GetCrosshairCoordinates() => crosshairCoordinates.Value;
|
||||
|
||||
public void RequestPseudoFire()
|
||||
{
|
||||
return crosshair != null ? crosshair.GetCrosshairCoordinates() : Vector2.zero;
|
||||
if (!IsSpawned)
|
||||
return;
|
||||
if (IsServer)
|
||||
ResolvePseudoShotOnServer();
|
||||
else
|
||||
RequestPseudoFireRpc();
|
||||
}
|
||||
|
||||
public void CreateAndPlaceTargets()
|
||||
public void RequestRadarScan()
|
||||
{
|
||||
if (targetController != null)
|
||||
targetController.CreateAndPlaceTargets();
|
||||
if (!IsSpawned)
|
||||
return;
|
||||
if (IsServer)
|
||||
ScanRadarOnServer();
|
||||
else
|
||||
RequestRadarScanRpc();
|
||||
}
|
||||
|
||||
public bool ActivateRadar()
|
||||
{
|
||||
if (!IsSpawned || RadarCooldownRemaining > 0d)
|
||||
return false;
|
||||
RequestRadarScan();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CreateAndPlaceTargets() => StartNextWave();
|
||||
|
||||
public bool StartNextWave()
|
||||
{
|
||||
return targetController != null && targetController.StartNextWave();
|
||||
if (!IsSpawned)
|
||||
return false;
|
||||
if (IsServer)
|
||||
return StartNextWaveOnServer();
|
||||
RequestStartNextWaveRpc();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetNearestTarget(out MapTarget target, out float distance)
|
||||
{
|
||||
if (crosshair != null)
|
||||
return crosshair.TryGetNearestTarget(out target, out distance);
|
||||
|
||||
target = null;
|
||||
distance = -1f;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ActivateRadar()
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void SetCrosshairCoordinatesRpc(Vector2 coordinates) =>
|
||||
SetCrosshairCoordinatesOnServer(coordinates);
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void RequestPseudoFireRpc() => ResolvePseudoShotOnServer();
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void RequestRadarScanRpc() => ScanRadarOnServer();
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void RequestStartNextWaveRpc() => StartNextWaveOnServer();
|
||||
|
||||
private void RequestCrosshairCoordinates(Vector2 coordinates)
|
||||
{
|
||||
return radarController != null && radarController.ActivateRadar();
|
||||
if (!IsSpawned)
|
||||
return;
|
||||
if (IsServer)
|
||||
SetCrosshairCoordinatesOnServer(coordinates);
|
||||
else
|
||||
SetCrosshairCoordinatesRpc(coordinates);
|
||||
}
|
||||
|
||||
private void SetCrosshairCoordinatesOnServer(Vector2 coordinates)
|
||||
{
|
||||
if (!IsServer)
|
||||
return;
|
||||
crosshairCoordinates.Value = coordinateSystem != null
|
||||
? coordinateSystem.ClampCoordinates(coordinates)
|
||||
: new Vector2(
|
||||
Mathf.Clamp(coordinates.x, 0f, MapCoordinateSystem.CoordinateMaximum),
|
||||
Mathf.Clamp(coordinates.y, 0f, MapCoordinateSystem.CoordinateMaximum));
|
||||
}
|
||||
|
||||
private bool StartNextWaveOnServer()
|
||||
{
|
||||
if (!IsServer || targetController == null ||
|
||||
!targetController.HasValidConfiguration())
|
||||
{
|
||||
Debug.LogWarning("[Map] Cannot start target wave: map or target configs are incomplete.", this);
|
||||
return false;
|
||||
}
|
||||
|
||||
activeTargets.Clear();
|
||||
radarMarks.Clear();
|
||||
currentWave.Value++;
|
||||
escapedTargetCount = 0;
|
||||
initialTargetCount = 0;
|
||||
waveIsActive = true;
|
||||
simulationAccumulator = 0f;
|
||||
|
||||
for (int index = 0; index < targetController.TargetsPerWave; index++)
|
||||
{
|
||||
int configIndex = targetController.GetRandomValidConfigIndex();
|
||||
TargetConfig config = targetController.GetConfig(configIndex);
|
||||
if (config == null)
|
||||
continue;
|
||||
|
||||
Vector2 coordinates = new(
|
||||
UnityEngine.Random.Range(0f, MapCoordinateSystem.CoordinateMaximum),
|
||||
UnityEngine.Random.Range(0f, MapCoordinateSystem.CoordinateMaximum));
|
||||
activeTargets.Add(new MapTargetNetworkState(
|
||||
nextTargetId++, configIndex, coordinates,
|
||||
config.Health, config.TimeBeforeAttack));
|
||||
initialTargetCount++;
|
||||
}
|
||||
|
||||
Debug.Log($"[Map] Wave {currentWave.Value} started with {initialTargetCount} targets.", this);
|
||||
if (initialTargetCount == 0)
|
||||
CompleteWaveOnServer();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SimulateTargets(float deltaTime)
|
||||
{
|
||||
for (int index = activeTargets.Count - 1; index >= 0; index--)
|
||||
{
|
||||
MapTargetNetworkState state = activeTargets[index];
|
||||
TargetConfig config = targetController.GetConfig(state.ConfigIndex);
|
||||
if (config == null)
|
||||
{
|
||||
ResolveEscapedTarget(index, state, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool isAirTarget = config.TargetPrefab is AirTarget;
|
||||
if (isAirTarget && mapPlayer != null && config.MovementSpeed > 0f)
|
||||
{
|
||||
Vector2 destination = mapPlayer.MapCoordinates;
|
||||
state.Coordinates = Vector2.MoveTowards(
|
||||
state.Coordinates, destination, config.MovementSpeed * deltaTime);
|
||||
if (Vector2.Distance(state.Coordinates, destination) <= config.StoppingDistance)
|
||||
{
|
||||
ResolveEscapedTarget(index, state, config);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state.AttackTimeRemaining = Mathf.Max(
|
||||
0f, state.AttackTimeRemaining - deltaTime);
|
||||
if (state.AttackTimeRemaining <= 0f)
|
||||
{
|
||||
ResolveEscapedTarget(index, state, config);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
activeTargets[index] = state;
|
||||
}
|
||||
|
||||
TryCompleteWaveOnServer();
|
||||
}
|
||||
|
||||
private void ResolveEscapedTarget(int index, MapTargetNetworkState target, TargetConfig config)
|
||||
{
|
||||
int damage = config != null ? config.BaseDamage : 1;
|
||||
escapedTargetCount++;
|
||||
activeTargets.RemoveAt(index);
|
||||
if (baseHealth != null)
|
||||
baseHealth.ApplyDamage(damage);
|
||||
Debug.Log($"[Map] Target {target.TargetId} reached the base. Base damage: {damage}.", this);
|
||||
}
|
||||
|
||||
private void ResolvePseudoShotOnServer()
|
||||
{
|
||||
if (!IsServer || !waveIsActive)
|
||||
{
|
||||
PublishShotResult(false, -1, -1f);
|
||||
return;
|
||||
}
|
||||
|
||||
int nearestIndex = -1;
|
||||
float nearestDistance = float.PositiveInfinity;
|
||||
for (int index = 0; index < activeTargets.Count; index++)
|
||||
{
|
||||
float distance = Vector2.Distance(
|
||||
crosshairCoordinates.Value, activeTargets[index].Coordinates);
|
||||
if (distance >= nearestDistance)
|
||||
continue;
|
||||
nearestDistance = distance;
|
||||
nearestIndex = index;
|
||||
}
|
||||
|
||||
if (nearestIndex >= 0 && nearestDistance <= hitRadius)
|
||||
{
|
||||
MapTargetNetworkState target = activeTargets[nearestIndex];
|
||||
TargetConfig config = targetController.GetConfig(target.ConfigIndex);
|
||||
bool missileMatches = ignoreMissileTypeForPrototype || config == null ||
|
||||
config.RequiredMissileType == MissileType.None ||
|
||||
config.RequiredMissileType == pseudoMissileType;
|
||||
if (missileMatches)
|
||||
{
|
||||
target.Health -= shotDamage;
|
||||
if (target.Health <= 0)
|
||||
activeTargets.RemoveAt(nearestIndex);
|
||||
else
|
||||
activeTargets[nearestIndex] = target;
|
||||
|
||||
PublishShotResult(true, target.TargetId, nearestDistance);
|
||||
Debug.Log($"[Map] HIT target {target.TargetId} at {nearestDistance:0.00} map units.", this);
|
||||
TryCompleteWaveOnServer();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int missedTargetId = nearestIndex >= 0 ? activeTargets[nearestIndex].TargetId : -1;
|
||||
PublishShotResult(false, missedTargetId, nearestDistance);
|
||||
float printedDistance = float.IsPositiveInfinity(nearestDistance) ? -1f : nearestDistance;
|
||||
Debug.Log($"[Map] MISS at {crosshairCoordinates.Value}. Nearest distance: {printedDistance:0.00}.", this);
|
||||
}
|
||||
|
||||
private void PublishShotResult(bool hit, int targetId, float distance)
|
||||
{
|
||||
lastShotResult.Value = new MapShotResultState(
|
||||
lastShotResult.Value.Sequence + 1, hit, targetId,
|
||||
crosshairCoordinates.Value,
|
||||
float.IsPositiveInfinity(distance) ? -1f : distance);
|
||||
}
|
||||
|
||||
private void ScanRadarOnServer()
|
||||
{
|
||||
if (!IsServer || radarController == null || NetworkManager == null ||
|
||||
NetworkManager.ServerTime.Time < radarReadyAt.Value)
|
||||
return;
|
||||
|
||||
radarMarks.Clear();
|
||||
foreach (MapTargetNetworkState target in activeTargets)
|
||||
radarMarks.Add(target.Coordinates);
|
||||
radarReadyAt.Value = NetworkManager.ServerTime.Time + radarController.Cooldown;
|
||||
Debug.Log($"[Map] Radar scan found {radarMarks.Count} targets.", this);
|
||||
}
|
||||
|
||||
private void TryCompleteWaveOnServer()
|
||||
{
|
||||
if (waveIsActive && activeTargets.Count == 0)
|
||||
CompleteWaveOnServer();
|
||||
}
|
||||
|
||||
private void CompleteWaveOnServer()
|
||||
{
|
||||
waveIsActive = false;
|
||||
resolvedSequence.Value++;
|
||||
if (initialTargetCount > 0 && escapedTargetCount == 0)
|
||||
destroyedSequence.Value++;
|
||||
Debug.Log(
|
||||
$"[Map] Wave {currentWave.Value} resolved. " +
|
||||
$"Destroyed: {Mathf.Max(0, initialTargetCount - escapedTargetCount)}, " +
|
||||
$"escaped: {escapedTargetCount}.", this);
|
||||
}
|
||||
|
||||
private void ConfigurePresentation()
|
||||
{
|
||||
mapPlayer?.Configure(coordinateSystem);
|
||||
targetController?.Configure(coordinateSystem, targetParent, mapPlayer);
|
||||
radarController?.Configure(coordinateSystem, radarMarkPrefab);
|
||||
crosshair?.Configure(
|
||||
coordinateSystem, relayX, relayY, crosshairVisual, targetController);
|
||||
}
|
||||
|
||||
private void ApplyPresentation()
|
||||
{
|
||||
crosshair?.SetCoordinates(crosshairCoordinates.Value);
|
||||
targetController?.ApplyNetworkTargets(CreateTargetSnapshot(), currentWave.Value);
|
||||
radarController?.ApplyRadarMarks(CreateRadarSnapshot());
|
||||
if (radarController != null)
|
||||
radarController.SetCooldownRemaining((float)RadarCooldownRemaining);
|
||||
}
|
||||
|
||||
private void BindRoundEvents()
|
||||
{
|
||||
if (roundEventsBound)
|
||||
return;
|
||||
if (roundGameLoop == null)
|
||||
roundGameLoop = FindFirstObjectByType<RoundGameLoopController>();
|
||||
if (roundGameLoop == null || !roundGameLoop.IsSpawned)
|
||||
return;
|
||||
roundGameLoop.PhaseChanged += HandleRoundPhaseChanged;
|
||||
roundEventsBound = true;
|
||||
}
|
||||
|
||||
private void UnbindRoundEvents()
|
||||
{
|
||||
if (!roundEventsBound || roundGameLoop == null)
|
||||
return;
|
||||
roundGameLoop.PhaseChanged -= HandleRoundPhaseChanged;
|
||||
roundEventsBound = false;
|
||||
}
|
||||
|
||||
private void HandleRoundPhaseChanged(RoundPhase phase)
|
||||
{
|
||||
if (IsServer && phase == RoundPhase.WorkInProgress && !waveIsActive)
|
||||
StartNextWaveOnServer();
|
||||
}
|
||||
|
||||
private void HandleTargetListChanged(NetworkListEvent<MapTargetNetworkState> _) =>
|
||||
targetController?.ApplyNetworkTargets(CreateTargetSnapshot(), currentWave.Value);
|
||||
private void HandleRadarListChanged(NetworkListEvent<Vector2> _) =>
|
||||
radarController?.ApplyRadarMarks(CreateRadarSnapshot());
|
||||
private void HandleCrosshairChanged(Vector2 _, Vector2 current) =>
|
||||
crosshair?.SetCoordinates(current);
|
||||
|
||||
private void HandleWaveChanged(int _, int current)
|
||||
{
|
||||
targetController?.ApplyNetworkTargets(CreateTargetSnapshot(), current);
|
||||
WaveStarted?.Invoke(current);
|
||||
waveStarted?.Invoke(current);
|
||||
}
|
||||
|
||||
private void HandleRadarReadyAtChanged(double _, double __)
|
||||
{
|
||||
if (radarController != null)
|
||||
radarController.SetCooldownRemaining((float)RadarCooldownRemaining);
|
||||
}
|
||||
|
||||
private void HandleShotResultChanged(MapShotResultState previous, MapShotResultState current)
|
||||
{
|
||||
if (current.Sequence == 0 || current.Sequence == previous.Sequence)
|
||||
return;
|
||||
ShotResolved?.Invoke(current);
|
||||
shotResolved?.Invoke(current.Hit);
|
||||
}
|
||||
|
||||
private void HandleResolvedSequenceChanged(uint previous, uint current)
|
||||
{
|
||||
if (current == previous)
|
||||
return;
|
||||
AllTargetsResolved?.Invoke();
|
||||
allTargetsResolved?.Invoke();
|
||||
}
|
||||
|
||||
private void HandleDestroyedSequenceChanged(uint previous, uint current)
|
||||
{
|
||||
if (current == previous)
|
||||
return;
|
||||
AllTargetsDestroyed?.Invoke();
|
||||
allTargetsDestroyed?.Invoke();
|
||||
}
|
||||
|
||||
private MapTargetNetworkState[] CreateTargetSnapshot()
|
||||
{
|
||||
MapTargetNetworkState[] snapshot = new MapTargetNetworkState[activeTargets.Count];
|
||||
for (int index = 0; index < snapshot.Length; index++)
|
||||
snapshot[index] = activeTargets[index];
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private Vector2[] CreateRadarSnapshot()
|
||||
{
|
||||
Vector2[] snapshot = new Vector2[radarMarks.Count];
|
||||
for (int index = 0; index < snapshot.Length; index++)
|
||||
snapshot[index] = radarMarks[index];
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class MapCrosshairController : MonoBehaviour
|
||||
{
|
||||
[Header("Nearest Target (Runtime)")]
|
||||
[SerializeField] private MapTarget nearestTarget = null;
|
||||
[SerializeField] private MapTarget nearestTarget;
|
||||
[SerializeField] private float distanceToNearestTarget = -1f;
|
||||
|
||||
private MapCoordinateSystem coordinateSystem;
|
||||
@@ -13,79 +12,42 @@ public sealed class MapCrosshairController : MonoBehaviour
|
||||
private Transform relayX;
|
||||
private Transform relayY;
|
||||
private Transform crosshairVisual;
|
||||
private float movementSpeed;
|
||||
private bool useKeyboardInput;
|
||||
private float horizontalAxis;
|
||||
private float verticalAxis;
|
||||
private Vector2 crosshairCoordinates;
|
||||
private Vector3 relayXOffset;
|
||||
private Vector3 relayYOffset;
|
||||
private bool isConfigured;
|
||||
|
||||
public MapCoordinateSystem CoordinateSystem => coordinateSystem;
|
||||
public Vector2 Coordinates => crosshairCoordinates;
|
||||
|
||||
public void Configure(
|
||||
MapCoordinateSystem newCoordinateSystem,
|
||||
Transform newRelayX,
|
||||
Transform newRelayY,
|
||||
Transform newCrosshairVisual,
|
||||
float newMovementSpeed,
|
||||
bool enableKeyboardInput,
|
||||
MapTargetController newTargetController)
|
||||
{
|
||||
coordinateSystem = newCoordinateSystem;
|
||||
relayX = newRelayX;
|
||||
relayY = newRelayY;
|
||||
crosshairVisual = newCrosshairVisual;
|
||||
movementSpeed = Mathf.Max(0f, newMovementSpeed);
|
||||
useKeyboardInput = enableKeyboardInput;
|
||||
targetController = newTargetController;
|
||||
crosshairCoordinates = Vector2.zero;
|
||||
isConfigured = CalibrateObjects();
|
||||
|
||||
if (isConfigured)
|
||||
ApplyCoordinates();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
public void SetCoordinates(Vector2 coordinates)
|
||||
{
|
||||
if (!isConfigured)
|
||||
return;
|
||||
|
||||
if (useKeyboardInput)
|
||||
ReadKeyboardInput();
|
||||
|
||||
Vector2 movement = new(horizontalAxis, verticalAxis);
|
||||
crosshairCoordinates += movement * (movementSpeed * Time.deltaTime);
|
||||
crosshairCoordinates = coordinateSystem.ClampCoordinates(crosshairCoordinates);
|
||||
crosshairCoordinates = coordinateSystem.ClampCoordinates(coordinates);
|
||||
ApplyCoordinates();
|
||||
UpdateNearestTarget();
|
||||
}
|
||||
|
||||
public void SetHorizontalAxis(float value)
|
||||
{
|
||||
horizontalAxis = Mathf.Clamp(value, -1f, 1f);
|
||||
}
|
||||
|
||||
public void SetVerticalAxis(float value)
|
||||
{
|
||||
verticalAxis = Mathf.Clamp(value, -1f, 1f);
|
||||
}
|
||||
|
||||
public Vector2 GetCrosshairCoordinates()
|
||||
{
|
||||
return crosshairCoordinates;
|
||||
}
|
||||
|
||||
public MapTarget GetNearestTarget()
|
||||
{
|
||||
return nearestTarget;
|
||||
}
|
||||
|
||||
public float GetDistanceToNearestTarget()
|
||||
{
|
||||
return distanceToNearestTarget;
|
||||
}
|
||||
public Vector2 GetCrosshairCoordinates() => crosshairCoordinates;
|
||||
public MapTarget GetNearestTarget() => nearestTarget;
|
||||
public float GetDistanceToNearestTarget() => distanceToNearestTarget;
|
||||
|
||||
public bool TryGetNearestTarget(out MapTarget target, out float distance)
|
||||
{
|
||||
@@ -94,62 +56,49 @@ public sealed class MapCrosshairController : MonoBehaviour
|
||||
return target != null;
|
||||
}
|
||||
|
||||
private void ReadKeyboardInput()
|
||||
{
|
||||
Keyboard keyboard = Keyboard.current;
|
||||
if (keyboard == null)
|
||||
{
|
||||
SetHorizontalAxis(0f);
|
||||
SetVerticalAxis(0f);
|
||||
return;
|
||||
}
|
||||
|
||||
SetHorizontalAxis(
|
||||
(keyboard.lKey.isPressed ? 1f : 0f) - (keyboard.jKey.isPressed ? 1f : 0f));
|
||||
SetVerticalAxis(
|
||||
(keyboard.iKey.isPressed ? 1f : 0f) - (keyboard.kKey.isPressed ? 1f : 0f));
|
||||
}
|
||||
|
||||
private void ApplyCoordinates()
|
||||
{
|
||||
Vector3 targetWorldPosition = coordinateSystem.CoordinatesToWorldPosition(crosshairCoordinates);
|
||||
crosshairVisual.position = targetWorldPosition;
|
||||
crosshairVisual.position =
|
||||
coordinateSystem.CoordinatesToWorldPosition(crosshairCoordinates);
|
||||
|
||||
if (relayX != null)
|
||||
{
|
||||
relayX.position = coordinateSystem.CoordinatesToWorldPosition(
|
||||
new Vector2(crosshairCoordinates.x, 0f)) + relayXOffset;
|
||||
}
|
||||
|
||||
if (relayY != null)
|
||||
{
|
||||
relayY.position = coordinateSystem.CoordinatesToWorldPosition(
|
||||
new Vector2(0f, crosshairCoordinates.y)) + relayYOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CalibrateObjects()
|
||||
{
|
||||
if (coordinateSystem == null || crosshairVisual == null || !coordinateSystem.HasValidCorners())
|
||||
if (coordinateSystem == null || crosshairVisual == null ||
|
||||
!coordinateSystem.HasValidCorners())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector3 origin = coordinateSystem.CoordinatesToWorldPosition(Vector2.zero);
|
||||
Vector3 horizontalDirection =
|
||||
coordinateSystem.CoordinatesToWorldPosition(new Vector2(MapCoordinateSystem.CoordinateMaximum, 0f)) -
|
||||
origin;
|
||||
Vector3 verticalDirection =
|
||||
coordinateSystem.CoordinatesToWorldPosition(new Vector2(0f, MapCoordinateSystem.CoordinateMaximum)) -
|
||||
origin;
|
||||
|
||||
Vector3 horizontalDirection = coordinateSystem.CoordinatesToWorldPosition(
|
||||
new Vector2(MapCoordinateSystem.CoordinateMaximum, 0f)) - origin;
|
||||
Vector3 verticalDirection = coordinateSystem.CoordinatesToWorldPosition(
|
||||
new Vector2(0f, MapCoordinateSystem.CoordinateMaximum)) - origin;
|
||||
if (horizontalDirection.sqrMagnitude <= Mathf.Epsilon ||
|
||||
verticalDirection.sqrMagnitude <= Mathf.Epsilon)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
horizontalDirection.Normalize();
|
||||
verticalDirection.Normalize();
|
||||
|
||||
if (relayX != null)
|
||||
relayXOffset = Vector3.ProjectOnPlane(relayX.position - origin, horizontalDirection);
|
||||
|
||||
if (relayY != null)
|
||||
relayYOffset = Vector3.ProjectOnPlane(relayY.position - origin, verticalDirection);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -157,7 +106,6 @@ public sealed class MapCrosshairController : MonoBehaviour
|
||||
{
|
||||
nearestTarget = null;
|
||||
distanceToNearestTarget = -1f;
|
||||
|
||||
if (targetController == null)
|
||||
return;
|
||||
|
||||
|
||||
@@ -1,90 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class MapRadarController : MonoBehaviour
|
||||
{
|
||||
[Header("Radar Settings")]
|
||||
[SerializeField, Min(0f)] private float cooldown = 5f;
|
||||
[SerializeField] private bool useKeyboardInput = true;
|
||||
|
||||
[Header("Radar State (Runtime)")]
|
||||
[SerializeField] private float cooldownRemaining;
|
||||
|
||||
private readonly List<GameObject> radarMarks = new();
|
||||
private MapCoordinateSystem coordinateSystem;
|
||||
private MapTargetController targetController;
|
||||
private MapTarget radarMarkPrefab;
|
||||
private float nextActivationTime;
|
||||
private bool isConfigured;
|
||||
|
||||
public float Cooldown => Mathf.Max(0f, cooldown);
|
||||
public float CooldownRemaining => cooldownRemaining;
|
||||
public bool IsReady => cooldownRemaining <= 0f;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
cooldown = Mathf.Max(0f, cooldown);
|
||||
}
|
||||
private void OnValidate() => cooldown = Mathf.Max(0f, cooldown);
|
||||
|
||||
public void Configure(
|
||||
MapCoordinateSystem newCoordinateSystem,
|
||||
MapTargetController newTargetController,
|
||||
MapTarget newRadarMarkPrefab)
|
||||
{
|
||||
coordinateSystem = newCoordinateSystem;
|
||||
targetController = newTargetController;
|
||||
radarMarkPrefab = newRadarMarkPrefab;
|
||||
isConfigured = coordinateSystem != null &&
|
||||
coordinateSystem.HasValidCorners() &&
|
||||
targetController != null &&
|
||||
radarMarkPrefab != null;
|
||||
|
||||
if (targetController != null)
|
||||
targetController.SetTargetVisualsVisible(false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
cooldownRemaining = Mathf.Max(0f, nextActivationTime - Time.time);
|
||||
public void SetCooldownRemaining(float seconds) =>
|
||||
cooldownRemaining = Mathf.Max(0f, seconds);
|
||||
|
||||
if (!useKeyboardInput)
|
||||
public void ApplyRadarMarks(IReadOnlyList<Vector2> coordinates)
|
||||
{
|
||||
if (coordinateSystem == null || !coordinateSystem.HasValidCorners() ||
|
||||
radarMarkPrefab == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Keyboard keyboard = Keyboard.current;
|
||||
if (keyboard != null && keyboard.oKey.wasPressedThisFrame)
|
||||
ActivateRadar();
|
||||
}
|
||||
|
||||
public bool ActivateRadar()
|
||||
{
|
||||
if (!isConfigured || Time.time < nextActivationTime)
|
||||
return false;
|
||||
|
||||
PlaceRadarMarks();
|
||||
nextActivationTime = Time.time + Mathf.Max(0f, cooldown);
|
||||
cooldownRemaining = Mathf.Max(0f, nextActivationTime - Time.time);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void PlaceRadarMarks()
|
||||
{
|
||||
int targetCount = targetController.Targets.Count;
|
||||
EnsureRadarMarkCount(targetCount);
|
||||
|
||||
EnsureRadarMarkCount(coordinates.Count);
|
||||
for (int index = 0; index < radarMarks.Count; index++)
|
||||
{
|
||||
bool hasTarget = index < targetCount && targetController.Targets[index] != null;
|
||||
bool active = index < coordinates.Count;
|
||||
GameObject radarMark = radarMarks[index];
|
||||
radarMark.SetActive(hasTarget);
|
||||
|
||||
if (!hasTarget)
|
||||
continue;
|
||||
|
||||
MapTarget target = targetController.Targets[index];
|
||||
radarMark.name = $"Radar Mark - {target.name}";
|
||||
radarMark.transform.position =
|
||||
coordinateSystem.CoordinatesToWorldPosition(target.MapCoordinates);
|
||||
radarMark.SetActive(active);
|
||||
if (active)
|
||||
{
|
||||
radarMark.name = $"Radar Mark {index + 1}";
|
||||
radarMark.transform.position =
|
||||
coordinateSystem.CoordinatesToWorldPosition(coordinates[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,9 +66,9 @@ public sealed class MapRadarController : MonoBehaviour
|
||||
|
||||
private static void PrepareRadarMark(GameObject radarMark)
|
||||
{
|
||||
MapTarget targetMovement = radarMark.GetComponent<MapTarget>();
|
||||
if (targetMovement != null)
|
||||
targetMovement.enabled = false;
|
||||
MapTarget target = radarMark.GetComponent<MapTarget>();
|
||||
if (target != null)
|
||||
target.enabled = false;
|
||||
|
||||
foreach (Collider targetCollider in radarMark.GetComponentsInChildren<Collider>(true))
|
||||
targetCollider.enabled = false;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
@@ -6,17 +5,18 @@ public class MapTarget : MonoBehaviour
|
||||
{
|
||||
[Header("Target State (Runtime)")]
|
||||
[SerializeField] private TargetConfig config;
|
||||
[SerializeField] private int targetId = -1;
|
||||
[SerializeField] private float timeUntilAttack;
|
||||
[SerializeField] private Vector2 mapCoordinates;
|
||||
[SerializeField] private int currentHealth;
|
||||
[SerializeField] private MissileType requiredMissileType;
|
||||
[SerializeField] private bool isReadyToAttack;
|
||||
|
||||
private MapCoordinateSystem coordinateSystem;
|
||||
private MapPlayer player;
|
||||
private bool isConfigured;
|
||||
private Renderer[] targetRenderers;
|
||||
private Collider[] targetColliders;
|
||||
|
||||
public int TargetId => targetId;
|
||||
public Vector2 MapCoordinates => mapCoordinates;
|
||||
public MapCoordinateSystem CoordinateSystem => coordinateSystem;
|
||||
public TargetConfig Config => config;
|
||||
@@ -24,34 +24,26 @@ public class MapTarget : MonoBehaviour
|
||||
public float TimeUntilAttack => timeUntilAttack;
|
||||
public int CurrentHealth => currentHealth;
|
||||
public MissileType RequiredMissileType => requiredMissileType;
|
||||
public bool IsReadyToAttack => isReadyToAttack;
|
||||
|
||||
public event Action<MapTarget> AttackReady;
|
||||
|
||||
public void Configure(
|
||||
TargetConfig newConfig,
|
||||
MapCoordinateSystem newCoordinateSystem,
|
||||
Vector2 startCoordinates,
|
||||
MapPlayer newPlayer)
|
||||
{
|
||||
config = newConfig;
|
||||
coordinateSystem = newCoordinateSystem;
|
||||
player = newPlayer;
|
||||
timeUntilAttack = config != null ? config.TimeBeforeAttack : 0f;
|
||||
currentHealth = config != null ? config.Health : 1;
|
||||
requiredMissileType = config != null ? config.RequiredMissileType : MissileType.None;
|
||||
isReadyToAttack = false;
|
||||
isConfigured = coordinateSystem != null && coordinateSystem.HasValidCorners();
|
||||
SetMapCoordinates(startCoordinates);
|
||||
}
|
||||
|
||||
protected virtual void Update()
|
||||
public void ApplyNetworkState(MapTargetNetworkState state)
|
||||
{
|
||||
if (!isConfigured)
|
||||
return;
|
||||
|
||||
TickAttackTimer(Time.deltaTime);
|
||||
TickMovement(Time.deltaTime);
|
||||
targetId = state.TargetId;
|
||||
timeUntilAttack = state.AttackTimeRemaining;
|
||||
currentHealth = state.Health;
|
||||
requiredMissileType = config != null
|
||||
? config.RequiredMissileType
|
||||
: MissileType.None;
|
||||
SetMapCoordinates(state.Coordinates);
|
||||
}
|
||||
|
||||
public void SetMapCoordinates(Vector2 coordinates)
|
||||
@@ -62,41 +54,34 @@ public class MapTarget : MonoBehaviour
|
||||
Mathf.Clamp(coordinates.x, 0f, MapCoordinateSystem.CoordinateMaximum),
|
||||
Mathf.Clamp(coordinates.y, 0f, MapCoordinateSystem.CoordinateMaximum));
|
||||
|
||||
if (isConfigured)
|
||||
if (coordinateSystem != null && coordinateSystem.HasValidCorners())
|
||||
transform.position = coordinateSystem.CoordinatesToWorldPosition(mapCoordinates);
|
||||
}
|
||||
|
||||
public Vector2 GetMapCoordinates()
|
||||
{
|
||||
return mapCoordinates;
|
||||
}
|
||||
public Vector2 GetMapCoordinates() => mapCoordinates;
|
||||
|
||||
public void SetVisualsVisible(bool isVisible)
|
||||
{
|
||||
if (targetRenderers == null)
|
||||
targetRenderers = GetComponentsInChildren<Renderer>(true);
|
||||
if (targetColliders == null)
|
||||
targetColliders = GetComponentsInChildren<Collider>(true);
|
||||
|
||||
foreach (Renderer targetRenderer in targetRenderers)
|
||||
{
|
||||
if (targetRenderer != null)
|
||||
targetRenderer.enabled = isVisible;
|
||||
}
|
||||
|
||||
// Map targets are presentation objects, never physical world objects.
|
||||
foreach (Collider targetCollider in targetColliders)
|
||||
{
|
||||
if (targetCollider != null)
|
||||
targetCollider.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void TickMovement(float deltaTime)
|
||||
{
|
||||
}
|
||||
|
||||
private void TickAttackTimer(float deltaTime)
|
||||
{
|
||||
if (isReadyToAttack)
|
||||
return;
|
||||
|
||||
timeUntilAttack = Mathf.Max(0f, timeUntilAttack - deltaTime);
|
||||
if (timeUntilAttack > 0f)
|
||||
return;
|
||||
|
||||
isReadyToAttack = true;
|
||||
AttackReady?.Invoke(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class MapTargetController : MonoBehaviour
|
||||
{
|
||||
[Header("Random Wave Settings")]
|
||||
[Header("Wave Settings")]
|
||||
[SerializeField, Min(0)] private int targetsPerWave = 5;
|
||||
[SerializeField] private TargetConfig[] randomTargetConfigs = System.Array.Empty<TargetConfig>();
|
||||
[SerializeField] private TargetConfig[] randomTargetConfigs = Array.Empty<TargetConfig>();
|
||||
[SerializeField] private bool showLiveTargetVisuals;
|
||||
|
||||
[Header("Current Wave (Runtime)")]
|
||||
[SerializeField, Min(0)] private int currentWave;
|
||||
[SerializeField] private List<MapTarget> currentWaveTargets = new();
|
||||
|
||||
private readonly List<MapTarget> pooledTargets = new();
|
||||
private readonly Dictionary<int, MapTarget> targetsById = new();
|
||||
private readonly HashSet<int> synchronizedTargetIds = new();
|
||||
private MapCoordinateSystem coordinateSystem;
|
||||
private Transform targetParent;
|
||||
private MapPlayer player;
|
||||
private bool isConfigured;
|
||||
|
||||
public IReadOnlyList<MapTarget> Targets => currentWaveTargets;
|
||||
public IReadOnlyList<TargetConfig> TargetConfigs => randomTargetConfigs;
|
||||
public int CurrentWave => currentWave;
|
||||
public int TargetsPerWave => targetsPerWave;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (currentWave == 0)
|
||||
StartNextWave();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
targetsPerWave = Mathf.Max(0, targetsPerWave);
|
||||
@@ -42,9 +40,10 @@ public sealed class MapTargetController : MonoBehaviour
|
||||
coordinateSystem = newCoordinateSystem;
|
||||
targetParent = newTargetParent != null ? newTargetParent : transform;
|
||||
player = newPlayer;
|
||||
isConfigured = coordinateSystem != null && coordinateSystem.HasValidCorners();
|
||||
|
||||
pooledTargets.Clear();
|
||||
targetsById.Clear();
|
||||
currentWaveTargets.Clear();
|
||||
foreach (MapTarget existingTarget in targetParent.GetComponentsInChildren<MapTarget>(true))
|
||||
{
|
||||
existingTarget.SetVisualsVisible(false);
|
||||
@@ -53,44 +52,78 @@ public sealed class MapTargetController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public bool StartNextWave()
|
||||
public void ApplyNetworkTargets(
|
||||
IReadOnlyList<MapTargetNetworkState> states,
|
||||
int wave)
|
||||
{
|
||||
if (!isConfigured || !HasValidTargetConfig())
|
||||
currentWave = Mathf.Max(0, wave);
|
||||
synchronizedTargetIds.Clear();
|
||||
|
||||
for (int index = 0; index < states.Count; index++)
|
||||
{
|
||||
Debug.LogWarning("Target controller has no coordinate system or valid TargetConfig.", this);
|
||||
return false;
|
||||
}
|
||||
MapTargetNetworkState state = states[index];
|
||||
TargetConfig config = GetConfig(state.ConfigIndex);
|
||||
if (config == null || config.TargetPrefab == null)
|
||||
continue;
|
||||
|
||||
RetireCurrentWave();
|
||||
currentWave++;
|
||||
synchronizedTargetIds.Add(state.TargetId);
|
||||
if (!targetsById.TryGetValue(state.TargetId, out MapTarget target) || target == null)
|
||||
{
|
||||
target = GetPooledTarget(config);
|
||||
if (target == null)
|
||||
target = Instantiate(config.TargetPrefab, targetParent);
|
||||
|
||||
for (int index = 0; index < targetsPerWave; index++)
|
||||
{
|
||||
TargetConfig config = GetRandomValidConfig();
|
||||
if (config == null)
|
||||
break;
|
||||
targetsById[state.TargetId] = target;
|
||||
currentWaveTargets.Add(target);
|
||||
}
|
||||
|
||||
MapTarget target = GetPooledTarget(config);
|
||||
if (target == null)
|
||||
target = Instantiate(config.TargetPrefab, targetParent);
|
||||
|
||||
target.name = $"Wave {currentWave} - {config.name} - {index + 1}";
|
||||
target.Configure(config, coordinateSystem, GetRandomCoordinates(), player);
|
||||
target.SetVisualsVisible(false);
|
||||
target.name = $"Wave {currentWave} - {config.name} - {state.TargetId}";
|
||||
target.Configure(config, coordinateSystem, player);
|
||||
target.ApplyNetworkState(state);
|
||||
target.SetVisualsVisible(showLiveTargetVisuals);
|
||||
target.gameObject.SetActive(true);
|
||||
currentWaveTargets.Add(target);
|
||||
}
|
||||
|
||||
return currentWaveTargets.Count > 0 || targetsPerWave == 0;
|
||||
for (int index = currentWaveTargets.Count - 1; index >= 0; index--)
|
||||
{
|
||||
MapTarget target = currentWaveTargets[index];
|
||||
if (target != null && synchronizedTargetIds.Contains(target.TargetId))
|
||||
continue;
|
||||
|
||||
RetireTarget(target);
|
||||
currentWaveTargets.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateAndPlaceTargets()
|
||||
public TargetConfig GetConfig(int index) =>
|
||||
index >= 0 && index < randomTargetConfigs.Length
|
||||
? randomTargetConfigs[index]
|
||||
: null;
|
||||
|
||||
public int GetRandomValidConfigIndex()
|
||||
{
|
||||
StartNextWave();
|
||||
if (randomTargetConfigs == null || randomTargetConfigs.Length == 0)
|
||||
return -1;
|
||||
|
||||
int startIndex = UnityEngine.Random.Range(0, randomTargetConfigs.Length);
|
||||
for (int offset = 0; offset < randomTargetConfigs.Length; offset++)
|
||||
{
|
||||
int index = (startIndex + offset) % randomTargetConfigs.Length;
|
||||
TargetConfig config = randomTargetConfigs[index];
|
||||
if (config != null && config.TargetPrefab != null)
|
||||
return index;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public bool HasValidConfiguration() =>
|
||||
coordinateSystem != null && coordinateSystem.HasValidCorners() &&
|
||||
GetRandomValidConfigIndex() >= 0;
|
||||
|
||||
public void SetTargetVisualsVisible(bool isVisible)
|
||||
{
|
||||
showLiveTargetVisuals = isVisible;
|
||||
foreach (MapTarget target in currentWaveTargets)
|
||||
{
|
||||
if (target != null)
|
||||
@@ -98,28 +131,15 @@ public sealed class MapTargetController : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public MapTarget GetTarget(int index)
|
||||
{
|
||||
return index >= 0 && index < currentWaveTargets.Count
|
||||
public MapTarget GetTarget(int index) =>
|
||||
index >= 0 && index < currentWaveTargets.Count
|
||||
? currentWaveTargets[index]
|
||||
: null;
|
||||
}
|
||||
|
||||
private void RetireCurrentWave()
|
||||
public bool StartNextWave()
|
||||
{
|
||||
foreach (MapTarget target in currentWaveTargets)
|
||||
{
|
||||
if (target == null)
|
||||
continue;
|
||||
|
||||
target.SetVisualsVisible(false);
|
||||
target.gameObject.SetActive(false);
|
||||
|
||||
if (!pooledTargets.Contains(target))
|
||||
pooledTargets.Add(target);
|
||||
}
|
||||
|
||||
currentWaveTargets.Clear();
|
||||
MapController mapController = FindFirstObjectByType<MapController>();
|
||||
return mapController != null && mapController.StartNextWave();
|
||||
}
|
||||
|
||||
private MapTarget GetPooledTarget(TargetConfig config)
|
||||
@@ -143,30 +163,15 @@ public sealed class MapTargetController : MonoBehaviour
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool HasValidTargetConfig()
|
||||
private void RetireTarget(MapTarget target)
|
||||
{
|
||||
return GetRandomValidConfig() != null;
|
||||
}
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
private TargetConfig GetRandomValidConfig()
|
||||
{
|
||||
if (randomTargetConfigs == null || randomTargetConfigs.Length == 0)
|
||||
return null;
|
||||
|
||||
int startIndex = Random.Range(0, randomTargetConfigs.Length);
|
||||
for (int offset = 0; offset < randomTargetConfigs.Length; offset++)
|
||||
{
|
||||
TargetConfig config = randomTargetConfigs[(startIndex + offset) % randomTargetConfigs.Length];
|
||||
if (config != null && config.TargetPrefab != null)
|
||||
return config;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Vector2 GetRandomCoordinates()
|
||||
{
|
||||
float maximum = MapCoordinateSystem.CoordinateMaximum;
|
||||
return new Vector2(Random.Range(0f, maximum), Random.Range(0f, maximum));
|
||||
targetsById.Remove(target.TargetId);
|
||||
target.SetVisualsVisible(false);
|
||||
target.gameObject.SetActive(false);
|
||||
if (!pooledTargets.Contains(target))
|
||||
pooledTargets.Add(target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
public struct MapTargetNetworkState : INetworkSerializable,
|
||||
IEquatable<MapTargetNetworkState>
|
||||
{
|
||||
public int TargetId;
|
||||
public int ConfigIndex;
|
||||
public Vector2 Coordinates;
|
||||
public int Health;
|
||||
public float AttackTimeRemaining;
|
||||
|
||||
public MapTargetNetworkState(
|
||||
int targetId,
|
||||
int configIndex,
|
||||
Vector2 coordinates,
|
||||
int health,
|
||||
float attackTimeRemaining)
|
||||
{
|
||||
TargetId = targetId;
|
||||
ConfigIndex = configIndex;
|
||||
Coordinates = coordinates;
|
||||
Health = health;
|
||||
AttackTimeRemaining = attackTimeRemaining;
|
||||
}
|
||||
|
||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer)
|
||||
where T : IReaderWriter
|
||||
{
|
||||
serializer.SerializeValue(ref TargetId);
|
||||
serializer.SerializeValue(ref ConfigIndex);
|
||||
serializer.SerializeValue(ref Coordinates);
|
||||
serializer.SerializeValue(ref Health);
|
||||
serializer.SerializeValue(ref AttackTimeRemaining);
|
||||
}
|
||||
|
||||
public bool Equals(MapTargetNetworkState other) =>
|
||||
TargetId == other.TargetId &&
|
||||
ConfigIndex == other.ConfigIndex &&
|
||||
Coordinates == other.Coordinates &&
|
||||
Health == other.Health &&
|
||||
AttackTimeRemaining.Equals(other.AttackTimeRemaining);
|
||||
}
|
||||
|
||||
public struct MapShotResultState : INetworkSerializable,
|
||||
IEquatable<MapShotResultState>
|
||||
{
|
||||
public uint Sequence;
|
||||
public bool Hit;
|
||||
public int TargetId;
|
||||
public Vector2 Coordinates;
|
||||
public float Distance;
|
||||
|
||||
public MapShotResultState(
|
||||
uint sequence,
|
||||
bool hit,
|
||||
int targetId,
|
||||
Vector2 coordinates,
|
||||
float distance)
|
||||
{
|
||||
Sequence = sequence;
|
||||
Hit = hit;
|
||||
TargetId = targetId;
|
||||
Coordinates = coordinates;
|
||||
Distance = distance;
|
||||
}
|
||||
|
||||
public void NetworkSerialize<T>(BufferSerializer<T> serializer)
|
||||
where T : IReaderWriter
|
||||
{
|
||||
serializer.SerializeValue(ref Sequence);
|
||||
serializer.SerializeValue(ref Hit);
|
||||
serializer.SerializeValue(ref TargetId);
|
||||
serializer.SerializeValue(ref Coordinates);
|
||||
serializer.SerializeValue(ref Distance);
|
||||
}
|
||||
|
||||
public bool Equals(MapShotResultState other) =>
|
||||
Sequence == other.Sequence && Hit == other.Hit &&
|
||||
TargetId == other.TargetId && Coordinates == other.Coordinates &&
|
||||
Distance.Equals(other.Distance);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 374f32cae69b9184fb3fffe2969c6cce
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public sealed class NetworkBaseHealth : NetworkBehaviour
|
||||
{
|
||||
[SerializeField, Min(1)] private int maximumHealth = 10;
|
||||
[SerializeField] private UnityEvent<int> healthChanged;
|
||||
[SerializeField] private UnityEvent depleted;
|
||||
|
||||
private readonly NetworkVariable<int> currentHealth = new(
|
||||
0,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
public int MaximumHealth => Mathf.Max(1, maximumHealth);
|
||||
public int CurrentHealth => currentHealth.Value;
|
||||
public bool IsDepleted => CurrentHealth <= 0;
|
||||
|
||||
public event Action<int> Changed;
|
||||
public event Action Depleted;
|
||||
|
||||
private void OnValidate() => maximumHealth = Mathf.Max(1, maximumHealth);
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
currentHealth.OnValueChanged += HandleHealthChanged;
|
||||
|
||||
if (IsServer)
|
||||
currentHealth.Value = MaximumHealth;
|
||||
|
||||
ApplyState(currentHealth.Value, false);
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
currentHealth.OnValueChanged -= HandleHealthChanged;
|
||||
}
|
||||
|
||||
public bool ApplyDamage(int amount)
|
||||
{
|
||||
if (!IsServer || !IsSpawned || amount <= 0 || IsDepleted)
|
||||
return false;
|
||||
|
||||
currentHealth.Value = Mathf.Max(0, CurrentHealth - amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ResetHealth()
|
||||
{
|
||||
if (IsServer && IsSpawned)
|
||||
currentHealth.Value = MaximumHealth;
|
||||
}
|
||||
|
||||
[ContextMenu("Debug/Apply One Damage (Server Play Mode)")]
|
||||
private void ApplyDebugDamage()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
ApplyDamage(1);
|
||||
}
|
||||
|
||||
private void HandleHealthChanged(int previous, int current) =>
|
||||
ApplyState(current, previous > 0 && current <= 0);
|
||||
|
||||
private void ApplyState(int health, bool becameDepleted)
|
||||
{
|
||||
Changed?.Invoke(health);
|
||||
healthChanged?.Invoke(health);
|
||||
|
||||
if (!becameDepleted)
|
||||
return;
|
||||
|
||||
Depleted?.Invoke();
|
||||
depleted?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f171a6cd9e9609e4e8a4822dbefe982f
|
||||
@@ -9,6 +9,7 @@ public sealed class TargetConfig : ScriptableObject
|
||||
[Header("Target Stats")]
|
||||
[SerializeField, Min(0f)] private float timeBeforeAttack = 5f;
|
||||
[SerializeField, Min(1)] private int health = 100;
|
||||
[SerializeField, Min(1)] private int baseDamage = 1;
|
||||
[SerializeField] private MissileType requiredMissileType = MissileType.Standard;
|
||||
|
||||
[Header("Air Target Movement")]
|
||||
@@ -18,6 +19,7 @@ public sealed class TargetConfig : ScriptableObject
|
||||
public MapTarget TargetPrefab => targetPrefab;
|
||||
public float TimeBeforeAttack => timeBeforeAttack;
|
||||
public int Health => health;
|
||||
public int BaseDamage => baseDamage;
|
||||
public MissileType RequiredMissileType => requiredMissileType;
|
||||
public float MovementSpeed => movementSpeed;
|
||||
public float StoppingDistance => stoppingDistance;
|
||||
@@ -26,6 +28,7 @@ public sealed class TargetConfig : ScriptableObject
|
||||
{
|
||||
timeBeforeAttack = Mathf.Max(0f, timeBeforeAttack);
|
||||
health = Mathf.Max(1, health);
|
||||
baseDamage = Mathf.Max(1, baseDamage);
|
||||
movementSpeed = Mathf.Max(0f, movementSpeed);
|
||||
stoppingDistance = Mathf.Max(0f, stoppingDistance);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user