加入了浮动小游戏功能和设置界面

运行时请手动修改运行库目录fdBrowser
This commit is contained in:
FloatGaming
2025-12-24 06:02:06 +08:00
parent 1ab80e504e
commit eb9b6ba78a
1131 changed files with 148096 additions and 313 deletions
+24
View File
@@ -0,0 +1,24 @@
using System.Diagnostics;
using UnityEngine;
public class LaunchH5 : MonoBehaviour
{
public RectTransform panel; // 绑定 Panel
public string hostPath = "WebView2Host/WebView2Host.exe"; // Host.exe 相对路径
void Start()
{
string h5Path = $"file:///{UnityEngine.Application.streamingAssetsPath}/H5/index.html";
Vector3[] corners = new Vector3[4];
panel.GetWorldCorners(corners);
int x = Mathf.RoundToInt(corners[0].x);
int y = Mathf.RoundToInt(Screen.height - corners[1].y); // Unity 左下原点 -> Win32 左上原点
int width = Mathf.RoundToInt(panel.rect.width);
int height = Mathf.RoundToInt(panel.rect.height);
ProcessStartInfo psi = new ProcessStartInfo(hostPath);
psi.Arguments = $"{x} {y} {width} {height} \"{h5Path}\"";
Process.Start(psi);
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 26d60afd151e3004fb6b19bce29ecb85
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 526806832437a7f44954fa4a2b79718c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+301
View File
@@ -0,0 +1,301 @@
using System.Diagnostics;
using System.IO;
using UnityEngine;
using System;
using System.Collections;
using Debug = UnityEngine.Debug;
public class WebViewLauncher : MonoBehaviour
{
public static WebViewLauncher Instance { get; private set; }
[Header("Web Settings")]
public string startUrl = "https://www.baidu.com";
public Vector2 windowSize = new Vector2(960, 600);
public Vector2 windowPosition = new Vector2(100, 100);
[Header("Editor Only Path")]
public string editorExeRoot = @"E:\Unity\ban_total\fdBrowser";
[Header("H5 Folder Paths")]
public string h5EditorPath = @"E:\Unity\ban_total\ban_test\Bansonic_Data\StreamingAssets\h5LG";
public string officialDocsPath = "_officialDocs";
public string officialGamesPath = "_officialGames";
public string thirdPartyGamesPath = "_thirdPartyGames";
private Process webProcess;
// Event invoked when web process main window handle becomes available
public event Action WebViewReady;
void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
void Start()
{
// 扫描 H5 文件夹
string h5Folder = GetH5FolderPath();
if (!Directory.Exists(h5Folder))
{
Directory.CreateDirectory(h5Folder);
Debug.Log("Created H5 folder: " + h5Folder);
}
var h5SubFolders = ScanH5Folders(h5Folder);
if (h5SubFolders.Count > 0)
{
Debug.Log("Found H5 folders: " + string.Join(", ", h5SubFolders));
string selectedFolder = h5SubFolders[0]; // 选择第一个
startUrl = "file://" + Path.Combine(h5Folder, selectedFolder, "index.html");
Debug.Log("Selected H5 folder: " + selectedFolder + ", URL: " + startUrl);
}
else
{
Debug.LogWarning("No H5 subfolders with index.html found in: " + h5Folder);
}
LaunchWebView();
}
void OnDestroy()
{
KillWebView();
}
public void KillWebView()
{
if (webProcess != null && !webProcess.HasExited)
{
webProcess.Kill();
webProcess = null;
}
}
void LaunchWebView()
{
string exePath = GetExePath();
Debug.Log("WebView exe path: " + exePath);
if (!File.Exists(exePath))
{
Debug.LogError("fdBrowser.exe not found: " + exePath);
return;
}
// 初始参数可以给 0,后续由 PanelFollower 接管
string args = $"0 0 800 600 \"{startUrl}\"";
Debug.Log($"Launching: {exePath} {args}");
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = exePath,
Arguments = args,
UseShellExecute = true, // 改为 true 以确保 exe 能启动
WorkingDirectory = Path.GetDirectoryName(exePath)
};
try
{
webProcess = Process.Start(psi);
Debug.Log("WebView2Host started successfully. Process ID: " + webProcess.Id);
// Start coroutine to wait until main window handle is ready
StartCoroutine(WaitForMainWindowHandle());
}
catch (System.Exception e)
{
Debug.LogError("Failed to start WebView2Host: " + e.Message + "\nPath: " + exePath + "\nArgs: " + args);
}
}
IEnumerator WaitForMainWindowHandle()
{
if (webProcess == null)
yield break;
float timeout = 5f; // seconds
float elapsed = 0f;
while (elapsed < timeout)
{
try
{
webProcess.Refresh();
if (webProcess.MainWindowHandle != IntPtr.Zero)
{
Debug.Log($"WebViewLauncher: MainWindowHandle ready: 0x{webProcess.MainWindowHandle.ToInt64():X}");
WebViewReady?.Invoke();
yield break;
}
}
catch (Exception e)
{
Debug.LogWarning("WebViewLauncher.WaitForMainWindowHandle exception: " + e.Message);
}
yield return null;
elapsed += Time.deltaTime;
}
Debug.LogWarning("WebViewLauncher: timeout waiting for main window handle");
// still invoke event so follower can attempt positioning later
WebViewReady?.Invoke();
}
/// <summary>
/// 从外部请求打开一个 url。如果 webview 没有运行则启动;如果已运行则尝试通过 WM_COPYDATA 发送 url 给已有窗口,失败则重启。
/// 传入的 url 建议使用本地文件路径或以 file:// 开头的路径。
/// </summary>
/// <param name="url"></param>
public void OpenUrl(string url)
{
if (string.IsNullOrEmpty(url))
{
Debug.LogWarning("OpenUrl called with empty url");
return;
}
// Normalize to absolute file:// or keep as web URL
string final;
try
{
if (Uri.TryCreate(url, UriKind.Absolute, out var parsed) && !string.IsNullOrEmpty(parsed.Scheme))
{
// If it's already a file/http/https URI, use its AbsoluteUri
final = parsed.AbsoluteUri;
}
else
{
// Treat as local file path
string abs = Path.GetFullPath(url);
final = new Uri(abs).AbsoluteUri; // yields file:///C:/...
}
}
catch (Exception e)
{
Debug.LogWarning("OpenUrl: failed to normalize url '" + url + "' : " + e.Message);
final = url;
}
Debug.Log($"OpenUrl requested. original='{url}', normalized='{final}'");
startUrl = final;
// if process not running, launch; otherwise try to send url to existing window
if (webProcess == null || webProcess.HasExited)
{
Debug.Log("WebView process not running. Launching with URL: " + final);
LaunchWebView();
return;
}
try
{
Debug.Log($"WebView process running. PID={webProcess.Id}, MainWindowHandle=0x{webProcess.MainWindowHandle.ToInt64():X}");
}
catch { }
// 尝试通过 WM_COPYDATA 将 URL 发送给已有进程窗口(需要 fdBrowser 实现接收处理)
bool sent = WebViewWin32.TrySendUrlToWindow(webProcess, final);
Debug.Log($"TrySendUrlToWindow returned: {sent}");
if (sent)
{
Debug.Log("Sent URL to existing WebView process: " + final);
// Notify followers that webview content may have changed and they should reapply positioning
WebViewReady?.Invoke();
return;
}
// 发送失败,退回到重启进程打开 URL 的方式
Debug.Log("Failed to send URL to existing process, restarting to open: " + final);
KillWebView();
LaunchWebView();
// after relaunch, WaitForMainWindowHandle will invoke WebViewReady when handle ready
}
string GetExePath()
{
#if UNITY_EDITOR
return Path.Combine(editorExeRoot, "fdBrowser.exe");
#else
// 优先使用 StreamingAssets 路径
string streamingPath = Path.Combine(Application.streamingAssetsPath, "fdBrowser", "fdBrowser.exe");
if (File.Exists(streamingPath))
{
Debug.Log("Using StreamingAssets path: " + streamingPath);
return streamingPath;
}
// 回退到游戏根目录
string gameRoot = Path.GetDirectoryName(Application.dataPath);
string fallbackPath = Path.Combine(gameRoot, "fdBrowser", "fdBrowser.exe");
Debug.Log("Using fallback path: " + fallbackPath);
return fallbackPath;
#endif
}
string GetH5FolderPath()
{
#if UNITY_EDITOR
return h5EditorPath;
#else
return Path.Combine(Application.streamingAssetsPath, "h5LG");
#endif
}
System.Collections.Generic.List<string> ScanH5Folders(string folderPath)
{
var result = new System.Collections.Generic.List<string>();
if (!Directory.Exists(folderPath))
{
Debug.LogWarning("H5 folder does not exist: " + folderPath);
return result;
}
Debug.Log("Scanning H5 folders in: " + folderPath);
string[] targetFolders = { officialDocsPath, officialGamesPath, thirdPartyGamesPath };
foreach (string folderName in targetFolders)
{
string subFolderPath = Path.Combine(folderPath, folderName);
if (!Directory.Exists(subFolderPath))
{
Directory.CreateDirectory(subFolderPath);
Debug.Log("Created subfolder: " + subFolderPath);
}
string indexPath = Path.Combine(subFolderPath, "index.html");
if (!File.Exists(indexPath))
{
File.WriteAllText(indexPath, "<html><body><h1>" + folderName + "</h1></body></html>");
Debug.Log("Created index.html in: " + subFolderPath);
}
if (File.Exists(indexPath))
{
result.Add(folderName);
Debug.Log("Found H5 folder with index.html: " + folderName);
}
}
Debug.Log("Total H5 folders found: " + result.Count);
return result;
}
/// <summary>
/// 给 PanelFollower 调用
/// </summary>
public bool TryUpdateWebViewRect(int x, int y, int w, int h)
{
if (webProcess == null || webProcess.HasExited)
return false;
return WebViewWin32.TrySetWindowRect(webProcess, x, y, w, h);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 49cec9d0b1891a24597a6ddbd149774a
@@ -0,0 +1,156 @@
using UnityEngine;
public class WebViewPanelFollower : MonoBehaviour
{
public RectTransform panelRect; // 拖拽要跟随的 UI Panel 的 RectTransform
public Camera uiCamera; // UI 相机,通常是 Canvas 的 Event Camera
private Rect lastPanelRect;
private WebViewWin32.RECT lastUnityWindowRect;
private bool hasLastRect = false;
// track subscription
private bool subscribedToWebViewReady = false;
void OnEnable()
{
TrySubscribeWebViewReady();
}
void OnDisable()
{
if (subscribedToWebViewReady && WebViewLauncher.Instance != null)
{
WebViewLauncher.Instance.WebViewReady -= OnWebViewReady;
subscribedToWebViewReady = false;
}
}
void TrySubscribeWebViewReady()
{
if (!subscribedToWebViewReady && WebViewLauncher.Instance != null)
{
WebViewLauncher.Instance.WebViewReady += OnWebViewReady;
subscribedToWebViewReady = true;
}
}
void Update()
{
// Ensure subscription in case WebViewLauncher wasn't ready earlier
TrySubscribeWebViewReady();
if (panelRect == null || uiCamera == null || WebViewLauncher.Instance == null)
return;
// 获取当前 panel 的屏幕矩形
Rect currentPanelRect = GetPanelScreenRect(panelRect);
// 获取当前 Unity 窗口位置(仅在 build 中需要)
WebViewWin32.RECT currentUnityWindowRect = new WebViewWin32.RECT();
#if !UNITY_EDITOR
if (!WebViewWin32.TryGetClientScreenRect(System.Diagnostics.Process.GetCurrentProcess(), out currentUnityWindowRect))
return;
#endif
// 检查是否需要更新
if (!hasLastRect ||
!ApproximatelyEqual(currentPanelRect, lastPanelRect)
#if !UNITY_EDITOR
|| !WindowRectsEqual(currentUnityWindowRect, lastUnityWindowRect)
#endif
)
{
int x, y;
#if UNITY_EDITOR
// 在编辑器中,直接使用屏幕坐标(相对于屏幕左上角)
x = Mathf.RoundToInt(currentPanelRect.x);
y = Mathf.RoundToInt(Screen.height - currentPanelRect.y - currentPanelRect.height);
#else
// 在 build 中,使用 Unity 窗口位置 + 相对坐标
x = Mathf.RoundToInt(currentUnityWindowRect.Left + currentPanelRect.x);
y = Mathf.RoundToInt(currentUnityWindowRect.Top + (Screen.height - currentPanelRect.y - currentPanelRect.height));
#endif
int w = Mathf.RoundToInt(currentPanelRect.width);
int h = Mathf.RoundToInt(currentPanelRect.height);
// 更新 WebView 窗口
bool success = WebViewLauncher.Instance.TryUpdateWebViewRect(x, y, w, h);
if (success)
{
lastPanelRect = currentPanelRect;
#if !UNITY_EDITOR
lastUnityWindowRect = currentUnityWindowRect;
#endif
hasLastRect = true;
}
}
}
void OnWebViewReady()
{
// Force next update to recalc and send position, and also perform immediate update
hasLastRect = false;
UpdatePositionNow();
}
// perform immediate position update (same logic as in Update)
void UpdatePositionNow()
{
if (panelRect == null || uiCamera == null || WebViewLauncher.Instance == null)
return;
Rect currentPanelRect = GetPanelScreenRect(panelRect);
WebViewWin32.RECT currentUnityWindowRect = new WebViewWin32.RECT();
#if !UNITY_EDITOR
if (!WebViewWin32.TryGetClientScreenRect(System.Diagnostics.Process.GetCurrentProcess(), out currentUnityWindowRect))
return;
#endif
int x, y;
#if UNITY_EDITOR
x = Mathf.RoundToInt(currentPanelRect.x);
y = Mathf.RoundToInt(Screen.height - currentPanelRect.y - currentPanelRect.height);
#else
x = Mathf.RoundToInt(currentUnityWindowRect.Left + currentPanelRect.x);
y = Mathf.RoundToInt(currentUnityWindowRect.Top + (Screen.height - currentPanelRect.y - currentPanelRect.height));
#endif
int w = Mathf.RoundToInt(currentPanelRect.width);
int h = Mathf.RoundToInt(currentPanelRect.height);
bool success = WebViewLauncher.Instance.TryUpdateWebViewRect(x, y, w, h);
if (success)
{
lastPanelRect = currentPanelRect;
#if !UNITY_EDITOR
lastUnityWindowRect = currentUnityWindowRect;
#endif
hasLastRect = true;
}
}
Rect GetPanelScreenRect(RectTransform rect)
{
Vector3[] corners = new Vector3[4];
rect.GetWorldCorners(corners);
Vector2 min = RectTransformUtility.WorldToScreenPoint(uiCamera, corners[0]);
Vector2 max = RectTransformUtility.WorldToScreenPoint(uiCamera, corners[2]);
return new Rect(min.x, min.y, max.x - min.x, max.y - min.y);
}
bool ApproximatelyEqual(Rect a, Rect b)
{
return Mathf.Approximately(a.x, b.x) &&
Mathf.Approximately(a.y, b.y) &&
Mathf.Approximately(a.width, b.width) &&
Mathf.Approximately(a.height, b.height);
}
bool WindowRectsEqual(WebViewWin32.RECT a, WebViewWin32.RECT b)
{
return a.Left == b.Left && a.Top == b.Top && a.Right == b.Right && a.Bottom == b.Bottom;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fd1a1128616940247a10d6841baf0686
+171
View File
@@ -0,0 +1,171 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using UnityEngine;
public static class WebViewWin32
{
[DllImport("user32.dll")]
static extern bool SetWindowPos(
IntPtr hWnd,
IntPtr hWndInsertAfter,
int X, int Y, int cx, int cy,
uint uFlags);
[DllImport("user32.dll")]
static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct COPYDATASTRUCT
{
public IntPtr dwData;
public int cbData;
public IntPtr lpData;
}
const uint SWP_NOZORDER = 0x0004;
const uint SWP_NOACTIVATE = 0x0010;
const uint WM_COPYDATA = 0x004A;
public static bool TrySetWindowRect(Process process, int x, int y, int width, int height)
{
if (process == null || process.HasExited)
return false;
if (process.MainWindowHandle == IntPtr.Zero)
{
process.Refresh();
if (process.MainWindowHandle == IntPtr.Zero)
return false;
}
SetWindowPos(
process.MainWindowHandle,
IntPtr.Zero,
x,
y,
width,
height,
SWP_NOZORDER | SWP_NOACTIVATE
);
return true;
}
public static bool TryGetWindowRect(Process process, out RECT rect)
{
rect = new RECT();
if (process == null || process.HasExited)
return false;
if (process.MainWindowHandle == IntPtr.Zero)
{
process.Refresh();
if (process.MainWindowHandle == IntPtr.Zero)
return false;
}
return GetWindowRect(process.MainWindowHandle, out rect);
}
public static bool TryGetClientScreenRect(Process process, out RECT rect)
{
rect = new RECT();
if (process == null || process.HasExited)
return false;
if (process.MainWindowHandle == IntPtr.Zero)
{
process.Refresh();
if (process.MainWindowHandle == IntPtr.Zero)
return false;
}
POINT point = new POINT { X = 0, Y = 0 };
if (!ClientToScreen(process.MainWindowHandle, ref point))
return false;
rect.Left = point.X;
rect.Top = point.Y;
rect.Right = point.X + UnityEngine.Screen.width;
rect.Bottom = point.Y + UnityEngine.Screen.height;
return true;
}
/// <summary>
/// 尝试向指定进程的主窗口发送 WM_COPYDATA 消息,lParam 为包含 Unicode 字符串的 COPYDATASTRUCT。
/// 接收方需要处理 WM_COPYDATA 并解释字符串为 URL 并导航。
/// 返回 true 表示接收方可能已处理(基于 SendMessage 返回值),false 表示发送或接收未成功。
/// </summary>
public static bool TrySendUrlToWindow(Process process, string url)
{
if (process == null || process.HasExited || string.IsNullOrEmpty(url))
return false;
if (process.MainWindowHandle == IntPtr.Zero)
{
process.Refresh();
if (process.MainWindowHandle == IntPtr.Zero)
return false;
}
IntPtr hWnd = process.MainWindowHandle;
IntPtr ptrString = IntPtr.Zero;
IntPtr ptrCds = IntPtr.Zero;
try
{
// Allocate Unicode string (null-terminated)
ptrString = Marshal.StringToHGlobalUni(url);
var cds = new COPYDATASTRUCT
{
dwData = IntPtr.Zero,
cbData = (url.Length + 1) * 2,
lpData = ptrString
};
int size = Marshal.SizeOf(typeof(COPYDATASTRUCT));
ptrCds = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(cds, ptrCds, false);
IntPtr res = SendMessage(hWnd, WM_COPYDATA, IntPtr.Zero, ptrCds);
long resVal = res.ToInt64();
UnityEngine.Debug.Log($"WebViewWin32.TrySendUrlToWindow: Sent WM_COPYDATA to hWnd=0x{hWnd.ToInt64():X}, url={url}, SendMessage result={resVal}");
// Treat non-zero as handled/accepted by receiver. If receiver returns zero, consider not handled.
return resVal != 0;
}
catch (Exception e)
{
UnityEngine.Debug.LogWarning("WebViewWin32.TrySendUrlToWindow exception: " + e.Message);
return false;
}
finally
{
if (ptrCds != IntPtr.Zero) Marshal.FreeHGlobal(ptrCds);
if (ptrString != IntPtr.Zero) Marshal.FreeHGlobal(ptrString);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6162b5a372404e246864c6f49105e618
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 170eecb5841893f439d955a3b5a94529
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,147 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System;
public class game_card_Prefab : MonoBehaviour, IPointerClickHandler
{
[Header("Prefab基本信息")]
public Image card_icon_image;
public Text card_name_text;
public Text authorName_text;
public Text description_text;
public Text _isOfficial_text;
public Text source_text;
public Text yyyy_mm_dd;
[Header("Optional clickable Button (auto-find if null)")]
public Button clickButton;
// 存储对应 index.html 的本地路径(不含 file:// 前缀)
private string indexFilePath;
// 给 loadLittleGamesPrefab 调用以填充文本和路径
public void SetData(string indexPath, string cardName, string authorName, string description, string isOfficial, string source, string yyyyMmDd)
{
indexFilePath = indexPath;
if (card_name_text != null) card_name_text.text = cardName ?? "";
if (authorName_text != null) authorName_text.text = authorName ?? "";
if (description_text != null) description_text.text = description ?? "";
if (_isOfficial_text != null) _isOfficial_text.text = isOfficial ?? "";
if (source_text != null) source_text.text = source ?? "";
if (yyyy_mm_dd != null) yyyy_mm_dd.text = yyyyMmDd ?? "";
UnityEngine.Debug.Log($"game_card_Prefab.SetData: indexPath='{indexFilePath}', card_name='{cardName}', author='{authorName}', isOfficial='{isOfficial}', source='{source}'");
}
void Awake()
{
// If no explicit Button assigned, try to find one on this GameObject or children
if (clickButton == null)
{
clickButton = GetComponent<Button>();
if (clickButton == null)
clickButton = GetComponentInChildren<Button>(true);
}
if (clickButton == null)
{
// Try to auto-add a Button to this GameObject so clicks are captured.
// Ensure there's an Image component (Button requires Image for Raycast target)
Image img = GetComponent<Image>();
if (img == null)
{
img = gameObject.AddComponent<Image>();
img.color = new Color(1, 1, 1, 0); // transparent
}
clickButton = gameObject.AddComponent<Button>();
UnityEngine.Debug.Log("game_card_Prefab.Awake: No Button found; added Image+Button to prefab at runtime to capture clicks.");
}
// Check EventSystem presence
if (EventSystem.current == null)
{
UnityEngine.Debug.LogWarning("game_card_Prefab.Awake: No EventSystem in scene. UI clicks and IPointerClickHandler won't work without an EventSystem.");
}
}
void OnEnable()
{
if (clickButton != null)
{
clickButton.onClick.AddListener(OnButtonClicked);
UnityEngine.Debug.Log("game_card_Prefab: added onClick listener to Button");
}
}
void OnDisable()
{
if (clickButton != null)
{
clickButton.onClick.RemoveListener(OnButtonClicked);
UnityEngine.Debug.Log("game_card_Prefab: removed onClick listener from Button");
}
}
void OnButtonClicked()
{
UnityEngine.Debug.Log($"game_card_Prefab.OnButtonClicked: indexPath='{indexFilePath}'");
OpenIndexPath();
}
// 点击时尝试通过 WebViewLauncher 打开对应的 index.html
public void OnPointerClick(PointerEventData eventData)
{
UnityEngine.Debug.Log($"game_card_Prefab.OnPointerClick: indexPath='{indexFilePath}'");
OpenIndexPath();
}
void OpenIndexPath()
{
if (string.IsNullOrEmpty(indexFilePath))
{
UnityEngine.Debug.LogWarning("game_card_Prefab clicked but index path is empty");
return;
}
// 优先使用 WebViewLauncher 单例
if (WebViewLauncher.Instance != null)
{
UnityEngine.Debug.Log("Calling WebViewLauncher.OpenUrl with: " + indexFilePath);
WebViewLauncher.Instance.OpenUrl(indexFilePath);
}
else
{
UnityEngine.Debug.LogWarning("WebViewLauncher instance not found. Attempting to start new launcher.");
// 尝试查找场景中的 WebViewLauncher 或创建一个新的 GameObject
var launcherObj = GameObject.FindObjectOfType<WebViewLauncher>();
if (launcherObj != null)
{
UnityEngine.Debug.Log("Found launcher in scene, calling OpenUrl: " + indexFilePath);
launcherObj.OpenUrl(indexFilePath);
}
else
{
// Create a temporary launcher object to start webview
UnityEngine.Debug.Log("Creating temporary WebViewLauncher and calling OpenUrl: " + indexFilePath);
GameObject go = new GameObject("WebViewLauncher_Temp");
var launcher = go.AddComponent<WebViewLauncher>();
launcher.OpenUrl(indexFilePath);
GameObject.DontDestroyOnLoad(go);
}
}
}
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9e862c8c48e4f8b49adae5e986da6e73
@@ -0,0 +1,739 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &219047244547433570
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 872492842950718926}
- component: {fileID: 7480212223569318349}
- component: {fileID: 9193153990832272737}
m_Layer: 5
m_Name: author_name
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &872492842950718926
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 219047244547433570}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3566709735925945696}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 10.717, y: 14.07}
m_SizeDelta: {x: 252.359, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7480212223569318349
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 219047244547433570}
m_CullTransparentMesh: 1
--- !u!114 &9193153990832272737
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 219047244547433570}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: cc180dff846d13a4d88ddaed6f77e5cd, type: 3}
m_FontSize: 12
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 1
m_MaxSize: 40
m_Alignment: 6
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: author_name
--- !u!1 &964402434877421462
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 189438553718290088}
- component: {fileID: 8767921256928017929}
- component: {fileID: 5425439562977293950}
m_Layer: 5
m_Name: yyyyddmm
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &189438553718290088
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 964402434877421462}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3566709735925945696}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 171.06035, y: -15.7}
m_SizeDelta: {x: 38.41, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8767921256928017929
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 964402434877421462}
m_CullTransparentMesh: 1
--- !u!114 &5425439562977293950
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 964402434877421462}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: 775c674e81662c644b64550d2e8f74e0, type: 3}
m_FontSize: 10
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 1
m_MaxSize: 40
m_Alignment: 5
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: 'YYYY
MM
DD'
--- !u!1 &2391360557458902149
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 604392950021451895}
- component: {fileID: 5600536436187702497}
- component: {fileID: 3389633104739560455}
m_Layer: 5
m_Name: card_name
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &604392950021451895
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2391360557458902149}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3566709735925945696}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 15.512863, y: 20.5}
m_SizeDelta: {x: 262.884, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5600536436187702497
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2391360557458902149}
m_CullTransparentMesh: 1
--- !u!114 &3389633104739560455
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2391360557458902149}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: cc180dff846d13a4d88ddaed6f77e5cd, type: 3}
m_FontSize: 20
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 2
m_MaxSize: 40
m_Alignment: 3
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: CardNameHere
--- !u!1 &3532408215758252597
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4583698199465520998}
- component: {fileID: 301933785134177564}
- component: {fileID: 6004099248657222439}
m_Layer: 5
m_Name: Image
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4583698199465520998
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3532408215758252597}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3566709735925945696}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -160, y: 0}
m_SizeDelta: {x: 50, y: 50}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &301933785134177564
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3532408215758252597}
m_CullTransparentMesh: 1
--- !u!114 &6004099248657222439
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3532408215758252597}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 4997328413415617422, guid: a8ff5cf9bafe9f544bfde136473da3cc, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &5110857998175197173
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2622440068852620012}
- component: {fileID: 82439723018948691}
- component: {fileID: 5166008895911254744}
m_Layer: 5
m_Name: _isOfficial
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2622440068852620012
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5110857998175197173}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3566709735925945696}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 144.2521, y: 19.9}
m_SizeDelta: {x: 94.529, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &82439723018948691
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5110857998175197173}
m_CullTransparentMesh: 1
--- !u!114 &5166008895911254744
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5110857998175197173}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: cc180dff846d13a4d88ddaed6f77e5cd, type: 3}
m_FontSize: 12
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 1
m_MaxSize: 40
m_Alignment: 5
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: _isOfficial
--- !u!1 &5331475481345189989
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3566709735925945696}
- component: {fileID: 3041038222081474245}
- component: {fileID: 6180074520896239301}
- component: {fileID: 3634709248270434128}
m_Layer: 5
m_Name: botton
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3566709735925945696
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5331475481345189989}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4583698199465520998}
- {fileID: 604392950021451895}
- {fileID: 872492842950718926}
- {fileID: 2019306417107768352}
- {fileID: 2622440068852620012}
- {fileID: 2138782682665805939}
- {fileID: 189438553718290088}
m_Father: {fileID: 9116228060429372210}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 5.929, y: 0}
m_SizeDelta: {x: 424.852, y: 100}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3041038222081474245
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5331475481345189989}
m_CullTransparentMesh: 1
--- !u!114 &6180074520896239301
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5331475481345189989}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &3634709248270434128
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5331475481345189989}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 6180074520896239301}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!1 &8844525127622122013
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2019306417107768352}
- component: {fileID: 6796448552036844055}
- component: {fileID: 4780041332280513338}
m_Layer: 5
m_Name: source
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2019306417107768352
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8844525127622122013}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3566709735925945696}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 158.28172, y: 16.64}
m_SizeDelta: {x: 66.47, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6796448552036844055
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8844525127622122013}
m_CullTransparentMesh: 1
--- !u!114 &4780041332280513338
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8844525127622122013}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: cc180dff846d13a4d88ddaed6f77e5cd, type: 3}
m_FontSize: 12
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 1
m_MaxSize: 40
m_Alignment: 8
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: source
--- !u!1 &8979588775758457773
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 9116228060429372210}
- component: {fileID: 1340434185471581111}
m_Layer: 5
m_Name: game_card_prefab
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &9116228060429372210
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8979588775758457773}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3566709735925945696}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 100, y: 100}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1340434185471581111
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8979588775758457773}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9e862c8c48e4f8b49adae5e986da6e73, type: 3}
m_Name:
m_EditorClassIdentifier:
card_icon_image: {fileID: 6004099248657222439}
card_name_text: {fileID: 3389633104739560455}
authorName_text: {fileID: 9193153990832272737}
description_text: {fileID: 8118505406259206449}
_isOfficial_text: {fileID: 5166008895911254744}
source_text: {fileID: 4780041332280513338}
yyyy_mm_dd: {fileID: 5425439562977293950}
clickButton: {fileID: 0}
--- !u!1 &9033496028734522550
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2138782682665805939}
- component: {fileID: 3647945832167543742}
- component: {fileID: 8118505406259206449}
m_Layer: 5
m_Name: description
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2138782682665805939
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9033496028734522550}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3566709735925945696}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 10.130814, y: -15.7}
m_SizeDelta: {x: 253.531, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3647945832167543742
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9033496028734522550}
m_CullTransparentMesh: 1
--- !u!114 &8118505406259206449
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9033496028734522550}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: 775c674e81662c644b64550d2e8f74e0, type: 3}
m_FontSize: 15
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 1
m_MaxSize: 40
m_Alignment: 3
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text: "\u7B80\u4ECB\u4E0D\u5F97\u8D85\u8FC71234567891011121314151617181920212223242526272829"
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a6f011f2d2221db49a0f07a8cf5900ca
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,230 @@
using UnityEngine;
using System.IO;
using System.Collections.Generic;
using System.Xml;
public class loadLittleGamesPrefab : MonoBehaviour
{
[Header("Scroll Views")]
public GameObject officialDocs_scrollView_content;
public GameObject officialGames_scrollView_content;
public GameObject thirdPartyGames_scrollView_content;
[Header("Prefab")]
public GameObject littleGames_card_Prefab;
[Header("H5 Root Path (optional, leave empty to use WebViewLauncher settings)")]
public string h5RootPath;
void Start()
{
LoadAllLittleGames();
}
/// <summary>
/// 在 WebViewLauncher 的 h5 目录下扫描 three categories 的所有子文件夹(递归一层),
/// 找到含有 index.html 的目录(例如: ...\h5LG\_thirdPartyGames\kjb\index.html),
/// 读取同目录下的 info.xml(如果存在),并把 prefab 实例化到对应的 content 中。
///
/// info.xml 示例格式(放在同目录下,编码 UTF-8):
/// <?xml version="1.0" encoding="utf-8"?>
/// <Info>
/// <card_name>示例游戏名</card_name>
/// <authorName>作者名</authorName>
/// <description>这里是描述文本</description>
/// <isOfficial>True</isOfficial>
/// <source>来源信息</source>
/// <yyyy_mm_dd>2025-12-23</yyyy_mm_dd>
/// </Info>
///
/// 请保证节点名如上所示以便能被正确读取。
/// </summary>
public void LoadAllLittleGames()
{
string root = GetH5RootPath();
if (string.IsNullOrEmpty(root) || !Directory.Exists(root))
{
Debug.LogWarning("H5 root path not found: " + root);
return;
}
// 目标一级子目录名称(与 WebViewLauncher 中保持一致)
string[] categories = { "_officialDocs", "_officialGames", "_thirdPartyGames" };
foreach (string cat in categories)
{
string catPath = Path.Combine(root, cat);
if (!Directory.Exists(catPath))
continue;
// 遍历 catPath 下的第一层子目录,检查是否含有 index.html
string[] subdirs = Directory.GetDirectories(catPath);
foreach (string subdir in subdirs)
{
string indexPath = Path.Combine(subdir, "index.html");
if (!File.Exists(indexPath))
{
// 也可能 index.html 在更深一层,但按照约定通常是 subdir/index.html
continue;
}
// 读取 info.xml(可选)
string infoXmlPath = Path.Combine(subdir, "info.xml");
var meta = ReadInfoXml(infoXmlPath);
// 根据分类选择目标 content
GameObject targetContent = GetContentByCategory(cat);
if (targetContent == null || littleGames_card_Prefab == null)
{
Debug.LogWarning("Missing prefab or content for category: " + cat);
continue;
}
// 实例化 prefab
GameObject go = Instantiate(littleGames_card_Prefab, targetContent.transform);
// 设置数据(indexPath 使用本地文件绝对路径)
string absIndexPath = indexPath; // 使用绝对路径,WebViewLauncher.OpenUrl 会添加 file://
var prefabComp = go.GetComponent<game_card_Prefab>();
if (prefabComp != null)
{
// 若来自官方分类,覆盖 isOfficial 与 source 字段为固定值
string finalIsOfficial = meta.GetValueOrDefault("isOfficial");
string finalSource = meta.GetValueOrDefault("source");
if (cat == "_officialDocs" || cat == "_officialGames")
{
finalIsOfficial = "官方";
finalSource = "小游戏DLC";
}
else
{
// 非官方分类统一标记为第三方来源
finalIsOfficial = "第三方来源";
// 若 info.xml 没有 source,则可以保留原值或设置默认,这里若无则为空
if (string.IsNullOrEmpty(finalSource))
finalSource = "";
}
// 格式化日期为 yyyy\nmm\ndd 的形式
string rawDate = meta.GetValueOrDefault("yyyy_mm_dd");
string formattedDate = FormatDateWithNewlines(rawDate);
prefabComp.SetData(absIndexPath,
meta.GetValueOrDefault("card_name"),
meta.GetValueOrDefault("authorName"),
meta.GetValueOrDefault("description"),
finalIsOfficial,
finalSource,
formattedDate
);
}
}
}
}
string GetH5RootPath()
{
if (!string.IsNullOrEmpty(h5RootPath) && Directory.Exists(h5RootPath))
return h5RootPath;
// 从 WebViewLauncher 获取编辑器路径
var w = GameObject.FindObjectOfType<WebViewLauncher>();
if (w != null)
{
#if UNITY_EDITOR
return w.h5EditorPath;
#else
return Path.Combine(Application.streamingAssetsPath, "h5LG");
#endif
}
// 作为回退,使用 StreamingAssets/h5LG
return Path.Combine(Application.streamingAssetsPath, "h5LG");
}
GameObject GetContentByCategory(string categoryName)
{
switch (categoryName)
{
case "_officialDocs": return officialDocs_scrollView_content;
case "_officialGames": return officialGames_scrollView_content;
case "_thirdPartyGames": return thirdPartyGames_scrollView_content;
default: return null;
}
}
Dictionary<string, string> ReadInfoXml(string xmlPath)
{
var dict = new Dictionary<string, string>();
if (string.IsNullOrEmpty(xmlPath) || !File.Exists(xmlPath))
return dict;
try
{
var doc = new XmlDocument();
doc.Load(xmlPath);
var root = doc.DocumentElement;
if (root == null)
return dict;
// 读取预定义字段
string[] keys = { "card_name", "authorName", "description", "isOfficial", "source", "yyyy_mm_dd" };
foreach (var k in keys)
{
var node = root.SelectSingleNode(k);
if (node != null)
dict[k] = node.InnerText;
}
}
catch (System.Exception e)
{
Debug.LogWarning("Failed to read info.xml: " + xmlPath + " error: " + e.Message);
}
return dict;
}
// 将日期文本尝试解析成 yyyy\nmm\ndd 格式,若解析失败则做简单替换(支持 2025-12-23, 2025/12/23, 2025_12_23, 20251223 等)
string FormatDateWithNewlines(string raw)
{
if (string.IsNullOrEmpty(raw))
return "";
// 尝试按常见分隔符拆分
char[] seps = new char[] { '-', '/', '_', ' ' };
var parts = raw.Split(seps, System.StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 3)
{
return parts[0] + "\n" + parts[1] + "\n" + parts[2];
}
// 尝试连续数字 8 位
var digits = System.Text.RegularExpressions.Regex.Replace(raw, "\\D", "");
if (digits.Length == 8)
{
string y = digits.Substring(0, 4);
string m = digits.Substring(4, 2);
string d = digits.Substring(6, 2);
return y + "\n" + m + "\n" + d;
}
// 回退:尝试用第一个 4 位作为年,接下来的两位为月,之后为日
if (digits.Length >= 6)
{
string y = digits.Substring(0, 4);
string m = digits.Length >= 6 ? digits.Substring(4, 2) : "";
string d = digits.Length >= 8 ? digits.Substring(6, 2) : "";
return y + "\n" + m + "\n" + d;
}
// 最后回退直接把原文本按字符换行(有限长度)
return raw.Replace("-", "\n").Replace("/", "\n").Replace("_", "\n");
}
// Update is called once per frame
void Update()
{
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9d009618c8f589b49a50efcd92597ce2
+22 -20
View File
@@ -209,7 +209,7 @@ public class HoldNote : BaseNote
{
Debug.Log($"[HoldNote] START段超时自动Miss(自动): {noteColor}");
// 只做 Miss 结果展示与登记
// register as missed
JudgeManager.Instance.RegisterStartJudged(noteID, false);
InputManager.Instance?.ShowJudgeResult(trackIndex, "Miss");
@@ -255,7 +255,7 @@ public class HoldNote : BaseNote
float maxWindow = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
if (Mathf.Abs(pressTimeLocal - hitTime) <= maxWindow)
{
HandleStart();
HandleStart(false);
isJudged = true; // mark judged to avoid duplicates
}
else
@@ -269,7 +269,7 @@ public class HoldNote : BaseNote
// legacy fallback (shouldn't normally hit because above handles Start)
if (Input.GetKeyDown(keyToPress))
{
HandleStart();
HandleStart(false);
isJudged = true; // 无论成功失败都标记为已判定,避免重复尝试
}
}
@@ -348,7 +348,7 @@ public class HoldNote : BaseNote
{
Debug.Log($"[HoldNote] Start段离开判定线未判定,补偿 Miss 并回收: {noteColor}");
// 确保未判定的 Start 段在离开判定线时登记为 Miss
HandleStart();
HandleStart(true);
isJudged = true;
ReturnToPool();
}
@@ -422,44 +422,52 @@ public class HoldNote : BaseNote
}
}
private void HandleStart()
private void HandleStart(bool forceMiss = false)
{
if (!JudgeManager.Instance.TryResolveStart(noteID))
{
Debug.Log($"[HoldNote] START 已被判定,跳过: {noteColor}");
return;
}
// If forced miss (e.g. leaving judge zone without press), register miss immediately
if (forceMiss)
{
Debug.Log($"[HoldNote] START 强制 Miss (未按下): {noteColor}");
ScoreManager.Instance.countMiss += 1;
JudgeManager.Instance.RegisterStartJudged(noteID, false);
isHoldActive = false;
InputManager.Instance?.ShowJudgeResult(trackIndex, "Miss");
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
teamUIController.Instance?.OnJudgeResult("Miss");
return;
}
float pressTime = Time.time;
float rawOffsetMs = (hitTime - pressTime) * 1000f;
float offset = Mathf.Abs(pressTime - hitTime);
string result;
// compute scaled judgement windows for hold note start
// further scale windows according to visualSpeedMultiplier so slow visuals get more leniency
float visualScaleFactor = Mathf.Clamp(visualSpeedMultiplier, 0.5f, 2f);
float pRange = (judgeConfig?.perfectRange ?? 0.1f) * holdWindowMultiplier * visualScaleFactor;
float gRange = (judgeConfig?.greatRange ?? 0.2f) * holdWindowMultiplier * visualScaleFactor;
float gdRange = (judgeConfig?.goodRange ?? 0.3f) * holdWindowMultiplier * visualScaleFactor;
// evaluate Start judgement using scaled windows
if (offset <= pRange)
{
result = "Perfect";
ScoreManager.Instance.countPerfect += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
Debug.Log($"[HoldNote] START判定成功(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, true);
isHoldActive = true; // 成功判定Start,长按状态激活
isHoldActive = true;
PlayHitAnimation();
AnimationController.Global?.StartHoldParticles(noteColor);
// Trigger skills ... (unchanged)
}
else if (offset <= gRange)
{
result = "Great";
ScoreManager.Instance.countGreat += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
Debug.Log($"[HoldNote] START判定 Great(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, true);
isHoldActive = true;
PlayHitAnimation();
@@ -470,7 +478,6 @@ public class HoldNote : BaseNote
result = "Good";
ScoreManager.Instance.countGood += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
Debug.Log($"[HoldNote] START判定 Good(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, true);
isHoldActive = true;
PlayHitAnimation();
@@ -479,14 +486,9 @@ public class HoldNote : BaseNote
else
{
result = "Miss";
if (JudgeManager.Instance.TryResolveStart(noteID))
{
ScoreManager.Instance.countMiss += 1;
}
Debug.Log($"[HoldNote] START Miss(偏差={offset:F2}秒): {noteColor}");
ScoreManager.Instance.countMiss += 1;
JudgeManager.Instance.RegisterStartJudged(noteID, false);
isHoldActive = false;
try
{
bool triggered = SkillBuilder.Instance?.NotifyNoteHit(this.trackIndex, result, SkillDefinition.NoteTypeTrigger.Tap, this.noteID) ?? false;
@@ -49,6 +49,12 @@ public class InputManager : MonoBehaviour
}
}
private void Start()
{
// Ensure UI shows current bindings from KeyBindingManager/PlayerPrefs
RefreshKeyLabels();
}
private void Update()
{
foreach (string color in new string[] { "red", "green", "yellow", "purple", "blue" })
@@ -87,6 +93,20 @@ public class InputManager : MonoBehaviour
}
}
// Refresh displayed labels for key bindings (called after rebind)
public void RefreshKeyLabels()
{
string[] colors = new string[] { "red", "green", "yellow", "purple", "blue" };
if (trackKeyTexts == null) return;
for (int i = 0; i < colors.Length && i < trackKeyTexts.Length; i++)
{
var txt = trackKeyTexts[i];
if (txt == null) continue;
KeyCode k = KeyBindingManager.GetKeyForColor(colors[i]);
txt.text = KeyBindingManager.GetDisplayName(k);
}
}
private int GetIndexForColor(string color)
{
// 保持与 trackJudgeTexts 相同的索引映射
@@ -4,71 +4,114 @@ using System.Collections.Generic;
public class KeyBindingManager : MonoBehaviour
{
private static Dictionary<string, KeyCode> keyBindings = new Dictionary<string, KeyCode>();
private static readonly string[] colors = { "red", "green", "yellow", "purple", "blue" };
private static readonly KeyCode[] defaultKeys = { KeyCode.D, KeyCode.F, KeyCode.Space, KeyCode.J, KeyCode.K };
private static bool initialized = false;
private void Awake()
{
Debug.Log("KeyBindingManager Awake() 被调用,开始加载按键映射...");
if (keyBindings.Count == 0)
if (!initialized)
{
LoadKeyBindings();
}
Debug.Log("KeyBindingManager Awake() 被调用,开始加载按键映射...");
}
/// <summary> 获取颜色对应的按键 </summary>
public static KeyCode GetKeyForColor(string color)
{
if (keyBindings.TryGetValue(color.ToLower(), out KeyCode key))
if (!initialized)
{
LoadKeyBindings();
}
if (string.IsNullOrEmpty(color)) return KeyCode.None;
var keyLower = color.ToLower();
if (keyBindings.TryGetValue(keyLower, out KeyCode key))
{
return key;
}
Debug.LogError($"未找到颜色 {color} 对应的按键!请检查 KeyBindingManager 是否正确初始化。");
// fallback: try to map by index order
for (int i = 0; i < colors.Length; i++)
{
if (colors[i] == keyLower)
{
return defaultKeys[i];
}
}
return KeyCode.None;
}
/// <summary> 修改按键绑定 </summary>
public static void ChangeKeyBinding(string color, KeyCode newKey)
{
if (keyBindings.ContainsKey(color.ToLower()))
{
keyBindings[color.ToLower()] = newKey;
}
else
{
keyBindings.Add(color.ToLower(), newKey);
}
if (string.IsNullOrEmpty(color)) return;
var keyLower = color.ToLower();
keyBindings[keyLower] = newKey;
SaveKeyBindings();
}
/// <summary> 存储按键绑定到 `PlayerPrefs` </summary>
private static void SaveKeyBindings()
{
foreach (var kvp in keyBindings)
for (int i = 0; i < colors.Length; i++)
{
PlayerPrefs.SetInt($"KeyBinding_{kvp.Key}", (int)kvp.Value);
var col = colors[i];
KeyCode k = defaultKeys[i];
if (keyBindings.TryGetValue(col, out KeyCode stored)) k = stored;
PlayerPrefs.SetInt($"KeyBinding_{col}", (int)k);
}
PlayerPrefs.Save();
}
/// <summary> 从 `PlayerPrefs` 加载按键绑定 </summary>
private static void LoadKeyBindings()
{
string[] colors = { "red", "green", "yellow", "purple", "blue" };
KeyCode[] defaultKeys = { KeyCode.D, KeyCode.F, KeyCode.Space, KeyCode.J, KeyCode.K };
keyBindings.Clear();
for (int i = 0; i < colors.Length; i++)
{
if (PlayerPrefs.HasKey($"KeyBinding_{colors[i]}"))
string col = colors[i];
if (PlayerPrefs.HasKey($"KeyBinding_{col}"))
{
keyBindings[colors[i]] = (KeyCode)PlayerPrefs.GetInt($"KeyBinding_{colors[i]}");
keyBindings[col] = (KeyCode)PlayerPrefs.GetInt($"KeyBinding_{col}");
}
else
{
keyBindings[colors[i]] = defaultKeys[i];
keyBindings[col] = defaultKeys[i];
}
}
initialized = true;
Debug.Log("KeyBindings 初始化成功:" + string.Join(", ", keyBindings));
}
// Return a human-friendly display string for a KeyCode (symbols shown as their character)
public static string GetDisplayName(KeyCode key)
{
if (key == KeyCode.Space) return "Space";
switch (key)
{
case KeyCode.Quote: return "'"; // single quote
case KeyCode.Semicolon: return ";";
case KeyCode.Comma: return ",";
case KeyCode.Period: return ".";
case KeyCode.Slash: return "/";
case KeyCode.Backslash: return "\\";
case KeyCode.LeftBracket: return "[";
case KeyCode.RightBracket: return "]";
case KeyCode.Minus: return "-";
case KeyCode.Equals: return "=";
case KeyCode.BackQuote: return "`";
case KeyCode.Keypad0: return "Num0";
case KeyCode.Keypad1: return "Num1";
case KeyCode.Keypad2: return "Num2";
case KeyCode.Keypad3: return "Num3";
case KeyCode.Keypad4: return "Num4";
case KeyCode.Keypad5: return "Num5";
case KeyCode.Keypad6: return "Num6";
case KeyCode.Keypad7: return "Num7";
case KeyCode.Keypad8: return "Num8";
case KeyCode.Keypad9: return "Num9";
default:
// For letters and named keys, use ToString(); for better readability, split "Alpha0" -> "0"
string s = key.ToString();
if (s.StartsWith("Alpha") && s.Length > 5) return s.Substring(5);
return s;
}
}
}
@@ -41,6 +41,17 @@ public class NoteSpawner : MonoBehaviour
private static int holdNoteIdCounter = 0; // 全局唯一长音符 ID 计数器
private const string NoteSpeedPrefKey = "noteSpeedMultiplier";
private void Awake()
{
// Load saved visual speed multiplier before any spawning logic uses it
float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, speedMultiplier);
saved = Mathf.Clamp(saved, SpeedMultiplierMin, SpeedMultiplierMax);
speedMultiplier = saved;
if (GameConfig.verboseLogs) Debug.Log($"[NoteSpawner] Loaded speedMultiplier={speedMultiplier} from PlayerPrefs");
}
public void LoadBeatmap(Beatmap loadedBeatmap)
{
if (isSpawning) return;
@@ -0,0 +1,494 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class controllerSettings : MonoBehaviour
{
[Header("按键绑定")]
public Button[] keyButtons; // assign 5 buttons in Inspector
public Text errorText; // UI text to show error/success messages
[Header("Message Colors")]
public Color messageErrorColor = Color.red;
public Color messageSuccessColor = Color.green;
private Coroutine messageCoroutine = null;
[Header("音符流动速度滑动变阻器")]
public Slider noteSpeedMultipler_slider;
public Text noteSpeedMultipler_valueText;
public Button noteSpeed_DecreaseButton;
public Button noteSpeed_IncreaseButton;
public Button noteSpeed_ResetButton;
// internal colors/order must match KeyBindingManager usage
private readonly string[] colors = new string[] { "red", "green", "yellow", "purple", "blue" };
private bool isRebinding = false;
private int rebindingIndex = -1;
private Coroutine blinkCoroutine = null;
// store previous key so we can restore if user cancels
private KeyCode previousKey = KeyCode.None;
private bool previousKeyValid = false;
// slider range
private const float NoteSpeedMin = 0.8f;
private const float NoteSpeedMax = 1.25f;
private const string NoteSpeedPrefKey = "noteSpeedMultiplier";
// continuous change
private Coroutine continuousChangeCoroutine = null;
private const float ContinuousInitialDelay = 0.5f; // increased to avoid accidental long-press
private const float ContinuousRepeatRate = 0.05f;
private const float StepAmount = 0.01f;
void Start()
{
// initialize button labels from KeyBindingManager
RefreshButtonLabels();
// initialize errorText
if (errorText != null)
{
errorText.text = string.Empty;
}
// hook up listeners for key buttons
if (keyButtons != null)
{
for (int i = 0; i < keyButtons.Length && i < colors.Length; i++)
{
int idx = i;
keyButtons[i].onClick.RemoveAllListeners();
keyButtons[i].onClick.AddListener(() => OnKeyButtonClicked(idx));
}
}
// initialize slider
if (noteSpeedMultipler_slider != null)
{
noteSpeedMultipler_slider.minValue = NoteSpeedMin;
noteSpeedMultipler_slider.maxValue = NoteSpeedMax;
float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, 1.0f);
saved = Mathf.Clamp(saved, NoteSpeedMin, NoteSpeedMax);
noteSpeedMultipler_slider.value = saved;
UpdateNoteSpeedText(saved);
noteSpeedMultipler_slider.onValueChanged.RemoveAllListeners();
noteSpeedMultipler_slider.onValueChanged.AddListener(OnNoteSpeedSliderChanged);
// Add PointerUp event to save value when sliding ends (kept for compatibility)
EventTrigger trigger = noteSpeedMultipler_slider.gameObject.GetComponent<EventTrigger>();
if (trigger == null) trigger = noteSpeedMultipler_slider.gameObject.AddComponent<EventTrigger>();
trigger.triggers.RemoveAll(e => e.eventID == EventTriggerType.PointerUp);
var entry = new EventTrigger.Entry { eventID = EventTriggerType.PointerUp };
entry.callback.AddListener((data) => { OnNoteSpeedSliderPointerUp(); });
trigger.triggers.Add(entry);
}
// setup buttons
if (noteSpeed_DecreaseButton != null)
{
noteSpeed_DecreaseButton.onClick.RemoveAllListeners();
noteSpeed_DecreaseButton.onClick.AddListener(() => ChangeNoteSpeedBy(-StepAmount));
AddButtonContinuousEvents(noteSpeed_DecreaseButton, -StepAmount);
}
if (noteSpeed_IncreaseButton != null)
{
noteSpeed_IncreaseButton.onClick.RemoveAllListeners();
noteSpeed_IncreaseButton.onClick.AddListener(() => ChangeNoteSpeedBy(StepAmount));
AddButtonContinuousEvents(noteSpeed_IncreaseButton, StepAmount);
}
if (noteSpeed_ResetButton != null)
{
noteSpeed_ResetButton.onClick.RemoveAllListeners();
noteSpeed_ResetButton.onClick.AddListener(() => ResetNoteSpeed());
}
}
void OnDestroy()
{
if (keyButtons != null)
{
foreach (var b in keyButtons)
if (b != null) b.onClick.RemoveAllListeners();
}
if (noteSpeedMultipler_slider != null)
{
noteSpeedMultipler_slider.onValueChanged.RemoveListener(OnNoteSpeedSliderChanged);
// Do not remove EventTrigger to avoid affecting other listeners, but it's okay on destroy
}
if (noteSpeed_DecreaseButton != null) noteSpeed_DecreaseButton.onClick.RemoveAllListeners();
if (noteSpeed_IncreaseButton != null) noteSpeed_IncreaseButton.onClick.RemoveAllListeners();
if (noteSpeed_ResetButton != null) noteSpeed_ResetButton.onClick.RemoveAllListeners();
}
void Update()
{
// If waiting for a new key, also cancel if user clicks outside UI/buttons
if (isRebinding)
{
if (Input.GetMouseButtonDown(0))
{
if (!IsPointerOverAnyKeyButton())
{
// clicked outside the key buttons while rebinding -> cancel and restore
CancelRebind();
return;
}
}
}
if (!isRebinding) return;
// detect any keydown by iterating KeyCode values
foreach (KeyCode kc in Enum.GetValues(typeof(KeyCode)))
{
// ignore mouse buttons and joystick buttons
if (kc >= KeyCode.Mouse0 && kc <= KeyCode.Mouse6) continue;
if (kc >= KeyCode.JoystickButton0 && kc <= KeyCode.Joystick8Button19) continue;
if (Input.GetKeyDown(kc))
{
HandleRebindKey(kc);
break;
}
}
}
private bool IsPointerOverAnyKeyButton()
{
if (EventSystem.current == null) return false;
var pointerData = new PointerEventData(EventSystem.current) { position = Input.mousePosition };
var results = new List<RaycastResult>();
EventSystem.current.RaycastAll(pointerData, results);
if (results.Count == 0) return false;
foreach (var res in results)
{
var go = res.gameObject;
// check if this gameobject is one of the keyButtons or a child of one
for (int i = 0; i < keyButtons.Length; i++)
{
var btn = keyButtons[i];
if (btn == null) continue;
if (go == btn.gameObject || go.transform.IsChildOf(btn.transform))
return true;
}
}
return false;
}
private void OnKeyButtonClicked(int index)
{
if (isRebinding) return; // ignore while another rebind in progress
if (index < 0 || index >= colors.Length) return;
isRebinding = true;
rebindingIndex = index;
// remember previous key so we can restore if user cancels
previousKey = KeyBindingManager.GetKeyForColor(colors[index]);
previousKeyValid = true;
// start blinking underscore on the clicked button
blinkCoroutine = StartCoroutine(BlinkUnderscore(index));
// show persistent prompt
ShowPersistentMessage("按下新按键", messageErrorColor);
}
private IEnumerator BlinkUnderscore(int index)
{
Text txt = GetButtonText(index);
if (txt == null) yield break;
while (isRebinding && rebindingIndex == index)
{
txt.text = "_";
yield return new WaitForSeconds(0.5f);
txt.text = "";
yield return new WaitForSeconds(0.25f);
}
// restore label when finished
RefreshButtonLabel(index);
}
private void HandleRebindKey(KeyCode key)
{
// ignore ESC
if (key == KeyCode.Escape)
{
CancelRebind();
return;
}
// check if key already assigned to another color
for (int i = 0; i < colors.Length; i++)
{
if (i == rebindingIndex) continue;
KeyCode existing = KeyBindingManager.GetKeyForColor(colors[i]);
if (existing == key)
{
// conflict: reject and keep waiting
string msg = $"按键冲突:{KeyBindingManager.GetDisplayName(key)}已被分配给按键{i+1}。";
ShowMessage(msg, messageErrorColor, 2f);
StartCoroutine(FlashButtonInvalid(rebindingIndex));
return;
}
}
// accept binding
KeyBindingManager.ChangeKeyBinding(colors[rebindingIndex], key);
// update UI labels
RefreshButtonLabel(rebindingIndex);
InputManager.Instance?.RefreshKeyLabels();
// clear persistent prompt and show success message
ClearMessage();
ShowMessage("按键设置已保存", messageSuccessColor, 2f);
// finish
previousKeyValid = false;
previousKey = KeyCode.None;
StopRebind();
}
private void ShowMessage(string message, Color color, float duration = 2f)
{
if (errorText == null) return;
if (messageCoroutine != null) StopCoroutine(messageCoroutine);
messageCoroutine = StartCoroutine(ShowMessageCoroutine(message, color, duration));
}
private IEnumerator ShowMessageCoroutine(string message, Color color, float duration)
{
errorText.text = message;
errorText.color = color;
float t = 0f;
while (t < duration)
{
t += Time.deltaTime;
yield return null;
}
errorText.text = string.Empty;
messageCoroutine = null;
}
// Show a persistent message (until explicitly cleared)
private void ShowPersistentMessage(string message, Color color)
{
if (errorText == null) return;
// stop any timed message
if (messageCoroutine != null)
{
StopCoroutine(messageCoroutine);
messageCoroutine = null;
}
errorText.text = message;
errorText.color = color;
}
// Clear any displayed message immediately
private void ClearMessage()
{
if (messageCoroutine != null)
{
StopCoroutine(messageCoroutine);
messageCoroutine = null;
}
if (errorText != null) errorText.text = string.Empty;
}
private IEnumerator FlashButtonInvalid(int index)
{
Button b = GetButton(index);
if (b == null) yield break;
Color original = b.image.color;
b.image.color = Color.red;
yield return new WaitForSeconds(0.35f);
b.image.color = original;
}
private void CancelRebind()
{
// restore previous key if we have one
if (previousKeyValid && rebindingIndex >= 0 && rebindingIndex < colors.Length)
{
KeyBindingManager.ChangeKeyBinding(colors[rebindingIndex], previousKey);
InputManager.Instance?.RefreshKeyLabels();
ClearMessage();
ShowMessage("按键设置已保存", messageSuccessColor, 2f);
}
previousKeyValid = false;
previousKey = KeyCode.None;
StopRebind();
}
private void StopRebind()
{
isRebinding = false;
rebindingIndex = -1;
if (blinkCoroutine != null)
{
StopCoroutine(blinkCoroutine);
blinkCoroutine = null;
}
RefreshButtonLabels();
}
private void RefreshButtonLabels()
{
for (int i = 0; i < colors.Length; i++)
{
RefreshButtonLabel(i);
}
}
// update RefreshButtonLabel to use display name
private void RefreshButtonLabel(int index)
{
Text txt = GetButtonText(index);
if (txt == null) return;
KeyCode k = KeyBindingManager.GetKeyForColor(colors[index]);
txt.text = KeyBindingManager.GetDisplayName(k);
}
private Button GetButton(int index)
{
if (keyButtons == null) return null;
if (index < 0 || index >= keyButtons.Length) return null;
return keyButtons[index];
}
private Text GetButtonText(int index)
{
Button b = GetButton(index);
if (b == null) return null;
Text t = b.GetComponentInChildren<Text>();
return t;
}
private void OnNoteSpeedSliderChanged(float value)
{
UpdateNoteSpeedText(value);
// save on every change
SaveNoteSpeedValue(value);
}
private void OnNoteSpeedSliderPointerUp()
{
if (noteSpeedMultipler_slider != null)
{
float value = noteSpeedMultipler_slider.value;
PlayerPrefs.SetFloat(NoteSpeedPrefKey, value);
PlayerPrefs.Save();
}
}
private void UpdateNoteSpeedText(float value)
{
if (noteSpeedMultipler_valueText != null)
{
noteSpeedMultipler_valueText.text = value.ToString("F3");
}
}
private void SaveNoteSpeedValue(float value)
{
float v = Mathf.Clamp(value, NoteSpeedMin, NoteSpeedMax);
PlayerPrefs.SetFloat(NoteSpeedPrefKey, v);
PlayerPrefs.Save();
}
private void ChangeNoteSpeedBy(float delta)
{
if (noteSpeedMultipler_slider == null) return;
float cur = noteSpeedMultipler_slider.value;
float step = StepAmount;
float newVal = cur;
const float eps = 1e-5f;
if (delta > 0f)
{
float ceilStep = Mathf.Ceil(cur / step) * step;
if (Mathf.Abs(cur - ceilStep) < eps)
{
newVal = Mathf.Min(cur + step, NoteSpeedMax);
}
else
{
newVal = Mathf.Min(ceilStep, NoteSpeedMax);
}
}
else if (delta < 0f)
{
float floorStep = Mathf.Floor(cur / step) * step;
if (Mathf.Abs(cur - floorStep) < eps)
{
newVal = Mathf.Max(cur - step, NoteSpeedMin);
}
else
{
newVal = Mathf.Max(floorStep, NoteSpeedMin);
}
}
newVal = Mathf.Clamp(newVal, NoteSpeedMin, NoteSpeedMax);
// round to 3 decimals for display and consistency
newVal = (float)System.Math.Round(newVal, 3);
noteSpeedMultipler_slider.value = newVal; // will trigger OnNoteSpeedSliderChanged and save
}
private void ResetNoteSpeed()
{
if (noteSpeedMultipler_slider == null) return;
noteSpeedMultipler_slider.value = 1.0f; // triggers change and save
SaveNoteSpeedValue(1.0f);
UpdateNoteSpeedText(1.0f);
}
private void AddButtonContinuousEvents(Button btn, float delta)
{
EventTrigger trigger = btn.gameObject.GetComponent<EventTrigger>();
if (trigger == null) trigger = btn.gameObject.AddComponent<EventTrigger>();
// PointerDown -> start continuous change
var downEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerDown };
downEntry.callback.AddListener((data) => { StartContinuousChange(delta); });
trigger.triggers.Add(downEntry);
// PointerUp -> stop continuous change
var upEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerUp };
upEntry.callback.AddListener((data) => { StopContinuousChange(); });
trigger.triggers.Add(upEntry);
// Also stop on PointerExit in case cursor leaves button while pressed
var exitEntry = new EventTrigger.Entry { eventID = EventTriggerType.PointerExit };
exitEntry.callback.AddListener((data) => { StopContinuousChange(); });
trigger.triggers.Add(exitEntry);
}
private void StartContinuousChange(float delta)
{
// perform immediate single step
ChangeNoteSpeedBy(delta);
if (continuousChangeCoroutine != null) StopCoroutine(continuousChangeCoroutine);
continuousChangeCoroutine = StartCoroutine(ContinuousChangeRoutine(delta));
}
private void StopContinuousChange()
{
if (continuousChangeCoroutine != null)
{
StopCoroutine(continuousChangeCoroutine);
continuousChangeCoroutine = null;
}
}
private IEnumerator ContinuousChangeRoutine(float delta)
{
yield return new WaitForSeconds(ContinuousInitialDelay);
while (true)
{
ChangeNoteSpeedBy(delta);
yield return new WaitForSeconds(ContinuousRepeatRate);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 47a45212060d63a4f98f13e30622253a
+471
View File
@@ -0,0 +1,471 @@
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using System;
using System.Runtime.InteropServices;
public class graphicSettings : MonoBehaviour
{
public Dropdown screenMode_Dropdown;
public Dropdown resolution_Dropdown;
public Dropdown frameRate_Dropdown;
public Image resolutionLock_Image;
private List<Vector2Int> availableResolutions = new List<Vector2Int>();
private int tempFreeResOptionIndex = -1;
private Vector2Int lastScreenSize;
void Start()
{
InitScreenModeDropdown();
InitResolutionDropdown();
InitFrameRateDropdown();
lastScreenSize = new Vector2Int(Screen.width, Screen.height);
}
void OnDestroy()
{
if (screenMode_Dropdown != null)
screenMode_Dropdown.onValueChanged.RemoveListener(OnScreenModeChanged);
if (resolution_Dropdown != null)
resolution_Dropdown.onValueChanged.RemoveListener(OnResolutionChanged);
if (frameRate_Dropdown != null)
frameRate_Dropdown.onValueChanged.RemoveListener(OnFrameRateChanged);
}
void Update()
{
if (Screen.width != lastScreenSize.x || Screen.height != lastScreenSize.y)
{
lastScreenSize.x = Screen.width;
lastScreenSize.y = Screen.height;
OnUserResolutionChanged(new Vector2Int(Screen.width, Screen.height));
}
}
private void OnUserResolutionChanged(Vector2Int newSize)
{
if (Screen.fullScreenMode != FullScreenMode.Windowed)
return;
int match = availableResolutions.FindIndex(v => v.x == newSize.x && v.y == newSize.y);
if (match >= 0)
{
RemoveTempFreeOptionIfExists();
resolution_Dropdown.SetValueWithoutNotify(match);
}
else
{
EnsureTempFreeOptionExists();
resolution_Dropdown.SetValueWithoutNotify(tempFreeResOptionIndex);
}
}
private void EnsureTempFreeOptionExists()
{
if (resolution_Dropdown == null) return;
if (tempFreeResOptionIndex >= 0) return;
var opt = new Dropdown.OptionData("自由分辨率");
resolution_Dropdown.options.Add(opt);
tempFreeResOptionIndex = resolution_Dropdown.options.Count - 1;
}
private void RemoveTempFreeOptionIfExists()
{
if (resolution_Dropdown == null) return;
if (tempFreeResOptionIndex < 0) return;
if (tempFreeResOptionIndex < resolution_Dropdown.options.Count)
resolution_Dropdown.options.RemoveAt(tempFreeResOptionIndex);
tempFreeResOptionIndex = -1;
}
private void InitScreenModeDropdown()
{
if (screenMode_Dropdown == null) return;
var options = new List<string>() { "窗口(自由窗口)", "全屏(自适应)", "无边框窗口" };
screenMode_Dropdown.ClearOptions();
screenMode_Dropdown.AddOptions(options);
int current = MapFullScreenModeToIndex(Screen.fullScreenMode);
if (PlayerPrefs.HasKey("screenMode"))
{
int saved = PlayerPrefs.GetInt("screenMode", current);
saved = Mathf.Clamp(saved, 0, options.Count - 1);
current = saved;
ApplyScreenMode(current);
}
screenMode_Dropdown.SetValueWithoutNotify(current);
screenMode_Dropdown.onValueChanged.AddListener(OnScreenModeChanged);
}
private int MapFullScreenModeToIndex(FullScreenMode mode)
{
switch (mode)
{
case FullScreenMode.Windowed:
return 0;
case FullScreenMode.ExclusiveFullScreen:
return 1;
case FullScreenMode.FullScreenWindow:
return 2;
case FullScreenMode.MaximizedWindow:
return 0;
default:
return 0;
}
}
private void OnScreenModeChanged(int index)
{
ApplyScreenMode(index);
PlayerPrefs.SetInt("screenMode", index);
PlayerPrefs.Save();
bool isWindowed = (index == 0);
if (resolutionLock_Image != null)
resolutionLock_Image.gameObject.SetActive(!isWindowed);
if (resolution_Dropdown != null)
resolution_Dropdown.interactable = isWindowed;
}
private void ApplyScreenMode(int index)
{
try
{
if (index == 0)
{
Screen.fullScreenMode = FullScreenMode.Windowed;
Screen.fullScreen = false;
StopAllCoroutines();
StartCoroutine(ApplyWindowStyleNextFrame(true));
Screen.SetResolution(Screen.width, Screen.height, FullScreenMode.Windowed);
}
else if (index == 1)
{
Screen.fullScreenMode = FullScreenMode.ExclusiveFullScreen;
Screen.fullScreen = true;
Resolution res = Screen.currentResolution;
Screen.SetResolution(res.width, res.height, FullScreenMode.ExclusiveFullScreen, res.refreshRate);
}
else if (index == 2)
{
Screen.fullScreenMode = FullScreenMode.FullScreenWindow;
Screen.fullScreen = true;
Resolution res = Screen.currentResolution;
Screen.SetResolution(res.width, res.height, FullScreenMode.FullScreenWindow, res.refreshRate);
StopAllCoroutines();
StartCoroutine(ApplyWindowStyleNextFrame(false));
}
}
catch (System.Exception ex)
{
Debug.LogWarning($"ApplyScreenMode failed: {ex}");
}
}
private System.Collections.IEnumerator ApplyWindowStyleNextFrame(bool enable)
{
yield return null;
yield return new WaitForSeconds(0.05f);
EnableWindowedModeResizable(enable);
}
private void EnableWindowedModeResizable(bool enable)
{
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
try
{
IntPtr hWnd = GetForegroundWindow();
if (hWnd == IntPtr.Zero) return;
const int GWL_STYLE = -16;
const int WS_OVERLAPPEDWINDOW = unchecked((int)0x00CF0000);
const int WS_POPUP = unchecked((int)0x80000000);
if (IntPtr.Size == 8)
{
long style = GetWindowLongPtr64(hWnd, GWL_STYLE);
if (enable)
{
style &= ~((long)WS_POPUP);
style |= WS_OVERLAPPEDWINDOW;
}
else
{
style &= ~((long)WS_OVERLAPPEDWINDOW);
style |= (long)WS_POPUP;
}
SetWindowLongPtr64(hWnd, GWL_STYLE, style);
SetWindowPos(hWnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
}
else
{
int style = GetWindowLong32(hWnd, GWL_STYLE);
if (enable)
{
style &= ~WS_POPUP;
style |= WS_OVERLAPPEDWINDOW;
}
else
{
style &= ~WS_OVERLAPPEDWINDOW;
style |= WS_POPUP;
}
SetWindowLong32(hWnd, GWL_STYLE, style);
SetWindowPos(hWnd, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
}
}
catch (Exception e)
{
Debug.LogWarning($"EnableWindowedModeResizable failed: {e}");
}
#endif
}
private void InitResolutionDropdown()
{
if (resolution_Dropdown == null) return;
var candidates = new List<Vector2Int>() {
new Vector2Int(1024, 576),
new Vector2Int(1152, 648),
new Vector2Int(1280, 720),
new Vector2Int(1366, 768),
new Vector2Int(1600, 900),
new Vector2Int(1920, 1080),
new Vector2Int(2560, 1440),
new Vector2Int(3440, 1440),
new Vector2Int(3200, 1800),
new Vector2Int(3840, 2160),
new Vector2Int(5120, 2880),
new Vector2Int(7680, 4320),
new Vector2Int(15360, 8640)
};
int maxW = Screen.currentResolution.width;
int maxH = Screen.currentResolution.height;
const int MIN_W = 1024;
const int MIN_H = 720;
const int MAX_DIM = 16384;
availableResolutions.Clear();
var options = new List<string>();
foreach (var c in candidates)
{
if (c.x < MIN_W || c.y < MIN_H) continue;
if (c.x > MAX_DIM || c.y > MAX_DIM) continue;
if (c.x > maxW || c.y > maxH) continue;
availableResolutions.Add(c);
}
if (availableResolutions.Count == 0)
{
var cur = new Vector2Int(Screen.width, Screen.height);
if (cur.x >= MIN_W && cur.y >= MIN_H)
availableResolutions.Add(cur);
}
foreach (var r in availableResolutions)
{
options.Add($"{r.x} × {r.y}");
}
resolution_Dropdown.ClearOptions();
resolution_Dropdown.AddOptions(options);
int selected = 0;
for (int i = 0; i < availableResolutions.Count; i++)
{
if (availableResolutions[i].x == Screen.width && availableResolutions[i].y == Screen.height)
{
selected = i;
break;
}
}
int savedResIndex = PlayerPrefs.GetInt("resolutionIndex", -2);
if (savedResIndex >= 0 && savedResIndex < availableResolutions.Count)
{
resolution_Dropdown.SetValueWithoutNotify(savedResIndex);
var sav = availableResolutions[savedResIndex];
Screen.SetResolution(sav.x, sav.y, Screen.fullScreenMode, Screen.currentResolution.refreshRate);
RemoveTempFreeOptionIfExists();
}
else if (savedResIndex == -1)
{
int cw = PlayerPrefs.GetInt("customResW", -1);
int ch = PlayerPrefs.GetInt("customResH", -1);
if (cw > 0 && ch > 0)
{
Screen.SetResolution(cw, ch, Screen.fullScreenMode, Screen.currentResolution.refreshRate);
EnsureTempFreeOptionExists();
resolution_Dropdown.options[tempFreeResOptionIndex].text = $"{cw} × {ch}";
resolution_Dropdown.SetValueWithoutNotify(tempFreeResOptionIndex);
}
else
{
resolution_Dropdown.SetValueWithoutNotify(selected);
}
}
else
{
resolution_Dropdown.SetValueWithoutNotify(selected);
}
resolution_Dropdown.onValueChanged.AddListener(OnResolutionChanged);
int screenMode = PlayerPrefs.GetInt("screenMode", MapFullScreenModeToIndex(Screen.fullScreenMode));
bool isWindowed = (screenMode == 0) || (Screen.fullScreenMode == FullScreenMode.Windowed);
if (resolutionLock_Image != null)
resolutionLock_Image.gameObject.SetActive(!isWindowed);
resolution_Dropdown.interactable = isWindowed;
}
private void SyncResolutionDropdownToCurrent()
{
if (resolution_Dropdown == null) return;
int idx = availableResolutions.FindIndex(v => v.x == Screen.width && v.y == Screen.height);
if (idx >= 0)
{
RemoveTempFreeOptionIfExists();
resolution_Dropdown.SetValueWithoutNotify(idx);
}
else
{
EnsureTempFreeOptionExists();
resolution_Dropdown.options[tempFreeResOptionIndex].text = $"{Screen.width} × {Screen.height}";
resolution_Dropdown.SetValueWithoutNotify(tempFreeResOptionIndex);
}
}
private void OnResolutionChanged(int index)
{
if (index < 0) return;
if (index >= availableResolutions.Count)
{
PlayerPrefs.SetInt("resolutionIndex", -1);
PlayerPrefs.SetInt("customResW", Screen.width);
PlayerPrefs.SetInt("customResH", Screen.height);
PlayerPrefs.Save();
return;
}
if (index >= availableResolutions.Count) return;
var r = availableResolutions[index];
FullScreenMode mode = Screen.fullScreenMode;
int refresh = Screen.currentResolution.refreshRate;
Screen.SetResolution(r.x, r.y, mode, refresh);
PlayerPrefs.SetInt("resolutionIndex", index);
PlayerPrefs.Save();
RemoveTempFreeOptionIfExists();
if (mode == FullScreenMode.Windowed)
{
StopAllCoroutines();
StartCoroutine(ApplyWindowStyleNextFrame(true));
}
}
private void InitFrameRateDropdown()
{
if (frameRate_Dropdown == null) return;
var fpsOptions = new List<string>() { "24", "30", "60", "90", "120", "144", "165", "210", "240", "300", "无限制", "垂直同步" };
frameRate_Dropdown.ClearOptions();
frameRate_Dropdown.AddOptions(fpsOptions);
// Determine current setting
int saved = PlayerPrefs.GetInt("frameRateIndex", -999);
int selectIndex = 2; // default to 60
if (saved != -999 && saved >= 0 && saved < fpsOptions.Count)
{
selectIndex = saved;
}
else
{
if (QualitySettings.vSyncCount > 0)
selectIndex = fpsOptions.Count - 1; // VSync index
else
{
int current = Application.targetFrameRate;
if (current <= 0)
selectIndex = fpsOptions.Count - 2; // unlimited
else
{
// find nearest match
int[] candidates = new int[] { 24, 30, 60, 90, 120, 144, 165, 210, 240, 300 };
int best = 0; int bestDiff = int.MaxValue;
for (int i = 0; i < candidates.Length; i++)
{
int d = Math.Abs(candidates[i] - current);
if (d < bestDiff) { bestDiff = d; best = i; }
}
selectIndex = best;
}
}
}
frameRate_Dropdown.SetValueWithoutNotify(selectIndex);
frameRate_Dropdown.onValueChanged.AddListener(OnFrameRateChanged);
// apply selection
ApplyFrameRateByIndex(selectIndex);
}
private void OnFrameRateChanged(int index)
{
ApplyFrameRateByIndex(index);
PlayerPrefs.SetInt("frameRateIndex", index);
PlayerPrefs.Save();
}
private void ApplyFrameRateByIndex(int index)
{
if (index < 0) return;
int[] fpsValues = new int[] { 24, 30, 60, 90, 120, 144, 165, 210, 240, 300 };
if (index < fpsValues.Length)
{
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = fpsValues[index];
}
else if (index == fpsValues.Length)
{
// 无限制
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = -1; // unlimited / platform default
}
else if (index == fpsValues.Length + 1)
{
// 垂直同步
QualitySettings.vSyncCount = 1; // enable vsync
Application.targetFrameRate = -1;
}
}
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", EntryPoint = "GetWindowLong")]
private static extern int GetWindowLong32(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", EntryPoint = "SetWindowLong")]
private static extern int SetWindowLong32(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
private static extern long GetWindowLongPtr64(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
private static extern long SetWindowLongPtr64(IntPtr hWnd, int nIndex, long dwNewLong);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOZORDER = 0x0004;
private const uint SWP_FRAMECHANGED = 0x0020;
#endif
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f0d532c98d6ca9a4ea5a2bd80cc4f193
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 32d5540b04c3c3d41909f074d774d8bc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
using UnityEngine;
public class offsetDeterminer : MonoBehaviour
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 28e4b9a4015da4847a8e725526e80be9