Initial import

This commit is contained in:
ldbetteridge
2026-03-31 15:59:23 +01:00
commit 58da5d1d71
136 changed files with 10922 additions and 0 deletions

View 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);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f074ad7027c0ccc409dcab5f7d5abbde

View 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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4a2d497f840a0f54d823f815babd595c

View 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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ad9c0e075e79563498b1577211b6e351

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b197ee7ddd817294f9cd10d38d44efb9

View 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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 666bdebfedde0ad42b1fcc77e0e27843