feat: add reusable virtual hud input
This commit is contained in:
@@ -95,20 +95,31 @@ namespace XGame.EditorTools
|
||||
}
|
||||
}
|
||||
|
||||
public static void BuildVirtualInputFromCommandLine()
|
||||
{
|
||||
string jsonRoot = ResolveCommandLineJsonRoot();
|
||||
string[] files =
|
||||
{
|
||||
"UI_VirtualJoystick.json",
|
||||
"UI_VirtualActionPad.json"
|
||||
};
|
||||
|
||||
int built = 0;
|
||||
foreach (string file in files)
|
||||
{
|
||||
string output = BuildFromJsonFile(Path.Combine(jsonRoot, file));
|
||||
if (string.IsNullOrEmpty(output))
|
||||
{
|
||||
throw new Exception("[UIGen] " + file + " build failed.");
|
||||
}
|
||||
built++;
|
||||
}
|
||||
Debug.Log("[UIGen] virtual input prefabs built: " + built);
|
||||
}
|
||||
|
||||
public static void BuildAllFromCommandLine()
|
||||
{
|
||||
string jsonRoot = GetCommandLineValue("-uiJsonRoot");
|
||||
if (string.IsNullOrEmpty(jsonRoot))
|
||||
{
|
||||
jsonRoot = ResolveJsonRoot();
|
||||
}
|
||||
else if (!Path.IsPathRooted(jsonRoot))
|
||||
{
|
||||
string projectDir = Directory.GetParent(Application.dataPath).FullName;
|
||||
string repoDir = Directory.GetParent(projectDir)?.FullName ?? projectDir;
|
||||
jsonRoot = Path.Combine(repoDir, jsonRoot);
|
||||
}
|
||||
|
||||
string jsonRoot = ResolveCommandLineJsonRoot();
|
||||
IReadOnlyList<string> outputs = BuildAll(jsonRoot);
|
||||
if (outputs.Count == 0)
|
||||
{
|
||||
@@ -116,6 +127,24 @@ namespace XGame.EditorTools
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveCommandLineJsonRoot()
|
||||
{
|
||||
string jsonRoot = GetCommandLineValue("-uiJsonRoot");
|
||||
if (string.IsNullOrEmpty(jsonRoot))
|
||||
{
|
||||
return ResolveJsonRoot();
|
||||
}
|
||||
|
||||
if (Path.IsPathRooted(jsonRoot))
|
||||
{
|
||||
return jsonRoot;
|
||||
}
|
||||
|
||||
string projectDir = Directory.GetParent(Application.dataPath).FullName;
|
||||
string repoDir = Directory.GetParent(projectDir)?.FullName ?? projectDir;
|
||||
return Path.Combine(repoDir, jsonRoot);
|
||||
}
|
||||
|
||||
public static ValidationResult Validate(string jsonPath)
|
||||
{
|
||||
ValidationResult vr = new ValidationResult();
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using NUnit.Framework;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using XGame;
|
||||
using XGame.VirtualInput;
|
||||
|
||||
public sealed class LobbyWorldControllerVirtualJoystickTests
|
||||
{
|
||||
[Test]
|
||||
public void MapMoveInputForTest_UsesJoystickVectorWithCameraAxes()
|
||||
{
|
||||
Vector3 dir = LobbyWorldController.MapMoveInputForTest(
|
||||
new Vector2(1f, 0f),
|
||||
Vector3.forward,
|
||||
Vector3.right,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false);
|
||||
|
||||
Assert.That(dir, Is.EqualTo(Vector3.right));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MapMoveInputForTest_CombinesKeyboardAndJoystick()
|
||||
{
|
||||
Vector3 dir = LobbyWorldController.MapMoveInputForTest(
|
||||
new Vector2(0f, 1f),
|
||||
Vector3.forward,
|
||||
Vector3.right,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false);
|
||||
|
||||
Assert.That(dir, Is.EqualTo(Vector3.forward * 2f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReleaseJoystickUIForTest_ClearsVirtualJoystickReferences()
|
||||
{
|
||||
GameObject controllerObj = new GameObject("LobbyWorldControllerTest");
|
||||
GameObject joystickObj = new GameObject("UI_VirtualJoystick", typeof(RectTransform));
|
||||
try
|
||||
{
|
||||
LobbyWorldController controller = controllerObj.AddComponent<LobbyWorldController>();
|
||||
VirtualJoystick joystick = joystickObj.AddComponent<VirtualJoystick>();
|
||||
SetPrivateField(controller, "joystickRoot", joystickObj.GetComponent<RectTransform>());
|
||||
SetPrivateField(controller, "virtualJoystick", joystick);
|
||||
|
||||
InvokePrivate(controller, "ReleaseJoystickUI", false);
|
||||
|
||||
Assert.That(GetPrivateField(controller, "joystickRoot"), Is.Null);
|
||||
Assert.That(GetPrivateField(controller, "virtualJoystick"), Is.Null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(joystickObj);
|
||||
Object.DestroyImmediate(controllerObj);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetPrivateField(object target, string name, object value)
|
||||
{
|
||||
FieldInfo field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(field, Is.Not.Null);
|
||||
field.SetValue(target, value);
|
||||
}
|
||||
|
||||
private static object GetPrivateField(object target, string name)
|
||||
{
|
||||
FieldInfo field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(field, Is.Not.Null);
|
||||
return field.GetValue(target);
|
||||
}
|
||||
|
||||
private static void InvokePrivate(object target, string name, params object[] args)
|
||||
{
|
||||
MethodInfo method = target.GetType().GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(method, Is.Not.Null);
|
||||
method.Invoke(target, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2bd7fd1fc87460095c485b27dcc0e2c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,210 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using XGame.VirtualInput;
|
||||
|
||||
public sealed class VirtualActionButtonTests
|
||||
{
|
||||
private readonly List<GameObject> _roots = new List<GameObject>();
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
foreach (GameObject root in _roots)
|
||||
{
|
||||
if (root != null)
|
||||
{
|
||||
Object.DestroyImmediate(root);
|
||||
}
|
||||
}
|
||||
_roots.Clear();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PointerTap_EmitsDownThenTap()
|
||||
{
|
||||
VirtualActionButton button = CreateButton(ActionButtonId.Primary);
|
||||
List<ActionButtonEvent> events = Capture(button);
|
||||
|
||||
button.HandlePointerDownForTest(new Vector2(100f, 100f), 1, Time.unscaledTime);
|
||||
button.HandlePointerUpForTest(new Vector2(102f, 100f), 1, 0.1f);
|
||||
|
||||
Assert.That(events.Select(e => e.Phase), Is.EqualTo(new[] { ActionButtonPhase.Down, ActionButtonPhase.Tap }));
|
||||
Assert.That(events.Last().ButtonId, Is.EqualTo(ActionButtonId.Primary));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PointerDragPastThreshold_EmitsReleaseWithDirection()
|
||||
{
|
||||
VirtualActionButton button = CreateButton(ActionButtonId.Skill1);
|
||||
List<ActionButtonEvent> events = Capture(button);
|
||||
|
||||
button.HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f);
|
||||
button.HandleDragForTest(new Vector2(160f, 100f), 1, 0.1f);
|
||||
button.HandlePointerUpForTest(new Vector2(160f, 100f), 1, 0.2f);
|
||||
|
||||
ActionButtonEvent release = events.Last();
|
||||
Assert.That(release.Phase, Is.EqualTo(ActionButtonPhase.Release));
|
||||
Assert.That(release.Direction.x, Is.EqualTo(1f).Within(0.001f));
|
||||
Assert.That(release.Direction.y, Is.EqualTo(0f).Within(0.001f));
|
||||
Assert.That(release.Distance01, Is.GreaterThan(0.7f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PointerUpInsideCancelArea_EmitsCancel()
|
||||
{
|
||||
VirtualActionButton button = CreateButton(ActionButtonId.Skill2);
|
||||
RectTransform cancelArea = CreateRect("CancelArea", new Vector2(200f, 100f), new Vector2(80f, 80f));
|
||||
button.SetCancelArea(cancelArea);
|
||||
List<ActionButtonEvent> events = Capture(button);
|
||||
|
||||
button.HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f);
|
||||
button.HandleDragForTest(new Vector2(200f, 100f), 1, 0.1f);
|
||||
button.HandlePointerUpForTest(new Vector2(200f, 100f), 1, 0.2f);
|
||||
|
||||
Assert.That(events.Last().Phase, Is.EqualTo(ActionButtonPhase.Cancel));
|
||||
Assert.That(events.Last().IsCanceled, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Cooldown_EmitsBlockedOnly()
|
||||
{
|
||||
VirtualActionButton button = CreateButton(ActionButtonId.Skill3);
|
||||
List<ActionButtonEvent> events = Capture(button);
|
||||
|
||||
button.SetCooldown(3f, 5f);
|
||||
button.HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f);
|
||||
|
||||
Assert.That(events.Select(e => e.Phase), Is.EqualTo(new[] { ActionButtonPhase.Blocked }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ChargesZero_EmitsBlockedAndShowsChargeText()
|
||||
{
|
||||
VirtualActionButton button = CreateButton(ActionButtonId.Skill4, out _, out TMP_Text chargeText);
|
||||
List<ActionButtonEvent> events = Capture(button);
|
||||
|
||||
button.SetCharges(0);
|
||||
button.HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f);
|
||||
|
||||
Assert.That(events.Single().Phase, Is.EqualTo(ActionButtonPhase.Blocked));
|
||||
Assert.That(chargeText.text, Is.EqualTo("0"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BindNodes_FindsPrefabChildrenWithButtonPrefix()
|
||||
{
|
||||
VirtualActionButton button = CreateButton(ActionButtonId.Skill1, "Skill1", out _, out TMP_Text chargeText);
|
||||
|
||||
button.SetCharges(2);
|
||||
|
||||
Assert.That(chargeText.text, Is.EqualTo("2"));
|
||||
Assert.That(chargeText.gameObject.activeSelf, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Hold_EmitsHoldStartTickAndEnd()
|
||||
{
|
||||
VirtualActionButton button = CreateButton(ActionButtonId.Primary);
|
||||
List<ActionButtonEvent> events = Capture(button);
|
||||
|
||||
button.HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f);
|
||||
button.HandleDragForTest(new Vector2(102f, 100f), 1, 0.4f);
|
||||
button.HandleDragForTest(new Vector2(102f, 100f), 1, 0.51f);
|
||||
button.HandlePointerUpForTest(new Vector2(102f, 100f), 1, 0.6f);
|
||||
|
||||
Assert.That(events.Select(e => e.Phase), Does.Contain(ActionButtonPhase.HoldStart));
|
||||
Assert.That(events.Select(e => e.Phase), Does.Contain(ActionButtonPhase.HoldTick));
|
||||
Assert.That(events.Select(e => e.Phase), Does.Contain(ActionButtonPhase.HoldEnd));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Update_EmitsHoldWhilePointerIsStationary()
|
||||
{
|
||||
VirtualActionButton button = CreateButton(ActionButtonId.Primary);
|
||||
List<ActionButtonEvent> events = Capture(button);
|
||||
float startedAt = Time.unscaledTime - 0.4f;
|
||||
|
||||
button.HandlePointerDownForTest(new Vector2(100f, 100f), 1, startedAt);
|
||||
InvokePrivate(button, "Update");
|
||||
|
||||
Assert.That(events.Select(e => e.Phase), Does.Contain(ActionButtonPhase.HoldStart));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OnDisable_CancelsActivePointer()
|
||||
{
|
||||
VirtualActionButton button = CreateButton(ActionButtonId.Skill2);
|
||||
List<ActionButtonEvent> events = Capture(button);
|
||||
|
||||
button.HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f);
|
||||
InvokePrivate(button, "OnDisable");
|
||||
|
||||
Assert.That(events.Select(e => e.Phase), Is.EqualTo(new[] { ActionButtonPhase.Down, ActionButtonPhase.Cancel }));
|
||||
}
|
||||
|
||||
private VirtualActionButton CreateButton(ActionButtonId id)
|
||||
{
|
||||
return CreateButton(id, out _, out _);
|
||||
}
|
||||
|
||||
private VirtualActionButton CreateButton(ActionButtonId id, out Image cooldownMask, out TMP_Text chargeText)
|
||||
{
|
||||
return CreateButton(id, null, out cooldownMask, out chargeText);
|
||||
}
|
||||
|
||||
private VirtualActionButton CreateButton(ActionButtonId id, string childPrefix, out Image cooldownMask, out TMP_Text chargeText)
|
||||
{
|
||||
RectTransform rect = CreateRect("Btn_" + id, new Vector2(100f, 100f), new Vector2(100f, 100f));
|
||||
string prefix = string.IsNullOrEmpty(childPrefix) ? string.Empty : childPrefix + "_";
|
||||
new GameObject(prefix + "Icon", typeof(RectTransform), typeof(Image)).transform.SetParent(rect, false);
|
||||
cooldownMask = new GameObject(prefix + "CooldownMask", typeof(RectTransform), typeof(Image)).GetComponent<Image>();
|
||||
cooldownMask.transform.SetParent(rect, false);
|
||||
chargeText = new GameObject(prefix + "ChargeText", typeof(RectTransform), typeof(TextMeshProUGUI)).GetComponent<TMP_Text>();
|
||||
chargeText.transform.SetParent(rect, false);
|
||||
new GameObject(prefix + "CooldownText", typeof(RectTransform), typeof(TextMeshProUGUI)).transform.SetParent(rect, false);
|
||||
new GameObject(prefix + "AimIndicator", typeof(RectTransform)).transform.SetParent(rect, false);
|
||||
|
||||
VirtualActionButton button = rect.gameObject.AddComponent<VirtualActionButton>();
|
||||
button.ButtonId = id;
|
||||
SetPrivateField(button, "screenCenterOverride", new Vector2(100f, 100f));
|
||||
SetPrivateField(button, "screenRadiusOverride", 50f);
|
||||
InvokePrivate(button, "Awake");
|
||||
return button;
|
||||
}
|
||||
|
||||
private RectTransform CreateRect(string name, Vector2 position, Vector2 size)
|
||||
{
|
||||
GameObject go = new GameObject(name, typeof(RectTransform));
|
||||
_roots.Add(go);
|
||||
RectTransform rect = go.GetComponent<RectTransform>();
|
||||
rect.position = position;
|
||||
rect.sizeDelta = size;
|
||||
return rect;
|
||||
}
|
||||
|
||||
private static List<ActionButtonEvent> Capture(VirtualActionButton button)
|
||||
{
|
||||
var events = new List<ActionButtonEvent>();
|
||||
button.ButtonEvent += events.Add;
|
||||
return events;
|
||||
}
|
||||
|
||||
private static void SetPrivateField(object target, string name, object value)
|
||||
{
|
||||
FieldInfo field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(field, Is.Not.Null);
|
||||
field.SetValue(target, value);
|
||||
}
|
||||
|
||||
private static void InvokePrivate(object target, string name, params object[] args)
|
||||
{
|
||||
MethodInfo method = target.GetType().GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(method, Is.Not.Null);
|
||||
method.Invoke(target, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2742fc0e43284790950df7a4a8b5cc6e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,117 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using XGame.VirtualInput;
|
||||
|
||||
public sealed class VirtualActionPadTests
|
||||
{
|
||||
private GameObject _root;
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (_root != null)
|
||||
{
|
||||
Object.DestroyImmediate(_root);
|
||||
_root = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Awake_FindsFiveButtonsByNameAndAssignsIds()
|
||||
{
|
||||
VirtualActionPad pad = CreatePadWithButtons();
|
||||
|
||||
Assert.That(pad.GetButton(ActionButtonId.Primary), Is.Not.Null);
|
||||
Assert.That(pad.GetButton(ActionButtonId.Skill1), Is.Not.Null);
|
||||
Assert.That(pad.GetButton(ActionButtonId.Skill2), Is.Not.Null);
|
||||
Assert.That(pad.GetButton(ActionButtonId.Skill3), Is.Not.Null);
|
||||
Assert.That(pad.GetButton(ActionButtonId.Skill4), Is.Not.Null);
|
||||
Assert.That(pad.GetButton(ActionButtonId.Skill4).ButtonId, Is.EqualTo(ActionButtonId.Skill4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ChildButtonEvent_IsForwardedByPad()
|
||||
{
|
||||
VirtualActionPad pad = CreatePadWithButtons();
|
||||
var events = new List<ActionButtonEvent>();
|
||||
pad.ButtonEvent += events.Add;
|
||||
|
||||
pad.GetButton(ActionButtonId.Primary).HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f);
|
||||
|
||||
Assert.That(events.Count, Is.EqualTo(1));
|
||||
Assert.That(events[0].ButtonId, Is.EqualTo(ActionButtonId.Primary));
|
||||
Assert.That(events[0].Phase, Is.EqualTo(ActionButtonPhase.Down));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PadStateApi_ForwardsToChildButton()
|
||||
{
|
||||
VirtualActionPad pad = CreatePadWithButtons();
|
||||
var events = new List<ActionButtonEvent>();
|
||||
pad.ButtonEvent += events.Add;
|
||||
|
||||
pad.SetCharges(ActionButtonId.Skill1, 0);
|
||||
pad.GetButton(ActionButtonId.Skill1).HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f);
|
||||
|
||||
Assert.That(events.Count, Is.EqualTo(1));
|
||||
Assert.That(events[0].Phase, Is.EqualTo(ActionButtonPhase.Blocked));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetCancelAreaVisible_TogglesCancelArea()
|
||||
{
|
||||
VirtualActionPad pad = CreatePadWithButtons();
|
||||
Transform cancel = _root.transform.Find("CancelArea");
|
||||
Assert.That(cancel, Is.Not.Null);
|
||||
|
||||
pad.SetCancelAreaVisible(true);
|
||||
Assert.That(cancel.gameObject.activeSelf, Is.True);
|
||||
|
||||
pad.SetCancelAreaVisible(false);
|
||||
Assert.That(cancel.gameObject.activeSelf, Is.False);
|
||||
}
|
||||
|
||||
private VirtualActionPad CreatePadWithButtons()
|
||||
{
|
||||
_root = new GameObject("UI_VirtualActionPad", typeof(RectTransform));
|
||||
CreateButtonChild("Btn_Primary", ActionButtonId.Primary);
|
||||
CreateButtonChild("Btn_Skill1", ActionButtonId.Skill1);
|
||||
CreateButtonChild("Btn_Skill2", ActionButtonId.Skill2);
|
||||
CreateButtonChild("Btn_Skill3", ActionButtonId.Skill3);
|
||||
CreateButtonChild("Btn_Skill4", ActionButtonId.Skill4);
|
||||
GameObject cancel = new GameObject("CancelArea", typeof(RectTransform));
|
||||
cancel.transform.SetParent(_root.transform, false);
|
||||
cancel.SetActive(false);
|
||||
|
||||
VirtualActionPad pad = _root.AddComponent<VirtualActionPad>();
|
||||
InvokePrivate(pad, "Awake");
|
||||
return pad;
|
||||
}
|
||||
|
||||
private void CreateButtonChild(string name, ActionButtonId id)
|
||||
{
|
||||
GameObject child = new GameObject(name, typeof(RectTransform));
|
||||
child.transform.SetParent(_root.transform, false);
|
||||
VirtualActionButton button = child.AddComponent<VirtualActionButton>();
|
||||
button.ButtonId = id;
|
||||
SetPrivateField(button, "screenCenterOverride", new Vector2(100f, 100f));
|
||||
SetPrivateField(button, "screenRadiusOverride", 50f);
|
||||
InvokePrivate(button, "Awake");
|
||||
}
|
||||
|
||||
private static void SetPrivateField(object target, string name, object value)
|
||||
{
|
||||
FieldInfo field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(field, Is.Not.Null);
|
||||
field.SetValue(target, value);
|
||||
}
|
||||
|
||||
private static void InvokePrivate(object target, string name, params object[] args)
|
||||
{
|
||||
MethodInfo method = target.GetType().GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(method, Is.Not.Null);
|
||||
method.Invoke(target, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c66bc2b2fc0c435aa305004fd1d560e7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.IO;
|
||||
using NUnit.Framework;
|
||||
#if UNITY_EDITOR
|
||||
using XGame.EditorTools;
|
||||
#endif
|
||||
|
||||
public sealed class VirtualInputPrefabLayoutTests
|
||||
{
|
||||
[Test]
|
||||
public void VirtualJoystickJson_DefinesReusableJoystickPrefab()
|
||||
{
|
||||
string json = File.ReadAllText("../Doc/UIPrefabCreater/UI_VirtualJoystick.json");
|
||||
|
||||
Assert.That(json, Does.Contain("\"prefabName\": \"UI_VirtualJoystick\""));
|
||||
Assert.That(json, Does.Contain("\"prefabPath\": \"Assets/Game/Art/UI/Prefab/UI_VirtualJoystick.prefab\""));
|
||||
Assert.That(json, Does.Contain("\"name\": \"Knob\""));
|
||||
Assert.That(json, Does.Contain("\"typeName\": \"XGame.VirtualInput.VirtualJoystick\""));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VirtualActionPadJson_DefinesFiveButtonsAndCancelArea()
|
||||
{
|
||||
string json = File.ReadAllText("../Doc/UIPrefabCreater/UI_VirtualActionPad.json");
|
||||
|
||||
Assert.That(json, Does.Contain("\"prefabName\": \"UI_VirtualActionPad\""));
|
||||
Assert.That(json, Does.Contain("\"prefabPath\": \"Assets/Game/Art/UI/Prefab/UI_VirtualActionPad.prefab\""));
|
||||
Assert.That(json, Does.Contain("\"typeName\": \"XGame.VirtualInput.VirtualActionPad\""));
|
||||
Assert.That(json, Does.Contain("\"Btn_Primary\""));
|
||||
Assert.That(json, Does.Contain("\"Btn_Skill1\""));
|
||||
Assert.That(json, Does.Contain("\"Btn_Skill2\""));
|
||||
Assert.That(json, Does.Contain("\"Btn_Skill3\""));
|
||||
Assert.That(json, Does.Contain("\"Btn_Skill4\""));
|
||||
Assert.That(json, Does.Contain("\"CancelArea\""));
|
||||
Assert.That(json, Does.Contain("\"Primary_CooldownMask\""));
|
||||
Assert.That(json, Does.Contain("\"Skill1_ChargeText\""));
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[Test]
|
||||
public void VirtualInputJsonFiles_PassPrefabBuilderValidation()
|
||||
{
|
||||
Assert.That(JsonUIPrefabBuilder.Validate("../Doc/UIPrefabCreater/UI_VirtualJoystick.json").Ok, Is.True);
|
||||
Assert.That(JsonUIPrefabBuilder.Validate("../Doc/UIPrefabCreater/UI_VirtualActionPad.json").Ok, Is.True);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9cad9d7487d498986dca6b134d5a90b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using XGame.VirtualInput;
|
||||
|
||||
public sealed class VirtualJoystickTests
|
||||
{
|
||||
private GameObject _root;
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (_root != null)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(_root);
|
||||
_root = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ApplyPointerInsideDeadZone_ReportsInactiveAndZero()
|
||||
{
|
||||
VirtualJoystick joystick = CreateJoystick(out _);
|
||||
|
||||
ApplyPointer(joystick, new Vector2(106f, 100f));
|
||||
|
||||
Assert.That(joystick.IsActive, Is.False);
|
||||
Assert.That(joystick.Value, Is.EqualTo(Vector2.zero));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ApplyPointerBeyondRadius_ClampsValueAndKnobTravel()
|
||||
{
|
||||
VirtualJoystick joystick = CreateJoystick(out RectTransform knob);
|
||||
|
||||
ApplyPointer(joystick, new Vector2(300f, 100f));
|
||||
|
||||
Assert.That(joystick.IsActive, Is.True);
|
||||
Assert.That(joystick.Value.x, Is.EqualTo(1f).Within(0.001f));
|
||||
Assert.That(joystick.Value.y, Is.EqualTo(0f).Within(0.001f));
|
||||
Assert.That(knob.anchoredPosition.x, Is.EqualTo(24f).Within(0.001f));
|
||||
Assert.That(knob.anchoredPosition.y, Is.EqualTo(0f).Within(0.001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReleasePointer_ResetsValueAndRaisesEnded()
|
||||
{
|
||||
VirtualJoystick joystick = CreateJoystick(out RectTransform knob);
|
||||
int ended = 0;
|
||||
joystick.Ended += () => ended++;
|
||||
|
||||
ApplyPointer(joystick, new Vector2(300f, 100f));
|
||||
ReleasePointer(joystick);
|
||||
|
||||
Assert.That(joystick.IsActive, Is.False);
|
||||
Assert.That(joystick.Value, Is.EqualTo(Vector2.zero));
|
||||
Assert.That(knob.anchoredPosition, Is.EqualTo(Vector2.zero));
|
||||
Assert.That(ended, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsScreenPositionInAcquireArea_UsesAcquireRadiusMultiplier()
|
||||
{
|
||||
VirtualJoystick joystick = CreateJoystick(out _);
|
||||
|
||||
Assert.That(joystick.IsScreenPositionInAcquireArea(new Vector2(179f, 100f)), Is.True);
|
||||
Assert.That(joystick.IsScreenPositionInAcquireArea(new Vector2(181f, 100f)), Is.False);
|
||||
}
|
||||
|
||||
private VirtualJoystick CreateJoystick(out RectTransform knob)
|
||||
{
|
||||
_root = new GameObject("UI_VirtualJoystick", typeof(RectTransform));
|
||||
RectTransform rootRect = _root.GetComponent<RectTransform>();
|
||||
rootRect.sizeDelta = new Vector2(100f, 100f);
|
||||
|
||||
GameObject knobObject = new GameObject("Knob", typeof(RectTransform));
|
||||
knobObject.transform.SetParent(_root.transform, false);
|
||||
knob = knobObject.GetComponent<RectTransform>();
|
||||
knob.sizeDelta = new Vector2(52f, 52f);
|
||||
|
||||
VirtualJoystick joystick = _root.AddComponent<VirtualJoystick>();
|
||||
SetPrivateField(joystick, "screenCenterOverride", new Vector2(100f, 100f));
|
||||
SetPrivateField(joystick, "screenRadiusOverride", 50f);
|
||||
InvokePrivate(joystick, "Awake");
|
||||
return joystick;
|
||||
}
|
||||
|
||||
private static void ApplyPointer(VirtualJoystick joystick, Vector2 screenPosition)
|
||||
{
|
||||
InvokePrivate(joystick, "ApplyPointer", screenPosition);
|
||||
}
|
||||
|
||||
private static void ReleasePointer(VirtualJoystick joystick)
|
||||
{
|
||||
InvokePrivate(joystick, "ReleasePointer");
|
||||
}
|
||||
|
||||
private static void SetPrivateField(object target, string name, object value)
|
||||
{
|
||||
FieldInfo field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(field, Is.Not.Null);
|
||||
field.SetValue(target, value);
|
||||
}
|
||||
|
||||
private static void InvokePrivate(object target, string name, params object[] args)
|
||||
{
|
||||
MethodInfo method = target.GetType().GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.That(method, Is.Not.Null);
|
||||
method.Invoke(target, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 801fbb81f7954a04a5673c91a4d1a8a0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -5,6 +5,7 @@ using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.UI;
|
||||
using XGame.VirtualInput;
|
||||
using XWorld.Framework.Protocol;
|
||||
using UObject = UnityEngine.Object;
|
||||
|
||||
@@ -29,7 +30,7 @@ namespace XGame
|
||||
public bool IsPlaceholder; // Obj 当前是胶囊占位(真实角色预设还在异步加载),加载完成后会原地升级
|
||||
}
|
||||
|
||||
private const string JoystickPrefabPath = "Assets/Game/Art/UI/Prefab/UI_LobbyJoystick.prefab";
|
||||
private const string JoystickPrefabPath = "Assets/Game/Art/UI/Prefab/UI_VirtualJoystick.prefab";
|
||||
private const string CharacterConfigResourcePath = "Config/CharacterConfig";
|
||||
private const string SelectedCharacterKey = "CharacterSwitch.SelectedCharacterId";
|
||||
private const float MoveSpeed = 1f;
|
||||
@@ -39,7 +40,6 @@ namespace XGame
|
||||
private const float DirectionChangeMinInterval = 0.1f;
|
||||
private const float DirectionChangeDot = 0.98f;
|
||||
private const float JoystickRadius = 74f;
|
||||
private const float JoystickKnobRadius = 28f;
|
||||
private const float JoystickPadding = 100.0f;
|
||||
private const float JoystickDeadZone = 0.18f;
|
||||
private const float JoystickAcquireRadiusMultiplier = 1.6f;
|
||||
@@ -83,6 +83,7 @@ namespace XGame
|
||||
private float lastPinchDistance;
|
||||
private RectTransform joystickRoot;
|
||||
private RectTransform joystickWidget; // 视觉容器:新预设里的 "Joystick" 子节点(自带定位);找不到则回退为 joystickRoot
|
||||
private VirtualJoystick virtualJoystick;
|
||||
private RectTransform joystickKnob;
|
||||
private bool joystickMouseCaptured;
|
||||
private int joystickTouchFingerId = -1;
|
||||
@@ -151,6 +152,7 @@ namespace XGame
|
||||
{
|
||||
joystickRoot.gameObject.SetActive(false);
|
||||
}
|
||||
OnVirtualJoystickEnded();
|
||||
}
|
||||
|
||||
public void RefreshSelectedFigure()
|
||||
@@ -218,6 +220,11 @@ namespace XGame
|
||||
UpdateCamera();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
ReleaseJoystickUI(true);
|
||||
}
|
||||
|
||||
private void ApplyState(LobbyStateMsg state)
|
||||
{
|
||||
ReconcileSelfPlayerId(state);
|
||||
@@ -382,7 +389,6 @@ namespace XGame
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateJoystick();
|
||||
Vector3 dir = GetMoveDirection();
|
||||
bool moving = dir.sqrMagnitude > 0.001f;
|
||||
if (moving)
|
||||
@@ -418,17 +424,36 @@ namespace XGame
|
||||
Camera cam = Camera.main;
|
||||
Vector3 forward = cam != null ? cam.transform.forward : Vector3.forward;
|
||||
Vector3 right = cam != null ? cam.transform.right : Vector3.right;
|
||||
forward.y = 0f;
|
||||
right.y = 0f;
|
||||
forward = forward.sqrMagnitude > 0.001f ? forward.normalized : Vector3.forward;
|
||||
right = right.sqrMagnitude > 0.001f ? right.normalized : Vector3.right;
|
||||
return MapMoveInputForTest(
|
||||
joystickActive ? joystick : Vector2.zero,
|
||||
forward,
|
||||
right,
|
||||
Input.GetKey(KeyCode.W),
|
||||
Input.GetKey(KeyCode.S),
|
||||
Input.GetKey(KeyCode.A),
|
||||
Input.GetKey(KeyCode.D));
|
||||
}
|
||||
|
||||
public static Vector3 MapMoveInputForTest(
|
||||
Vector2 joystickInput,
|
||||
Vector3 cameraForward,
|
||||
Vector3 cameraRight,
|
||||
bool keyW,
|
||||
bool keyS,
|
||||
bool keyA,
|
||||
bool keyD)
|
||||
{
|
||||
cameraForward.y = 0f;
|
||||
cameraRight.y = 0f;
|
||||
Vector3 forward = cameraForward.sqrMagnitude > 0.001f ? cameraForward.normalized : Vector3.forward;
|
||||
Vector3 right = cameraRight.sqrMagnitude > 0.001f ? cameraRight.normalized : Vector3.right;
|
||||
|
||||
Vector3 dir = Vector3.zero;
|
||||
if (Input.GetKey(KeyCode.W)) dir += forward;
|
||||
if (Input.GetKey(KeyCode.S)) dir -= forward;
|
||||
if (Input.GetKey(KeyCode.A)) dir -= right;
|
||||
if (Input.GetKey(KeyCode.D)) dir += right;
|
||||
if (joystickActive) dir += right * joystick.x + forward * joystick.y;
|
||||
if (keyW) dir += forward;
|
||||
if (keyS) dir -= forward;
|
||||
if (keyA) dir -= right;
|
||||
if (keyD) dir += right;
|
||||
if (joystickInput.sqrMagnitude > 0.001f) dir += right * joystickInput.x + forward * joystickInput.y;
|
||||
return dir;
|
||||
}
|
||||
|
||||
@@ -917,7 +942,7 @@ namespace XGame
|
||||
rootObj = Instantiate(prefabObj);
|
||||
UIManager.AttachTo(UILayer.HUD, rootObj);
|
||||
|
||||
rootObj.name = "UI_LobbyJoystick";
|
||||
rootObj.name = "UI_VirtualJoystick";
|
||||
joystickRoot = rootObj.GetComponent<RectTransform>();
|
||||
if (joystickRoot == null)
|
||||
{
|
||||
@@ -926,6 +951,16 @@ namespace XGame
|
||||
joystickRoot.localScale = Vector3.one; // 兜底:设备包 prefab 根缩放异常时仍可见
|
||||
|
||||
// 视觉容器:新预设里是 "Joystick" 子节点(预设自带定位);找不到则用根并由 ApplyJoystickLayout 代码定位
|
||||
virtualJoystick = rootObj.GetComponent<VirtualJoystick>();
|
||||
if (virtualJoystick == null)
|
||||
{
|
||||
virtualJoystick = rootObj.AddComponent<VirtualJoystick>();
|
||||
}
|
||||
virtualJoystick.Changed -= OnVirtualJoystickChanged;
|
||||
virtualJoystick.Changed += OnVirtualJoystickChanged;
|
||||
virtualJoystick.Ended -= OnVirtualJoystickEnded;
|
||||
virtualJoystick.Ended += OnVirtualJoystickEnded;
|
||||
|
||||
Transform widget = joystickRoot.FindTransform("Joystick");
|
||||
joystickWidget = (widget as RectTransform) ?? joystickRoot;
|
||||
if (joystickWidget == joystickRoot)
|
||||
@@ -939,9 +974,7 @@ namespace XGame
|
||||
if (joystickKnob == null)
|
||||
{
|
||||
Debug.LogError("[LobbyWorldController] joystick prefab missing Knob node: " + JoystickPrefabPath);
|
||||
Destroy(rootObj);
|
||||
joystickRoot = null;
|
||||
joystickWidget = null;
|
||||
ReleaseJoystickUI(true);
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -949,6 +982,59 @@ namespace XGame
|
||||
UpdateJoystickVisual();
|
||||
}
|
||||
|
||||
private void ReleaseJoystickUI(bool destroyObject)
|
||||
{
|
||||
if (virtualJoystick != null)
|
||||
{
|
||||
virtualJoystick.Changed -= OnVirtualJoystickChanged;
|
||||
virtualJoystick.Ended -= OnVirtualJoystickEnded;
|
||||
}
|
||||
|
||||
GameObject rootObj = joystickRoot != null ? joystickRoot.gameObject : null;
|
||||
virtualJoystick = null;
|
||||
joystickRoot = null;
|
||||
joystickWidget = null;
|
||||
joystickKnob = null;
|
||||
joystickMouseCaptured = false;
|
||||
joystickTouchFingerId = -1;
|
||||
joystickLoading = false;
|
||||
OnVirtualJoystickEnded();
|
||||
|
||||
if (destroyObject && rootObj != null)
|
||||
{
|
||||
DestroyUnityObject(rootObj);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DestroyUnityObject(UObject obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
Destroy(obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
DestroyImmediate(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnVirtualJoystickChanged(Vector2 value, bool active)
|
||||
{
|
||||
joystick = active ? value : Vector2.zero;
|
||||
joystickActive = active;
|
||||
}
|
||||
|
||||
private void OnVirtualJoystickEnded()
|
||||
{
|
||||
joystick = Vector2.zero;
|
||||
joystickActive = false;
|
||||
}
|
||||
|
||||
private Transform GetHudParent()
|
||||
{
|
||||
RectTransform hud = UIManager.GetLayer(UILayer.HUD);
|
||||
@@ -1229,9 +1315,7 @@ namespace XGame
|
||||
|
||||
private bool IsInJoystickArea(Vector2 screenPosition)
|
||||
{
|
||||
Vector2 center = GetJoystickScreenCenter();
|
||||
float radius = GetJoystickScreenRadius();
|
||||
return Vector2.Distance(screenPosition, center) <= radius * JoystickAcquireRadiusMultiplier;
|
||||
return virtualJoystick != null && virtualJoystick.IsScreenPositionInAcquireArea(screenPosition);
|
||||
}
|
||||
|
||||
private bool EnsureCharacterConfig()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d025722ff61490e967ec8bb7e4d68dd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,498 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace XGame.VirtualInput
|
||||
{
|
||||
public enum ActionButtonId
|
||||
{
|
||||
Primary,
|
||||
Skill1,
|
||||
Skill2,
|
||||
Skill3,
|
||||
Skill4
|
||||
}
|
||||
|
||||
public enum ActionButtonPhase
|
||||
{
|
||||
Down,
|
||||
Drag,
|
||||
HoldStart,
|
||||
HoldTick,
|
||||
HoldEnd,
|
||||
Tap,
|
||||
Release,
|
||||
Cancel,
|
||||
Blocked
|
||||
}
|
||||
|
||||
public readonly struct ActionButtonEvent
|
||||
{
|
||||
public ActionButtonEvent(
|
||||
ActionButtonId buttonId,
|
||||
ActionButtonPhase phase,
|
||||
Vector2 screenPosition,
|
||||
Vector2 localVector,
|
||||
Vector2 direction,
|
||||
float distance01,
|
||||
float heldSeconds,
|
||||
bool isCanceled,
|
||||
int pointerId)
|
||||
{
|
||||
ButtonId = buttonId;
|
||||
Phase = phase;
|
||||
ScreenPosition = screenPosition;
|
||||
LocalVector = localVector;
|
||||
Direction = direction;
|
||||
Distance01 = distance01;
|
||||
HeldSeconds = heldSeconds;
|
||||
IsCanceled = isCanceled;
|
||||
PointerId = pointerId;
|
||||
}
|
||||
|
||||
public ActionButtonId ButtonId { get; }
|
||||
public ActionButtonPhase Phase { get; }
|
||||
public Vector2 ScreenPosition { get; }
|
||||
public Vector2 LocalVector { get; }
|
||||
public Vector2 Direction { get; }
|
||||
public float Distance01 { get; }
|
||||
public float HeldSeconds { get; }
|
||||
public bool IsCanceled { get; }
|
||||
public int PointerId { get; }
|
||||
}
|
||||
|
||||
public sealed class VirtualActionButton : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
|
||||
{
|
||||
[SerializeField] public ActionButtonId ButtonId;
|
||||
[SerializeField] private float holdThresholdSeconds = 0.35f;
|
||||
[SerializeField] private float holdTickIntervalSeconds = 0.1f;
|
||||
[SerializeField] private float releaseDistanceThreshold01 = 0.25f;
|
||||
[SerializeField] private float maxDragRadiusMultiplier = 1.5f;
|
||||
|
||||
private Image icon;
|
||||
private Image cooldownMask;
|
||||
private TMP_Text cooldownText;
|
||||
private TMP_Text chargeText;
|
||||
private GameObject aimIndicator;
|
||||
private RectTransform cancelArea;
|
||||
private RectTransform rectTransform;
|
||||
private Button unityButton;
|
||||
private CanvasGroup canvasGroup;
|
||||
|
||||
private bool enabledByState = true;
|
||||
private int charges = -1;
|
||||
private float cooldownRemaining;
|
||||
private float cooldownDuration;
|
||||
private bool pointerDown;
|
||||
private int activePointerId = int.MinValue;
|
||||
private float pointerDownTime;
|
||||
private bool holdStarted;
|
||||
private float nextHoldTickTime;
|
||||
private Vector2 lastScreenPosition;
|
||||
private ActionButtonEvent lastDragEvent;
|
||||
|
||||
private Vector2? screenCenterOverride;
|
||||
private float? screenRadiusOverride;
|
||||
|
||||
public bool IsInteractable => enabledByState && cooldownRemaining <= 0f && charges != 0;
|
||||
public event Action<ActionButtonEvent> ButtonEvent;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
BindNodes();
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
CancelActivePointer(Time.unscaledTime);
|
||||
SetAimVisible(false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (cooldownRemaining > 0f)
|
||||
{
|
||||
cooldownRemaining = Mathf.Max(0f, cooldownRemaining - Time.unscaledDeltaTime);
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
if (pointerDown)
|
||||
{
|
||||
MaybeEmitHold(Time.unscaledTime, activePointerId, lastScreenPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData)
|
||||
{
|
||||
HandlePointerDown(eventData.position, eventData.pointerId, Time.unscaledTime);
|
||||
}
|
||||
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
HandleDrag(eventData.position, eventData.pointerId, Time.unscaledTime);
|
||||
}
|
||||
|
||||
public void OnPointerUp(PointerEventData eventData)
|
||||
{
|
||||
HandlePointerUp(eventData.position, eventData.pointerId, Time.unscaledTime);
|
||||
}
|
||||
|
||||
public void HandlePointerDownForTest(Vector2 screenPosition, int pointerId, float now)
|
||||
{
|
||||
HandlePointerDown(screenPosition, pointerId, now);
|
||||
}
|
||||
|
||||
public void HandleDragForTest(Vector2 screenPosition, int pointerId, float now)
|
||||
{
|
||||
HandleDrag(screenPosition, pointerId, now);
|
||||
}
|
||||
|
||||
public void HandlePointerUpForTest(Vector2 screenPosition, int pointerId, float now)
|
||||
{
|
||||
HandlePointerUp(screenPosition, pointerId, now);
|
||||
}
|
||||
|
||||
public void SetCooldown(float remainingSeconds, float durationSeconds)
|
||||
{
|
||||
cooldownRemaining = Mathf.Max(0f, remainingSeconds);
|
||||
cooldownDuration = Mathf.Max(0f, durationSeconds);
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
public void SetCharges(int count)
|
||||
{
|
||||
charges = count;
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
public void SetButtonEnabled(bool enabled)
|
||||
{
|
||||
enabledByState = enabled;
|
||||
RefreshVisuals();
|
||||
}
|
||||
|
||||
public void SetCancelArea(RectTransform area)
|
||||
{
|
||||
cancelArea = area;
|
||||
}
|
||||
|
||||
public void SetIcon(Sprite sprite)
|
||||
{
|
||||
if (icon == null)
|
||||
{
|
||||
BindNodes();
|
||||
}
|
||||
|
||||
if (icon != null)
|
||||
{
|
||||
icon.sprite = sprite;
|
||||
icon.enabled = sprite != null;
|
||||
}
|
||||
}
|
||||
|
||||
private void BindNodes()
|
||||
{
|
||||
rectTransform = transform as RectTransform;
|
||||
unityButton = GetComponent<Button>();
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
icon = FindImage("Icon");
|
||||
cooldownMask = FindImage("CooldownMask");
|
||||
cooldownText = FindText("CooldownText");
|
||||
chargeText = FindText("ChargeText");
|
||||
|
||||
Transform aim = FindRoleTransform("AimIndicator");
|
||||
if (aim == null)
|
||||
{
|
||||
aim = FindRoleTransform("DragArrow");
|
||||
}
|
||||
aimIndicator = aim != null ? aim.gameObject : null;
|
||||
SetAimVisible(false);
|
||||
}
|
||||
|
||||
private Image FindImage(string childName)
|
||||
{
|
||||
Transform child = FindRoleTransform(childName);
|
||||
return child != null ? child.GetComponent<Image>() : null;
|
||||
}
|
||||
|
||||
private TMP_Text FindText(string childName)
|
||||
{
|
||||
Transform child = FindRoleTransform(childName);
|
||||
return child != null ? child.GetComponent<TMP_Text>() : null;
|
||||
}
|
||||
|
||||
private Transform FindRoleTransform(string roleName)
|
||||
{
|
||||
Transform child = transform.FindTransform(roleName);
|
||||
if (child != null)
|
||||
{
|
||||
return child;
|
||||
}
|
||||
|
||||
string suffix = "_" + roleName;
|
||||
return FindDescendantEndingWith(transform, suffix);
|
||||
}
|
||||
|
||||
private static Transform FindDescendantEndingWith(Transform root, string suffix)
|
||||
{
|
||||
if (root == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < root.childCount; i++)
|
||||
{
|
||||
Transform child = root.GetChild(i);
|
||||
if (child.name.EndsWith(suffix, StringComparison.Ordinal))
|
||||
{
|
||||
return child;
|
||||
}
|
||||
|
||||
Transform nested = FindDescendantEndingWith(child, suffix);
|
||||
if (nested != null)
|
||||
{
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void HandlePointerDown(Vector2 screenPosition, int pointerId, float now)
|
||||
{
|
||||
if (pointerDown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsInteractable)
|
||||
{
|
||||
Emit(MakeEvent(ActionButtonPhase.Blocked, screenPosition, pointerId, now));
|
||||
return;
|
||||
}
|
||||
|
||||
pointerDown = true;
|
||||
activePointerId = pointerId;
|
||||
pointerDownTime = now;
|
||||
nextHoldTickTime = now + holdThresholdSeconds + holdTickIntervalSeconds;
|
||||
holdStarted = false;
|
||||
lastScreenPosition = screenPosition;
|
||||
lastDragEvent = MakeEvent(ActionButtonPhase.Down, screenPosition, pointerId, now);
|
||||
SetAimVisible(false);
|
||||
Emit(lastDragEvent);
|
||||
}
|
||||
|
||||
private void HandleDrag(Vector2 screenPosition, int pointerId, float now)
|
||||
{
|
||||
if (!pointerDown || pointerId != activePointerId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lastScreenPosition = screenPosition;
|
||||
MaybeEmitHold(now, pointerId, screenPosition);
|
||||
lastDragEvent = MakeEvent(ActionButtonPhase.Drag, screenPosition, pointerId, now);
|
||||
SetAimVisible(true);
|
||||
Emit(lastDragEvent);
|
||||
}
|
||||
|
||||
private void HandlePointerUp(Vector2 screenPosition, int pointerId, float now)
|
||||
{
|
||||
if (!pointerDown || pointerId != activePointerId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MaybeEmitHold(now, pointerId, screenPosition);
|
||||
if (holdStarted)
|
||||
{
|
||||
Emit(MakeEvent(ActionButtonPhase.HoldEnd, screenPosition, pointerId, now));
|
||||
}
|
||||
|
||||
ActionButtonEvent finalEvent = MakeEvent(GetReleasePhase(screenPosition), screenPosition, pointerId, now);
|
||||
Emit(finalEvent);
|
||||
ResetPointerState();
|
||||
SetAimVisible(false);
|
||||
}
|
||||
|
||||
private void MaybeEmitHold(float now, int pointerId, Vector2 screenPosition)
|
||||
{
|
||||
if (!holdStarted && now - pointerDownTime >= holdThresholdSeconds)
|
||||
{
|
||||
holdStarted = true;
|
||||
Emit(MakeEvent(ActionButtonPhase.HoldStart, screenPosition, pointerId, now));
|
||||
}
|
||||
|
||||
if (!holdStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (now >= nextHoldTickTime)
|
||||
{
|
||||
Emit(MakeEvent(ActionButtonPhase.HoldTick, screenPosition, pointerId, nextHoldTickTime));
|
||||
nextHoldTickTime += holdTickIntervalSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
private ActionButtonPhase GetReleasePhase(Vector2 screenPosition)
|
||||
{
|
||||
if (IsInCancelArea(screenPosition))
|
||||
{
|
||||
return ActionButtonPhase.Cancel;
|
||||
}
|
||||
|
||||
return MakeEvent(ActionButtonPhase.Release, screenPosition, activePointerId, Time.unscaledTime).Distance01 >= releaseDistanceThreshold01
|
||||
? ActionButtonPhase.Release
|
||||
: ActionButtonPhase.Tap;
|
||||
}
|
||||
|
||||
private ActionButtonEvent MakeEvent(ActionButtonPhase phase, Vector2 screenPosition, int pointerId, float now)
|
||||
{
|
||||
Vector2 localVector = screenPosition - GetScreenCenter();
|
||||
float radius = GetScreenRadius() * maxDragRadiusMultiplier;
|
||||
float distance01 = Mathf.Clamp01(localVector.magnitude / Mathf.Max(1f, radius));
|
||||
Vector2 direction = localVector.sqrMagnitude > 0.000001f ? localVector.normalized : Vector2.zero;
|
||||
bool canceled = phase == ActionButtonPhase.Cancel || IsInCancelArea(screenPosition);
|
||||
return new ActionButtonEvent(
|
||||
ButtonId,
|
||||
phase,
|
||||
screenPosition,
|
||||
localVector,
|
||||
direction,
|
||||
distance01,
|
||||
Mathf.Max(0f, now - pointerDownTime),
|
||||
canceled,
|
||||
pointerId);
|
||||
}
|
||||
|
||||
private bool IsInCancelArea(Vector2 screenPosition)
|
||||
{
|
||||
if (cancelArea == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return RectTransformUtility.RectangleContainsScreenPoint(cancelArea, screenPosition, GetUICamera(cancelArea));
|
||||
}
|
||||
|
||||
private void RefreshVisuals()
|
||||
{
|
||||
bool interactable = IsInteractable;
|
||||
if (unityButton != null)
|
||||
{
|
||||
unityButton.interactable = interactable;
|
||||
}
|
||||
|
||||
if (canvasGroup != null)
|
||||
{
|
||||
canvasGroup.alpha = interactable ? 1f : 0.55f;
|
||||
canvasGroup.interactable = interactable;
|
||||
}
|
||||
|
||||
if (cooldownMask != null)
|
||||
{
|
||||
cooldownMask.fillAmount = cooldownDuration > 0f ? Mathf.Clamp01(cooldownRemaining / cooldownDuration) : 0f;
|
||||
cooldownMask.gameObject.SetActive(cooldownRemaining > 0f);
|
||||
}
|
||||
|
||||
if (cooldownText != null)
|
||||
{
|
||||
cooldownText.text = cooldownRemaining > 0f ? Mathf.CeilToInt(cooldownRemaining).ToString() : string.Empty;
|
||||
cooldownText.gameObject.SetActive(cooldownRemaining > 0f);
|
||||
}
|
||||
|
||||
if (chargeText != null)
|
||||
{
|
||||
chargeText.text = charges >= 0 ? charges.ToString() : string.Empty;
|
||||
chargeText.gameObject.SetActive(charges >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAimVisible(bool visible)
|
||||
{
|
||||
if (aimIndicator != null)
|
||||
{
|
||||
aimIndicator.SetActive(visible);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetPointerState()
|
||||
{
|
||||
pointerDown = false;
|
||||
activePointerId = int.MinValue;
|
||||
holdStarted = false;
|
||||
lastScreenPosition = Vector2.zero;
|
||||
lastDragEvent = default;
|
||||
}
|
||||
|
||||
private void CancelActivePointer(float now)
|
||||
{
|
||||
if (!pointerDown)
|
||||
{
|
||||
ResetPointerState();
|
||||
return;
|
||||
}
|
||||
|
||||
MaybeEmitHold(now, activePointerId, lastScreenPosition);
|
||||
if (holdStarted)
|
||||
{
|
||||
Emit(MakeEvent(ActionButtonPhase.HoldEnd, lastScreenPosition, activePointerId, now));
|
||||
}
|
||||
|
||||
Emit(MakeEvent(ActionButtonPhase.Cancel, lastScreenPosition, activePointerId, now));
|
||||
ResetPointerState();
|
||||
}
|
||||
|
||||
private void Emit(ActionButtonEvent evt)
|
||||
{
|
||||
ButtonEvent?.Invoke(evt);
|
||||
}
|
||||
|
||||
private Vector2 GetScreenCenter()
|
||||
{
|
||||
if (screenCenterOverride.HasValue)
|
||||
{
|
||||
return screenCenterOverride.Value;
|
||||
}
|
||||
|
||||
RectTransform rect = rectTransform != null ? rectTransform : transform as RectTransform;
|
||||
return rect != null ? RectTransformUtility.WorldToScreenPoint(GetUICamera(rect), rect.position) : Vector2.zero;
|
||||
}
|
||||
|
||||
private float GetScreenRadius()
|
||||
{
|
||||
if (screenRadiusOverride.HasValue)
|
||||
{
|
||||
return Mathf.Max(1f, screenRadiusOverride.Value);
|
||||
}
|
||||
|
||||
RectTransform rect = rectTransform != null ? rectTransform : transform as RectTransform;
|
||||
if (rect == null)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
Vector3[] corners = new Vector3[4];
|
||||
rect.GetWorldCorners(corners);
|
||||
Camera cam = GetUICamera(rect);
|
||||
Vector2 s0 = RectTransformUtility.WorldToScreenPoint(cam, corners[0]);
|
||||
Vector2 s2 = RectTransformUtility.WorldToScreenPoint(cam, corners[2]);
|
||||
return Mathf.Max(1f, Mathf.Min(Mathf.Abs(s2.x - s0.x), Mathf.Abs(s2.y - s0.y)) * 0.5f);
|
||||
}
|
||||
|
||||
private Camera GetUICamera(RectTransform rect)
|
||||
{
|
||||
Canvas canvas = rect != null ? rect.GetComponentInParent<Canvas>() : null;
|
||||
if (canvas == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
canvas = canvas.rootCanvas;
|
||||
return canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e719656ace242789c22b78b47abcc66
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XGame.VirtualInput
|
||||
{
|
||||
public sealed class VirtualActionPad : MonoBehaviour
|
||||
{
|
||||
private readonly Dictionary<ActionButtonId, VirtualActionButton> buttons =
|
||||
new Dictionary<ActionButtonId, VirtualActionButton>();
|
||||
|
||||
private RectTransform cancelArea;
|
||||
|
||||
public event Action<ActionButtonEvent> ButtonEvent;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
BindNodes();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach (VirtualActionButton button in buttons.Values)
|
||||
{
|
||||
if (button != null)
|
||||
{
|
||||
button.ButtonEvent -= OnChildButtonEvent;
|
||||
}
|
||||
}
|
||||
buttons.Clear();
|
||||
}
|
||||
|
||||
public VirtualActionButton GetButton(ActionButtonId id)
|
||||
{
|
||||
buttons.TryGetValue(id, out VirtualActionButton button);
|
||||
return button;
|
||||
}
|
||||
|
||||
public void SetCooldown(ActionButtonId id, float remainingSeconds, float durationSeconds)
|
||||
{
|
||||
VirtualActionButton button = GetButton(id);
|
||||
if (button != null)
|
||||
{
|
||||
button.SetCooldown(remainingSeconds, durationSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCharges(ActionButtonId id, int count)
|
||||
{
|
||||
VirtualActionButton button = GetButton(id);
|
||||
if (button != null)
|
||||
{
|
||||
button.SetCharges(count);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetButtonEnabled(ActionButtonId id, bool enabled)
|
||||
{
|
||||
VirtualActionButton button = GetButton(id);
|
||||
if (button != null)
|
||||
{
|
||||
button.SetButtonEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCancelAreaVisible(bool visible)
|
||||
{
|
||||
if (cancelArea != null)
|
||||
{
|
||||
cancelArea.gameObject.SetActive(visible);
|
||||
}
|
||||
}
|
||||
|
||||
private void BindNodes()
|
||||
{
|
||||
Transform cancel = transform.FindTransform("CancelArea");
|
||||
cancelArea = cancel as RectTransform;
|
||||
RegisterButton(ActionButtonId.Primary, "Btn_Primary");
|
||||
RegisterButton(ActionButtonId.Skill1, "Btn_Skill1");
|
||||
RegisterButton(ActionButtonId.Skill2, "Btn_Skill2");
|
||||
RegisterButton(ActionButtonId.Skill3, "Btn_Skill3");
|
||||
RegisterButton(ActionButtonId.Skill4, "Btn_Skill4");
|
||||
SetCancelAreaVisible(false);
|
||||
}
|
||||
|
||||
private void RegisterButton(ActionButtonId id, string childName)
|
||||
{
|
||||
Transform child = transform.FindTransform(childName);
|
||||
if (child == null)
|
||||
{
|
||||
Debug.LogWarning("[VirtualActionPad] missing child button: " + childName, this);
|
||||
return;
|
||||
}
|
||||
|
||||
VirtualActionButton button = child.GetComponent<VirtualActionButton>();
|
||||
if (button == null)
|
||||
{
|
||||
button = child.gameObject.AddComponent<VirtualActionButton>();
|
||||
}
|
||||
|
||||
button.ButtonId = id;
|
||||
button.SetCancelArea(cancelArea);
|
||||
button.ButtonEvent -= OnChildButtonEvent;
|
||||
button.ButtonEvent += OnChildButtonEvent;
|
||||
buttons[id] = button;
|
||||
}
|
||||
|
||||
private void OnChildButtonEvent(ActionButtonEvent evt)
|
||||
{
|
||||
if (evt.Phase == ActionButtonPhase.Drag || evt.Phase == ActionButtonPhase.HoldStart)
|
||||
{
|
||||
SetCancelAreaVisible(cancelArea != null);
|
||||
}
|
||||
else if (evt.Phase == ActionButtonPhase.Release || evt.Phase == ActionButtonPhase.Tap || evt.Phase == ActionButtonPhase.Cancel || evt.Phase == ActionButtonPhase.Blocked)
|
||||
{
|
||||
SetCancelAreaVisible(false);
|
||||
}
|
||||
|
||||
ButtonEvent?.Invoke(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c5500d410634275b54631c131475f3d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,273 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace XGame.VirtualInput
|
||||
{
|
||||
public sealed class VirtualJoystick : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private RectTransform joystickRoot;
|
||||
[SerializeField] private RectTransform knob;
|
||||
[SerializeField] private float deadZone = 0.18f;
|
||||
[SerializeField] private float acquireRadiusMultiplier = 1.6f;
|
||||
|
||||
private Vector2 value;
|
||||
private bool isActive;
|
||||
private bool isCaptured;
|
||||
private bool sentStarted;
|
||||
private int touchFingerId = -1;
|
||||
|
||||
private Vector2? screenCenterOverride;
|
||||
private float? screenRadiusOverride;
|
||||
|
||||
public Vector2 Value => value;
|
||||
public bool IsActive => isActive;
|
||||
|
||||
public event Action Started;
|
||||
public event Action<Vector2, bool> Changed;
|
||||
public event Action Ended;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
BindNodes();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
ReleasePointer();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isActiveAndEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (UnityEngine.Input.touchCount > 0)
|
||||
{
|
||||
UpdateTouchInput();
|
||||
return;
|
||||
}
|
||||
|
||||
touchFingerId = -1;
|
||||
UpdateMouseInput();
|
||||
}
|
||||
|
||||
public bool IsScreenPositionInAcquireArea(Vector2 screenPosition)
|
||||
{
|
||||
return Vector2.Distance(screenPosition, GetScreenCenter()) <= GetScreenRadius() * acquireRadiusMultiplier;
|
||||
}
|
||||
|
||||
private void BindNodes()
|
||||
{
|
||||
if (joystickRoot == null)
|
||||
{
|
||||
Transform widget = transform.Find("Joystick");
|
||||
joystickRoot = widget as RectTransform;
|
||||
}
|
||||
if (joystickRoot == null)
|
||||
{
|
||||
joystickRoot = transform as RectTransform;
|
||||
}
|
||||
|
||||
if (knob == null)
|
||||
{
|
||||
Transform knobTransform = transform.FindTransform("Knob");
|
||||
knob = knobTransform as RectTransform;
|
||||
}
|
||||
|
||||
if (knob == null)
|
||||
{
|
||||
Debug.LogError("[VirtualJoystick] missing Knob node: " + name, this);
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMouseInput()
|
||||
{
|
||||
if (UnityEngine.Input.GetMouseButtonDown(0))
|
||||
{
|
||||
isCaptured = IsScreenPositionInAcquireArea(UnityEngine.Input.mousePosition);
|
||||
}
|
||||
|
||||
if (UnityEngine.Input.GetMouseButtonUp(0))
|
||||
{
|
||||
isCaptured = false;
|
||||
ReleasePointer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCaptured && UnityEngine.Input.GetMouseButton(0))
|
||||
{
|
||||
ApplyPointer(UnityEngine.Input.mousePosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReleasePointer();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTouchInput()
|
||||
{
|
||||
if (touchFingerId >= 0)
|
||||
{
|
||||
for (int i = 0; i < UnityEngine.Input.touchCount; i++)
|
||||
{
|
||||
Touch touch = UnityEngine.Input.GetTouch(i);
|
||||
if (touch.fingerId != touchFingerId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
|
||||
{
|
||||
touchFingerId = -1;
|
||||
ReleasePointer();
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyPointer(touch.position);
|
||||
return;
|
||||
}
|
||||
|
||||
touchFingerId = -1;
|
||||
ReleasePointer();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < UnityEngine.Input.touchCount; i++)
|
||||
{
|
||||
Touch touch = UnityEngine.Input.GetTouch(i);
|
||||
if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsScreenPositionInAcquireArea(touch.position))
|
||||
{
|
||||
touchFingerId = touch.fingerId;
|
||||
ApplyPointer(touch.position);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ReleasePointer();
|
||||
}
|
||||
|
||||
private void ApplyPointer(Vector2 screenPosition)
|
||||
{
|
||||
if (!sentStarted)
|
||||
{
|
||||
sentStarted = true;
|
||||
Started?.Invoke();
|
||||
}
|
||||
|
||||
Vector2 raw = (screenPosition - GetScreenCenter()) / GetScreenRadius();
|
||||
Vector2 nextValue = Vector2.ClampMagnitude(raw, 1f);
|
||||
bool nextActive = nextValue.magnitude >= deadZone;
|
||||
if (!nextActive)
|
||||
{
|
||||
nextValue = Vector2.zero;
|
||||
}
|
||||
|
||||
bool changed = (nextValue - value).sqrMagnitude > 0.000001f || nextActive != isActive;
|
||||
value = nextValue;
|
||||
isActive = nextActive;
|
||||
UpdateKnob();
|
||||
|
||||
if (changed)
|
||||
{
|
||||
Changed?.Invoke(value, isActive);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleasePointer()
|
||||
{
|
||||
bool hadState = sentStarted || isActive || value.sqrMagnitude > 0.000001f;
|
||||
sentStarted = false;
|
||||
isCaptured = false;
|
||||
touchFingerId = -1;
|
||||
value = Vector2.zero;
|
||||
isActive = false;
|
||||
UpdateKnob();
|
||||
|
||||
if (hadState)
|
||||
{
|
||||
Changed?.Invoke(value, isActive);
|
||||
Ended?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateKnob()
|
||||
{
|
||||
if (knob != null)
|
||||
{
|
||||
knob.anchoredPosition = value * GetKnobTravelRadius();
|
||||
}
|
||||
}
|
||||
|
||||
private float GetKnobTravelRadius()
|
||||
{
|
||||
RectTransform root = joystickRoot != null ? joystickRoot : transform as RectTransform;
|
||||
if (root == null)
|
||||
{
|
||||
return GetScreenRadius();
|
||||
}
|
||||
|
||||
float rootHalf = Mathf.Min(root.rect.width, root.rect.height) * 0.5f;
|
||||
float knobHalf = knob != null ? Mathf.Min(knob.rect.width, knob.rect.height) * 0.5f : 0f;
|
||||
return Mathf.Max(1f, rootHalf - knobHalf);
|
||||
}
|
||||
|
||||
private Vector2 GetScreenCenter()
|
||||
{
|
||||
if (screenCenterOverride.HasValue)
|
||||
{
|
||||
return screenCenterOverride.Value;
|
||||
}
|
||||
|
||||
RectTransform root = joystickRoot != null ? joystickRoot : transform as RectTransform;
|
||||
if (root == null)
|
||||
{
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
return RectTransformUtility.WorldToScreenPoint(GetUICamera(), root.position);
|
||||
}
|
||||
|
||||
private float GetScreenRadius()
|
||||
{
|
||||
if (screenRadiusOverride.HasValue)
|
||||
{
|
||||
return Mathf.Max(1f, screenRadiusOverride.Value);
|
||||
}
|
||||
|
||||
RectTransform root = joystickRoot != null ? joystickRoot : transform as RectTransform;
|
||||
if (root == null)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
Camera cam = GetUICamera();
|
||||
Vector3[] corners = new Vector3[4];
|
||||
root.GetWorldCorners(corners);
|
||||
Vector2 s0 = RectTransformUtility.WorldToScreenPoint(cam, corners[0]);
|
||||
Vector2 s2 = RectTransformUtility.WorldToScreenPoint(cam, corners[2]);
|
||||
return Mathf.Max(1f, Mathf.Min(Mathf.Abs(s2.x - s0.x), Mathf.Abs(s2.y - s0.y)) * 0.5f);
|
||||
}
|
||||
|
||||
private Camera GetUICamera()
|
||||
{
|
||||
RectTransform root = joystickRoot != null ? joystickRoot : transform as RectTransform;
|
||||
Canvas canvas = root != null ? root.GetComponentInParent<Canvas>() : null;
|
||||
if (canvas == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
canvas = canvas.rootCanvas;
|
||||
return canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 014f26c5eef24321b81f131f41a05a1d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user