using ProjectM.Simulation; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Client { /// /// Client-only twin-stick input gather. Samples the new Input System action map (the generated /// ProjectMInput wrapper over Assets/Settings/Project M Input.inputactions) once per /// frame and writes on the locally-owned player ghost (filtered to /// ). Runs in — NOT the /// prediction loop — so devices are read once per frame, never re-read during rollback. /// /// Implemented as a managed (not a Burst ISystem) because it holds /// and reads the managed Input System wrapper. Fire is an : the event field /// is reset each frame and raised via Set() on the press edge, so a single click fires /// exactly once; netcode accumulates the absolute Count into the command buffer across the /// frame→tick boundary (read back in AbilityFireSystem as the predicted-spawn key). /// /// /// NOTE: Input System types are fully qualified (e.g. UnityEngine.Vector2) and /// using UnityEngine.InputSystem; is intentionally omitted — that namespace defines a /// PlayerInput type that collides with and /// makes the Entities generator bind RefRW<PlayerInput> to the managed class (a /// spurious CS8377). The generated ProjectMInput wrapper lives in this assembly. /// /// [UpdateInGroup(typeof(GhostInputSystemGroup))] [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] public partial class PlayerInputGatherSystem : SystemBase { private ProjectMInput _controls; // Active input scheme (last meaningful actuation wins). Selects KBM-cursor vs gamepad-stick aim and // is published to AimPresentation for the on-screen cursor / world reticle. Starts on mouse+keyboard. private byte _scheme = InputSchemeId.KeyboardMouse; // Last valid KBM cursor aim, held when the projection is degenerate (cursor over the player / above the // horizon) so facing never snaps to zero while the mouse is the active device. private float2 _lastKbmAim = new float2(0f, 1f); // Main camera for cursor -> ground projection; re-resolved when null (e.g. after a domain reload). private UnityEngine.Camera _camera; // World radius around the player inside which mouse facing is HELD (no spin when the cursor passes // over/near the character). Passed to AimMath.PlanarAimFromRay's deadZoneRadius. private const float KbmAimDeadZone = 0.6f; protected override void OnCreate() { RequireForUpdate(); _controls = new ProjectMInput(); _controls.Gameplay.Enable(); } protected override void OnDestroy() { if (_controls != null) { _controls.Gameplay.Disable(); _controls.Dispose(); _controls = null; } } protected override void OnUpdate() { var gameplay = _controls.Gameplay; // Movement is source-agnostic (WASD or left stick) — read from the merged action. float2 move = (float2)gameplay.Move.ReadValue(); bool firePressed = gameplay.Fire.WasPressedThisFrame() && !BuildPaletteState.Active; // no fire while placing a build // --- Active-device detection: last meaningful actuation wins; hold last when idle --- var gamepad = UnityEngine.InputSystem.Gamepad.current; var mouse = UnityEngine.InputSystem.Mouse.current; var keyboard = UnityEngine.InputSystem.Keyboard.current; bool dashPressed = ((keyboard != null && keyboard.leftShiftKey.wasPressedThisFrame) || (gamepad != null && gamepad.buttonEast.wasPressedThisFrame)) && !BuildPaletteState.Active; float2 rightStick = float2.zero; bool gamepadActive = false; if (gamepad != null) { rightStick = (float2)gamepad.rightStick.ReadValue(); float2 leftStick = (float2)gamepad.leftStick.ReadValue(); gamepadActive = math.lengthsq(rightStick) > 0.04f || math.lengthsq(leftStick) > 0.04f || gamepad.rightTrigger.isPressed || gamepad.leftTrigger.isPressed || gamepad.buttonSouth.isPressed || gamepad.buttonEast.isPressed || gamepad.buttonWest.isPressed || gamepad.buttonNorth.isPressed; } bool kbmActive = false; if (mouse != null) { float2 delta = (float2)mouse.delta.ReadValue(); kbmActive = math.lengthsq(delta) > 0.5f || mouse.leftButton.isPressed || mouse.rightButton.isPressed; } if (!kbmActive && keyboard != null) { kbmActive = keyboard.wKey.isPressed || keyboard.aKey.isPressed || keyboard.sKey.isPressed || keyboard.dKey.isPressed || keyboard.upArrowKey.isPressed || keyboard.downArrowKey.isPressed || keyboard.leftArrowKey.isPressed || keyboard.rightArrowKey.isPressed || keyboard.spaceKey.isPressed || keyboard.leftShiftKey.isPressed; } if (gamepadActive && kbmActive) { // Both actuated this frame: break the tie by whichever device updated most recently. double gpT = gamepad != null ? gamepad.lastUpdateTime : double.MinValue; double kbmT = math.max( mouse != null ? mouse.lastUpdateTime : double.MinValue, keyboard != null ? keyboard.lastUpdateTime : double.MinValue); _scheme = gpT > kbmT ? InputSchemeId.Gamepad : InputSchemeId.KeyboardMouse; } else if (gamepadActive) _scheme = InputSchemeId.Gamepad; else if (kbmActive) _scheme = InputSchemeId.KeyboardMouse; // else: neither actuated — keep the previous scheme. // Publish for the presentation layer (cursor visibility / world reticle). AimPresentation.Scheme = _scheme; // Mouse cursor projection needs the camera; resolve/cache it (Camera.main or the rig's camera). if (_camera == null) _camera = ResolveCamera(); UnityEngine.Vector2 cursorScreen = mouse != null ? mouse.position.ReadValue() : default; foreach (var (input, xform) in SystemAPI.Query, RefRO>().WithAll()) { float2 aim; if (_scheme == InputSchemeId.Gamepad) { // Right-stick directional aim. A resting stick yields zero so PlayerAimSystem falls back to // the movement heading (existing controller-first behaviour). aim = rightStick; if (math.lengthsq(aim) < 0.04f) aim = float2.zero; } else { // Mouse: face the cursor. Project the cursor ray onto the player's movement plane and aim // from the player toward that point. Held to the last valid aim when degenerate, so the // character keeps facing the cursor even while standing still (the core jank fix). float3 playerPos = xform.ValueRO.Position; if (_camera != null && mouse != null) { UnityEngine.Ray ray = _camera.ScreenPointToRay(new UnityEngine.Vector3(cursorScreen.x, cursorScreen.y, 0f)); _lastKbmAim = AimMath.PlanarAimFromRay( (float3)ray.origin, (float3)ray.direction, playerPos, playerPos.y, _lastKbmAim, KbmAimDeadZone); } aim = _lastKbmAim; } input.ValueRW.Move = move; input.ValueRW.Aim = aim; input.ValueRW.Scheme = _scheme; // Reset the per-frame event, then raise it on the press edge. Netcode latches the absolute // Count into the command buffer; AbilityFireSystem reads it as the SpawnId key. input.ValueRW.Fire = default; if (firePressed) input.ValueRW.Fire.Set(); input.ValueRW.Dash = default; if (dashPressed) input.ValueRW.Dash.Set(); } } private UnityEngine.Camera ResolveCamera() { var cam = UnityEngine.Camera.main; if (cam == null) { var rig = UnityEngine.Object.FindAnyObjectByType(); if (rig != null) cam = rig.GetComponent(); } return cam; } } }