chunk 1: core gameplay scripts scenes runtime assets

This commit is contained in:
2026-04-06 11:02:34 +03:00
parent fa0388bc79
commit 0d11a097d8
703 changed files with 2292651 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.EnhancedTouch;
using UnityEngine.InputSystem.OnScreen;
using UnityEngine.InputSystem.Layouts;
using EnhancedTouchSupport = UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport;
namespace DeviantMobile.Input
{
// Variant that targets actions directly instead of raw control paths.
[RequireComponent(typeof(RectTransform))]
public class MobileControlManagerAction : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
[Header("Platform / Visibility")]
[Tooltip("If true, on-screen controls will be visible when running in the Editor (useful for testing). If false, controls are hidden in editor/desktop builds and only shown on mobile platforms.")]
[SerializeField] private bool allowShowInEditor = false;
// Helper to determine whether on-screen controls should be active for the current platform.
private bool ShouldShowOnScreenControls()
{
if (Application.platform == RuntimePlatform.Android || Application.platform == RuntimePlatform.IPhonePlayer)
return true;
if (Application.isEditor && allowShowInEditor)
return true;
return false;
}
[System.Serializable]
public class ActionBinding
{
[Tooltip("InputActionReference to trigger for this tap count. Leave empty to ignore.")]
public InputActionReference action;
[Tooltip("Optional label for clarity only (not used at runtime)")]
public string label;
}
[Header("Tap to Action Bindings (index 0=single, 1=double, 2=triple, 3=quad)")]
public ActionBinding[] actions = new ActionBinding[4];
[Header("Tap Settings")]
[Tooltip("Max delay between taps to register as a multi-tap (seconds)")]
[Range(0.1f, 0.6f)] public float multiTapWindow = 0.3f;
[Tooltip("How long to hold the virtual press (seconds)")]
[Range(0.01f, 0.2f)] public float pressDuration = 0.05f;
private int tapCount = 0;
private float lastTapTime = -1f;
private Coroutine finalizeCoroutine;
// Internally we build OnScreenPulseControl(s) that point at the bound actions' control paths.
private readonly List<OnScreenPulseControl> _pulsers = new List<OnScreenPulseControl>(4);
void Awake()
{
BuildOrRefreshPulsers();
// Ensure visibility matches platform (do this early so UI is correct in hierarchy)
gameObject.SetActive(ShouldShowOnScreenControls());
}
void OnEnable()
{
// Ensure EnhancedTouch is available so Unity's UI and On-Screen controls receive multi-touch data.
EnhancedTouchSupport.Enable();
BuildOrRefreshPulsers();
gameObject.SetActive(ShouldShowOnScreenControls());
}
void OnDisable()
{
// Balance the Enable call; safe to invoke even if already disabled elsewhere.
EnhancedTouchSupport.Disable();
}
public void OnPointerDown(PointerEventData eventData) { }
public void OnPointerUp(PointerEventData eventData)
{
var now = Time.unscaledTime;
if (lastTapTime > 0 && (now - lastTapTime) <= multiTapWindow)
tapCount++;
else
tapCount = 1;
lastTapTime = now;
if (finalizeCoroutine != null)
StopCoroutine(finalizeCoroutine);
finalizeCoroutine = StartCoroutine(FinalizeTapAfterWindow());
}
private IEnumerator FinalizeTapAfterWindow()
{
yield return new WaitForSecondsRealtime(multiTapWindow);
TriggerForTapCount(tapCount);
tapCount = 0;
lastTapTime = -1f;
finalizeCoroutine = null;
}
private void TriggerForTapCount(int taps)
{
int index = Mathf.Clamp(taps, 1, 4) - 1;
if (index >= _pulsers.Count) return;
var pulser = _pulsers[index];
if (pulser == null) return;
pulser.Pulse(1f, pressDuration);
}
private void BuildOrRefreshPulsers()
{
_pulsers.Clear();
foreach (var existing in GetComponents<OnScreenPulseControl>())
{
if (Application.isPlaying)
Destroy(existing);
else
DestroyImmediate(existing);
}
// If actions not set, nothing to do.
if (actions == null) return;
// We resolve each InputActionReference to a suitable control path.
for (int i = 0; i < Mathf.Min(4, actions.Length); i++)
{
var binding = actions[i];
if (binding == null || binding.action == null || binding.action.action == null)
{
_pulsers.Add(null);
continue;
}
// Determine a control path. Prefer the first non-empty binding path.
string path = ResolvePreferredControlPath(binding.action.action);
if (string.IsNullOrWhiteSpace(path))
{
_pulsers.Add(null);
continue;
}
var pulser = gameObject.AddComponent<OnScreenPulseControl>();
// Set the control path resolved from the InputAction.
// In the InputAction asset, ensure the action is bound to a control that matches this path
// (for example a Button control on a touch/gamepad). The OnScreenPulseControl will send
// press/release values to that control path, which the InputSystem action can receive.
pulser.controlPath = path;
_pulsers.Add(pulser);
}
}
private static string ResolvePreferredControlPath(InputAction action)
{
// Try active controls if the action is enabled
if (action.enabled && action.controls.Count > 0)
{
// Use the first control's path
return action.controls[0].path;
}
// Fallback to the first binding that has a path
foreach (var b in action.bindings)
{
if (!string.IsNullOrWhiteSpace(b.effectivePath))
return b.effectivePath;
if (!string.IsNullOrWhiteSpace(b.path))
return b.path;
}
return null;
}
#if UNITY_EDITOR
void OnValidate()
{
if (actions == null || actions.Length != 4)
{
var old = actions;
actions = new ActionBinding[4];
if (old != null)
{
for (int i = 0; i < Mathf.Min(4, old.Length); i++)
actions[i] = old[i];
}
}
if (Application.isPlaying == false)
UnityEditor.EditorApplication.delayCall += () => {
if (this != null)
BuildOrRefreshPulsers();
};
}
#endif
}
}

View File

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

View File

@@ -0,0 +1,152 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.EnhancedTouch;
using UnityEngine.InputSystem.OnScreen;
using UnityEngine.InputSystem.Layouts;
using EnhancedTouchSupport = UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport;
namespace DeviantMobile.Input
{
// Drag-based on-screen stick that writes a Vector2 to the Input System.
// Bind your Move action to the same control path (e.g. <Gamepad>/leftStick).
[AddComponentMenu("Input/Mobile On-Screen Stick")]
public class MobileOnScreenStick : OnScreenControl, IPointerDownHandler, IPointerUpHandler, IDragHandler
{
[Header("Platform / Visibility")]
[Tooltip("If true, on-screen stick will be visible when running in the Editor (useful for testing). If false, controls are hidden in editor/desktop builds and only shown on mobile platforms.")]
[SerializeField] private bool allowShowInEditor = false;
// Helper to determine whether on-screen controls should be active for the current platform.
private bool ShouldShowOnScreenControls()
{
if (Application.platform == RuntimePlatform.Android || Application.platform == RuntimePlatform.IPhonePlayer)
return true;
if (Application.isEditor && allowShowInEditor)
return true;
return false;
}
[Header("Control Path")]
[Tooltip("Input System control path to write the Vector2 to. Commonly '<Gamepad>/leftStick'.")]
[SerializeField, InputControl(layout = "Vector2")] private string m_ControlPath = "<Gamepad>/leftStick";
protected override string controlPathInternal
{
get => m_ControlPath;
set => m_ControlPath = value;
}
[Header("UI References")]
[SerializeField] private RectTransform background; // Joystick background area
[SerializeField] private RectTransform handle; // Joystick handle/knob
[Header("Behavior")]
[Tooltip("Max radius in pixels the handle can travel from center.")]
[SerializeField] private float maxRadius = 100f;
[Tooltip("Deadzone as a fraction of radius (0..1).")]
[Range(0f, 0.5f)] [SerializeField] private float deadZone = 0.1f;
[Tooltip("Return handle to center on pointer up.")]
[SerializeField] private bool snapBackOnRelease = true;
private bool _pressed;
private Vector2 _centerLocal;
void Awake()
{
if (background == null)
background = GetComponent<RectTransform>();
if (handle == null && background != null && background.childCount > 0)
handle = background.GetChild(0) as RectTransform;
// Disable the UI for non-mobile platforms by default.
gameObject.SetActive(ShouldShowOnScreenControls());
}
void OnEnable()
{
// Ensure the Enhanced Touch pipeline is active so pointer/touch events map correctly to OnScreenControl.
EnhancedTouchSupport.Enable();
}
void OnDisable()
{
EnhancedTouchSupport.Disable();
}
public void OnPointerDown(PointerEventData eventData)
{
_pressed = true;
CacheCenter();
OnDrag(eventData); // update immediately
}
public void OnDrag(PointerEventData eventData)
{
if (background == null)
return;
RectTransformUtility.ScreenPointToLocalPointInRectangle(background, eventData.position, eventData.pressEventCamera, out var localPoint);
var delta = localPoint - _centerLocal;
// Clamp to radius
if (delta.magnitude > maxRadius)
delta = delta.normalized * maxRadius;
// Normalize to [-1,1]
var norm = (maxRadius > 0f) ? (delta / maxRadius) : Vector2.zero;
// Apply deadzone
var mag = norm.magnitude;
if (mag < deadZone)
norm = Vector2.zero;
else if (mag > 1f)
norm = norm.normalized;
// Move handle visually
if (handle != null)
handle.anchoredPosition = delta;
// Send to Input System
SendValueToControl(norm);
}
public void OnPointerUp(PointerEventData eventData)
{
_pressed = false;
if (snapBackOnRelease)
{
ResetHandle();
}
SendValueToControl(Vector2.zero);
}
private void CacheCenter()
{
if (background != null)
{
// Center is the pivot point local position (0 if centered pivot). Use rect center for safety.
_centerLocal = background.rect.center;
}
else
{
_centerLocal = Vector2.zero;
}
}
private void ResetHandle()
{
if (handle != null)
handle.anchoredPosition = Vector2.zero;
}
#if UNITY_EDITOR
void OnValidate()
{
if (background == null)
background = GetComponent<RectTransform>();
if (handle == null && background != null && background.childCount > 0)
handle = background.GetChild(0) as RectTransform;
}
#endif
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 30717cdf983b9fcbfb655ca63637870e

View File

@@ -0,0 +1,41 @@
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem.OnScreen;
using UnityEngine.InputSystem.Layouts;
namespace DeviantMobile.Input
{
// A lightweight on-screen control we can pulse to simulate a button press.
[AddComponentMenu("Input/On-Screen Pulse Control")]
public class OnScreenPulseControl : OnScreenControl
{
// Shows an Input Control picker in the inspector; default layout Button.
[SerializeField]
[InputControl(layout = "Button")]
private string m_ControlPath;
protected override string controlPathInternal
{
get => m_ControlPath;
set => m_ControlPath = value;
}
public void Pulse(float value = 1f, float duration = 0.05f)
{
// Send press then release after duration
SendValueToControl(value);
if (duration > 0f)
// Use unscaled time so UI pulses are consistent across timeScale changes.
StartCoroutine(ReleaseAfter(duration));
else
SendValueToControl(0f);
}
private IEnumerator ReleaseAfter(float duration)
{
// Use real-time wait to avoid being affected by Time.timeScale changes.
yield return new WaitForSecondsRealtime(duration);
SendValueToControl(0f);
}
}
}

View File

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