增加skill、加入界面基础加载

This commit is contained in:
tianmo committed 2026-10-13 18:40:42 +08:00
1 parent 839165ea63
commit 5921a4884e
128 files changed
+17935 -5023

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
using TMPro;
using UnityEngine;
/// <summary>
/// 把实时帧率写进 target 指定的文本,用来在真机上直接看性能。
/// 节点引用一律在 Inspector / 预制体里指定,代码里不按名字查找节点。
/// </summary>
[DisallowMultipleComponent]
public class FpsDisplay : MonoBehaviour
{
[Tooltip("显示帧率的文本,必须在 Inspector 里指定")]
[SerializeField] private TMP_Text target;
[Tooltip("刷新间隔(秒)。0.25 = 每秒刷 4 次,数字不会跳得太厉害")]
[SerializeField] private float updateInterval = 0.25f;
[Tooltip("文本格式,{0} 是帧率数值。例:FPS:{0:0}f/s")]
[SerializeField] private string format = "FPS:{0:0}f/s";
private float _accumulatedTime;
private int _frames;
private void Awake()
{
if (target == null)
Debug.LogError("[FpsDisplay] 没有指定显示帧率的文本(target),请在 Inspector 里拖入。", this);
}
private void Update()
{
if (target == null)
return;
_accumulatedTime += Time.unscaledDeltaTime;
_frames++;
if (_accumulatedTime < updateInterval)
return;
target.text = string.Format(format, _frames / _accumulatedTime);
_accumulatedTime = 0f;
_frames = 0;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5cd887902ce124147b00d8e0173c77f1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+54
View File
@@ -0,0 +1,54 @@
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
/// <summary>
/// Main 启动页。
/// 点击事件在 Button 的 On Click() 里接线(Inspector 配置),代码里不查找节点。
/// </summary>
public class MainMenu : MonoBehaviour
{
[Tooltip("点「启动」后要加载的场景名,必须已经加进 Build Settings")]
[SerializeField] private string startSceneName = "Start";
private bool _loading;
/// <summary>启动:加载目标场景,加载完成后卸载 Main 场景自己。</summary>
public void OnStartClicked()
{
if (_loading) return; // 防连点,避免同时加载出两份
_loading = true;
StartCoroutine(LoadThenUnloadSelf());
}
/// <summary>退出游戏。</summary>
public void OnExitClicked()
{
Application.Quit();
#if UNITY_EDITOR
// 编辑器里 Application.Quit 不生效,方便本地测试
UnityEditor.EditorApplication.isPlaying = false;
#endif
}
private IEnumerator LoadThenUnloadSelf()
{
Scene mainScene = gameObject.scene;
AsyncOperation load = SceneManager.LoadSceneAsync(startSceneName, LoadSceneMode.Additive);
if (load == null)
{
Debug.LogError("[MainMenu] 场景 \"" + startSceneName + "\" 加载失败,确认它已加入 Build Settings。", this);
_loading = false;
yield break;
}
yield return load; // 等目标场景加载完成
Scene loaded = SceneManager.GetSceneByName(startSceneName);
if (loaded.IsValid())
SceneManager.SetActiveScene(loaded);
SceneManager.UnloadSceneAsync(mainScene); // 加载完成后卸载 Main
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0c630a6a725d34ce393e88e4152a280c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+34
View File
@@ -0,0 +1,34 @@
using UnityEngine;
/// <summary>
/// 暂停按钮。点击后在指定父节点下实例化暂停菜单预制体。
/// 菜单预制体、父节点都在 Inspector 里指定,代码不查找节点。
/// </summary>
[DisallowMultipleComponent]
public class PauseButton : MonoBehaviour
{
[Tooltip("点击后要显示的暂停菜单预制体")]
[SerializeField] private GameObject pauseMenuPrefab;
[Tooltip("菜单生成到哪个父节点下;留空则用本按钮的父节点(一般是 Canvas)")]
[SerializeField] private Transform menuParent;
private GameObject _instance;
public void OnPauseClicked()
{
if (pauseMenuPrefab == null)
{
Debug.LogError("[PauseButton] 没有指定暂停菜单预制体(pauseMenuPrefab)。", this);
return;
}
if (_instance != null) // 菜单已经开着,不重复生成
return;
Transform parent = menuParent != null ? menuParent : transform.parent;
_instance = Instantiate(pauseMenuPrefab, parent);
Time.timeScale = 0f; // 暂停游戏;不需要真暂停就删掉这一行
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96966a3c5c8764fc4bedfd83928fce19
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+53
View File
@@ -0,0 +1,53 @@
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
/// <summary>
/// 暂停菜单:继续游戏 / 回到 Main 场景。
/// 按钮点击在各自 Button 的 On Click() 里接线(Inspector 配置)。
/// </summary>
[DisallowMultipleComponent]
public class PauseMenu : MonoBehaviour
{
[Tooltip("「回到Main」要加载的场景名,必须已经加进 Build Settings")]
[SerializeField] private string mainSceneName = "Main";
private bool _leaving;
/// <summary>继续游戏:恢复时间,关掉自己。</summary>
public void OnContinueClicked()
{
Time.timeScale = 1f;
Destroy(gameObject);
}
/// <summary>回到 Main:加载 Main 场景,加载完成后卸载当前场景。</summary>
public void OnBackToMainClicked()
{
if (_leaving) return;
_leaving = true;
Time.timeScale = 1f;
StartCoroutine(LoadMainThenUnloadCurrent());
}
private IEnumerator LoadMainThenUnloadCurrent()
{
Scene currentScene = gameObject.scene;
AsyncOperation load = SceneManager.LoadSceneAsync(mainSceneName, LoadSceneMode.Additive);
if (load == null)
{
Debug.LogError("[PauseMenu] 场景 \"" + mainSceneName + "\" 加载失败,确认它已加入 Build Settings。", this);
_leaving = false;
yield break;
}
yield return load; // 等 Main 加载完成
Scene loaded = SceneManager.GetSceneByName(mainSceneName);
if (loaded.IsValid())
SceneManager.SetActiveScene(loaded);
SceneManager.UnloadSceneAsync(currentScene); // 卸载当前场景
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e23dc01f92265409eb1d1cfafc1ad5dc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+152
View File
@@ -0,0 +1,152 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// 屏幕虚拟摇杆(移动轮盘)。挂在预制体根节点上,子物体约定按名字查找:
/// Background —— 底盘(RectTransform + Image)
/// Handle —— 摇杆头(RectTransform + Image)
///
/// 鼠标按住拖拽或手指触摸都可以操作,对外输出归一化方向 <see cref="Value"/>:
/// x = 左右(右为正),y = 前后(上为正),模长 0~1 表示推杆幅度。
/// 松手后方向归零。
/// </summary>
[DisallowMultipleComponent]
public class VirtualJoystick : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
{
[Header("引用(留空则按子物体名自动查找)")]
[SerializeField] private RectTransform background;
[SerializeField] private RectTransform handle;
[Header("手感(距离单位是 UI 像素,以底盘轴心为圆心)")]
[Tooltip("摇杆头允许离开中心的最大距离。拖到这个距离就是满值输入(Value 模长 = 1),再往外拖摇杆头也不动。" +
"底盘 280、摇杆头 120 时,80 ≈ 摇杆头刚好贴到底盘边缘。")]
[SerializeField] private float maxDragRadius = 80f;
[Tooltip("死区:拖动距离小于「最大半径 × 该比例」时不输出,避免手抖")]
[Range(0f, 0.9f)]
[SerializeField] private float deadZone = 0.12f;
[Tooltip("松手后摇杆头是否弹回中心")]
[SerializeField] private bool returnToCenter = true;
[Tooltip("松手后是否隐藏整个轮盘(移动端常用)")]
[SerializeField] private bool hideOnRelease = false;
/// <summary>归一化输入方向,x = 左右,y = 前后,模长 0~1。</summary>
public Vector2 Value { get; private set; }
/// <summary>当前是否按住了轮盘。</summary>
public bool IsPressed { get; private set; }
private Canvas _canvas;
private Vector2 _handleHomePosition;
private void Awake()
{
if (background == null)
background = FindChildRect("Background");
// 没有名为 Background 的子物体时,就把挂脚本的这个节点本身当作底盘
if (background == null)
background = transform as RectTransform;
if (handle == null)
handle = FindChildRect("Handle");
_canvas = GetComponentInParent<Canvas>();
if (handle != null)
_handleHomePosition = handle.anchoredPosition;
}
private void OnEnable()
{
ResetInput();
}
public void OnPointerDown(PointerEventData eventData)
{
IsPressed = true;
gameObject.SetActive(true);
UpdateFromPointer(eventData);
}
public void OnDrag(PointerEventData eventData)
{
IsPressed = true;
UpdateFromPointer(eventData);
}
public void OnPointerUp(PointerEventData eventData)
{
IsPressed = false;
if (returnToCenter)
ResetInput();
if (hideOnRelease)
gameObject.SetActive(false);
}
/// <summary>把摇杆归零,手柄回到初始位置。</summary>
public void ResetInput()
{
Value = Vector2.zero;
if (handle != null)
handle.anchoredPosition = _handleHomePosition;
}
private void UpdateFromPointer(PointerEventData eventData)
{
if (background == null) return;
Camera cam = _canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceOverlay
? null
: (eventData.pressEventCamera != null ? eventData.pressEventCamera : (_canvas != null ? _canvas.worldCamera : null));
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(background, eventData.position, cam, out Vector2 local))
return;
// local 是相对底盘轴心的像素偏移,直接按「最大拖动半径」归一化
float radius = Mathf.Max(1f, maxDragRadius);
float distance = local.magnitude;
Vector2 direction = distance > 0.0001f ? local / distance : Vector2.zero;
float magnitude = Mathf.Clamp01(distance / radius);
if (magnitude < deadZone)
{
Value = Vector2.zero;
}
else
{
// 死区外的部分重新映射到 0~1,避免刚出死区就一大跳
float remapped = Mathf.InverseLerp(deadZone, 1f, magnitude);
Value = direction * Mathf.Clamp01(remapped);
}
if (handle != null)
handle.anchoredPosition = _handleHomePosition + direction * (magnitude * radius);
}
private RectTransform FindChildRect(string childName)
{
Transform child = transform.Find(childName);
if (child == null)
{
// 允许更深层级,比如 Root/Art/Background
foreach (RectTransform rt in GetComponentsInChildren<RectTransform>(true))
{
if (rt != null && rt != transform && rt.name == childName)
return rt;
}
return null;
}
return child as RectTransform;
}
private void OnValidate()
{
if (handle == null)
handle = FindChildRect("Handle");
if (background == null)
background = FindChildRect("Background") ?? transform as RectTransform;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a40b5077dc83e4f33a9f0718325ff7e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: