浮动小游戏已经基本搞好

新做了两个设置界面
一些bug修复和优化
This commit is contained in:
FloatGaming
2025-12-28 04:55:18 +08:00
parent eb9b6ba78a
commit 0963263ff4
557 changed files with 296886 additions and 487 deletions
+14733 -205
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5d91e03c8efeb234d9459bb72c7c1b63
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+68
View File
@@ -0,0 +1,68 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!241 &24100000
AudioMixerController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: master
m_OutputGroup: {fileID: 0}
m_MasterGroup: {fileID: 24300002}
m_Snapshots:
- {fileID: 24500006}
m_StartSnapshot: {fileID: 24500006}
m_SuspendThreshold: -80
m_EnableSuspend: 1
m_UpdateMode: 0
m_ExposedParameters: []
m_AudioMixerGroupViews:
- guids:
- 1529ad54cf74b5f4985b087ec1fd1e7f
name: View
m_CurrentViewIndex: 0
m_TargetSnapshot: {fileID: 24500006}
--- !u!243 &24300002
AudioMixerGroupController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Master
m_AudioMixer: {fileID: 24100000}
m_GroupID: 1529ad54cf74b5f4985b087ec1fd1e7f
m_Children: []
m_Volume: c8c70230870be5e48bb845dfafee9376
m_Pitch: 6b6b95e31a7b02a49938315306b8e45e
m_Send: 00000000000000000000000000000000
m_Effects:
- {fileID: 24400004}
m_UserColorIndex: 0
m_Mute: 0
m_Solo: 0
m_BypassEffects: 0
--- !u!244 &24400004
AudioMixerEffectController:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_EffectID: 12a08001bed866844be3ec0b9da00f48
m_EffectName: Attenuation
m_MixLevel: c7e75a4c3776e1c4b8b0a5cad989b591
m_Parameters: []
m_SendTarget: {fileID: 0}
m_EnableWetMix: 0
m_Bypass: 0
--- !u!245 &24500006
AudioMixerSnapshotController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Snapshot
m_AudioMixer: {fileID: 24100000}
m_SnapshotID: d04e357e5e5d8844ebe07f1d3039c651
m_FloatValues: {}
m_TransitionOverrides: {}
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1ec6b149cf654494d8d97065d8f2ccac
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 24100000
userData:
assetBundleName:
assetBundleVariant:
+440 -7
View File
@@ -3,6 +3,9 @@ using System.IO;
using UnityEngine;
using System;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Text.RegularExpressions;
using Debug = UnityEngine.Debug;
@@ -29,6 +32,30 @@ public class WebViewLauncher : MonoBehaviour
// Event invoked when web process main window handle becomes available
public event Action WebViewReady;
[Header("Control Buttons (optional)")]
public Button refreshButton; // 刷新当前webview页面
public Button closeButton; // 关闭webview
public Button restartButton; // 重启webview
public Button backButton; // 返回
public Button forwardButton; // 前进
public Button startRestartButton; // 新增:根据运行状态启动或重启 webview2
public Button forceKillButton; // 强制结束进程按钮
public Button hideWindowButton; // 隐藏webview窗口
public Button showWindowButton; // 显示webview窗口
[Header("Autostart")]
public UnityEngine.UI.Toggle autostartToggle; // UI toggle to control auto-start
private bool autostart = true;
private const string PREF_AUTOSTART = "WebView_AutoStart";
[Header("Homepage / URL UI")]
public Button homepageButton; // 返回主页
public InputField urlInputField; // 显示/输入当前 url
[Tooltip("Homepage URL (can be file:// or http(s) or local path)")]
public string homepageUrl = "";
[Header("Go button")]
public Button goButton; // 点击跳转,和回车效果一致
void Awake()
{
if (Instance != null)
@@ -38,11 +65,52 @@ public class WebViewLauncher : MonoBehaviour
}
Instance = this;
DontDestroyOnLoad(gameObject);
// wire up optional UI buttons
if (refreshButton != null) refreshButton.onClick.AddListener(RefreshWebView);
if (closeButton != null) closeButton.onClick.AddListener(CloseWebView);
if (restartButton != null) restartButton.onClick.AddListener(RestartWebView);
if (backButton != null) backButton.onClick.AddListener(GoBackWebView);
if (forwardButton != null) forwardButton.onClick.AddListener(GoForwardWebView);
if (startRestartButton != null) startRestartButton.onClick.AddListener(OnStartRestartButtonClicked);
if (forceKillButton != null) forceKillButton.onClick.AddListener(OnForceKillClicked);
if (hideWindowButton != null) hideWindowButton.onClick.AddListener(OnHideWindowClicked);
if (showWindowButton != null) showWindowButton.onClick.AddListener(OnShowWindowClicked);
if (homepageButton != null) homepageButton.onClick.AddListener(OnHomepageClicked);
if (urlInputField != null) urlInputField.onEndEdit.AddListener(OnUrlInputEndEdit);
if (goButton != null) goButton.onClick.AddListener(OnGoButtonClicked);
// Load autostart preference and bind toggle
autostart = PlayerPrefs.GetInt(PREF_AUTOSTART, 1) == 1;
if (autostartToggle != null)
{
autostartToggle.isOn = autostart;
autostartToggle.onValueChanged.AddListener(OnAutostartToggleChanged);
}
// set default homepage if empty
if (string.IsNullOrEmpty(homepageUrl))
{
#if UNITY_EDITOR
string candidate = Path.Combine(h5EditorPath, "_officialDocs", "openings", "index.html");
if (File.Exists(candidate))
homepageUrl = new Uri(candidate).AbsoluteUri;
else
homepageUrl = startUrl;
#else
string candidate = Path.Combine(Application.streamingAssetsPath, "h5LG", "_officialDocs", "openings", "index.html");
if (File.Exists(candidate))
homepageUrl = new Uri(candidate).AbsoluteUri;
else
homepageUrl = startUrl;
#endif
}
}
void Start()
{
// 扫描 H5 文件夹
// Ensure H5 root exists
string h5Folder = GetH5FolderPath();
if (!Directory.Exists(h5Folder))
{
@@ -50,12 +118,28 @@ public class WebViewLauncher : MonoBehaviour
Debug.Log("Created H5 folder: " + h5Folder);
}
// Prefer explicit openings index as startup page when available
string preferred = Path.Combine(h5Folder, "_officialDocs", "openings", "index.html");
if (File.Exists(preferred))
{
startUrl = new Uri(preferred).AbsoluteUri;
Debug.Log("Using preferred startup page: " + startUrl);
// show initial url in input field
if (urlInputField != null)
urlInputField.text = GetDisplayUrl(startUrl);
if (autostart) LaunchWebView();
return;
}
// 扫描 H5 文件夹
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");
string candidatePath = Path.Combine(h5Folder, selectedFolder, "index.html");
startUrl = new Uri(candidatePath).AbsoluteUri; // ensure proper file:/// URI
Debug.Log("Selected H5 folder: " + selectedFolder + ", URL: " + startUrl);
}
else
@@ -63,19 +147,87 @@ public class WebViewLauncher : MonoBehaviour
Debug.LogWarning("No H5 subfolders with index.html found in: " + h5Folder);
}
LaunchWebView();
// show initial url in input field if available (use shortened display for local files that are indexed)
if (urlInputField != null)
{
urlInputField.text = GetDisplayUrl(startUrl);
}
if (autostart) LaunchWebView();
}
void OnDestroy()
{
KillWebView();
// remove listeners
if (refreshButton != null) refreshButton.onClick.RemoveListener(RefreshWebView);
if (closeButton != null) closeButton.onClick.RemoveListener(CloseWebView);
if (restartButton != null) restartButton.onClick.RemoveListener(RestartWebView);
if (backButton != null) backButton.onClick.RemoveListener(GoBackWebView);
if (forwardButton != null) forwardButton.onClick.RemoveListener(GoForwardWebView);
if (startRestartButton != null) startRestartButton.onClick.RemoveListener(OnStartRestartButtonClicked);
if (forceKillButton != null) forceKillButton.onClick.RemoveListener(OnForceKillClicked);
if (hideWindowButton != null) hideWindowButton.onClick.RemoveListener(OnHideWindowClicked);
if (showWindowButton != null) showWindowButton.onClick.RemoveListener(OnShowWindowClicked);
if (autostartToggle != null) autostartToggle.onValueChanged.RemoveListener(OnAutostartToggleChanged);
if (homepageButton != null) homepageButton.onClick.RemoveListener(OnHomepageClicked);
if (urlInputField != null) urlInputField.onEndEdit.RemoveListener(OnUrlInputEndEdit);
if (goButton != null) goButton.onClick.RemoveListener(OnGoButtonClicked);
}
void Update()
{
// if input field focused and Enter pressed -> navigate
if (urlInputField != null && urlInputField.isFocused && (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter)))
{
string text = urlInputField.text;
if (!string.IsNullOrEmpty(text))
{
OpenUrl(text);
// remove focus
EventSystem.current?.SetSelectedGameObject(null);
urlInputField.DeactivateInputField();
}
}
// clicking elsewhere should unfocus input field (end edit)
if (Input.GetMouseButtonDown(0) && urlInputField != null && urlInputField.isFocused)
{
var sel = EventSystem.current != null ? EventSystem.current.currentSelectedGameObject : null;
if (sel != urlInputField.gameObject)
{
urlInputField.DeactivateInputField();
EventSystem.current?.SetSelectedGameObject(null);
}
}
}
void OnGoButtonClicked()
{
if (urlInputField == null) return;
string text = urlInputField.text;
if (!string.IsNullOrEmpty(text))
{
OpenUrl(text);
EventSystem.current?.SetSelectedGameObject(null);
urlInputField.DeactivateInputField();
}
}
public void KillWebView()
{
if (webProcess != null && !webProcess.HasExited)
{
webProcess.Kill();
try
{
webProcess.Kill();
}
catch (Exception e)
{
Debug.LogWarning("KillWebView failed: " + e.Message);
}
webProcess = null;
}
}
@@ -169,10 +321,24 @@ public class WebViewLauncher : MonoBehaviour
string final;
try
{
if (Uri.TryCreate(url, UriKind.Absolute, out var parsed) && !string.IsNullOrEmpty(parsed.Scheme))
if (IsLikelyWebUrl(url))
{
// If it's already a file/http/https URI, use its AbsoluteUri
final = parsed.AbsoluteUri;
if (!Uri.TryCreate(url, UriKind.Absolute, out var parsed) || string.IsNullOrEmpty(parsed.Scheme))
{
final = "http://" + url;
}
else
{
final = parsed.AbsoluteUri;
}
}
else if (url.StartsWith("localhost::"))
{
// Map localhost::display back to actual file URI
string relativePath = url.Substring("localhost::".Length).Replace('/', Path.DirectorySeparatorChar);
string h5Folder = GetH5FolderPath();
string fullPath = Path.Combine(h5Folder, relativePath);
final = new Uri(fullPath).AbsoluteUri;
}
else
{
@@ -190,6 +356,10 @@ public class WebViewLauncher : MonoBehaviour
Debug.Log($"OpenUrl requested. original='{url}', normalized='{final}'");
startUrl = final;
// update input field display
if (urlInputField != null)
urlInputField.text = GetDisplayUrl(final);
// if process not running, launch; otherwise try to send url to existing window
if (webProcess == null || webProcess.HasExited)
{
@@ -222,6 +392,160 @@ public class WebViewLauncher : MonoBehaviour
// after relaunch, WaitForMainWindowHandle will invoke WebViewReady when handle ready
}
// Try to send a simple command string to the webview process via WM_COPYDATA. Returns true when SendMessage returned non-zero.
bool TrySendCommand(string cmd)
{
if (webProcess == null || webProcess.HasExited)
{
Debug.LogWarning("TrySendCommand: webProcess not running");
return false;
}
bool sent = WebViewWin32.TrySendUrlToWindow(webProcess, cmd);
Debug.Log($"TrySendCommand('{cmd}') returned: {sent}");
return sent;
}
// UI control methods
public void RefreshWebView()
{
// Try sending a refresh command; fallback to restarting to refresh
if (TrySendCommand("CMD:REFRESH"))
{
Debug.Log("Sent REFRESH command to webview");
return;
}
Debug.Log("REFRESH command not supported, restarting webview to refresh");
KillWebView();
LaunchWebView();
}
public void CloseWebView()
{
Debug.Log("Closing webview process");
KillWebView();
}
public void RestartWebView()
{
Debug.Log("Restarting webview process");
KillWebView();
LaunchWebView();
}
public void GoBackWebView()
{
if (TrySendCommand("CMD:BACK"))
{
Debug.Log("Sent BACK command to webview");
return;
}
Debug.LogWarning("BACK command not supported by webview");
}
public void GoForwardWebView()
{
if (TrySendCommand("CMD:FORWARD"))
{
Debug.Log("Sent FORWARD command to webview");
return;
}
Debug.LogWarning("FORWARD command not supported by webview");
}
void OnHomepageClicked()
{
// Always navigate to StreamingAssets/h5LG/_officialDocs/openings/index.html
string candidate = Path.Combine(Application.streamingAssetsPath, "h5LG", "_officialDocs", "openings", "index.html");
if (!File.Exists(candidate))
{
Debug.LogWarning("Homepage index not found at: " + candidate + ". Falling back to homepageUrl or startUrl.");
if (!string.IsNullOrEmpty(homepageUrl))
{
LaunchWebViewWithUrl(homepageUrl);
}
else
{
LaunchWebViewWithUrl(startUrl);
}
return;
}
string fileUri = new Uri(candidate).AbsoluteUri;
LaunchWebViewWithUrl(fileUri);
// OpenUrl will update the input field using GetDisplayUrl(final). Ensure display shows the shortened localhost form.
if (urlInputField != null)
urlInputField.text = GetDisplayUrl(fileUri);
}
void OnUrlInputEndEdit(string text)
{
if (string.IsNullOrEmpty(text)) return;
OpenUrl(text);
}
void OnStartRestartButtonClicked()
{
if (webProcess == null || webProcess.HasExited)
{
Debug.Log("WebView process not running. Starting webview.");
StartWebView();
}
else
{
Debug.Log("WebView process running. Restarting webview.");
RestartWebView();
}
}
void OnForceKillClicked()
{
Debug.Log("Force killing webview process");
KillWebView();
}
void OnHideWindowClicked()
{
if (webProcess == null || webProcess.HasExited)
{
Debug.LogWarning("HideWindow: webview process not running");
return;
}
bool ok = WebViewWin32.TryShowWindow(webProcess, false);
Debug.Log("HideWindow clicked, result: " + ok);
}
void OnShowWindowClicked()
{
if (webProcess == null || webProcess.HasExited)
{
Debug.LogWarning("ShowWindow: webview process not running");
return;
}
bool ok = WebViewWin32.TryShowWindow(webProcess, true);
Debug.Log("ShowWindow clicked, result: " + ok);
}
public void StartWebView()
{
LaunchWebView();
}
public void StopWebView()
{
KillWebView();
}
void OnAutostartToggleChanged(bool value)
{
autostart = value;
PlayerPrefs.SetInt(PREF_AUTOSTART, autostart ? 1 : 0);
PlayerPrefs.Save();
Debug.Log("Autostart toggled: " + autostart);
}
string GetExePath()
{
#if UNITY_EDITOR
@@ -298,4 +622,113 @@ public class WebViewLauncher : MonoBehaviour
return WebViewWin32.TrySetWindowRect(webProcess, x, y, w, h);
}
string GetDisplayUrl(string url)
{
if (string.IsNullOrEmpty(url))
return url;
try
{
Uri uri = new Uri(url);
if (uri.Scheme == Uri.UriSchemeFile)
{
string localPath = uri.LocalPath;
string h5Folder = GetH5FolderPath();
if (localPath.StartsWith(h5Folder, StringComparison.OrdinalIgnoreCase))
{
string relativePath = localPath.Substring(h5Folder.Length).TrimStart(Path.DirectorySeparatorChar);
return $"localhost::{relativePath.Replace(Path.DirectorySeparatorChar, '/')}";
}
}
}
catch (Exception e)
{
Debug.LogWarning("GetDisplayUrl: failed to parse url '" + url + "' : " + e.Message);
}
return url;
}
bool IsLikelyWebUrl(string url)
{
return Regex.IsMatch(url, @"^(https?:\/\/|www\.|[a-zA-Z0-9-]+\.[a-zA-Z]{2,})");
}
string NormalizeToAbsolute(string url)
{
if (string.IsNullOrEmpty(url))
{
Debug.LogWarning("NormalizeToAbsolute called with empty url");
return url;
}
try
{
if (Uri.TryCreate(url, UriKind.Absolute, out var parsed) && parsed.IsAbsoluteUri)
{
return parsed.AbsoluteUri;
}
if (url.StartsWith("localhost::"))
{
// Map localhost::display back to actual file URI
string relativePath = url.Substring("localhost::".Length).Replace('/', Path.DirectorySeparatorChar);
string h5Folder = GetH5FolderPath();
string fullPath = Path.Combine(h5Folder, relativePath);
return new Uri(fullPath).AbsoluteUri;
}
// Treat as local file path
string abs = Path.GetFullPath(url);
return new Uri(abs).AbsoluteUri; // yields file:///C:/...
}
catch (Exception e)
{
Debug.LogWarning("NormalizeToAbsolute: failed to normalize url '" + url + "' : " + e.Message);
return url;
}
}
void LaunchWebViewWithUrl(string url)
{
string finalUrl = NormalizeToAbsolute(url);
Debug.Log($"LaunchWebViewWithUrl requested. original='{url}', normalized='{finalUrl}'");
startUrl = finalUrl;
// update input field display
if (urlInputField != null)
urlInputField.text = GetDisplayUrl(finalUrl);
// 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: " + finalUrl);
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, finalUrl);
Debug.Log($"TrySendUrlToWindow returned: {sent}");
if (sent)
{
Debug.Log("Sent URL to existing WebView process: " + finalUrl);
// 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: " + finalUrl);
KillWebView();
LaunchWebView();
// after relaunch, WaitForMainWindowHandle will invoke WebViewReady when handle ready
}
}
+99
View File
@@ -21,6 +21,9 @@ public static class WebViewWin32
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
@@ -45,10 +48,26 @@ public static class WebViewWin32
public IntPtr lpData;
}
// SetWindowPos flags
const uint SWP_NOZORDER = 0x0004;
const uint SWP_NOACTIVATE = 0x0010;
const uint SWP_NOMOVE = 0x0002;
const uint SWP_NOSIZE = 0x0001;
// HWND insert-after special values
static readonly IntPtr HWND_TOP = new IntPtr(0);
static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
const uint WM_COPYDATA = 0x004A;
// ShowWindow commands
const int SW_HIDE = 0;
const int SW_SHOW = 5;
const int SW_MINIMIZE = 6;
const int SW_RESTORE = 9;
public static bool TrySetWindowRect(Process process, int x, int y, int width, int height)
{
if (process == null || process.HasExited)
@@ -74,6 +93,44 @@ public static class WebViewWin32
return true;
}
/// <summary>
/// Place the given process window directly above the target process window in Z-order
/// and set its position/size. This ensures the webview sits just above the Unity window.
/// </summary>
public static bool TrySetWindowRectAboveProcess(Process process, Process targetProcess, int x, int y, int width, int height)
{
if (process == null || process.HasExited || targetProcess == null || targetProcess.HasExited)
return false;
if (process.MainWindowHandle == IntPtr.Zero)
{
process.Refresh();
if (process.MainWindowHandle == IntPtr.Zero)
return false;
}
if (targetProcess.MainWindowHandle == IntPtr.Zero)
{
targetProcess.Refresh();
if (targetProcess.MainWindowHandle == IntPtr.Zero)
return false;
}
// Place process window after targetProcess window in Z-order (i.e. on top of target)
// Do not use SWP_NOZORDER so that z-order is changed.
bool ok = SetWindowPos(
process.MainWindowHandle,
targetProcess.MainWindowHandle,
x,
y,
width,
height,
SWP_NOACTIVATE
);
return ok;
}
public static bool TryGetWindowRect(Process process, out RECT rect)
{
rect = new RECT();
@@ -114,6 +171,48 @@ public static class WebViewWin32
return true;
}
/// <summary>
/// Try to show or hide the given process main window.
/// </summary>
public static bool TryShowWindow(Process process, bool show)
{
if (process == null || process.HasExited)
return false;
if (process.MainWindowHandle == IntPtr.Zero)
{
process.Refresh();
if (process.MainWindowHandle == IntPtr.Zero)
return false;
}
try
{
if (!show)
{
// Minimize instead of hide
return ShowWindow(process.MainWindowHandle, SW_MINIMIZE);
}
else
{
// Restore and bring to top
bool r = ShowWindow(process.MainWindowHandle, SW_RESTORE);
try
{
// bring to top without making it topmost
SetWindowPos(process.MainWindowHandle, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
catch { }
return r;
}
}
catch (Exception e)
{
UnityEngine.Debug.LogWarning("TryShowWindow failed: " + e.Message);
return false;
}
}
/// <summary>
/// 尝试向指定进程的主窗口发送 WM_COPYDATA 消息,lParam 为包含 Unicode 字符串的 COPYDATASTRUCT。
/// 接收方需要处理 WM_COPYDATA 并解释字符串为 URL 并导航。
@@ -2,6 +2,7 @@ using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System;
using System.IO;
public class game_card_Prefab : MonoBehaviour, IPointerClickHandler
{
@@ -9,6 +10,7 @@ public class game_card_Prefab : MonoBehaviour, IPointerClickHandler
public Image card_icon_image;
public Text card_name_text;
public Text authorName_text;
public Text versionCode_text;
public Text description_text;
public Text _isOfficial_text;
public Text source_text;
@@ -21,7 +23,7 @@ public class game_card_Prefab : MonoBehaviour, IPointerClickHandler
private string indexFilePath;
// 给 loadLittleGamesPrefab 调用以填充文本和路径
public void SetData(string indexPath, string cardName, string authorName, string description, string isOfficial, string source, string yyyyMmDd)
public void SetData(string indexPath, string cardName, string authorName, string description, string isOfficial, string source, string yyyyMmDd, string version = null)
{
indexFilePath = indexPath;
if (card_name_text != null) card_name_text.text = cardName ?? "";
@@ -31,7 +33,11 @@ public class game_card_Prefab : MonoBehaviour, IPointerClickHandler
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}'");
// version
string ver = string.IsNullOrEmpty(version) ? "0.0.0" : version;
if (versionCode_text != null) versionCode_text.text = ver;
UnityEngine.Debug.Log($"game_card_Prefab.SetData: indexPath='{indexFilePath}', card_name='{cardName}', author='{authorName}', isOfficial='{isOfficial}', source='{source}', version='{ver}'");
}
void Awake()
@@ -110,6 +116,21 @@ public class game_card_Prefab : MonoBehaviour, IPointerClickHandler
{
UnityEngine.Debug.Log("Calling WebViewLauncher.OpenUrl with: " + indexFilePath);
WebViewLauncher.Instance.OpenUrl(indexFilePath);
// After initiating navigation, set the input field display to localhost::parent/index.html
try
{
if (WebViewLauncher.Instance != null && WebViewLauncher.Instance.urlInputField != null)
{
string parent = Path.GetFileName(Path.GetDirectoryName(indexFilePath));
string display = $"localhost::{parent}/{Path.GetFileName(indexFilePath)}";
WebViewLauncher.Instance.urlInputField.text = display;
}
}
catch (Exception e)
{
UnityEngine.Debug.LogWarning("Failed to set url input display: " + e.Message);
}
}
else
{
@@ -120,6 +141,20 @@ public class game_card_Prefab : MonoBehaviour, IPointerClickHandler
{
UnityEngine.Debug.Log("Found launcher in scene, calling OpenUrl: " + indexFilePath);
launcherObj.OpenUrl(indexFilePath);
try
{
if (launcherObj.urlInputField != null)
{
string parent = Path.GetFileName(Path.GetDirectoryName(indexFilePath));
string display = $"localhost::{parent}/{Path.GetFileName(indexFilePath)}";
launcherObj.urlInputField.text = display;
}
}
catch (Exception e)
{
UnityEngine.Debug.LogWarning("Failed to set url input display: " + e.Message);
}
}
else
{
@@ -129,6 +164,20 @@ public class game_card_Prefab : MonoBehaviour, IPointerClickHandler
var launcher = go.AddComponent<WebViewLauncher>();
launcher.OpenUrl(indexFilePath);
GameObject.DontDestroyOnLoad(go);
try
{
if (launcher.urlInputField != null)
{
string parent = Path.GetFileName(Path.GetDirectoryName(indexFilePath));
string display = $"localhost::{parent}/{Path.GetFileName(indexFilePath)}";
launcher.urlInputField.text = display;
}
}
catch (Exception e)
{
UnityEngine.Debug.LogWarning("Failed to set url input display: " + e.Message);
}
}
}
}
@@ -34,8 +34,8 @@ RectTransform:
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_AnchoredPosition: {x: -12.6, y: 14.07}
m_SizeDelta: {x: 204.359, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7480212223569318349
CanvasRenderer:
@@ -316,6 +316,85 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &3596489648732047302
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6607639485689222063}
- component: {fileID: 552011013437464851}
- component: {fileID: 6022216317311205539}
m_Layer: 5
m_Name: version_code
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6607639485689222063
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3596489648732047302}
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: 105.7, y: 15.6}
m_SizeDelta: {x: 47.359, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &552011013437464851
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3596489648732047302}
m_CullTransparentMesh: 1
--- !u!114 &6022216317311205539
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3596489648732047302}
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: 10
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: 1.0.1
--- !u!1 &5110857998175197173
GameObject:
m_ObjectHideFlags: 0
@@ -429,6 +508,7 @@ RectTransform:
- {fileID: 4583698199465520998}
- {fileID: 604392950021451895}
- {fileID: 872492842950718926}
- {fileID: 6607639485689222063}
- {fileID: 2019306417107768352}
- {fileID: 2622440068852620012}
- {fileID: 2138782682665805939}
@@ -653,6 +733,7 @@ MonoBehaviour:
card_icon_image: {fileID: 6004099248657222439}
card_name_text: {fileID: 3389633104739560455}
authorName_text: {fileID: 9193153990832272737}
versionCode_text: {fileID: 6022216317311205539}
description_text: {fileID: 8118505406259206449}
_isOfficial_text: {fileID: 5166008895911254744}
source_text: {fileID: 4780041332280513338}
@@ -1,7 +1,9 @@
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using System.Collections.Generic;
using System.Xml;
using System.Text.RegularExpressions;
public class loadLittleGamesPrefab : MonoBehaviour
{
@@ -12,44 +14,66 @@ public class loadLittleGamesPrefab : MonoBehaviour
[Header("Prefab")]
public GameObject littleGames_card_Prefab;
[Tooltip("默认icon图")]
public Sprite default_card_icon_sprite;
[Header("Refresh")]
public Button refreshButton; // 点击触发重新扫描并刷新左侧列表
[Header("H5 Root Path (optional, leave empty to use WebViewLauncher settings)")]
public string h5RootPath;
void Awake()
{
// attach refresh listener if button provided
if (refreshButton != null)
{
refreshButton.onClick.AddListener(RefreshList);
}
}
void OnDestroy()
{
if (refreshButton != null)
refreshButton.onClick.RemoveListener(RefreshList);
}
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>
// Clear children of a content transform
void ClearContent(GameObject content)
{
if (content == null) return;
for (int i = content.transform.childCount - 1; i >= 0; i--)
{
var child = content.transform.GetChild(i).gameObject;
Destroy(child);
}
}
// Public method to allow external callers to refresh
public void RefreshList()
{
// clear all three contents then reload
ClearContent(officialDocs_scrollView_content);
ClearContent(officialGames_scrollView_content);
ClearContent(thirdPartyGames_scrollView_content);
LoadAllLittleGames();
}
public void LoadAllLittleGames()
{
string root = GetH5RootPath();
string root = ResolveH5RootPath();
if (string.IsNullOrEmpty(root) || !Directory.Exists(root))
{
Debug.LogWarning("H5 root path not found: " + root);
return;
}
// 目标一级子目录名称(与 WebViewLauncher 中保持一致)
string[] categories = { "_officialDocs", "_officialGames", "_thirdPartyGames" };
string[] categories = new string[] { "_officialDocs", "_officialGames", "_thirdPartyGames" };
foreach (string cat in categories)
{
@@ -57,95 +81,148 @@ public class loadLittleGamesPrefab : MonoBehaviour
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))
string indexHtml = Path.Combine(subdir, "index.html");
if (!File.Exists(indexHtml))
continue;
// read xml if exists
string xmlPath = Path.Combine(subdir, "info.xml");
Dictionary<string, string> meta = null;
if (File.Exists(xmlPath))
meta = ReadInfoXml(xmlPath);
// target content
GameObject target = GetTargetContent(cat);
if (target == null || littleGames_card_Prefab == null)
{
// 也可能 index.html 在更深一层,但按照约定通常是 subdir/index.html
Debug.LogWarning("Missing target content or prefab for category: " + cat);
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);
GameObject item = Instantiate(littleGames_card_Prefab, target.transform);
var card = item.GetComponent<game_card_Prefab>();
if (card == null)
continue;
// defaults
string defName = "未知Html文件";
string defAuthor = "未知作者";
string defDesc = "暂无相关描述信息";
string defIsOfficial = "未知来源";
string defSource = "自定义渠道";
string defDateRaw = "19700101";
string cardName = GetMeta(meta, "card_name") ?? defName;
string author = GetMeta(meta, "authorName") ?? defAuthor;
string desc = GetMeta(meta, "description") ?? defDesc;
string isOfficial = GetMeta(meta, "isOfficial");
string source = GetMeta(meta, "source");
string dateRaw = GetMeta(meta, "yyyy_mm_dd");
string cardIcon = GetMeta(meta, "card_icon");
string versionCode = GetMeta(meta, "versionCode");
if (string.IsNullOrEmpty(dateRaw)) dateRaw = defDateRaw;
if (cat == "_officialDocs" || cat == "_officialGames")
{
isOfficial = "官方";
source = "小游戏DLC";
}
else
{
if (string.IsNullOrEmpty(isOfficial)) isOfficial = "第三方来源";
if (string.IsNullOrEmpty(source)) source = defSource;
}
// 实例化 prefab
GameObject go = Instantiate(littleGames_card_Prefab, targetContent.transform);
// ensure no nulls
if (string.IsNullOrEmpty(cardName)) cardName = defName;
if (string.IsNullOrEmpty(author)) author = defAuthor;
if (string.IsNullOrEmpty(desc)) desc = defDesc;
if (string.IsNullOrEmpty(isOfficial)) isOfficial = defIsOfficial;
if (string.IsNullOrEmpty(source)) source = defSource;
// 设置数据(indexPath 使用本地文件绝对路径)
string absIndexPath = indexPath; // 使用绝对路径,WebViewLauncher.OpenUrl 会添加 file://
var prefabComp = go.GetComponent<game_card_Prefab>();
if (prefabComp != null)
string formattedDate = FormatDate(dateRaw);
card.SetData(indexHtml, cardName, author, desc, isOfficial, source, formattedDate, versionCode);
// Set default icon first (if provided)
if (card.card_icon_image != null && default_card_icon_sprite != null)
{
// 若来自官方分类,覆盖 isOfficial 与 source 字段为固定值
string finalIsOfficial = meta.GetValueOrDefault("isOfficial");
string finalSource = meta.GetValueOrDefault("source");
card.card_icon_image.sprite = default_card_icon_sprite;
card.card_icon_image.enabled = true;
}
if (cat == "_officialDocs" || cat == "_officialGames")
// load icon if specified (override default)
if (!string.IsNullOrEmpty(cardIcon))
{
string iconPath = cardIcon;
if (!Path.IsPathRooted(iconPath))
iconPath = Path.Combine(subdir, iconPath);
if (File.Exists(iconPath))
{
finalIsOfficial = "官方";
finalSource = "小游戏DLC";
try
{
byte[] bytes = File.ReadAllBytes(iconPath);
Texture2D tex = new Texture2D(2, 2);
if (tex.LoadImage(bytes))
{
Sprite sp = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f));
if (card.card_icon_image != null)
{
card.card_icon_image.sprite = sp;
card.card_icon_image.enabled = true;
}
}
else
{
Debug.LogWarning("Failed to LoadImage for card_icon: " + iconPath + ". Using default sprite if available.");
}
}
catch (System.Exception e)
{
Debug.LogWarning("Failed to load card_icon: " + e.Message + ". Using default sprite if available.");
}
}
else
{
// 非官方分类统一标记为第三方来源
finalIsOfficial = "第三方来源";
// 若 info.xml 没有 source,则可以保留原值或设置默认,这里若无则为空
if (string.IsNullOrEmpty(finalSource))
finalSource = "";
Debug.LogWarning("card_icon not found: " + iconPath + ". Using default sprite if available.");
}
// 格式化日期为 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
);
}
// if no cardIcon specified and no default was assigned earlier, ensure image disabled
else
{
if (card.card_icon_image != null && card.card_icon_image.sprite == null)
card.card_icon_image.enabled = false;
}
}
}
}
string GetH5RootPath()
string ResolveH5RootPath()
{
if (!string.IsNullOrEmpty(h5RootPath) && Directory.Exists(h5RootPath))
return h5RootPath;
// 从 WebViewLauncher 获取编辑器路径
var w = GameObject.FindObjectOfType<WebViewLauncher>();
if (w != null)
var launcher = GameObject.FindObjectOfType<WebViewLauncher>();
if (launcher != null)
{
#if UNITY_EDITOR
return w.h5EditorPath;
return launcher.h5EditorPath;
#else
return Path.Combine(Application.streamingAssetsPath, "h5LG");
#endif
}
// 作为回退,使用 StreamingAssets/h5LG
return Path.Combine(Application.streamingAssetsPath, "h5LG");
}
GameObject GetContentByCategory(string categoryName)
GameObject GetTargetContent(string category)
{
switch (categoryName)
switch (category)
{
case "_officialDocs": return officialDocs_scrollView_content;
case "_officialGames": return officialGames_scrollView_content;
@@ -162,54 +239,43 @@ public class loadLittleGamesPrefab : MonoBehaviour
try
{
var doc = new XmlDocument();
XmlDocument doc = new XmlDocument();
doc.Load(xmlPath);
var root = doc.DocumentElement;
if (root == null)
return dict;
XmlElement root = doc.DocumentElement;
if (root == null) return dict;
// 读取预定义字段
string[] keys = { "card_name", "authorName", "description", "isOfficial", "source", "yyyy_mm_dd" };
foreach (var k in keys)
string[] keys = new string[] { "card_name", "authorName", "description", "isOfficial", "source", "yyyy_mm_dd", "card_icon", "versionCode" };
foreach (string k in keys)
{
var node = root.SelectSingleNode(k);
if (node != null)
dict[k] = node.InnerText;
XmlNode node = root.SelectSingleNode(k);
if (node != null) dict[k] = node.InnerText;
}
}
catch (System.Exception e)
catch (System.Exception ex)
{
Debug.LogWarning("Failed to read info.xml: " + xmlPath + " error: " + e.Message);
Debug.LogWarning("ReadInfoXml failed: " + ex.Message);
}
return dict;
}
// 将日期文本尝试解析成 yyyy\nmm\ndd 格式,若解析失败则做简单替换(支持 2025-12-23, 2025/12/23, 2025_12_23, 20251223 等)
string FormatDateWithNewlines(string raw)
string GetMeta(Dictionary<string, string> meta, string key)
{
if (string.IsNullOrEmpty(raw))
return "";
if (meta == null) return null;
string v;
if (meta.TryGetValue(key, out v)) return v;
return null;
}
// 尝试按常见分隔符拆分
string FormatDate(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];
}
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 位作为年,接下来的两位为月,之后为日
string digits = Regex.Replace(raw, "\\D", "");
if (digits.Length == 8) return digits.Substring(0, 4) + "\n" + digits.Substring(4, 2) + "\n" + digits.Substring(6, 2);
if (digits.Length >= 6)
{
string y = digits.Substring(0, 4);
@@ -218,13 +284,8 @@ public class loadLittleGamesPrefab : MonoBehaviour
return y + "\n" + m + "\n" + d;
}
// 最后回退直接把原文本按字符换行(有限长度)
return raw.Replace("-", "\n").Replace("/", "\n").Replace("_", "\n");
}
// Update is called once per frame
void Update()
{
}
void Update() { }
}
+80
View File
@@ -0,0 +1,80 @@
using UnityEngine;
using UnityEngine.UI;
public class audioMerger : MonoBehaviour
{
[Header("sv object")]
public GameObject sv_audioSettings;
[Header("所有声音给我站一边")]
[Tooltip("因为静音键我要出现")]
public Toggle enableGlobalMute_toggle;
[Header("一堆slider")]
public Slider mainVolumn_slider;
public Slider noteHit_slider;
public Slider musicInGame_slider;
public Slider musicOutGame_slider;
public Slider skillEffect_slider;
public Slider uiInteration_slider;
public Slider cvVolumn_slider;
[Header("配套的一堆text")]
public Text mainVolumn_text;
public Text noteHit_text;
public Text musicInGame_text;
public Text musicOutGame_text;
public Text skillEffect_text;
public Text uiInteration_text;
public Text cvVolumn_text;
// PlayerPrefs key for storing the toggle state
const string PlayerPrefKey_EnableGlobalMute = "EnableGlobalMute";
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
// Load saved state (default: false)
bool isOn = PlayerPrefs.GetInt(PlayerPrefKey_EnableGlobalMute, 0) == 1;
// Apply to toggle (if available) and sv_audioSettings
if (enableGlobalMute_toggle != null)
{
// Prevent accidentally invoking listeners while initializing
enableGlobalMute_toggle.onValueChanged.RemoveAllListeners();
enableGlobalMute_toggle.isOn = isOn;
enableGlobalMute_toggle.onValueChanged.AddListener(OnEnableGlobalMuteChanged);
}
if (sv_audioSettings != null)
{
// 非模式:当 toggle 为开启 (isOn == true) 时,禁用 sv_audioSettings
sv_audioSettings.SetActive(!isOn);
}
}
void OnDestroy()
{
if (enableGlobalMute_toggle != null)
{
enableGlobalMute_toggle.onValueChanged.RemoveListener(OnEnableGlobalMuteChanged);
}
}
// Listener called when the toggle value changes
void OnEnableGlobalMuteChanged(bool isOn)
{
// Save to PlayerPrefs
PlayerPrefs.SetInt(PlayerPrefKey_EnableGlobalMute, isOn ? 1 : 0);
PlayerPrefs.Save();
// 应用非模式:当 toggle 为开启时,禁用 sv_audioSettings
if (sv_audioSettings != null)
{
sv_audioSettings.SetActive(!isOn);
}
}
// Update is called once per frame
void Update()
{
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2ee999443e0788e4ba1f60dec644e601
+213
View File
@@ -0,0 +1,213 @@
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using System.Text.RegularExpressions;
using System;
using System.Text;
using System.Runtime.InteropServices;
public class readDeviceInfo : MonoBehaviour
{
public Button reread_button;
public Button export_button;
public Text deviceInfo_text;
void Start()
{
if (reread_button != null)
{
reread_button.onClick.AddListener(read_deviceInfo);
}
if (export_button != null)
{
export_button.onClick.AddListener(ExportToFile);
}
if (deviceInfo_text != null)
deviceInfo_text.supportRichText = true;
read_deviceInfo();
}
void OnDestroy()
{
if (reread_button != null)
reread_button.onClick.RemoveListener(read_deviceInfo);
if (export_button != null)
export_button.onClick.RemoveListener(ExportToFile);
}
void Update()
{
}
private void read_deviceInfo()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
void AddEntry(string title, object value)
{
string str;
if (value == null) str = "";
else if (value is bool b) str = b ? "启用" : "禁用";
else str = value.ToString();
sb.AppendLine($"<b>{title}</b>");
sb.AppendLine(str);
sb.AppendLine();
}
AddEntry("平台", Application.platform.ToString());
AddEntry("运行目录", Application.dataPath);
AddEntry("persistent 数据路径", Application.persistentDataPath);
AddEntry("操作系统", SystemInfo.operatingSystem);
AddEntry("设备名称", SystemInfo.deviceName);
AddEntry("设备型号", SystemInfo.deviceModel);
AddEntry("设备唯一ID", SystemInfo.deviceUniqueIdentifier);
AddEntry("CPU", SystemInfo.processorType);
AddEntry("CPU核心数", SystemInfo.processorCount);
AddEntry("系统内存 (MB)", SystemInfo.systemMemorySize);
AddEntry("图形设备名称", SystemInfo.graphicsDeviceName);
AddEntry("图形设备供应商", SystemInfo.graphicsDeviceVendor);
AddEntry("DirectX", SystemInfo.graphicsDeviceType.ToString());
AddEntry("显存 (MB)", SystemInfo.graphicsMemorySize);
AddEntry("屏幕分辨率", $"{Screen.currentResolution.width} x {Screen.currentResolution.height} @ {Screen.currentResolution.refreshRate}Hz");
AddEntry("屏幕DPI", Screen.dpi);
AddEntry("显示模式", Screen.fullScreenMode.ToString());
try
{
AddEntry("电池电量", $"{SystemInfo.batteryLevel * 100:F0}%");
}
catch
{
AddEntry("电池状态", "不支持");
}
AddEntry("浮动·本索尼克谱面编辑器", "v1.1h02e");
AddEntry("浮动·浮动小游戏引擎", "v1.301");
AddEntry("本游戏版本号", "code::testingVersion1062");
AddEntry("登录渠道", "Steam Client");
AddEntry(".NET Framework", "8.0 +");
AddEntry("最大纹理尺寸", SystemInfo.maxTextureSize);
AddEntry("禁用后台运行", Application.runInBackground);
AddEntry("多线程渲染", SystemInfo.graphicsMultiThreaded);
string output = sb.ToString();
if (deviceInfo_text != null)
{
deviceInfo_text.text = output;
}
}
void ExportToFile()
{
if (deviceInfo_text == null)
{
return;
}
string path = ShowSaveFileDialog();
if (string.IsNullOrEmpty(path))
{
return;
}
try
{
string raw = deviceInfo_text.text;
// Remove <b> tags and add colon after title
string cleaned = Regex.Replace(raw, "<b>(.*?)</b>", "$1:");
File.WriteAllText(path, cleaned);
}
catch (System.Exception)
{
}
}
string ShowSaveFileDialog()
{
#if UNITY_EDITOR
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string path = UnityEditor.EditorUtility.SaveFilePanel("保存设备信息", desktop, "device_info", "txt");
return string.IsNullOrEmpty(path) ? null : path;
#elif UNITY_STANDALONE_WIN
try
{
// Use native Windows SaveFile dialog via comdlg32 GetSaveFileName
OpenFileName ofn = new OpenFileName();
ofn.structSize = Marshal.SizeOf(ofn);
ofn.filter = "Text files (*.txt)\0*.txt\0All files (*.*)\0*.*\0";
ofn.file = new string('\0', 260);
ofn.maxFile = ofn.file.Length;
ofn.fileTitle = new string('\0', 260);
ofn.maxFileTitle = ofn.fileTitle.Length;
ofn.initialDir = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
ofn.title = "保存设备信息";
ofn.defExt = "txt";
ofn.flags = OpenFileNameFlags.OFN_OVERWRITEPROMPT;
if (GetSaveFileName(ofn))
{
// Trim any trailing nulls
return ofn.file.TrimEnd('\0');
}
}
catch (Exception)
{
}
return null;
#else
string defaultPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "device_info.txt");
return defaultPath;
#endif
}
#if UNITY_STANDALONE_WIN
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private class OpenFileName
{
public int structSize = 0;
public IntPtr dlgOwner = IntPtr.Zero;
public IntPtr instance = IntPtr.Zero;
public string filter = null;
public string customFilter = null;
public int maxCustFilter = 0;
public int filterIndex = 0;
public string file = null;
public int maxFile = 0;
public string fileTitle = null;
public int maxFileTitle = 0;
public string initialDir = null;
public string title = null;
public int flags = 0;
public short fileOffset = 0;
public short fileExtension = 0;
public string defExt = null;
public IntPtr custData = IntPtr.Zero;
public IntPtr hook = IntPtr.Zero;
public string templateName = null;
public IntPtr reservedPtr = IntPtr.Zero;
public int reservedInt = 0;
public int flagsEx = 0;
}
private static class OpenFileNameFlags
{
public const int OFN_OVERWRITEPROMPT = 0x00000002;
}
[DllImport("comdlg32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool GetSaveFileName([In, Out] OpenFileName ofn);
#endif
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9af26f04de2ee934ab267f3448336566
@@ -0,0 +1,215 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections;
using System.Collections.Generic;
public class selectSettingsOptions : MonoBehaviour
{
[Header("用户")]
public Button userSettingsButton;
public GameObject userSettingsGameObject;
public Image userSettings_selectedMark;
[Header("画面设置")]
public Button graphicSettingsButton;
public GameObject graphicSettingsGameObject;
public Image graphicSettings_selectedMark;
[Header("声音设置")]
public Button audioSettingsButton;
public GameObject audioSettingsGameObject;
public Image audioSettings_selectedMark;
[Header("操作设置")]
public Button controlSettingsButton;
public GameObject controlSettingsGameObject;
public Image controlSettings_selectedMark;
[Header("游戏设置")]
public Button gameSettingsButton;
public GameObject gameSettingsGameObject;
public Image gameSettings_selectedMark;
[Header("关于")]
public Button aboutUsButton;
public GameObject aboutUsGameObject;
public Image aboutUs_selectedMark;
[Header("设备信息")]
public Button deviceInfoButton;
public GameObject deviceInfoGameObject;
public Image deviceInfo_selectedMark;
[Header("Fade Settings")]
[Tooltip("时间(秒)")] public float fadeDuration = 0.25f;
[Tooltip("Hover alpha when mouse over (0-1)")] public float hoverAlpha = 0.75f;
[Tooltip("Alpha when selected (常亮)")] public float selectedAlpha = 1f;
// internal state
private class OptionEntry
{
public Button button;
public GameObject panel;
public Image mark;
public Coroutine fadeCoroutine;
public bool selected;
}
private List<OptionEntry> options = new List<OptionEntry>();
private OptionEntry currentSelected = null;
void Awake()
{
options.Clear();
AddOption(userSettingsButton, userSettingsGameObject, userSettings_selectedMark);
AddOption(graphicSettingsButton, graphicSettingsGameObject, graphicSettings_selectedMark);
AddOption(audioSettingsButton, audioSettingsGameObject, audioSettings_selectedMark);
AddOption(controlSettingsButton, controlSettingsGameObject, controlSettings_selectedMark);
AddOption(gameSettingsButton, gameSettingsGameObject, gameSettings_selectedMark);
AddOption(aboutUsButton, aboutUsGameObject, aboutUs_selectedMark);
AddOption(deviceInfoButton, deviceInfoGameObject, deviceInfo_selectedMark);
}
void Start()
{
// default select userSettings
if (userSettingsButton != null)
SelectOptionByButton(userSettingsButton);
}
void OnDestroy()
{
foreach (var opt in options)
{
if (opt.button != null)
opt.button.onClick.RemoveAllListeners();
RemoveEventTrigger(opt.button);
}
}
void AddOption(Button btn, GameObject panel, Image mark)
{
var entry = new OptionEntry { button = btn, panel = panel, mark = mark, selected = false, fadeCoroutine = null };
options.Add(entry);
if (btn != null)
{
btn.onClick.AddListener(() => OnOptionClicked(entry));
EnsureEventTrigger(btn, entry);
}
if (mark != null)
{
var c = mark.color;
c.a = 0f;
mark.color = c;
mark.enabled = false;
}
if (panel != null)
panel.SetActive(false);
}
void EnsureEventTrigger(Button btn, OptionEntry entry)
{
if (btn == null) return;
EventTrigger trig = btn.GetComponent<EventTrigger>();
if (trig == null) trig = btn.gameObject.AddComponent<EventTrigger>();
// pointer enter
var entryEnter = new EventTrigger.Entry { eventID = EventTriggerType.PointerEnter };
entryEnter.callback.AddListener((data) => { OnOptionPointerEnter(entry); });
trig.triggers.Add(entryEnter);
// pointer exit
var entryExit = new EventTrigger.Entry { eventID = EventTriggerType.PointerExit };
entryExit.callback.AddListener((data) => { OnOptionPointerExit(entry); });
trig.triggers.Add(entryExit);
}
void RemoveEventTrigger(Button btn)
{
if (btn == null) return;
var trig = btn.GetComponent<EventTrigger>();
if (trig != null) Destroy(trig);
}
void OnOptionPointerEnter(OptionEntry entry)
{
if (entry == null) return;
if (entry.selected) return;
StartFade(entry, hoverAlpha);
}
void OnOptionPointerExit(OptionEntry entry)
{
if (entry == null) return;
if (entry.selected) return;
StartFade(entry, 0f);
}
void OnOptionClicked(OptionEntry entry)
{
if (entry == null) return;
SelectEntry(entry);
}
void SelectOptionByButton(Button btn)
{
var entry = options.Find(o => o.button == btn);
if (entry != null) SelectEntry(entry);
}
void SelectEntry(OptionEntry entry)
{
if (entry == null) return;
if (currentSelected != null && currentSelected != entry)
{
currentSelected.selected = false;
StartFade(currentSelected, 0f);
if (currentSelected.panel != null) currentSelected.panel.SetActive(false);
}
currentSelected = entry;
currentSelected.selected = true;
if (currentSelected.panel != null) currentSelected.panel.SetActive(true);
StartFade(currentSelected, selectedAlpha);
}
void StartFade(OptionEntry entry, float targetAlpha)
{
if (entry == null || entry.mark == null) return;
entry.mark.enabled = true;
if (entry.fadeCoroutine != null) StopCoroutine(entry.fadeCoroutine);
entry.fadeCoroutine = StartCoroutine(FadeImage(entry.mark, targetAlpha, fadeDuration, () =>
{
if (Mathf.Approximately(targetAlpha, 0f))
{
if (!entry.selected)
entry.mark.enabled = false;
}
}));
}
IEnumerator FadeImage(Image img, float targetAlpha, float duration, System.Action onComplete = null)
{
if (img == null) yield break;
float start = img.color.a;
float t = 0f;
while (t < duration)
{
t += Time.unscaledDeltaTime;
float a = Mathf.Lerp(start, targetAlpha, duration <= 0f ? 1f : (t / duration));
var c = img.color;
c.a = a;
img.color = c;
yield return null;
}
var fc = img.color;
fc.a = targetAlpha;
img.color = fc;
onComplete?.Invoke();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e27c16de0caf79944b0631760576ae31
+39
View File
@@ -0,0 +1,39 @@
using UnityEngine;
using UnityEngine.UI;
public class userSettings : MonoBehaviour
{
[Header("登陆渠道")]
[Tooltip("渠道")]
public Text user_login_source_text;
[Tooltip("渠道ID")]
public Text user_source_uid_text;
[Header("语言设置")]
[Tooltip("当前语言")]
public Dropdown language_dropdown;
[Header("存档管理")]
[Tooltip("导出存档")]
public Button export_saveData_button;
[Tooltip("导入存档")]
public Button import_saveData_button;
[Tooltip("清理存档")]
public Button clearup_saveData_button;
[Header("查看详细高光")]
[Tooltip("查看按钮")]
public Button view_detailedHighlight_button;
// 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: 0ff926d5dac4bae45a8e098937f5ae52