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;
|
||||||
|
using UnityEngine.Events;
|
||||||
using UnityEngine.Serialization;
|
using UnityEngine.Serialization;
|
||||||
|
|
||||||
[DisallowMultipleComponent]
|
[DisallowMultipleComponent]
|
||||||
public sealed class MapController : MonoBehaviour
|
[RequireComponent(typeof(NetworkObject))]
|
||||||
|
public sealed class MapController : NetworkBehaviour
|
||||||
{
|
{
|
||||||
[Header("Main References")]
|
[Header("Main References")]
|
||||||
[SerializeField] private MapCrosshairController crosshair = null;
|
[SerializeField] private MapCrosshairController crosshair;
|
||||||
[SerializeField] private MapCoordinateSystem coordinateSystem = null;
|
[SerializeField] private MapCoordinateSystem coordinateSystem;
|
||||||
[SerializeField] private MapPlayer mapPlayer = null;
|
[SerializeField] private MapPlayer mapPlayer;
|
||||||
|
[SerializeField] private RoundGameLoopController roundGameLoop;
|
||||||
|
[SerializeField] private NetworkBaseHealth baseHealth;
|
||||||
|
|
||||||
[Header("Target System")]
|
[Header("Target System")]
|
||||||
[SerializeField] private MapTargetController targetController = null;
|
[SerializeField] private MapTargetController targetController;
|
||||||
[SerializeField] private Transform targetParent = null;
|
[SerializeField] private Transform targetParent;
|
||||||
|
[SerializeField, Min(0.02f)] private float simulationStep = 0.1f;
|
||||||
|
|
||||||
[Header("Radar System")]
|
[Header("Radar System")]
|
||||||
[SerializeField] private MapRadarController radarController = null;
|
[SerializeField] private MapRadarController radarController;
|
||||||
[FormerlySerializedAs("targetPrefab")]
|
[FormerlySerializedAs("targetPrefab")]
|
||||||
[SerializeField] private MapTarget radarMarkPrefab = null;
|
[SerializeField] private MapTarget radarMarkPrefab;
|
||||||
|
|
||||||
[Header("Crosshair Objects")]
|
[Header("Crosshair Objects")]
|
||||||
[SerializeField] private Transform relayX = null;
|
[SerializeField] private Transform relayX;
|
||||||
[SerializeField] private Transform relayY = null;
|
[SerializeField] private Transform relayY;
|
||||||
[SerializeField] private Transform crosshairVisual = null;
|
[SerializeField] private Transform crosshairVisual;
|
||||||
|
|
||||||
[Header("Input")]
|
[Header("Pseudo Shot")]
|
||||||
[SerializeField, Min(0f)] private float movementSpeed = 1f;
|
[SerializeField, Min(0.01f)] private float hitRadius = 0.65f;
|
||||||
[SerializeField] private bool useKeyboardInput = true;
|
[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 MapCoordinateSystem CoordinateSystem => coordinateSystem;
|
||||||
public Transform CrosshairVisual => crosshairVisual;
|
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)
|
simulationStep = Mathf.Max(0.02f, simulationStep);
|
||||||
mapPlayer.Configure(coordinateSystem);
|
hitRadius = Mathf.Max(0.01f, hitRadius);
|
||||||
|
shotDamage = Mathf.Max(1, shotDamage);
|
||||||
|
}
|
||||||
|
|
||||||
if (targetController != null)
|
public override void OnNetworkSpawn()
|
||||||
targetController.Configure(coordinateSystem, targetParent, mapPlayer);
|
{
|
||||||
|
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)
|
if (radarController != null)
|
||||||
radarController.Configure(coordinateSystem, targetController, radarMarkPrefab);
|
radarController.SetCooldownRemaining((float)RadarCooldownRemaining);
|
||||||
|
if (!IsServer || !IsSpawned || !waveIsActive)
|
||||||
if (crosshair == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
crosshair.Configure(
|
simulationAccumulator += Time.deltaTime;
|
||||||
coordinateSystem,
|
while (simulationAccumulator >= simulationStep)
|
||||||
relayX,
|
|
||||||
relayY,
|
|
||||||
crosshairVisual,
|
|
||||||
movementSpeed,
|
|
||||||
useKeyboardInput,
|
|
||||||
targetController);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetHorizontalAxis(float value)
|
|
||||||
{
|
{
|
||||||
if (crosshair != null)
|
simulationAccumulator -= simulationStep;
|
||||||
crosshair.SetHorizontalAxis(value);
|
SimulateTargets(simulationStep);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetVerticalAxis(float value)
|
public void SetHorizontalNormalized(float value)
|
||||||
{
|
{
|
||||||
if (crosshair != null)
|
Vector2 next = crosshairCoordinates.Value;
|
||||||
crosshair.SetVerticalAxis(value);
|
next.x = Mathf.Clamp01(value) * MapCoordinateSystem.CoordinateMaximum;
|
||||||
|
RequestCrosshairCoordinates(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 GetCrosshairCoordinates()
|
public void SetVerticalNormalized(float value)
|
||||||
{
|
{
|
||||||
return crosshair != null ? crosshair.GetCrosshairCoordinates() : Vector2.zero;
|
Vector2 next = crosshairCoordinates.Value;
|
||||||
|
next.y = Mathf.Clamp01(value) * MapCoordinateSystem.CoordinateMaximum;
|
||||||
|
RequestCrosshairCoordinates(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateAndPlaceTargets()
|
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()
|
||||||
{
|
{
|
||||||
if (targetController != null)
|
if (!IsSpawned)
|
||||||
targetController.CreateAndPlaceTargets();
|
return;
|
||||||
|
if (IsServer)
|
||||||
|
ResolvePseudoShotOnServer();
|
||||||
|
else
|
||||||
|
RequestPseudoFireRpc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RequestRadarScan()
|
||||||
|
{
|
||||||
|
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()
|
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)
|
public bool TryGetNearestTarget(out MapTarget target, out float distance)
|
||||||
{
|
{
|
||||||
if (crosshair != null)
|
if (crosshair != null)
|
||||||
return crosshair.TryGetNearestTarget(out target, out distance);
|
return crosshair.TryGetNearestTarget(out target, out distance);
|
||||||
|
|
||||||
target = null;
|
target = null;
|
||||||
distance = -1f;
|
distance = -1f;
|
||||||
return false;
|
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;
|
||||||
using UnityEngine.InputSystem;
|
|
||||||
|
|
||||||
[DisallowMultipleComponent]
|
[DisallowMultipleComponent]
|
||||||
public sealed class MapCrosshairController : MonoBehaviour
|
public sealed class MapCrosshairController : MonoBehaviour
|
||||||
{
|
{
|
||||||
[Header("Nearest Target (Runtime)")]
|
[Header("Nearest Target (Runtime)")]
|
||||||
[SerializeField] private MapTarget nearestTarget = null;
|
[SerializeField] private MapTarget nearestTarget;
|
||||||
[SerializeField] private float distanceToNearestTarget = -1f;
|
[SerializeField] private float distanceToNearestTarget = -1f;
|
||||||
|
|
||||||
private MapCoordinateSystem coordinateSystem;
|
private MapCoordinateSystem coordinateSystem;
|
||||||
@@ -13,79 +12,42 @@ public sealed class MapCrosshairController : MonoBehaviour
|
|||||||
private Transform relayX;
|
private Transform relayX;
|
||||||
private Transform relayY;
|
private Transform relayY;
|
||||||
private Transform crosshairVisual;
|
private Transform crosshairVisual;
|
||||||
private float movementSpeed;
|
|
||||||
private bool useKeyboardInput;
|
|
||||||
private float horizontalAxis;
|
|
||||||
private float verticalAxis;
|
|
||||||
private Vector2 crosshairCoordinates;
|
private Vector2 crosshairCoordinates;
|
||||||
private Vector3 relayXOffset;
|
private Vector3 relayXOffset;
|
||||||
private Vector3 relayYOffset;
|
private Vector3 relayYOffset;
|
||||||
private bool isConfigured;
|
private bool isConfigured;
|
||||||
|
|
||||||
public MapCoordinateSystem CoordinateSystem => coordinateSystem;
|
public MapCoordinateSystem CoordinateSystem => coordinateSystem;
|
||||||
|
public Vector2 Coordinates => crosshairCoordinates;
|
||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
MapCoordinateSystem newCoordinateSystem,
|
MapCoordinateSystem newCoordinateSystem,
|
||||||
Transform newRelayX,
|
Transform newRelayX,
|
||||||
Transform newRelayY,
|
Transform newRelayY,
|
||||||
Transform newCrosshairVisual,
|
Transform newCrosshairVisual,
|
||||||
float newMovementSpeed,
|
|
||||||
bool enableKeyboardInput,
|
|
||||||
MapTargetController newTargetController)
|
MapTargetController newTargetController)
|
||||||
{
|
{
|
||||||
coordinateSystem = newCoordinateSystem;
|
coordinateSystem = newCoordinateSystem;
|
||||||
relayX = newRelayX;
|
relayX = newRelayX;
|
||||||
relayY = newRelayY;
|
relayY = newRelayY;
|
||||||
crosshairVisual = newCrosshairVisual;
|
crosshairVisual = newCrosshairVisual;
|
||||||
movementSpeed = Mathf.Max(0f, newMovementSpeed);
|
|
||||||
useKeyboardInput = enableKeyboardInput;
|
|
||||||
targetController = newTargetController;
|
targetController = newTargetController;
|
||||||
crosshairCoordinates = Vector2.zero;
|
|
||||||
isConfigured = CalibrateObjects();
|
isConfigured = CalibrateObjects();
|
||||||
|
|
||||||
if (isConfigured)
|
|
||||||
ApplyCoordinates();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
public void SetCoordinates(Vector2 coordinates)
|
||||||
{
|
{
|
||||||
if (!isConfigured)
|
if (!isConfigured)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (useKeyboardInput)
|
crosshairCoordinates = coordinateSystem.ClampCoordinates(coordinates);
|
||||||
ReadKeyboardInput();
|
|
||||||
|
|
||||||
Vector2 movement = new(horizontalAxis, verticalAxis);
|
|
||||||
crosshairCoordinates += movement * (movementSpeed * Time.deltaTime);
|
|
||||||
crosshairCoordinates = coordinateSystem.ClampCoordinates(crosshairCoordinates);
|
|
||||||
ApplyCoordinates();
|
ApplyCoordinates();
|
||||||
UpdateNearestTarget();
|
UpdateNearestTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetHorizontalAxis(float value)
|
public Vector2 GetCrosshairCoordinates() => crosshairCoordinates;
|
||||||
{
|
public MapTarget GetNearestTarget() => nearestTarget;
|
||||||
horizontalAxis = Mathf.Clamp(value, -1f, 1f);
|
public float GetDistanceToNearestTarget() => distanceToNearestTarget;
|
||||||
}
|
|
||||||
|
|
||||||
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 bool TryGetNearestTarget(out MapTarget target, out float distance)
|
public bool TryGetNearestTarget(out MapTarget target, out float distance)
|
||||||
{
|
{
|
||||||
@@ -94,62 +56,49 @@ public sealed class MapCrosshairController : MonoBehaviour
|
|||||||
return target != null;
|
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()
|
private void ApplyCoordinates()
|
||||||
{
|
{
|
||||||
Vector3 targetWorldPosition = coordinateSystem.CoordinatesToWorldPosition(crosshairCoordinates);
|
crosshairVisual.position =
|
||||||
crosshairVisual.position = targetWorldPosition;
|
coordinateSystem.CoordinatesToWorldPosition(crosshairCoordinates);
|
||||||
|
|
||||||
if (relayX != null)
|
if (relayX != null)
|
||||||
|
{
|
||||||
relayX.position = coordinateSystem.CoordinatesToWorldPosition(
|
relayX.position = coordinateSystem.CoordinatesToWorldPosition(
|
||||||
new Vector2(crosshairCoordinates.x, 0f)) + relayXOffset;
|
new Vector2(crosshairCoordinates.x, 0f)) + relayXOffset;
|
||||||
|
}
|
||||||
|
|
||||||
if (relayY != null)
|
if (relayY != null)
|
||||||
|
{
|
||||||
relayY.position = coordinateSystem.CoordinatesToWorldPosition(
|
relayY.position = coordinateSystem.CoordinatesToWorldPosition(
|
||||||
new Vector2(0f, crosshairCoordinates.y)) + relayYOffset;
|
new Vector2(0f, crosshairCoordinates.y)) + relayYOffset;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool CalibrateObjects()
|
private bool CalibrateObjects()
|
||||||
{
|
{
|
||||||
if (coordinateSystem == null || crosshairVisual == null || !coordinateSystem.HasValidCorners())
|
if (coordinateSystem == null || crosshairVisual == null ||
|
||||||
|
!coordinateSystem.HasValidCorners())
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Vector3 origin = coordinateSystem.CoordinatesToWorldPosition(Vector2.zero);
|
Vector3 origin = coordinateSystem.CoordinatesToWorldPosition(Vector2.zero);
|
||||||
Vector3 horizontalDirection =
|
Vector3 horizontalDirection = coordinateSystem.CoordinatesToWorldPosition(
|
||||||
coordinateSystem.CoordinatesToWorldPosition(new Vector2(MapCoordinateSystem.CoordinateMaximum, 0f)) -
|
new Vector2(MapCoordinateSystem.CoordinateMaximum, 0f)) - origin;
|
||||||
origin;
|
Vector3 verticalDirection = coordinateSystem.CoordinatesToWorldPosition(
|
||||||
Vector3 verticalDirection =
|
new Vector2(0f, MapCoordinateSystem.CoordinateMaximum)) - origin;
|
||||||
coordinateSystem.CoordinatesToWorldPosition(new Vector2(0f, MapCoordinateSystem.CoordinateMaximum)) -
|
|
||||||
origin;
|
|
||||||
|
|
||||||
if (horizontalDirection.sqrMagnitude <= Mathf.Epsilon ||
|
if (horizontalDirection.sqrMagnitude <= Mathf.Epsilon ||
|
||||||
verticalDirection.sqrMagnitude <= Mathf.Epsilon)
|
verticalDirection.sqrMagnitude <= Mathf.Epsilon)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
horizontalDirection.Normalize();
|
horizontalDirection.Normalize();
|
||||||
verticalDirection.Normalize();
|
verticalDirection.Normalize();
|
||||||
|
|
||||||
if (relayX != null)
|
if (relayX != null)
|
||||||
relayXOffset = Vector3.ProjectOnPlane(relayX.position - origin, horizontalDirection);
|
relayXOffset = Vector3.ProjectOnPlane(relayX.position - origin, horizontalDirection);
|
||||||
|
|
||||||
if (relayY != null)
|
if (relayY != null)
|
||||||
relayYOffset = Vector3.ProjectOnPlane(relayY.position - origin, verticalDirection);
|
relayYOffset = Vector3.ProjectOnPlane(relayY.position - origin, verticalDirection);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +106,6 @@ public sealed class MapCrosshairController : MonoBehaviour
|
|||||||
{
|
{
|
||||||
nearestTarget = null;
|
nearestTarget = null;
|
||||||
distanceToNearestTarget = -1f;
|
distanceToNearestTarget = -1f;
|
||||||
|
|
||||||
if (targetController == null)
|
if (targetController == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,90 +1,56 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.InputSystem;
|
|
||||||
|
|
||||||
[DisallowMultipleComponent]
|
[DisallowMultipleComponent]
|
||||||
public sealed class MapRadarController : MonoBehaviour
|
public sealed class MapRadarController : MonoBehaviour
|
||||||
{
|
{
|
||||||
[Header("Radar Settings")]
|
[Header("Radar Settings")]
|
||||||
[SerializeField, Min(0f)] private float cooldown = 5f;
|
[SerializeField, Min(0f)] private float cooldown = 5f;
|
||||||
[SerializeField] private bool useKeyboardInput = true;
|
|
||||||
|
|
||||||
[Header("Radar State (Runtime)")]
|
[Header("Radar State (Runtime)")]
|
||||||
[SerializeField] private float cooldownRemaining;
|
[SerializeField] private float cooldownRemaining;
|
||||||
|
|
||||||
private readonly List<GameObject> radarMarks = new();
|
private readonly List<GameObject> radarMarks = new();
|
||||||
private MapCoordinateSystem coordinateSystem;
|
private MapCoordinateSystem coordinateSystem;
|
||||||
private MapTargetController targetController;
|
|
||||||
private MapTarget radarMarkPrefab;
|
private MapTarget radarMarkPrefab;
|
||||||
private float nextActivationTime;
|
|
||||||
private bool isConfigured;
|
|
||||||
|
|
||||||
|
public float Cooldown => Mathf.Max(0f, cooldown);
|
||||||
public float CooldownRemaining => cooldownRemaining;
|
public float CooldownRemaining => cooldownRemaining;
|
||||||
public bool IsReady => cooldownRemaining <= 0f;
|
public bool IsReady => cooldownRemaining <= 0f;
|
||||||
|
|
||||||
private void OnValidate()
|
private void OnValidate() => cooldown = Mathf.Max(0f, cooldown);
|
||||||
{
|
|
||||||
cooldown = Mathf.Max(0f, cooldown);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
MapCoordinateSystem newCoordinateSystem,
|
MapCoordinateSystem newCoordinateSystem,
|
||||||
MapTargetController newTargetController,
|
|
||||||
MapTarget newRadarMarkPrefab)
|
MapTarget newRadarMarkPrefab)
|
||||||
{
|
{
|
||||||
coordinateSystem = newCoordinateSystem;
|
coordinateSystem = newCoordinateSystem;
|
||||||
targetController = newTargetController;
|
|
||||||
radarMarkPrefab = newRadarMarkPrefab;
|
radarMarkPrefab = newRadarMarkPrefab;
|
||||||
isConfigured = coordinateSystem != null &&
|
|
||||||
coordinateSystem.HasValidCorners() &&
|
|
||||||
targetController != null &&
|
|
||||||
radarMarkPrefab != null;
|
|
||||||
|
|
||||||
if (targetController != null)
|
|
||||||
targetController.SetTargetVisualsVisible(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
public void SetCooldownRemaining(float seconds) =>
|
||||||
{
|
cooldownRemaining = Mathf.Max(0f, seconds);
|
||||||
cooldownRemaining = Mathf.Max(0f, nextActivationTime - Time.time);
|
|
||||||
|
|
||||||
if (!useKeyboardInput)
|
public void ApplyRadarMarks(IReadOnlyList<Vector2> coordinates)
|
||||||
|
{
|
||||||
|
if (coordinateSystem == null || !coordinateSystem.HasValidCorners() ||
|
||||||
|
radarMarkPrefab == null)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Keyboard keyboard = Keyboard.current;
|
|
||||||
if (keyboard != null && keyboard.oKey.wasPressedThisFrame)
|
|
||||||
ActivateRadar();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ActivateRadar()
|
EnsureRadarMarkCount(coordinates.Count);
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
for (int index = 0; index < radarMarks.Count; index++)
|
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];
|
GameObject radarMark = radarMarks[index];
|
||||||
radarMark.SetActive(hasTarget);
|
radarMark.SetActive(active);
|
||||||
|
if (active)
|
||||||
if (!hasTarget)
|
{
|
||||||
continue;
|
radarMark.name = $"Radar Mark {index + 1}";
|
||||||
|
|
||||||
MapTarget target = targetController.Targets[index];
|
|
||||||
radarMark.name = $"Radar Mark - {target.name}";
|
|
||||||
radarMark.transform.position =
|
radarMark.transform.position =
|
||||||
coordinateSystem.CoordinatesToWorldPosition(target.MapCoordinates);
|
coordinateSystem.CoordinatesToWorldPosition(coordinates[index]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,9 +66,9 @@ public sealed class MapRadarController : MonoBehaviour
|
|||||||
|
|
||||||
private static void PrepareRadarMark(GameObject radarMark)
|
private static void PrepareRadarMark(GameObject radarMark)
|
||||||
{
|
{
|
||||||
MapTarget targetMovement = radarMark.GetComponent<MapTarget>();
|
MapTarget target = radarMark.GetComponent<MapTarget>();
|
||||||
if (targetMovement != null)
|
if (target != null)
|
||||||
targetMovement.enabled = false;
|
target.enabled = false;
|
||||||
|
|
||||||
foreach (Collider targetCollider in radarMark.GetComponentsInChildren<Collider>(true))
|
foreach (Collider targetCollider in radarMark.GetComponentsInChildren<Collider>(true))
|
||||||
targetCollider.enabled = false;
|
targetCollider.enabled = false;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
[DisallowMultipleComponent]
|
[DisallowMultipleComponent]
|
||||||
@@ -6,17 +5,18 @@ public class MapTarget : MonoBehaviour
|
|||||||
{
|
{
|
||||||
[Header("Target State (Runtime)")]
|
[Header("Target State (Runtime)")]
|
||||||
[SerializeField] private TargetConfig config;
|
[SerializeField] private TargetConfig config;
|
||||||
|
[SerializeField] private int targetId = -1;
|
||||||
[SerializeField] private float timeUntilAttack;
|
[SerializeField] private float timeUntilAttack;
|
||||||
[SerializeField] private Vector2 mapCoordinates;
|
[SerializeField] private Vector2 mapCoordinates;
|
||||||
[SerializeField] private int currentHealth;
|
[SerializeField] private int currentHealth;
|
||||||
[SerializeField] private MissileType requiredMissileType;
|
[SerializeField] private MissileType requiredMissileType;
|
||||||
[SerializeField] private bool isReadyToAttack;
|
|
||||||
|
|
||||||
private MapCoordinateSystem coordinateSystem;
|
private MapCoordinateSystem coordinateSystem;
|
||||||
private MapPlayer player;
|
private MapPlayer player;
|
||||||
private bool isConfigured;
|
|
||||||
private Renderer[] targetRenderers;
|
private Renderer[] targetRenderers;
|
||||||
|
private Collider[] targetColliders;
|
||||||
|
|
||||||
|
public int TargetId => targetId;
|
||||||
public Vector2 MapCoordinates => mapCoordinates;
|
public Vector2 MapCoordinates => mapCoordinates;
|
||||||
public MapCoordinateSystem CoordinateSystem => coordinateSystem;
|
public MapCoordinateSystem CoordinateSystem => coordinateSystem;
|
||||||
public TargetConfig Config => config;
|
public TargetConfig Config => config;
|
||||||
@@ -24,34 +24,26 @@ public class MapTarget : MonoBehaviour
|
|||||||
public float TimeUntilAttack => timeUntilAttack;
|
public float TimeUntilAttack => timeUntilAttack;
|
||||||
public int CurrentHealth => currentHealth;
|
public int CurrentHealth => currentHealth;
|
||||||
public MissileType RequiredMissileType => requiredMissileType;
|
public MissileType RequiredMissileType => requiredMissileType;
|
||||||
public bool IsReadyToAttack => isReadyToAttack;
|
|
||||||
|
|
||||||
public event Action<MapTarget> AttackReady;
|
|
||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
TargetConfig newConfig,
|
TargetConfig newConfig,
|
||||||
MapCoordinateSystem newCoordinateSystem,
|
MapCoordinateSystem newCoordinateSystem,
|
||||||
Vector2 startCoordinates,
|
|
||||||
MapPlayer newPlayer)
|
MapPlayer newPlayer)
|
||||||
{
|
{
|
||||||
config = newConfig;
|
config = newConfig;
|
||||||
coordinateSystem = newCoordinateSystem;
|
coordinateSystem = newCoordinateSystem;
|
||||||
player = newPlayer;
|
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)
|
targetId = state.TargetId;
|
||||||
return;
|
timeUntilAttack = state.AttackTimeRemaining;
|
||||||
|
currentHealth = state.Health;
|
||||||
TickAttackTimer(Time.deltaTime);
|
requiredMissileType = config != null
|
||||||
TickMovement(Time.deltaTime);
|
? config.RequiredMissileType
|
||||||
|
: MissileType.None;
|
||||||
|
SetMapCoordinates(state.Coordinates);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetMapCoordinates(Vector2 coordinates)
|
public void SetMapCoordinates(Vector2 coordinates)
|
||||||
@@ -62,41 +54,34 @@ public class MapTarget : MonoBehaviour
|
|||||||
Mathf.Clamp(coordinates.x, 0f, MapCoordinateSystem.CoordinateMaximum),
|
Mathf.Clamp(coordinates.x, 0f, MapCoordinateSystem.CoordinateMaximum),
|
||||||
Mathf.Clamp(coordinates.y, 0f, MapCoordinateSystem.CoordinateMaximum));
|
Mathf.Clamp(coordinates.y, 0f, MapCoordinateSystem.CoordinateMaximum));
|
||||||
|
|
||||||
if (isConfigured)
|
if (coordinateSystem != null && coordinateSystem.HasValidCorners())
|
||||||
transform.position = coordinateSystem.CoordinatesToWorldPosition(mapCoordinates);
|
transform.position = coordinateSystem.CoordinatesToWorldPosition(mapCoordinates);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 GetMapCoordinates()
|
public Vector2 GetMapCoordinates() => mapCoordinates;
|
||||||
{
|
|
||||||
return mapCoordinates;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetVisualsVisible(bool isVisible)
|
public void SetVisualsVisible(bool isVisible)
|
||||||
{
|
{
|
||||||
if (targetRenderers == null)
|
if (targetRenderers == null)
|
||||||
targetRenderers = GetComponentsInChildren<Renderer>(true);
|
targetRenderers = GetComponentsInChildren<Renderer>(true);
|
||||||
|
if (targetColliders == null)
|
||||||
|
targetColliders = GetComponentsInChildren<Collider>(true);
|
||||||
|
|
||||||
foreach (Renderer targetRenderer in targetRenderers)
|
foreach (Renderer targetRenderer in targetRenderers)
|
||||||
{
|
{
|
||||||
if (targetRenderer != null)
|
if (targetRenderer != null)
|
||||||
targetRenderer.enabled = isVisible;
|
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)
|
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 System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
[DisallowMultipleComponent]
|
[DisallowMultipleComponent]
|
||||||
public sealed class MapTargetController : MonoBehaviour
|
public sealed class MapTargetController : MonoBehaviour
|
||||||
{
|
{
|
||||||
[Header("Random Wave Settings")]
|
[Header("Wave Settings")]
|
||||||
[SerializeField, Min(0)] private int targetsPerWave = 5;
|
[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)")]
|
[Header("Current Wave (Runtime)")]
|
||||||
[SerializeField, Min(0)] private int currentWave;
|
[SerializeField, Min(0)] private int currentWave;
|
||||||
[SerializeField] private List<MapTarget> currentWaveTargets = new();
|
[SerializeField] private List<MapTarget> currentWaveTargets = new();
|
||||||
|
|
||||||
private readonly List<MapTarget> pooledTargets = new();
|
private readonly List<MapTarget> pooledTargets = new();
|
||||||
|
private readonly Dictionary<int, MapTarget> targetsById = new();
|
||||||
|
private readonly HashSet<int> synchronizedTargetIds = new();
|
||||||
private MapCoordinateSystem coordinateSystem;
|
private MapCoordinateSystem coordinateSystem;
|
||||||
private Transform targetParent;
|
private Transform targetParent;
|
||||||
private MapPlayer player;
|
private MapPlayer player;
|
||||||
private bool isConfigured;
|
|
||||||
|
|
||||||
public IReadOnlyList<MapTarget> Targets => currentWaveTargets;
|
public IReadOnlyList<MapTarget> Targets => currentWaveTargets;
|
||||||
|
public IReadOnlyList<TargetConfig> TargetConfigs => randomTargetConfigs;
|
||||||
public int CurrentWave => currentWave;
|
public int CurrentWave => currentWave;
|
||||||
public int TargetsPerWave => targetsPerWave;
|
public int TargetsPerWave => targetsPerWave;
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
if (currentWave == 0)
|
|
||||||
StartNextWave();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnValidate()
|
private void OnValidate()
|
||||||
{
|
{
|
||||||
targetsPerWave = Mathf.Max(0, targetsPerWave);
|
targetsPerWave = Mathf.Max(0, targetsPerWave);
|
||||||
@@ -42,9 +40,10 @@ public sealed class MapTargetController : MonoBehaviour
|
|||||||
coordinateSystem = newCoordinateSystem;
|
coordinateSystem = newCoordinateSystem;
|
||||||
targetParent = newTargetParent != null ? newTargetParent : transform;
|
targetParent = newTargetParent != null ? newTargetParent : transform;
|
||||||
player = newPlayer;
|
player = newPlayer;
|
||||||
isConfigured = coordinateSystem != null && coordinateSystem.HasValidCorners();
|
|
||||||
|
|
||||||
pooledTargets.Clear();
|
pooledTargets.Clear();
|
||||||
|
targetsById.Clear();
|
||||||
|
currentWaveTargets.Clear();
|
||||||
foreach (MapTarget existingTarget in targetParent.GetComponentsInChildren<MapTarget>(true))
|
foreach (MapTarget existingTarget in targetParent.GetComponentsInChildren<MapTarget>(true))
|
||||||
{
|
{
|
||||||
existingTarget.SetVisualsVisible(false);
|
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);
|
MapTargetNetworkState state = states[index];
|
||||||
return false;
|
TargetConfig config = GetConfig(state.ConfigIndex);
|
||||||
}
|
if (config == null || config.TargetPrefab == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
RetireCurrentWave();
|
synchronizedTargetIds.Add(state.TargetId);
|
||||||
currentWave++;
|
if (!targetsById.TryGetValue(state.TargetId, out MapTarget target) || target == null)
|
||||||
|
|
||||||
for (int index = 0; index < targetsPerWave; index++)
|
|
||||||
{
|
{
|
||||||
TargetConfig config = GetRandomValidConfig();
|
target = GetPooledTarget(config);
|
||||||
if (config == null)
|
|
||||||
break;
|
|
||||||
|
|
||||||
MapTarget target = GetPooledTarget(config);
|
|
||||||
if (target == null)
|
if (target == null)
|
||||||
target = Instantiate(config.TargetPrefab, targetParent);
|
target = Instantiate(config.TargetPrefab, targetParent);
|
||||||
|
|
||||||
target.name = $"Wave {currentWave} - {config.name} - {index + 1}";
|
targetsById[state.TargetId] = target;
|
||||||
target.Configure(config, coordinateSystem, GetRandomCoordinates(), player);
|
|
||||||
target.SetVisualsVisible(false);
|
|
||||||
target.gameObject.SetActive(true);
|
|
||||||
currentWaveTargets.Add(target);
|
currentWaveTargets.Add(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentWaveTargets.Count > 0 || targetsPerWave == 0;
|
target.name = $"Wave {currentWave} - {config.name} - {state.TargetId}";
|
||||||
|
target.Configure(config, coordinateSystem, player);
|
||||||
|
target.ApplyNetworkState(state);
|
||||||
|
target.SetVisualsVisible(showLiveTargetVisuals);
|
||||||
|
target.gameObject.SetActive(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateAndPlaceTargets()
|
for (int index = currentWaveTargets.Count - 1; index >= 0; index--)
|
||||||
{
|
{
|
||||||
StartNextWave();
|
MapTarget target = currentWaveTargets[index];
|
||||||
|
if (target != null && synchronizedTargetIds.Contains(target.TargetId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
RetireTarget(target);
|
||||||
|
currentWaveTargets.RemoveAt(index);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TargetConfig GetConfig(int index) =>
|
||||||
|
index >= 0 && index < randomTargetConfigs.Length
|
||||||
|
? randomTargetConfigs[index]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
public int GetRandomValidConfigIndex()
|
||||||
|
{
|
||||||
|
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)
|
public void SetTargetVisualsVisible(bool isVisible)
|
||||||
{
|
{
|
||||||
|
showLiveTargetVisuals = isVisible;
|
||||||
foreach (MapTarget target in currentWaveTargets)
|
foreach (MapTarget target in currentWaveTargets)
|
||||||
{
|
{
|
||||||
if (target != null)
|
if (target != null)
|
||||||
@@ -98,28 +131,15 @@ public sealed class MapTargetController : MonoBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public MapTarget GetTarget(int index)
|
public MapTarget GetTarget(int index) =>
|
||||||
{
|
index >= 0 && index < currentWaveTargets.Count
|
||||||
return index >= 0 && index < currentWaveTargets.Count
|
|
||||||
? currentWaveTargets[index]
|
? currentWaveTargets[index]
|
||||||
: null;
|
: null;
|
||||||
}
|
|
||||||
|
|
||||||
private void RetireCurrentWave()
|
public bool StartNextWave()
|
||||||
{
|
{
|
||||||
foreach (MapTarget target in currentWaveTargets)
|
MapController mapController = FindFirstObjectByType<MapController>();
|
||||||
{
|
return mapController != null && mapController.StartNextWave();
|
||||||
if (target == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
target.SetVisualsVisible(false);
|
|
||||||
target.gameObject.SetActive(false);
|
|
||||||
|
|
||||||
if (!pooledTargets.Contains(target))
|
|
||||||
pooledTargets.Add(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentWaveTargets.Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MapTarget GetPooledTarget(TargetConfig config)
|
private MapTarget GetPooledTarget(TargetConfig config)
|
||||||
@@ -143,30 +163,15 @@ public sealed class MapTargetController : MonoBehaviour
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HasValidTargetConfig()
|
private void RetireTarget(MapTarget target)
|
||||||
{
|
{
|
||||||
return GetRandomValidConfig() != null;
|
if (target == null)
|
||||||
}
|
return;
|
||||||
|
|
||||||
private TargetConfig GetRandomValidConfig()
|
targetsById.Remove(target.TargetId);
|
||||||
{
|
target.SetVisualsVisible(false);
|
||||||
if (randomTargetConfigs == null || randomTargetConfigs.Length == 0)
|
target.gameObject.SetActive(false);
|
||||||
return null;
|
if (!pooledTargets.Contains(target))
|
||||||
|
pooledTargets.Add(target);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")]
|
[Header("Target Stats")]
|
||||||
[SerializeField, Min(0f)] private float timeBeforeAttack = 5f;
|
[SerializeField, Min(0f)] private float timeBeforeAttack = 5f;
|
||||||
[SerializeField, Min(1)] private int health = 100;
|
[SerializeField, Min(1)] private int health = 100;
|
||||||
|
[SerializeField, Min(1)] private int baseDamage = 1;
|
||||||
[SerializeField] private MissileType requiredMissileType = MissileType.Standard;
|
[SerializeField] private MissileType requiredMissileType = MissileType.Standard;
|
||||||
|
|
||||||
[Header("Air Target Movement")]
|
[Header("Air Target Movement")]
|
||||||
@@ -18,6 +19,7 @@ public sealed class TargetConfig : ScriptableObject
|
|||||||
public MapTarget TargetPrefab => targetPrefab;
|
public MapTarget TargetPrefab => targetPrefab;
|
||||||
public float TimeBeforeAttack => timeBeforeAttack;
|
public float TimeBeforeAttack => timeBeforeAttack;
|
||||||
public int Health => health;
|
public int Health => health;
|
||||||
|
public int BaseDamage => baseDamage;
|
||||||
public MissileType RequiredMissileType => requiredMissileType;
|
public MissileType RequiredMissileType => requiredMissileType;
|
||||||
public float MovementSpeed => movementSpeed;
|
public float MovementSpeed => movementSpeed;
|
||||||
public float StoppingDistance => stoppingDistance;
|
public float StoppingDistance => stoppingDistance;
|
||||||
@@ -26,6 +28,7 @@ public sealed class TargetConfig : ScriptableObject
|
|||||||
{
|
{
|
||||||
timeBeforeAttack = Mathf.Max(0f, timeBeforeAttack);
|
timeBeforeAttack = Mathf.Max(0f, timeBeforeAttack);
|
||||||
health = Mathf.Max(1, health);
|
health = Mathf.Max(1, health);
|
||||||
|
baseDamage = Mathf.Max(1, baseDamage);
|
||||||
movementSpeed = Mathf.Max(0f, movementSpeed);
|
movementSpeed = Mathf.Max(0f, movementSpeed);
|
||||||
stoppingDistance = Mathf.Max(0f, stoppingDistance);
|
stoppingDistance = Mathf.Max(0f, stoppingDistance);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using Unity.Netcode;
|
using Unity.Netcode;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Events;
|
using UnityEngine.Events;
|
||||||
@@ -13,6 +14,9 @@ public sealed class NetworkButtonInteractable : NetworkInteractable
|
|||||||
|
|
||||||
public bool IsPressed => isPressed.Value;
|
public bool IsPressed => isPressed.Value;
|
||||||
|
|
||||||
|
public event Action Pressed;
|
||||||
|
public event Action<bool> StateChanged;
|
||||||
|
|
||||||
public override void OnNetworkSpawn()
|
public override void OnNetworkSpawn()
|
||||||
{
|
{
|
||||||
isPressed.OnValueChanged += HandleStateChanged;
|
isPressed.OnValueChanged += HandleStateChanged;
|
||||||
@@ -28,11 +32,15 @@ public sealed class NetworkButtonInteractable : NetworkInteractable
|
|||||||
|
|
||||||
protected override void InteractOnServer(ulong senderClientId, NetworkObject playerObject)
|
protected override void InteractOnServer(ulong senderClientId, NetworkObject playerObject)
|
||||||
{
|
{
|
||||||
|
Pressed?.Invoke();
|
||||||
isPressed.Value = !isPressed.Value;
|
isPressed.Value = !isPressed.Value;
|
||||||
ApplyState(isPressed.Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleStateChanged(bool _, bool currentValue) => ApplyState(currentValue);
|
private void HandleStateChanged(bool _, bool currentValue) => ApplyState(currentValue);
|
||||||
|
|
||||||
private void ApplyState(bool currentValue) => buttonStateChanged?.Invoke(currentValue);
|
private void ApplyState(bool currentValue)
|
||||||
|
{
|
||||||
|
StateChanged?.Invoke(currentValue);
|
||||||
|
buttonStateChanged?.Invoke(currentValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using Unity.Netcode;
|
using Unity.Netcode;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Events;
|
using UnityEngine.Events;
|
||||||
@@ -52,7 +53,10 @@ public sealed class NetworkPhysicalLeverInteractable :
|
|||||||
public bool IsControlled => controllingClientId.Value != NoController;
|
public bool IsControlled => controllingClientId.Value != NoController;
|
||||||
public bool IsControlledByLocalPlayer =>
|
public bool IsControlledByLocalPlayer =>
|
||||||
NetworkManager != null && IsControlledBy(NetworkManager.LocalClientId);
|
NetworkManager != null && IsControlledBy(NetworkManager.LocalClientId);
|
||||||
public string ReleasePrompt => "[Hold E] Pulling lever";
|
public string ReleasePrompt => "[Hold LMB] Pulling lever";
|
||||||
|
|
||||||
|
public event Action<float> ValueChanged;
|
||||||
|
public event Action<bool> StateChanged;
|
||||||
|
|
||||||
public override string InteractionPrompt => IsControlledByLocalPlayer
|
public override string InteractionPrompt => IsControlledByLocalPlayer
|
||||||
? ReleasePrompt
|
? ReleasePrompt
|
||||||
@@ -304,11 +308,15 @@ public sealed class NetworkPhysicalLeverInteractable :
|
|||||||
private void HandleValueChanged(float _, float currentValue)
|
private void HandleValueChanged(float _, float currentValue)
|
||||||
{
|
{
|
||||||
ApplyVisual(currentValue);
|
ApplyVisual(currentValue);
|
||||||
|
ValueChanged?.Invoke(currentValue);
|
||||||
leverValueChanged?.Invoke(currentValue);
|
leverValueChanged?.Invoke(currentValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleActivationChanged(bool _, bool currentState) =>
|
private void HandleActivationChanged(bool _, bool currentState)
|
||||||
|
{
|
||||||
|
StateChanged?.Invoke(currentState);
|
||||||
leverStateChanged?.Invoke(currentState);
|
leverStateChanged?.Invoke(currentState);
|
||||||
|
}
|
||||||
|
|
||||||
private void ApplyVisual(float value)
|
private void ApplyVisual(float value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
using System;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public sealed class NetworkPhysicalWheelInteractable :
|
||||||
|
NetworkInteractable,
|
||||||
|
IPlayerInteractionSession
|
||||||
|
{
|
||||||
|
private const ulong NoController = ulong.MaxValue;
|
||||||
|
|
||||||
|
[Header("Wheel visual")]
|
||||||
|
[SerializeField] private Transform wheelVisual;
|
||||||
|
[SerializeField] private Vector3 neutralLocalEulerAngles;
|
||||||
|
[SerializeField] private Vector3 localRotationAxis = Vector3.right;
|
||||||
|
[SerializeField] private Vector3 localReferenceDirection = Vector3.up;
|
||||||
|
[SerializeField, Range(-720f, 0f)] private float minimumAngle = -180f;
|
||||||
|
[SerializeField, Range(0f, 720f)] private float maximumAngle = 180f;
|
||||||
|
[SerializeField, Range(0f, 1f)] private float initialNormalizedValue = 0.5f;
|
||||||
|
[SerializeField, Min(1f)] private float maximumRotationSpeed = 360f;
|
||||||
|
|
||||||
|
[Header("Control")]
|
||||||
|
[SerializeField, Min(0.5f)] private float maximumControlDistance = 4f;
|
||||||
|
[SerializeField] private UnityEvent<float> normalizedValueChanged;
|
||||||
|
|
||||||
|
private readonly NetworkVariable<ulong> controllingClientId = new(
|
||||||
|
NoController,
|
||||||
|
NetworkVariableReadPermission.Everyone,
|
||||||
|
NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
private readonly NetworkVariable<float> synchronizedAngle = new(
|
||||||
|
0f,
|
||||||
|
NetworkVariableReadPermission.Everyone,
|
||||||
|
NetworkVariableWritePermission.Server);
|
||||||
|
|
||||||
|
private float lastPointerAngle;
|
||||||
|
private bool hasPointerAngle;
|
||||||
|
|
||||||
|
public float Angle => synchronizedAngle.Value;
|
||||||
|
public float NormalizedValue => Mathf.InverseLerp(
|
||||||
|
minimumAngle,
|
||||||
|
maximumAngle,
|
||||||
|
synchronizedAngle.Value);
|
||||||
|
public bool IsControlled => controllingClientId.Value != NoController;
|
||||||
|
public bool IsControlledByLocalPlayer =>
|
||||||
|
NetworkManager != null && IsControlledBy(NetworkManager.LocalClientId);
|
||||||
|
public string ReleasePrompt => "[Hold LMB] Turning wheel";
|
||||||
|
|
||||||
|
public override string InteractionPrompt => IsControlledByLocalPlayer
|
||||||
|
? ReleasePrompt
|
||||||
|
: IsControlled
|
||||||
|
? "Wheel in use"
|
||||||
|
: InteractionLabel;
|
||||||
|
|
||||||
|
public override bool IsInteractionAvailable =>
|
||||||
|
base.IsInteractionAvailable && (!IsControlled || IsControlledByLocalPlayer);
|
||||||
|
|
||||||
|
public event Action<float> ValueChanged;
|
||||||
|
|
||||||
|
private Quaternion NeutralLocalRotation => Quaternion.Euler(neutralLocalEulerAngles);
|
||||||
|
private Vector3 RotationAxis => localRotationAxis.sqrMagnitude > 0.0001f
|
||||||
|
? localRotationAxis.normalized
|
||||||
|
: Vector3.right;
|
||||||
|
private Vector3 ReferenceDirection => localReferenceDirection.sqrMagnitude > 0.0001f
|
||||||
|
? localReferenceDirection.normalized
|
||||||
|
: Vector3.up;
|
||||||
|
|
||||||
|
private void Reset()
|
||||||
|
{
|
||||||
|
wheelVisual = transform;
|
||||||
|
neutralLocalEulerAngles = wheelVisual.localEulerAngles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
if (maximumAngle <= minimumAngle)
|
||||||
|
maximumAngle = minimumAngle + 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkSpawn()
|
||||||
|
{
|
||||||
|
PlayerInteractionSessionRegistry.Register(this);
|
||||||
|
if (IsServer)
|
||||||
|
{
|
||||||
|
synchronizedAngle.Value = Mathf.Lerp(
|
||||||
|
minimumAngle,
|
||||||
|
maximumAngle,
|
||||||
|
initialNormalizedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronizedAngle.OnValueChanged += HandleAngleChanged;
|
||||||
|
ApplyVisual(synchronizedAngle.Value);
|
||||||
|
InvokeValueChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkDespawn()
|
||||||
|
{
|
||||||
|
PlayerInteractionSessionRegistry.Unregister(this);
|
||||||
|
synchronizedAngle.OnValueChanged -= HandleAngleChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FixedUpdate()
|
||||||
|
{
|
||||||
|
if (!IsServer || !IsSpawned || !IsControlled || wheelVisual == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!TryGetPlayerObject(controllingClientId.Value, out NetworkObject playerObject) ||
|
||||||
|
!IsPlayerWithinControlDistance(playerObject))
|
||||||
|
{
|
||||||
|
EndControl();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryCalculatePointerAngle(playerObject, out float pointerAngle))
|
||||||
|
{
|
||||||
|
hasPointerAngle = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPointerAngle)
|
||||||
|
{
|
||||||
|
lastPointerAngle = pointerAngle;
|
||||||
|
hasPointerAngle = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float pointerDelta = Mathf.DeltaAngle(lastPointerAngle, pointerAngle);
|
||||||
|
lastPointerAngle = pointerAngle;
|
||||||
|
float maximumStep = maximumRotationSpeed * Time.fixedDeltaTime;
|
||||||
|
float nextAngle = Mathf.Clamp(
|
||||||
|
synchronizedAngle.Value + Mathf.Clamp(pointerDelta, -maximumStep, maximumStep),
|
||||||
|
minimumAngle,
|
||||||
|
maximumAngle);
|
||||||
|
if (!Mathf.Approximately(nextAngle, synchronizedAngle.Value))
|
||||||
|
synchronizedAngle.Value = nextAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsControlledBy(ulong clientId) =>
|
||||||
|
controllingClientId.Value == clientId;
|
||||||
|
|
||||||
|
public void RequestRelease()
|
||||||
|
{
|
||||||
|
if (IsSpawned && IsControlledByLocalPlayer)
|
||||||
|
RequestReleaseServerRpc();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CanInteractOnServer(ulong senderClientId) =>
|
||||||
|
!IsControlled || IsControlledBy(senderClientId);
|
||||||
|
|
||||||
|
protected override void InteractOnServer(
|
||||||
|
ulong senderClientId,
|
||||||
|
NetworkObject playerObject)
|
||||||
|
{
|
||||||
|
if (IsControlledBy(senderClientId))
|
||||||
|
EndControl();
|
||||||
|
else
|
||||||
|
BeginControl(senderClientId, playerObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||||
|
private void RequestReleaseServerRpc(RpcParams rpcParams = default)
|
||||||
|
{
|
||||||
|
if (IsControlledBy(rpcParams.Receive.SenderClientId))
|
||||||
|
EndControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeginControl(ulong clientId, NetworkObject playerObject)
|
||||||
|
{
|
||||||
|
controllingClientId.Value = clientId;
|
||||||
|
hasPointerAngle = TryCalculatePointerAngle(playerObject, out lastPointerAngle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EndControl()
|
||||||
|
{
|
||||||
|
controllingClientId.Value = NoController;
|
||||||
|
hasPointerAngle = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsPlayerWithinControlDistance(NetworkObject playerObject)
|
||||||
|
{
|
||||||
|
float maximumDistanceSquared = maximumControlDistance * maximumControlDistance;
|
||||||
|
return (playerObject.transform.position - wheelVisual.position).sqrMagnitude <=
|
||||||
|
maximumDistanceSquared;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryCalculatePointerAngle(
|
||||||
|
NetworkObject playerObject,
|
||||||
|
out float pointerAngle)
|
||||||
|
{
|
||||||
|
pointerAngle = 0f;
|
||||||
|
FirstPersonCameraController cameraController =
|
||||||
|
playerObject.GetComponent<FirstPersonCameraController>();
|
||||||
|
Transform aimTransform = cameraController != null
|
||||||
|
? cameraController.AimTransform
|
||||||
|
: playerObject.transform;
|
||||||
|
|
||||||
|
Transform wheelParent = wheelVisual.parent;
|
||||||
|
Vector3 axisWorld = wheelParent != null
|
||||||
|
? wheelParent.TransformDirection(NeutralLocalRotation * RotationAxis)
|
||||||
|
: NeutralLocalRotation * RotationAxis;
|
||||||
|
Vector3 referenceWorld = wheelParent != null
|
||||||
|
? wheelParent.TransformDirection(NeutralLocalRotation * ReferenceDirection)
|
||||||
|
: NeutralLocalRotation * ReferenceDirection;
|
||||||
|
|
||||||
|
Ray aimRay = new(aimTransform.position, aimTransform.forward);
|
||||||
|
Plane wheelPlane = new(axisWorld, wheelVisual.position);
|
||||||
|
Vector3 targetPoint;
|
||||||
|
if (wheelPlane.Raycast(aimRay, out float enter) && enter >= 0f)
|
||||||
|
{
|
||||||
|
targetPoint = aimRay.GetPoint(enter);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float distanceAlongRay = Mathf.Max(
|
||||||
|
0.25f,
|
||||||
|
Vector3.Dot(wheelVisual.position - aimRay.origin, aimRay.direction));
|
||||||
|
targetPoint = aimRay.GetPoint(distanceAlongRay);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 pointerDirection = Vector3.ProjectOnPlane(
|
||||||
|
targetPoint - wheelVisual.position,
|
||||||
|
axisWorld);
|
||||||
|
if (pointerDirection.sqrMagnitude < 0.0001f)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
pointerAngle = Vector3.SignedAngle(
|
||||||
|
referenceWorld,
|
||||||
|
pointerDirection.normalized,
|
||||||
|
axisWorld);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleAngleChanged(float _, float current) =>
|
||||||
|
ApplyNetworkValue(current);
|
||||||
|
|
||||||
|
private void ApplyNetworkValue(float angle)
|
||||||
|
{
|
||||||
|
ApplyVisual(angle);
|
||||||
|
InvokeValueChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InvokeValueChanged()
|
||||||
|
{
|
||||||
|
float value = NormalizedValue;
|
||||||
|
ValueChanged?.Invoke(value);
|
||||||
|
normalizedValueChanged?.Invoke(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyVisual(float angle)
|
||||||
|
{
|
||||||
|
if (wheelVisual != null)
|
||||||
|
{
|
||||||
|
wheelVisual.localRotation = NeutralLocalRotation *
|
||||||
|
Quaternion.AngleAxis(angle, RotationAxis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ContextMenu("Preview/Minimum")]
|
||||||
|
private void PreviewMinimum() => ApplyVisual(minimumAngle);
|
||||||
|
|
||||||
|
[ContextMenu("Preview/Center")]
|
||||||
|
private void PreviewCenter() => ApplyVisual(Mathf.Lerp(
|
||||||
|
minimumAngle,
|
||||||
|
maximumAngle,
|
||||||
|
0.5f));
|
||||||
|
|
||||||
|
[ContextMenu("Preview/Maximum")]
|
||||||
|
private void PreviewMaximum() => ApplyVisual(maximumAngle);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c645663bb9dcbaf43991bd38665923da
|
||||||
@@ -22,7 +22,7 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
|||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (!isInitialized || !IsOwner || Keyboard.current == null)
|
if (!isInitialized || !IsOwner)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (PlayerInteractionSessionRegistry.TryGetControlledSession(
|
if (PlayerInteractionSessionRegistry.TryGetControlledSession(
|
||||||
@@ -31,7 +31,9 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
|||||||
{
|
{
|
||||||
focusedInteractable = null;
|
focusedInteractable = null;
|
||||||
|
|
||||||
if (!Keyboard.current.eKey.isPressed && !activeSessionReleaseRequested)
|
bool isHoldingControl = Mouse.current != null &&
|
||||||
|
Mouse.current.leftButton.isPressed;
|
||||||
|
if (!isHoldingControl && !activeSessionReleaseRequested)
|
||||||
{
|
{
|
||||||
activeSessionReleaseRequested = true;
|
activeSessionReleaseRequested = true;
|
||||||
activeSession.RequestRelease();
|
activeSession.RequestRelease();
|
||||||
@@ -44,9 +46,14 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
|||||||
activeSessionReleaseRequested = false;
|
activeSessionReleaseRequested = false;
|
||||||
focusedInteractable = FindFocusedInteractable();
|
focusedInteractable = FindFocusedInteractable();
|
||||||
|
|
||||||
if (Keyboard.current.eKey.wasPressedThisFrame &&
|
if (focusedInteractable == null || !focusedInteractable.IsInteractionAvailable)
|
||||||
focusedInteractable != null &&
|
return;
|
||||||
focusedInteractable.IsInteractionAvailable)
|
|
||||||
|
bool startsHoldSession = focusedInteractable is IPlayerInteractionSession &&
|
||||||
|
Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame;
|
||||||
|
bool performsRegularInteraction = focusedInteractable is not IPlayerInteractionSession &&
|
||||||
|
Keyboard.current != null && Keyboard.current.eKey.wasPressedThisFrame;
|
||||||
|
if (startsHoldSession || performsRegularInteraction)
|
||||||
{
|
{
|
||||||
focusedInteractable.RequestInteraction();
|
focusedInteractable.RequestInteraction();
|
||||||
}
|
}
|
||||||
@@ -128,8 +135,11 @@ public sealed class PlayerInteractionController : NetworkBehaviour, IPlayerSyste
|
|||||||
if (focusedInteractable == null)
|
if (focusedInteractable == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
string inputPrompt = focusedInteractable is IPlayerInteractionSession
|
||||||
|
? "[Hold LMB] "
|
||||||
|
: "[E] ";
|
||||||
string prompt = focusedInteractable.IsInteractionAvailable
|
string prompt = focusedInteractable.IsInteractionAvailable
|
||||||
? "[E] " + focusedInteractable.InteractionPrompt
|
? inputPrompt + focusedInteractable.InteractionPrompt
|
||||||
: "In use";
|
: "In use";
|
||||||
DrawPrompt(prompt);
|
DrawPrompt(prompt);
|
||||||
}
|
}
|
||||||
|
|||||||
+3222
-1350
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user