This commit is contained in:
CortexCore
2025-02-24 23:02:43 +08:00
parent 41715e4413
commit 8261a458e2
105 changed files with 2934 additions and 696 deletions

View File

@@ -8,7 +8,8 @@
"GUID:14fe60d984bf9f84eac55c6ea033a8f4",
"GUID:d525ad6bd40672747bde77962f1c401e",
"GUID:49b49c76ee64f6b41bf28ef951cb0e50",
"GUID:517785bb4600a5140b47eac5fa49b8fc"
"GUID:517785bb4600a5140b47eac5fa49b8fc",
"GUID:d8b63aba1907145bea998dd612889d6b"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@@ -0,0 +1,115 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace BITKit.UX
{
public class OnScreenButton : OnScreenControl
{
public new class UxmlTraits : VisualElement.UxmlTraits
{
private readonly UxmlFloatAttributeDescription m_PressedValueAttribute = new ()
{
name = "PressedValue",
defaultValue = 1f,
};
private readonly UxmlBoolAttributeDescription m_ReleasePressAttribute = new()
{
name = "ReleasePress"
};
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var table = (OnScreenButton)ve;
table.PressedValue = m_PressedValueAttribute.GetValueFromBag(bag, cc);
table.ReleasePress = m_ReleasePressAttribute.GetValueFromBag(bag, cc);
}
}
public new class UxmlFactory : UxmlFactory<OnScreenButton, UxmlTraits> { }
public float PressedValue { get; set; }= 1f;
public bool ReleasePress { get; set; }
private bool _isPressed;
private int _pointerId=-1;
private Label _label;
private readonly ValidHandle _isTriggered = new();
public OnScreenButton()
{
RegisterCallback<PointerUpEvent>(OnPointerUp);
RegisterCallback<PointerDownEvent>(OnPointerDown);
RegisterCallback<PointerMoveEvent>(OnPointerMove);
this.AddManipulator(new Clickable(OnClick));
_label = this.Create<Label>();
_isTriggered.AddListener(x =>
{
if (x)
{
AddToClassList("selected");
}
else
{
RemoveFromClassList("selected");
}
var value = x?PressedValue : 0f;
SendValueToControl( value);
});
}
private void OnClick()
{
}
private void OnPointerMove(PointerMoveEvent evt)
{
if(_pointerId!=evt.pointerId)return;
if (ReleasePress)
{
_isPressed = true;
return;
}
if (_isPressed is false)
{
_isTriggered.AddElement(0);
_isPressed = true;
;
}
}
private void OnPointerDown(PointerDownEvent evt)
{
if(_pointerId is not -1)return;
_pointerId = evt.pointerId;
_isPressed = true;
_isTriggered.AddElement(0);
}
private void OnPointerUp(PointerUpEvent evt)
{
if(_pointerId!=evt.pointerId)return;
if (ReleasePress && _isPressed)
{
_isTriggered.AddElement(0);
_pointerId = -1;
return;
}
_pointerId = -1;
_isTriggered.RemoveElement(0);
}
protected override string ControlPathInternal { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8afc05714f63ce542a2e2ead78933966
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,330 @@
using System;
using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.UIElements;
namespace BITKit.UX
{
public abstract class OnScreenControl:VisualElement
{
public new class UxmlTraits : VisualElement.UxmlTraits
{
private readonly UxmlStringAttributeDescription m_ControlPathAttribute = new ()
{
name = "ControlPath"
};
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var table = (OnScreenControl)ve;
table.ControlPath = m_ControlPathAttribute.GetValueFromBag(bag, cc);
}
}
protected OnScreenControl()
{
RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
IsEnabled.AddDisableElements(this);
IsEnabled.AddListener(x =>
{
if (x)
OnEnable();
else OnDisable();
});
}
private void OnDetachFromPanel(DetachFromPanelEvent evt)
{
IsEnabled.AddDisableElements(32);
}
private void OnAttachToPanel(AttachToPanelEvent evt)
{
IsEnabled.RemoveDisableElements(32);
}
private void OnGeometryChanged(GeometryChangedEvent evt)
{
IsEnabled.SetDisableElements(this, !visible);
}
protected readonly ValidHandle IsEnabled = new();
/// <summary>
/// The control path (see <see cref="InputControlPath"/>) for the control that the on-screen
/// control will feed input into.
/// </summary>
/// <remarks>
/// A device will be created from the device layout referenced by the control path (see
/// <see cref="InputControlPath.TryGetDeviceLayout"/>). The path is then used to look up
/// <see cref="Control"/> on the device. The resulting control will be fed values from
/// the on-screen control.
///
/// Multiple on-screen controls sharing the same device layout will together create a single
/// virtual device. If, for example, one component uses <c>"&lt;Gamepad&gt;/buttonSouth"</c>
/// and another uses <c>"&lt;Gamepad&gt;/leftStick"</c> as the control path, a single
/// <see cref="Gamepad"/> will be created and the first component will feed data to
/// <see cref="Gamepad.buttonSouth"/> and the second component will feed data to
/// <see cref="Gamepad.leftStick"/>.
/// </remarks>
/// <seealso cref="InputControlPath"/>
public string ControlPath
{
get => ControlPathInternal;
set
{
IsEnabled.SetElements(1, string.IsNullOrEmpty(value) is false);
ControlPathInternal = value;
IsEnabled.AddDisableElements("Force");
IsEnabled.RemoveDisableElements("Force");
}
}
/// <summary>
/// The actual control that is fed input from the on-screen control.
/// </summary>
/// <remarks>
/// This is only valid while the on-screen control is enabled. Otherwise, it is <c>null</c>. Also,
/// if no <see cref="ControlPath"/> has been set, this will remain <c>null</c> even if the component is enabled.
/// </remarks>
public InputControl Control => _mControl;
private InputControl _mControl;
private OnScreenControl _mNextControlOnDevice;
private InputEventPtr _mInputEventPtr;
/// <summary>
/// Accessor for the <see cref="ControlPath"/> of the component. Must be implemented by subclasses.
/// </summary>
/// <remarks>
/// Moving the definition of how the control path is stored into subclasses allows them to
/// apply their own <see cref="InputControlAttribute"/> attributes to them and thus set their
/// own layout filters.
/// </remarks>
protected abstract string ControlPathInternal { get; set; }
private void SetupInputControl()
{
Debug.Assert(_mControl == null, "InputControl already initialized");
Debug.Assert(_mNextControlOnDevice == null, "Previous InputControl has not been properly uninitialized (m_NextControlOnDevice still set)");
Debug.Assert(!_mInputEventPtr.valid, "Previous InputControl has not been properly uninitialized (m_InputEventPtr still set)");
// Nothing to do if we don't have a control path.
var path = ControlPathInternal;
if (string.IsNullOrEmpty(path))
return;
// Determine what type of device to work with.
var layoutName = InputControlPath.TryGetDeviceLayout(path);
if (layoutName == null)
{
Debug.LogError(
$"Cannot determine device layout to use based on control path '{path}' used in {GetType().Name} component with {name}");
return;
}
// Try to find existing on-screen device that matches.
var internedLayoutName = new InternedString(layoutName);
var deviceInfoIndex = -1;
for (var i = 0; i < OnScreenDevices.Count; ++i)
{
////FIXME: this does not take things such as different device usages into account
if (OnScreenDevices[i].Device.layout != internedLayoutName) continue;
deviceInfoIndex = i;
break;
}
// If we don't have a matching one, create a new one.
InputDevice device;
if (deviceInfoIndex == -1)
{
// Try to create device.
try
{
device = InputSystem.AddDevice(layoutName);
}
catch (Exception exception)
{
Debug.LogError(
$"Could not create device with layout '{layoutName}' used in '{GetType().Name}' component");
Debug.LogException(exception);
return;
}
InputSystem.AddDeviceUsage(device, "OnScreen");
// Create event buffer.
var buffer = StateEvent.From(device, out var eventPtr, Allocator.Persistent);
// Add to list.
deviceInfoIndex = OnScreenDevices.Count;
OnScreenDevices.Add(new OnScreenDeviceInfo
{
EventPtr = eventPtr,
Buffer = buffer,
Device = device,
});
}
else
{
device = OnScreenDevices[deviceInfoIndex].Device;
}
// Try to find control on device.
_mControl = InputControlPath.TryFindControl(device, path);
if (_mControl == null)
{
Debug.LogError(
$"Cannot find control with path '{path}' on device of type '{layoutName}' referenced by component '{GetType().Name}' with {name}");
// Remove the device, if we just created one.
if (OnScreenDevices[deviceInfoIndex].FirstControl == null)
{
OnScreenDevices[deviceInfoIndex].Destroy();
OnScreenDevices.RemoveAt(deviceInfoIndex);
}
return;
}
_mInputEventPtr = OnScreenDevices[deviceInfoIndex].EventPtr;
// We have all we need. Permanently add us.
OnScreenDevices[deviceInfoIndex] =
OnScreenDevices[deviceInfoIndex].AddControl(this);
}
protected void SendValueToControl<TValue>(TValue value)
where TValue : struct
{
if (_mControl == null)
return;
if (!(_mControl is InputControl<TValue> control))
throw new ArgumentException(
$"The control path {ControlPath} yields a control of type {_mControl.GetType().Name} which is not an InputControl with value type {typeof(TValue).Name}", nameof(value));
////FIXME: this gives us a one-frame lag (use InputState.Change instead?)
_mInputEventPtr.time = InputState.currentTime;
control.WriteValueIntoEvent(value, _mInputEventPtr);
InputSystem.QueueEvent(_mInputEventPtr);
}
protected void SentDefaultValueToControl()
{
if (_mControl == null)
return;
////FIXME: this gives us a one-frame lag (use InputState.Change instead?)
_mInputEventPtr.time = InputState.currentTime;
_mControl.ResetToDefaultStateInEvent(_mInputEventPtr);
InputSystem.QueueEvent(_mInputEventPtr);
}
protected virtual void OnEnable()
{
SetupInputControl();
}
protected virtual void OnDisable()
{
if (_mControl == null)
return;
var device = _mControl.device;
for (var i = 0; i < OnScreenDevices.Count; ++i)
{
if (OnScreenDevices[i].Device != device)
continue;
var deviceInfo = OnScreenDevices[i].RemoveControl(this);
if (deviceInfo.FirstControl == null)
{
// We're the last on-screen control on this device. Remove the device.
OnScreenDevices[i].Destroy();
OnScreenDevices.RemoveAt(i);
}
else
{
OnScreenDevices[i] = deviceInfo;
// We're keeping the device , but we're disabling the on-screen representation
// for one of its controls. If the control isn't in default state, reset it
// to that now. This is what ensures that if, for example, OnScreenButton is
// disabled after OnPointerDown, we reset its button control to zero even
// though we will not see an OnPointerUp.
if (!_mControl.CheckStateIsAtDefault())
SentDefaultValueToControl();
}
_mControl = null;
_mInputEventPtr = new InputEventPtr();
Debug.Assert(_mNextControlOnDevice == null);
break;
}
}
private struct OnScreenDeviceInfo
{
public InputEventPtr EventPtr;
public NativeArray<byte> Buffer;
public InputDevice Device;
public OnScreenControl FirstControl;
public OnScreenDeviceInfo AddControl(OnScreenControl control)
{
control._mNextControlOnDevice = FirstControl;
FirstControl = control;
return this;
}
public OnScreenDeviceInfo RemoveControl(OnScreenControl control)
{
if (FirstControl == control)
FirstControl = control._mNextControlOnDevice;
else
{
for (OnScreenControl current = FirstControl._mNextControlOnDevice, previous = FirstControl;
current != null; previous = current, current = current._mNextControlOnDevice)
{
if (current != control)
continue;
previous._mNextControlOnDevice = current._mNextControlOnDevice;
break;
}
}
control._mNextControlOnDevice = null;
return this;
}
public void Destroy()
{
if (Buffer.IsCreated)
Buffer.Dispose();
if (Device != null)
InputSystem.RemoveDevice(Device);
Device = null;
Buffer = new NativeArray<byte>();
}
}
private static readonly List<OnScreenDeviceInfo> OnScreenDevices=new ();
internal string GetWarningMessage()
{
return $"{GetType()} needs to be attached as a child to a UI Canvas and have a RectTransform component to function properly.";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d6822f32ddff3b4f9fd962009912a81
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,92 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.UIElements;
namespace BITKit.UX
{
public class OnScreenGamepad : OnScreenControl
{
public new class UxmlFactory : UxmlFactory<OnScreenGamepad, UxmlTraits> { }
protected override string ControlPathInternal { get; set; }
private readonly Dictionary<Vector2Int, VisualElement> _buttonMap = new();
public OnScreenGamepad()
{
IsEnabled.AddListener(OnActive);
for (var y = 1; y >=-1; y--)
{
var row = this.Create<VisualElement>();
row.style.flexGrow = 1;
row.style.flexDirection = FlexDirection.Row;
for (var x = -1; x <= 1; x++)
{
var dir = (x, y) switch
{
(-1, 1) => "NW", // 西北
(0, 1) => "N", // 北
(1, 1) => "NE", // 东北
(-1, 0) => "W", // 西
(0, 0) => "C", // 中心
(1, 0) => "E", // 东
(-1, -1) => "SW", // 西南
(0, -1) => "S", // 南
(1, -1) => "SE", // 东南
_ => "NULL" // 其他无效位置
};
var value = (x, y) switch
{
(-1, -1) => new float2(-0.707f, -0.707f), // NW 西北
(0, -1) =>new float2 (0f, -1f), // N 北
(1, -1) =>new float2 (0.707f, -0.707f), // NE 东北
(-1, 0) => new float2(-1f, 0f), // W 西
(0, 0) =>new float2 (0f, 0f), // C 中心
(1, 0) =>new float2 (1f, 0f), // E 东
(-1, 1) => new float2(-0.707f, 0.707f), // SW 西南
(0, 1) =>new float2 (0f, 1f), // S 南
(1, 1) => new float2(0.707f, 0.707f), // SE 东南
_ => new float2(0f, 0f) // 默认返回 (0, 0) 无效位置
};
var button = row.Create<VisualElement>();
_buttonMap.TryAdd(new Vector2Int(x, y), button);
button.style.flexGrow = 1;
button.AddToClassList("gamepad-button");
button.AddToClassList($"gamepad-button--{dir.ToLower()}");
button.AddManipulator(new Clickable(()=>{}));
button.RegisterCallback<PointerOverEvent>(_ =>
{
SendValueToControl((Vector2)value);
});
button.RegisterCallback<PointerUpEvent>(_ =>
{
SendValueToControl((Vector2)default);
});
// var label = button.Create<Label>();
// label.style.flexGrow = 1;
//
// label.text = $"{value.x},{value.y}";
}
}
}
private void OnActive(bool obj)
{
if (obj is false)
{
SendValueToControl((Vector2)default);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4bf58213910164e4d8cab38b29830a7b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,80 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace BITKit.UX
{
public class OnScreenStick:OnScreenControl
{
public new class UxmlTraits : OnScreenControl.UxmlTraits
{
private readonly UxmlBoolAttributeDescription m_IsDelteaAttribute = new ()
{
name = "IsDelta"
};
private readonly UxmlFloatAttributeDescription m_MoveRangeAttribute = new()
{
name = "MoveRange",defaultValue = 32,
};
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var table = (OnScreenStick)ve;
table.IsDelta = m_IsDelteaAttribute.GetValueFromBag(bag, cc);
table.MoveRange=m_MoveRangeAttribute.GetValueFromBag(bag, cc);
}
}
public new class UxmlFactory : UxmlFactory<OnScreenStick, UxmlTraits> { }
public bool IsDelta { get; set; }
public float MoveRange { get; set; } = 32;
protected override string ControlPathInternal { get; set; }
private int _ignoreFrame=1;
private Vector2 _startPosition;
public OnScreenStick()
{
RegisterCallback<PointerDownEvent>(OnPointerDown);
RegisterCallback<PointerMoveEvent>(OnPointerMove);
RegisterCallback<PointerUpEvent>(OnPointerUp);
}
private void OnPointerUp(PointerUpEvent evt)
{
SendValueToControl(Vector2.zero);
_startPosition = evt.position;
_ignoreFrame = 1;
}
private void OnPointerDown(PointerDownEvent evt)
{
_ignoreFrame = 1;
_startPosition = evt.position;
}
private void OnPointerMove(PointerMoveEvent evt)
{
if (_ignoreFrame-- > 0)
{
return;
}
if (Vector2.Distance(evt.deltaPosition, default) > Vector2.Distance(_startPosition, evt.position))
{
return;
}
var pos = evt.deltaPosition;
if (IsDelta)
{
var newPos = evt.position;
pos = new Vector2(newPos.x, newPos.y) - _startPosition;
pos /= MoveRange;
}
SendValueToControl(new Vector2(pos.x, -pos.y));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e068da31183a7244198cbd23b00ebdbf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -11,9 +11,9 @@ namespace BITKit.UX
{
public UXInputAction(VisualElement visualElement,string controlPathInternal)
{
this.controlPathInternal = controlPathInternal;
this.ControlPathInternal = controlPathInternal;
}
protected sealed override string controlPathInternal { get; set; }
protected sealed override string ControlPathInternal { get; set; }
}
}