Initial import
This commit is contained in:
219
Assets/Scripts/Hybrid/CommandDispatcher.cs
Normal file
219
Assets/Scripts/Hybrid/CommandDispatcher.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
using Unity.Collections;
|
||||
using Unity.Physics;
|
||||
using UnityEngine;
|
||||
using EE2Clone.Components;
|
||||
using EE2Clone.NetCode;
|
||||
|
||||
namespace EE2Clone.Hybrid
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes right-click context commands: move, attack, gather, build/repair.
|
||||
/// Sends the appropriate RPC to the server based on what was right-clicked.
|
||||
/// Client-side MonoBehaviour.
|
||||
/// </summary>
|
||||
public class CommandDispatcher : MonoBehaviour
|
||||
{
|
||||
private Camera _mainCamera;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_mainCamera = Camera.main;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var input = RTSInputActions.Instance;
|
||||
if (input == null || _mainCamera == null) return;
|
||||
|
||||
if (input.RightClickPressed)
|
||||
HandleRightClick(input.MousePosition);
|
||||
|
||||
if (input.StopPressed)
|
||||
HandleStop();
|
||||
}
|
||||
|
||||
private void HandleRightClick(Vector2 screenPos)
|
||||
{
|
||||
var selection = SelectionManager.Instance;
|
||||
if (selection == null || selection.SelectedEntities.Count == 0) return;
|
||||
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null || !world.IsCreated) return;
|
||||
|
||||
var em = world.EntityManager;
|
||||
var ray = _mainCamera.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0));
|
||||
|
||||
// Raycast to find what was clicked
|
||||
var physicsWorldQuery = em.CreateEntityQuery(typeof(PhysicsWorldSingleton));
|
||||
if (physicsWorldQuery.IsEmpty) return;
|
||||
|
||||
var physicsWorld = physicsWorldQuery.GetSingleton<PhysicsWorldSingleton>();
|
||||
var rayInput = new RaycastInput
|
||||
{
|
||||
Start = ray.origin,
|
||||
End = ray.origin + ray.direction * 500f,
|
||||
Filter = new CollisionFilter
|
||||
{
|
||||
BelongsTo = ~0u,
|
||||
CollidesWith = ~0u,
|
||||
GroupIndex = 0
|
||||
}
|
||||
};
|
||||
|
||||
Entity hitEntity = Entity.Null;
|
||||
float3 hitPosition = float3.zero;
|
||||
|
||||
if (physicsWorld.CastRay(rayInput, out var hit))
|
||||
{
|
||||
hitEntity = physicsWorld.Bodies[hit.RigidBodyIndex].Entity;
|
||||
hitPosition = hit.Position;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No hit — use ground plane intersection
|
||||
if (ray.direction.y != 0)
|
||||
{
|
||||
float t = -ray.origin.y / ray.direction.y;
|
||||
if (t > 0)
|
||||
hitPosition = new float3(ray.origin.x + ray.direction.x * t, 0, ray.origin.z + ray.direction.z * t);
|
||||
}
|
||||
return; // Only move command if no entity hit
|
||||
}
|
||||
|
||||
// Determine command type based on what was clicked
|
||||
if (hitEntity != Entity.Null && em.Exists(hitEntity))
|
||||
{
|
||||
if (em.HasComponent<ResourceNodeTag>(hitEntity))
|
||||
{
|
||||
// Gather command for citizens
|
||||
SendGatherCommands(hitEntity);
|
||||
return;
|
||||
}
|
||||
|
||||
if (em.HasComponent<UnitTag>(hitEntity) || em.HasComponent<BuildingTag>(hitEntity))
|
||||
{
|
||||
if (em.HasComponent<OwnerPlayer>(hitEntity))
|
||||
{
|
||||
var owner = em.GetComponentData<OwnerPlayer>(hitEntity);
|
||||
// TODO: compare with local player id
|
||||
// For now, treat as enemy → attack
|
||||
SendAttackCommands(hitEntity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (em.HasComponent<BuildingTag>(hitEntity) && em.HasComponent<UnderConstructionTag>(hitEntity))
|
||||
{
|
||||
// Build/repair command for citizens
|
||||
SendBuildRepairCommands(hitEntity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Default: move command
|
||||
SendMoveCommands(hitPosition);
|
||||
}
|
||||
|
||||
private void SendMoveCommands(float3 position)
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null) return;
|
||||
var em = world.EntityManager;
|
||||
|
||||
foreach (var unit in SelectionManager.Instance.SelectedEntities)
|
||||
{
|
||||
if (!em.Exists(unit) || !em.HasComponent<UnitTag>(unit)) continue;
|
||||
|
||||
var rpcEntity = em.CreateEntity();
|
||||
em.AddComponentData(rpcEntity, new MoveCommandRpc
|
||||
{
|
||||
UnitEntity = unit,
|
||||
TargetPosition = position
|
||||
});
|
||||
em.AddComponent<SendRpcCommandRequest>(rpcEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendAttackCommands(Entity target)
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null) return;
|
||||
var em = world.EntityManager;
|
||||
|
||||
foreach (var unit in SelectionManager.Instance.SelectedEntities)
|
||||
{
|
||||
if (!em.Exists(unit) || !em.HasComponent<UnitTag>(unit)) continue;
|
||||
|
||||
var rpcEntity = em.CreateEntity();
|
||||
em.AddComponentData(rpcEntity, new AttackCommandRpc
|
||||
{
|
||||
AttackerEntity = unit,
|
||||
TargetEntity = target
|
||||
});
|
||||
em.AddComponent<SendRpcCommandRequest>(rpcEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendGatherCommands(Entity resourceNode)
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null) return;
|
||||
var em = world.EntityManager;
|
||||
|
||||
foreach (var unit in SelectionManager.Instance.SelectedEntities)
|
||||
{
|
||||
if (!em.Exists(unit) || !em.HasComponent<CitizenTag>(unit)) continue;
|
||||
|
||||
var rpcEntity = em.CreateEntity();
|
||||
em.AddComponentData(rpcEntity, new GatherCommandRpc
|
||||
{
|
||||
CitizenEntity = unit,
|
||||
ResourceNodeEntity = resourceNode
|
||||
});
|
||||
em.AddComponent<SendRpcCommandRequest>(rpcEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendBuildRepairCommands(Entity building)
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null) return;
|
||||
var em = world.EntityManager;
|
||||
|
||||
foreach (var unit in SelectionManager.Instance.SelectedEntities)
|
||||
{
|
||||
if (!em.Exists(unit) || !em.HasComponent<CitizenTag>(unit)) continue;
|
||||
|
||||
var rpcEntity = em.CreateEntity();
|
||||
em.AddComponentData(rpcEntity, new BuildRepairCommandRpc
|
||||
{
|
||||
CitizenEntity = unit,
|
||||
BuildingEntity = building
|
||||
});
|
||||
em.AddComponent<SendRpcCommandRequest>(rpcEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleStop()
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null) return;
|
||||
var em = world.EntityManager;
|
||||
|
||||
foreach (var unit in SelectionManager.Instance.SelectedEntities)
|
||||
{
|
||||
if (!em.Exists(unit) || !em.HasComponent<UnitTag>(unit)) continue;
|
||||
|
||||
var rpcEntity = em.CreateEntity();
|
||||
em.AddComponentData(rpcEntity, new StopCommandRpc
|
||||
{
|
||||
UnitEntity = unit
|
||||
});
|
||||
em.AddComponent<SendRpcCommandRequest>(rpcEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Hybrid/CommandDispatcher.cs.meta
Normal file
2
Assets/Scripts/Hybrid/CommandDispatcher.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f074ad7027c0ccc409dcab5f7d5abbde
|
||||
106
Assets/Scripts/Hybrid/RTSCameraController.cs
Normal file
106
Assets/Scripts/Hybrid/RTSCameraController.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace EE2Clone.Hybrid
|
||||
{
|
||||
/// <summary>
|
||||
/// RTS-style camera controller. Supports WASD/arrow pan, edge-of-screen pan,
|
||||
/// scroll zoom, and Q/E rotation. Client-side only (MonoBehaviour).
|
||||
/// </summary>
|
||||
public class RTSCameraController : MonoBehaviour
|
||||
{
|
||||
[Header("Movement")]
|
||||
[SerializeField] private float panSpeed = 30f;
|
||||
[SerializeField] private float edgePanThreshold = 15f;
|
||||
[SerializeField] private bool enableEdgePan = true;
|
||||
|
||||
[Header("Zoom")]
|
||||
[SerializeField] private float zoomSpeed = 10f;
|
||||
[SerializeField] private float minZoomHeight = 10f;
|
||||
[SerializeField] private float maxZoomHeight = 80f;
|
||||
|
||||
[Header("Rotation")]
|
||||
[SerializeField] private float rotateSpeed = 100f;
|
||||
|
||||
[Header("Bounds")]
|
||||
[SerializeField] private Vector2 mapMin = new(-128f, -128f);
|
||||
[SerializeField] private Vector2 mapMax = new(128f, 128f);
|
||||
|
||||
private Transform _cameraTransform;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_cameraTransform = transform;
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
var input = RTSInputActions.Instance;
|
||||
if (input == null) return;
|
||||
|
||||
HandlePan(input);
|
||||
HandleZoom(input);
|
||||
HandleRotation(input);
|
||||
ClampPosition();
|
||||
}
|
||||
|
||||
private void HandlePan(RTSInputActions input)
|
||||
{
|
||||
var panInput = input.CameraPan;
|
||||
var panDir = new Vector3(panInput.x, 0f, panInput.y);
|
||||
|
||||
// Edge-of-screen panning
|
||||
if (enableEdgePan)
|
||||
{
|
||||
var mousePos = input.MousePosition;
|
||||
if (mousePos.x <= edgePanThreshold) panDir.x -= 1f;
|
||||
if (mousePos.x >= Screen.width - edgePanThreshold) panDir.x += 1f;
|
||||
if (mousePos.y <= edgePanThreshold) panDir.z -= 1f;
|
||||
if (mousePos.y >= Screen.height - edgePanThreshold) panDir.z += 1f;
|
||||
}
|
||||
|
||||
if (panDir.sqrMagnitude > 0.01f)
|
||||
{
|
||||
panDir.Normalize();
|
||||
// Move relative to camera's forward (ignoring Y)
|
||||
var forward = _cameraTransform.forward;
|
||||
forward.y = 0f;
|
||||
forward.Normalize();
|
||||
var right = _cameraTransform.right;
|
||||
right.y = 0f;
|
||||
right.Normalize();
|
||||
|
||||
var move = (forward * panDir.z + right * panDir.x) * (panSpeed * Time.unscaledDeltaTime);
|
||||
_cameraTransform.position += move;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleZoom(RTSInputActions input)
|
||||
{
|
||||
var zoomInput = input.CameraZoom;
|
||||
if (Mathf.Abs(zoomInput) > 0.01f)
|
||||
{
|
||||
var pos = _cameraTransform.position;
|
||||
pos.y -= zoomInput * zoomSpeed * Time.unscaledDeltaTime;
|
||||
pos.y = Mathf.Clamp(pos.y, minZoomHeight, maxZoomHeight);
|
||||
_cameraTransform.position = pos;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleRotation(RTSInputActions input)
|
||||
{
|
||||
var rotateInput = input.CameraRotate;
|
||||
if (Mathf.Abs(rotateInput) > 0.01f)
|
||||
{
|
||||
_cameraTransform.Rotate(Vector3.up, rotateInput * rotateSpeed * Time.unscaledDeltaTime, Space.World);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClampPosition()
|
||||
{
|
||||
var pos = _cameraTransform.position;
|
||||
pos.x = Mathf.Clamp(pos.x, mapMin.x, mapMax.x);
|
||||
pos.z = Mathf.Clamp(pos.z, mapMin.y, mapMax.y);
|
||||
_cameraTransform.position = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Hybrid/RTSCameraController.cs.meta
Normal file
2
Assets/Scripts/Hybrid/RTSCameraController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a2d497f840a0f54d823f815babd595c
|
||||
140
Assets/Scripts/Hybrid/RTSInputActions.cs
Normal file
140
Assets/Scripts/Hybrid/RTSInputActions.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace EE2Clone.Hybrid
|
||||
{
|
||||
/// <summary>
|
||||
/// MonoBehaviour that reads from the Input System and exposes RTS-specific input state
|
||||
/// for the hybrid camera, selection, and UI systems. This is the bridge between
|
||||
/// Unity's Input System and our ECS command pipeline.
|
||||
/// </summary>
|
||||
public class RTSInputActions : MonoBehaviour
|
||||
{
|
||||
public static RTSInputActions Instance { get; private set; }
|
||||
|
||||
[Header("Input Action References")]
|
||||
[SerializeField] private InputActionAsset inputActions;
|
||||
|
||||
// Camera
|
||||
public Vector2 CameraPan { get; private set; }
|
||||
public float CameraZoom { get; private set; }
|
||||
public float CameraRotate { get; private set; }
|
||||
|
||||
// Selection
|
||||
public bool LeftClickPressed { get; private set; }
|
||||
public bool LeftClickReleased { get; private set; }
|
||||
public bool LeftClickHeld { get; private set; }
|
||||
public bool RightClickPressed { get; private set; }
|
||||
public Vector2 MousePosition { get; private set; }
|
||||
|
||||
// Modifiers
|
||||
public bool ShiftHeld { get; private set; }
|
||||
public bool CtrlHeld { get; private set; }
|
||||
|
||||
// Actions
|
||||
public bool AttackMovePressed { get; private set; }
|
||||
public bool PatrolPressed { get; private set; }
|
||||
public bool StopPressed { get; private set; }
|
||||
public bool DeletePressed { get; private set; }
|
||||
|
||||
// Control groups (0-9)
|
||||
public int ControlGroupPressed { get; private set; } = -1;
|
||||
|
||||
private InputAction _cameraPanAction;
|
||||
private InputAction _cameraZoomAction;
|
||||
private InputAction _cameraRotateAction;
|
||||
private InputAction _leftClickAction;
|
||||
private InputAction _rightClickAction;
|
||||
private InputAction _mousePositionAction;
|
||||
private InputAction _shiftAction;
|
||||
private InputAction _ctrlAction;
|
||||
private InputAction _attackMoveAction;
|
||||
private InputAction _patrolAction;
|
||||
private InputAction _stopAction;
|
||||
private InputAction _deleteAction;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (inputActions == null) return;
|
||||
|
||||
var rtsMap = inputActions.FindActionMap("RTS");
|
||||
if (rtsMap == null)
|
||||
{
|
||||
Debug.LogError("RTS action map not found in Input Actions asset!");
|
||||
return;
|
||||
}
|
||||
|
||||
_cameraPanAction = rtsMap.FindAction("CameraPan");
|
||||
_cameraZoomAction = rtsMap.FindAction("CameraZoom");
|
||||
_cameraRotateAction = rtsMap.FindAction("CameraRotate");
|
||||
_leftClickAction = rtsMap.FindAction("LeftClick");
|
||||
_rightClickAction = rtsMap.FindAction("RightClick");
|
||||
_mousePositionAction = rtsMap.FindAction("MousePosition");
|
||||
_shiftAction = rtsMap.FindAction("Shift");
|
||||
_ctrlAction = rtsMap.FindAction("Ctrl");
|
||||
_attackMoveAction = rtsMap.FindAction("AttackMove");
|
||||
_patrolAction = rtsMap.FindAction("Patrol");
|
||||
_stopAction = rtsMap.FindAction("Stop");
|
||||
_deleteAction = rtsMap.FindAction("Delete");
|
||||
|
||||
rtsMap.Enable();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (inputActions == null) return;
|
||||
var rtsMap = inputActions.FindActionMap("RTS");
|
||||
rtsMap?.Disable();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
CameraPan = _cameraPanAction?.ReadValue<Vector2>() ?? Vector2.zero;
|
||||
CameraZoom = _cameraZoomAction?.ReadValue<float>() ?? 0f;
|
||||
CameraRotate = _cameraRotateAction?.ReadValue<float>() ?? 0f;
|
||||
|
||||
LeftClickPressed = _leftClickAction?.WasPressedThisFrame() ?? false;
|
||||
LeftClickReleased = _leftClickAction?.WasReleasedThisFrame() ?? false;
|
||||
LeftClickHeld = _leftClickAction?.IsPressed() ?? false;
|
||||
RightClickPressed = _rightClickAction?.WasPressedThisFrame() ?? false;
|
||||
|
||||
MousePosition = _mousePositionAction?.ReadValue<Vector2>() ?? Vector2.zero;
|
||||
|
||||
ShiftHeld = _shiftAction?.IsPressed() ?? false;
|
||||
CtrlHeld = _ctrlAction?.IsPressed() ?? false;
|
||||
|
||||
AttackMovePressed = _attackMoveAction?.WasPressedThisFrame() ?? false;
|
||||
PatrolPressed = _patrolAction?.WasPressedThisFrame() ?? false;
|
||||
StopPressed = _stopAction?.WasPressedThisFrame() ?? false;
|
||||
DeletePressed = _deleteAction?.WasPressedThisFrame() ?? false;
|
||||
|
||||
// Control groups (keyboard 0-9)
|
||||
ControlGroupPressed = -1;
|
||||
for (int i = 0; i <= 9; i++)
|
||||
{
|
||||
if (Keyboard.current != null && Keyboard.current[Key.Digit0 + i].wasPressedThisFrame)
|
||||
{
|
||||
ControlGroupPressed = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Hybrid/RTSInputActions.cs.meta
Normal file
2
Assets/Scripts/Hybrid/RTSInputActions.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad9c0e075e79563498b1577211b6e351
|
||||
67
Assets/Scripts/Hybrid/SelectionBoxUI.cs
Normal file
67
Assets/Scripts/Hybrid/SelectionBoxUI.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace EE2Clone.Hybrid
|
||||
{
|
||||
/// <summary>
|
||||
/// Draws the rubber-band selection box on screen when the player is drag-selecting.
|
||||
/// </summary>
|
||||
public class SelectionBoxUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Color boxColor = new(0.2f, 0.8f, 0.2f, 0.25f);
|
||||
[SerializeField] private Color borderColor = new(0.2f, 0.8f, 0.2f, 0.8f);
|
||||
|
||||
private Texture2D _fillTexture;
|
||||
private Texture2D _borderTexture;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_fillTexture = new Texture2D(1, 1);
|
||||
_fillTexture.SetPixel(0, 0, Color.white);
|
||||
_fillTexture.Apply();
|
||||
|
||||
_borderTexture = new Texture2D(1, 1);
|
||||
_borderTexture.SetPixel(0, 0, Color.white);
|
||||
_borderTexture.Apply();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
var sel = SelectionManager.Instance;
|
||||
if (sel == null || !sel.IsDragging) return;
|
||||
|
||||
var start = sel.DragStart;
|
||||
var end = sel.DragEnd;
|
||||
|
||||
// Convert to GUI coordinates (Y is flipped)
|
||||
start.y = Screen.height - start.y;
|
||||
end.y = Screen.height - end.y;
|
||||
|
||||
var rect = new Rect(
|
||||
Mathf.Min(start.x, end.x),
|
||||
Mathf.Min(start.y, end.y),
|
||||
Mathf.Abs(end.x - start.x),
|
||||
Mathf.Abs(end.y - start.y)
|
||||
);
|
||||
|
||||
// Draw fill
|
||||
GUI.color = boxColor;
|
||||
GUI.DrawTexture(rect, _fillTexture);
|
||||
|
||||
// Draw border
|
||||
GUI.color = borderColor;
|
||||
float b = 2f;
|
||||
GUI.DrawTexture(new Rect(rect.x, rect.y, rect.width, b), _borderTexture);
|
||||
GUI.DrawTexture(new Rect(rect.x, rect.yMax - b, rect.width, b), _borderTexture);
|
||||
GUI.DrawTexture(new Rect(rect.x, rect.y, b, rect.height), _borderTexture);
|
||||
GUI.DrawTexture(new Rect(rect.xMax - b, rect.y, b, rect.height), _borderTexture);
|
||||
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_fillTexture != null) Destroy(_fillTexture);
|
||||
if (_borderTexture != null) Destroy(_borderTexture);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Hybrid/SelectionBoxUI.cs.meta
Normal file
2
Assets/Scripts/Hybrid/SelectionBoxUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b197ee7ddd817294f9cd10d38d44efb9
|
||||
243
Assets/Scripts/Hybrid/SelectionManager.cs
Normal file
243
Assets/Scripts/Hybrid/SelectionManager.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Physics;
|
||||
using Unity.Transforms;
|
||||
using UnityEngine;
|
||||
using EE2Clone.Components;
|
||||
|
||||
namespace EE2Clone.Hybrid
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles click-select, shift-add, and box-drag selection of entities.
|
||||
/// Client-side only. Adds/removes SelectedTag on ECS entities.
|
||||
/// </summary>
|
||||
public class SelectionManager : MonoBehaviour
|
||||
{
|
||||
public static SelectionManager Instance { get; private set; }
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private LayerMask selectableLayers = ~0;
|
||||
[SerializeField] private float dragThreshold = 5f;
|
||||
|
||||
public List<Entity> SelectedEntities { get; } = new();
|
||||
|
||||
// Control groups: index 0-9
|
||||
private readonly List<Entity>[] _controlGroups = new List<Entity>[10];
|
||||
|
||||
// Box selection state
|
||||
private bool _isDragging;
|
||||
private Vector2 _dragStart;
|
||||
|
||||
// Exposed for SelectionBoxUI
|
||||
public bool IsDragging => _isDragging;
|
||||
public Vector2 DragStart => _dragStart;
|
||||
public Vector2 DragEnd { get; private set; }
|
||||
|
||||
private Camera _mainCamera;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
||||
Instance = this;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
_controlGroups[i] = new List<Entity>();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_mainCamera = Camera.main;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var input = RTSInputActions.Instance;
|
||||
if (input == null || _mainCamera == null) return;
|
||||
|
||||
HandleSelection(input);
|
||||
HandleControlGroups(input);
|
||||
}
|
||||
|
||||
private void HandleSelection(RTSInputActions input)
|
||||
{
|
||||
if (input.LeftClickPressed)
|
||||
{
|
||||
_dragStart = input.MousePosition;
|
||||
_isDragging = false;
|
||||
}
|
||||
|
||||
if (input.LeftClickHeld)
|
||||
{
|
||||
DragEnd = input.MousePosition;
|
||||
if (Vector2.Distance(_dragStart, DragEnd) > dragThreshold)
|
||||
_isDragging = true;
|
||||
}
|
||||
|
||||
if (input.LeftClickReleased)
|
||||
{
|
||||
DragEnd = input.MousePosition;
|
||||
|
||||
if (_isDragging)
|
||||
BoxSelect(input.ShiftHeld);
|
||||
else
|
||||
ClickSelect(input.MousePosition, input.ShiftHeld);
|
||||
|
||||
_isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClickSelect(Vector2 screenPos, bool additive)
|
||||
{
|
||||
if (!additive)
|
||||
ClearSelection();
|
||||
|
||||
var ray = _mainCamera.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0));
|
||||
|
||||
// Raycast using Unity Physics (ECS)
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null || !world.IsCreated) return;
|
||||
|
||||
var physicsWorld = world.EntityManager
|
||||
.CreateEntityQuery(typeof(PhysicsWorldSingleton))
|
||||
.GetSingleton<PhysicsWorldSingleton>();
|
||||
|
||||
var rayInput = new RaycastInput
|
||||
{
|
||||
Start = ray.origin,
|
||||
End = ray.origin + ray.direction * 500f,
|
||||
Filter = new CollisionFilter
|
||||
{
|
||||
BelongsTo = ~0u,
|
||||
CollidesWith = ~0u,
|
||||
GroupIndex = 0
|
||||
}
|
||||
};
|
||||
|
||||
if (physicsWorld.CastRay(rayInput, out var hit))
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
var entity = physicsWorld.Bodies[hit.RigidBodyIndex].Entity;
|
||||
|
||||
if (em.HasComponent<UnitTag>(entity) || em.HasComponent<BuildingTag>(entity))
|
||||
{
|
||||
if (additive && SelectedEntities.Contains(entity))
|
||||
{
|
||||
SelectedEntities.Remove(entity);
|
||||
em.RemoveComponent<SelectedTag>(entity);
|
||||
}
|
||||
else if (!SelectedEntities.Contains(entity))
|
||||
{
|
||||
SelectedEntities.Add(entity);
|
||||
em.AddComponent<SelectedTag>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BoxSelect(bool additive)
|
||||
{
|
||||
if (!additive)
|
||||
ClearSelection();
|
||||
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null || !world.IsCreated) return;
|
||||
|
||||
var em = world.EntityManager;
|
||||
|
||||
var min = Vector2.Min(_dragStart, DragEnd);
|
||||
var max = Vector2.Max(_dragStart, DragEnd);
|
||||
|
||||
// Query all unit entities and check if their screen position is within the box
|
||||
var query = em.CreateEntityQuery(typeof(UnitTag), typeof(LocalTransform));
|
||||
var entities = query.ToEntityArray(Unity.Collections.Allocator.Temp);
|
||||
var transforms = query.ToComponentDataArray<LocalTransform>(Unity.Collections.Allocator.Temp);
|
||||
|
||||
for (int i = 0; i < entities.Length; i++)
|
||||
{
|
||||
var screenPos = _mainCamera.WorldToScreenPoint(transforms[i].Position);
|
||||
if (screenPos.z > 0 &&
|
||||
screenPos.x >= min.x && screenPos.x <= max.x &&
|
||||
screenPos.y >= min.y && screenPos.y <= max.y)
|
||||
{
|
||||
var entity = entities[i];
|
||||
if (!SelectedEntities.Contains(entity))
|
||||
{
|
||||
SelectedEntities.Add(entity);
|
||||
if (!em.HasComponent<SelectedTag>(entity))
|
||||
em.AddComponent<SelectedTag>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entities.Dispose();
|
||||
transforms.Dispose();
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world != null && world.IsCreated)
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
foreach (var entity in SelectedEntities)
|
||||
{
|
||||
if (em.Exists(entity) && em.HasComponent<SelectedTag>(entity))
|
||||
em.RemoveComponent<SelectedTag>(entity);
|
||||
}
|
||||
}
|
||||
SelectedEntities.Clear();
|
||||
}
|
||||
|
||||
private void HandleControlGroups(RTSInputActions input)
|
||||
{
|
||||
int group = input.ControlGroupPressed;
|
||||
if (group < 0) return;
|
||||
|
||||
if (input.CtrlHeld)
|
||||
{
|
||||
// Assign current selection to control group
|
||||
_controlGroups[group].Clear();
|
||||
_controlGroups[group].AddRange(SelectedEntities);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Recall control group
|
||||
ClearSelection();
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null || !world.IsCreated) return;
|
||||
var em = world.EntityManager;
|
||||
|
||||
foreach (var entity in _controlGroups[group])
|
||||
{
|
||||
if (em.Exists(entity))
|
||||
{
|
||||
SelectedEntities.Add(entity);
|
||||
if (!em.HasComponent<SelectedTag>(entity))
|
||||
em.AddComponent<SelectedTag>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Entity[] GetSelectedEntitiesOfType<T>() where T : unmanaged, IComponentData
|
||||
{
|
||||
var world = World.DefaultGameObjectInjectionWorld;
|
||||
if (world == null || !world.IsCreated) return System.Array.Empty<Entity>();
|
||||
|
||||
var em = world.EntityManager;
|
||||
var result = new List<Entity>();
|
||||
foreach (var entity in SelectedEntities)
|
||||
{
|
||||
if (em.Exists(entity) && em.HasComponent<T>(entity))
|
||||
result.Add(entity);
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Hybrid/SelectionManager.cs.meta
Normal file
2
Assets/Scripts/Hybrid/SelectionManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 666bdebfedde0ad42b1fcc77e0e27843
|
||||
Reference in New Issue
Block a user