加入了浮动小游戏功能和设置界面
运行时请手动修改运行库目录fdBrowser
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26d60afd151e3004fb6b19bce29ecb85
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 526806832437a7f44954fa4a2b79718c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user