[+] SinglePlayer Only Main Mode / Mouse touch improve (#21)

* [+]ExteraMouseInput[+]SinglePlayer Only Main Mode

* Format, reword config comment, fix default config

* Reword and remove extra commented code

* to SinglePlayer

* radius

---------

Co-authored-by: Menci <mencici@msn.com>
Co-authored-by: Clansty <i@gao4.pw>
This commit is contained in:
シャト 2025-01-30 01:15:55 +09:00 committed by GitHub
parent 13bb386a68
commit ef399b75d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 340 additions and 13 deletions

View File

@ -0,0 +1,317 @@
using AquaMai.Config.Attributes;
using HarmonyLib;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace AquaMai.Mods.GameSystem;
public partial class SinglePlayer
{
[ConfigEntry(
en: "Touch area radius size. Adjust according to the size of your finger, you can test in Test Mode.",
zh: "触摸区域半径大小。请根据手指大小调整,可以去 Test 中测试")]
public readonly static float radius = 25;
[ConfigEntry(
en: "Display touch points (only for touch input).",
zh: "显示触摸点(仅限触屏输入)")]
public readonly static bool displayArea = false;
static RectTransform rectTransform;
static Canvas leftCanvas;
static List<CustomCircleGraphic> circleGraphics = new List<CustomCircleGraphic>();
[HarmonyPostfix]
[HarmonyPatch(typeof(MouseTouchPanel), "Start")]
public static void MouseTouchPanelStart(MouseTouchPanel __instance)
{
if (!__instance.transform.parent.parent.name.Equals("LeftMonitor"))
{
return;
}
MelonLoader.MelonLogger.Msg("ExteraMouseInput: Start");
leftCanvas = __instance.transform.parent.GetComponent<Canvas>();
UnityEngine.Object.Destroy(__instance.transform.parent.GetComponent<GraphicRaycaster>());
MeshButtonRaycaster radiusRaycaster = __instance.transform.parent.gameObject.AddComponent<MeshButtonRaycaster>();
rectTransform = __instance.GetComponent<RectTransform>();
if (displayArea)
{
__instance.StartCoroutine(UpdateInputDisplay());
circleGraphics = new List<CustomCircleGraphic>(10);
for (int i = 0; i < 10; i++)
{
GameObject go = new GameObject("CircleGraphic" + i);
go.transform.SetParent(rectTransform);
go.transform.localPosition = Vector3.zero;
go.transform.localRotation = Quaternion.identity;
go.transform.localScale = Vector3.one;
CustomCircleGraphic circleGraphic = go.AddComponent<CustomCircleGraphic>();
circleGraphic.color = new Color(1, 1, 1, 0.5f);
circleGraphic.raycastTarget = false;
go.gameObject.SetActive(false);
circleGraphics.Add(circleGraphic);
}
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(MeshButton), "Awake")]
public static void MeshButtonAwake(MeshButton __instance, ref Vector2[] ___vertexArray)
{
RectTransform mouseTouchPanelRect = __instance.transform.parent.GetComponent<MouseTouchPanel>().GetComponent<RectTransform>();
CustomGraphic customGraphic = __instance.targetGraphic as CustomGraphic;
___vertexArray = new Vector2[customGraphic.vertex.Count];
for (int i = 0; i < customGraphic.vertex.Count; i++)
{
var localPos3 = new Vector3(customGraphic.vertex[i].x, customGraphic.vertex[i].y, 0f);
var rotatedPos3 = __instance.transform.localRotation * localPos3;//ApplyRot
var scaledPos3 = Vector3.Scale(rotatedPos3, __instance.transform.localScale); // ApplyScale
var finalPos3 = __instance.transform.position + scaledPos3;//ApplyPos
___vertexArray[i] = RectTransformUtility.WorldToScreenPoint(
Camera.main,
finalPos3);
RectTransformUtility.ScreenPointToLocalPointInRectangle(
mouseTouchPanelRect,
___vertexArray[i],
Camera.main,
out ___vertexArray[i]
);
}
}
//显示输入范围
static IEnumerator UpdateInputDisplay()
{
while (true)
{
for (int i = 0; i < circleGraphics.Count; i++)
{
if (Input.touchCount > i)
{
Vector2 touchPoint = Input.GetTouch(i).position;
Vector2 localPoint;
// 将屏幕点转换为本地空间
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, touchPoint, Camera.main, out localPoint);
circleGraphics[i].rectTransform.anchoredPosition = localPoint;
// 缩放半径
circleGraphics[i].radius = radius;
circleGraphics[i].gameObject.SetActive(true);
}
else
{
circleGraphics[i].gameObject.SetActive(false);
}
}
yield return null;
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(MeshButton), "IsPointInPolygon", new Type[] { typeof(Vector2[]), typeof(Vector2) })]
public static bool IsPointInPolygon(MeshButton __instance, Vector2[] polygon, Vector2 point, ref bool __result)
{
RectTransformUtility.ScreenPointToLocalPointInRectangle(
rectTransform,
point,
Camera.main,
out var localInputPoint
);
bool isInsidePolygon = false;
//检查是否在多边形顶点内
isInsidePolygon = IsVertDistance(polygon, localInputPoint, radius);
//检查是否在多边形边上
if (!isInsidePolygon)
{
isInsidePolygon = IsCircleIntersectingPolygonEdges(polygon, localInputPoint, radius);
}
// 检查是否在多边形内部
if (!isInsidePolygon)
{
isInsidePolygon = InPointInInternal(polygon, localInputPoint);
}
//if (isInsidePolygon)
//{
// foreach (var p in polygon)
// {
// var testo = new GameObject("A");
// testo.transform.SetParent(rectTransform);
// testo.transform.localPosition = p;
// testo.transform.localScale = Vector3.one * 0.2f;
// testo.AddComponent<RectTransform>();
// testo.AddComponent<Image>();
// }
//}
__result = isInsidePolygon;
return false;
}
#region
/// <summary>
/// 检查点是否在多边形的顶点内
/// </summary>
/// <returns></returns>
private static bool InPointInInternal(Vector2[] polygon, Vector2 localInputPoint)
{
bool isInsidePolygon = false;
int num = polygon.Length;
float x = localInputPoint.x;
float y = localInputPoint.y;
Vector2 prevVertex = polygon[num - 1];
float prevX = prevVertex.x;
float prevY = prevVertex.y;
for (int i = 0; i < polygon.Length; i++)
{
Vector2 currentVertex = polygon[i];
float currentX = currentVertex.x;
float currentY = currentVertex.y;
// 判断点是否在边的左右交替
if ((currentY > y ^ prevY > y) && (x < (prevX - currentX) * (y - currentY) / (prevY - currentY) + currentX))
{
isInsidePolygon = !isInsidePolygon;
}
prevX = currentX;
prevY = currentY;
}
return isInsidePolygon;
}
/// <summary>
/// 检查顶点是否在触摸点的范围
/// </summary>
/// <param name="polygon"></param>
/// <param name="circleCenter"></param>
/// <param name="radius"></param>
/// <returns></returns>
private static bool IsVertDistance(Vector2[] polygon, Vector2 circleCenter, float radius)
{
for (int i = 0; i < polygon.Length; i++)
{
Vector2 currentVertex = polygon[i];
float currentX = currentVertex.x;
float currentY = currentVertex.y;
if (Vector2.Distance(circleCenter, currentVertex) < radius)
{
return true;
}
}
return false;
}
/// <summary>
/// 检查圆是否与多边形的边相交
/// </summary>
/// <param name="polygon"></param>
/// <param name="circleCenter"></param>
/// <param name="radius"></param>
/// <returns></returns>
private static bool IsCircleIntersectingPolygonEdges(Vector2[] polygon, Vector2 circleCenter, float radius)
{
int vertexCount = polygon.Length;
for (int i = 0; i < vertexCount; i++)
{
Vector2 a = polygon[i];
Vector2 b = polygon[(i + 1) % vertexCount];
// 检查圆心到边的最短距离是否小于等于半径
if (DistanceFromPointToSegment(circleCenter, a, b) <= radius)
{
return true;
}
}
return false;
}
// 计算点到线段的最短距离
private static float DistanceFromPointToSegment(Vector2 point, Vector2 segmentStart, Vector2 segmentEnd)
{
Vector2 segment = segmentEnd - segmentStart;
float segmentLengthSquared = segment.sqrMagnitude;
if (segmentLengthSquared == 0f)
{
return Vector2.Distance(point, segmentStart); // 退化为一个点
}
float t = Mathf.Clamp(Vector2.Dot(point - segmentStart, segment) / segmentLengthSquared, 0f, 1f);
Vector2 projection = segmentStart + t * segment;
return Vector2.Distance(point, projection);
}
#endregion
#region
public class CustomCircleGraphic : Graphic
{
public float radius = 50f; // 圆的半径
public int segmentCount = 64; // 圆的细分段数
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
List<UIVertex> vertices = new List<UIVertex>();
List<int> indices = new List<int>();
// 添加圆心点
UIVertex centerVertex = UIVertex.simpleVert;
centerVertex.position = Vector3.zero; // 圆心
centerVertex.color = this.color;
vertices.Add(centerVertex);
// 添加圆周点
for (int i = 0; i <= segmentCount; i++) // 多加一个点,形成闭合圆
{
float angle = Mathf.Deg2Rad * (i * 360f / segmentCount);
float x = Mathf.Cos(angle) * radius;
float y = Mathf.Sin(angle) * radius;
UIVertex vertex = UIVertex.simpleVert;
vertex.position = new Vector3(x, y, 0f);
vertex.color = this.color;
vertices.Add(vertex);
if (i > 0)
{
// 形成三角形的索引
indices.Add(0); // 圆心
indices.Add(i); // 当前点
indices.Add(i - 1); // 上一个点
}
}
vh.AddUIVertexStream(vertices, indices);
}
}
public class MeshButtonRaycaster : BaseRaycaster
{
public override Camera eventCamera => Camera.main;
MeshButton[] meshButtons = new MeshButton[0];
protected override void Start()
{
meshButtons = transform.GetComponentsInChildren<MeshButton>(true);
}
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
foreach (var item in meshButtons)
{
// 检查按钮是否实现了 ICanvasRaycastFilter并调用过滤逻辑
var raycastFilter = item as ICanvasRaycastFilter;
if (!raycastFilter.IsRaycastLocationValid(eventData.position, eventCamera))
{
continue; // 如果过滤不通过,跳过这个按钮
}
}
}
}
#endregion
}

View File

@ -14,20 +14,32 @@ using Monitor.Entry;
using Monitor.Entry.Parts.Screens;
using UnityEngine;
using Fx;
using Type = System.Type;
namespace AquaMai.Mods.GameSystem;
// Hides the 2p (right hand side) UI.
// Note: this is not my original work. I simply interpreted the code and rewrote it as a mod.
[ConfigSection(
en: "Single player: Show 1P only, at the center of the screen.",
zh: "单人模式,不显示 2P")]
public class SinglePlayer
en: """
Single player: Show 1P only, at the center of the screen.
Enable radius for mouse input for a more realistic touchscreen experience.
""",
zh: """
2P
""")]
public partial class SinglePlayer
{
[ConfigEntry(
en: "Only show the main area, without the sub-monitor.",
zh: "只显示主区域,不显示副屏")]
public static bool HideSubMonitor = false;
[HarmonyPatch]
public class WhateverInitialize
{
public static IEnumerable<MethodBase> TargetMethods()
{
var lateInitialize = AccessTools.Method(typeof(Main.GameMain), "LateInitialize", [typeof(MonoBehaviour), typeof(Transform), typeof(Transform)]);
@ -38,19 +50,17 @@ public class SinglePlayer
public static void Prefix(MonoBehaviour gameMainObject, ref Transform left, ref Transform right)
{
Vector3 position = Camera.main.gameObject.transform.position;
Camera.main.gameObject.transform.position = new Vector3(position.x - 540f, position.y, position.z);
var yOffset = 0f;
if (HideSubMonitor)
{
yOffset = -420f;
Camera.main.orthographicSize = 540f;
}
Camera.main.gameObject.transform.position = new Vector3(position.x - 540f, position.y + yOffset, position.z);
right.localScale = Vector3.zero;
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(MeshButton), "IsPointInPolygon", new Type[] { typeof(Vector2[]), typeof(Vector2) })]
public static bool IsPointInPolygon(Vector2[] polygon, ref Vector2 point, MeshButton __instance, ref bool __result)
{
__result = RectTransformUtility.RectangleContainsScreenPoint(__instance.GetComponent<RectTransform>(), point, Camera.main);
return false;
}
[ConfigEntry(
en: "Automatically skip the countdown when logging in with a card in single-player mode.",
zh: "单人模式下刷卡登录直接进入下一个界面,无需跳过倒计时")]