浮动小游戏已经基本搞好

新做了两个设置界面
一些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
+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() { }
}