feat: 角色展示v1.2
This commit is contained in:
@@ -22,11 +22,12 @@
|
||||
<Reference Include="$(DuckovPath)\Duckov_Data\Managed\ItemStatsSystem.dll" Private="False"/>
|
||||
<Reference Include="$(DuckovPath)\Duckov_Data\Managed\Unity*" Private="False"/>
|
||||
<Reference Include="$(DuckovPath)\Duckov_Data\Managed\FMODUnity.dll" Private="False"/>
|
||||
<Reference Include="Eflatun.SceneReference">
|
||||
<HintPath>..\..\..\steam\steamapps\common\Escape from Duckov\Duckov_Data\Managed\Eflatun.SceneReference.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="$(DuckovPath)\Duckov_Data\Managed\UniTask.dll" Private="False"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Lib.Harmony" Version="2.4.1"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="PatchSceneLoaderNotifyPointerClick.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
64
CharacterPreview/Config.cs
Normal file
64
CharacterPreview/Config.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using System.IO;
|
||||
|
||||
namespace CharacterPreview
|
||||
{
|
||||
[Serializable]
|
||||
public class ConfigData
|
||||
{
|
||||
public Vector3 modelPosition;
|
||||
public Vector3 modelRotation;
|
||||
public float modelScale;
|
||||
public bool use = false;
|
||||
public bool tip = true;
|
||||
public bool hideCamera = true;
|
||||
public bool showSetEditButton = true;
|
||||
public bool canEdit = true;
|
||||
public float editSpeed = 1;
|
||||
public bool showEquipment = true;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Config
|
||||
{
|
||||
private string configSavePath;
|
||||
|
||||
public ConfigData data = new ConfigData();
|
||||
|
||||
public Config(string savePath)
|
||||
{
|
||||
configSavePath = savePath;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
data.use = true;
|
||||
var json = JsonUtility.ToJson(data, true);
|
||||
File.WriteAllText(configSavePath, json);
|
||||
}
|
||||
|
||||
public static Config Load(string savePath)
|
||||
{
|
||||
if (!File.Exists(savePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Create(savePath).Dispose();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Debug.LogError($"Failed to create empty config file at {savePath}: {ex.Message}");
|
||||
return new Config(savePath) { data = new ConfigData() };
|
||||
}
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(savePath);
|
||||
var loadedData = JsonUtility.FromJson<ConfigData>(json);
|
||||
|
||||
var config = new Config(savePath);
|
||||
config.data = loadedData ?? new ConfigData();
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
265
CharacterPreview/ControlModelMove.cs
Normal file
265
CharacterPreview/ControlModelMove.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using Unity.VisualScripting;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace CharacterPreview
|
||||
{
|
||||
public class ControlModelMove : MonoBehaviour, IDragHandler,IPointerDownHandler,IPointerEnterHandler,IPointerExitHandler
|
||||
{
|
||||
private RectTransform rectTransform;
|
||||
private Transform canvasRectTransform; //由滑动器自己创建的
|
||||
private Vector2 lastMousePosition;
|
||||
private Image image;
|
||||
private TMP_Text text;
|
||||
private Button editButton;
|
||||
private TMP_Text editButtonText;
|
||||
|
||||
public bool firstClick=true;
|
||||
|
||||
public float Speed => ModBehaviour.config.data.editSpeed;
|
||||
//防止鼠标在范围外捕获消息
|
||||
private bool canEdit = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
SetRectTransform();
|
||||
SetText();
|
||||
SetColor();
|
||||
if(ModBehaviour.config.data.showSetEditButton)
|
||||
SetEditButton();
|
||||
firstClick = ModBehaviour.config.data.tip;
|
||||
if (!firstClick)
|
||||
{
|
||||
HideTip();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (canvasRectTransform)
|
||||
{
|
||||
Destroy(canvasRectTransform.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!canEdit)
|
||||
return;
|
||||
var scroll = Input.GetAxis("Mouse ScrollWheel");
|
||||
if (scroll != 0)
|
||||
{
|
||||
if (Input.GetMouseButton(1))
|
||||
{
|
||||
ModBehaviour.modelMove.RotateAroundCameraZ(Speed * scroll * 8);
|
||||
}
|
||||
else if(Input.GetKey(KeyCode.LeftShift))
|
||||
{
|
||||
ModBehaviour.modelMove.Scale(Speed*scroll/4f);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBehaviour.modelMove.Move(new Vector3(0, 0, Speed * scroll * 2));
|
||||
}
|
||||
}
|
||||
|
||||
if (Input.GetMouseButtonDown(2))
|
||||
{
|
||||
if (Input.GetKey(KeyCode.LeftControl))
|
||||
{
|
||||
ModBehaviour.modelMove.RefreshPosition();
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBehaviour.modelMove.LookAtCamera();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetColor()
|
||||
{
|
||||
var color = new Color(0.9f, 0.8f, 0.3f, 0.1f);
|
||||
image = gameObject.GetComponent<Image>();
|
||||
if (!image)
|
||||
{
|
||||
image = gameObject.AddComponent<Image>();
|
||||
}
|
||||
|
||||
image.color = color;
|
||||
}
|
||||
|
||||
public void SetText()
|
||||
{
|
||||
text=gameObject.GetComponentInChildren<TMP_Text>();
|
||||
if (!text)
|
||||
{
|
||||
var textChilde = new GameObject("Text");
|
||||
textChilde.transform.SetParent(gameObject.transform);
|
||||
var rect = textChilde.AddComponent<RectTransform>();
|
||||
text= textChilde.AddComponent<TextMeshProUGUI>();
|
||||
|
||||
rect.anchorMax = Vector2.one;
|
||||
rect.anchorMin = Vector2.zero;
|
||||
rect.offsetMin = Vector2.zero;
|
||||
rect.offsetMax = Vector2.zero;
|
||||
}
|
||||
text.fontSize = 24;
|
||||
text.alignment=TextAlignmentOptions.Center;
|
||||
text.text = "在此区域可以编辑模型状态(点击区域关闭提示)\n" +
|
||||
"通过鼠标左键拖动修改角色的上下左右位置\n" +
|
||||
"通过鼠标滚轮修改角色的z轴位置\n" +
|
||||
"通过鼠标右键控制角色旋转\n" +
|
||||
"按住右键的情况下滚动滚轮可让角色转圈\n" +
|
||||
"按住shift滚动滚轮可缩放角色\n" +
|
||||
"点按鼠标中键可让角色朝向摄像头\n" +
|
||||
"按住ctrl并点击中键则恢复默认位置";
|
||||
}
|
||||
|
||||
public void SetRectTransform()
|
||||
{
|
||||
if (!rectTransform)
|
||||
{
|
||||
rectTransform = GetComponent<RectTransform>();
|
||||
}
|
||||
|
||||
if (!rectTransform)
|
||||
{
|
||||
rectTransform = gameObject.AddComponent<RectTransform>();
|
||||
}
|
||||
|
||||
if (!rectTransform.parent || rectTransform.parent.name != "Canvas")
|
||||
{
|
||||
var defaultCanvas = GameObject.Find("Canvas");
|
||||
if (!defaultCanvas)
|
||||
{
|
||||
defaultCanvas = new GameObject("ControlModelMoveCanvas");
|
||||
var canvas = defaultCanvas.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay; // 设置渲染模式为屏幕空间覆盖
|
||||
defaultCanvas.AddComponent<CanvasScaler>();
|
||||
defaultCanvas.AddComponent<GraphicRaycaster>();
|
||||
canvasRectTransform = defaultCanvas.transform;
|
||||
}
|
||||
|
||||
rectTransform.SetParent(defaultCanvas.transform);
|
||||
}
|
||||
|
||||
rectTransform.SetAsFirstSibling();
|
||||
|
||||
rectTransform.anchorMax = Vector2.one;
|
||||
rectTransform.anchorMin = new Vector2(0.5f, 0);
|
||||
rectTransform.offsetMax = Vector2.zero;
|
||||
rectTransform.offsetMin = Vector2.zero;
|
||||
}
|
||||
|
||||
public void SetEditButton()
|
||||
{
|
||||
if (!editButton)
|
||||
{
|
||||
var buttonObj = new GameObject("EditButton");
|
||||
buttonObj.transform.SetParent(transform, false);
|
||||
var buttonRect= buttonObj.AddComponent<RectTransform>();
|
||||
buttonRect.anchorMax=Vector2.right;
|
||||
buttonRect.anchorMin=Vector2.right;
|
||||
buttonRect.pivot = Vector2.right;
|
||||
buttonRect.sizeDelta=new Vector2(80f,30f);
|
||||
buttonRect.anchoredPosition = new Vector2(-200, 100);
|
||||
|
||||
|
||||
var button = buttonObj.AddComponent<Button>();
|
||||
var buttonImage = buttonObj.AddComponent<Image>();
|
||||
buttonImage.color = Color.green;
|
||||
button.image = buttonImage;
|
||||
|
||||
var textObj=new GameObject("Text");
|
||||
var txtRect = textObj.AddComponent<RectTransform>();
|
||||
txtRect.SetParent(buttonRect);
|
||||
txtRect.anchorMax=Vector2.one;
|
||||
txtRect.anchorMin=Vector2.zero;
|
||||
txtRect.offsetMax=Vector2.zero;
|
||||
txtRect.offsetMin=Vector2.zero;
|
||||
|
||||
var tmpText = txtRect.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
tmpText.text = "关闭编辑";
|
||||
tmpText.color = Color.white;
|
||||
tmpText.alignment = TextAlignmentOptions.Center;
|
||||
tmpText.fontSize = 14;
|
||||
tmpText.raycastTarget = false; // 文本RaycastTarget通常为false,除非文本本身需要互动
|
||||
|
||||
button.AddComponent<HideSelfOnLeisure>();
|
||||
|
||||
editButton = button;
|
||||
editButtonText = tmpText;
|
||||
}
|
||||
|
||||
editButton.onClick.RemoveAllListeners();
|
||||
editButton.onClick.AddListener(OnEditButton);
|
||||
RefreshEditButton();
|
||||
}
|
||||
public void HideTip()
|
||||
{
|
||||
var oldColor = Color.black;
|
||||
oldColor.a = 0.01f;
|
||||
image.color = oldColor;
|
||||
text.text = "";
|
||||
}
|
||||
|
||||
public void RefreshEditButton()
|
||||
{
|
||||
if (editButton)
|
||||
{
|
||||
editButton.image.color = ModBehaviour.config.data.canEdit ? Color.green : Color.red;
|
||||
editButtonText.text = ModBehaviour.config.data.canEdit ? "关闭编辑" : "开启编辑";
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
var shift = eventData.position - lastMousePosition;
|
||||
if (ModBehaviour.modelMove)
|
||||
{
|
||||
if (Input.GetMouseButton(0))
|
||||
{
|
||||
ModBehaviour.modelMove.Move(shift * Speed / 750);
|
||||
}
|
||||
else if (Input.GetMouseButton(1))
|
||||
{
|
||||
ModBehaviour.modelMove.Rotate(shift * Speed * 2);
|
||||
}
|
||||
}
|
||||
lastMousePosition=eventData.position;
|
||||
|
||||
}
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData)
|
||||
{
|
||||
lastMousePosition=eventData.position;
|
||||
if (firstClick)
|
||||
{
|
||||
HideTip();
|
||||
firstClick=false;
|
||||
ModBehaviour.config.data.tip = firstClick;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
canEdit = true;
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
canEdit = false;
|
||||
}
|
||||
|
||||
public void OnEditButton()
|
||||
{
|
||||
ModBehaviour.config.data.canEdit = !ModBehaviour.config.data.canEdit;
|
||||
RefreshEditButton();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
216
CharacterPreview/GameObjectUtils.cs
Normal file
216
CharacterPreview/GameObjectUtils.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CharacterPreview
|
||||
{
|
||||
public static class GameObjectUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// 在指定根对象下根据路径查找子对象,包括隐藏对象。
|
||||
/// 路径示例:"Parent/Child/Grandchild" 或 "MyObject"
|
||||
/// 如果路径以'/'开头,它会被视为相对于根对象。
|
||||
/// </summary>
|
||||
/// <param name="rootObject">起始查找的根GameObject。</param>
|
||||
/// <param name="path">要查找的对象的相对路径。</param>
|
||||
/// <returns>找到的GameObject,如果未找到则返回null。</returns>
|
||||
public static GameObject FindObjectByPath(GameObject rootObject, string path)
|
||||
{
|
||||
if (rootObject == null)
|
||||
{
|
||||
Debug.LogError("FindObjectByPath: rootObject cannot be null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
Debug.LogWarning("FindObjectByPath: Path is empty or whitespace. Returning the root object itself.");
|
||||
return rootObject;
|
||||
}
|
||||
|
||||
// 规范化路径,移除多余的斜杠
|
||||
var pathParts = path.Split(new char[] { '/' }, System.StringSplitOptions.RemoveEmptyEntries);
|
||||
var currentTransform = rootObject.transform;
|
||||
foreach (var part in pathParts)
|
||||
{
|
||||
Transform foundChild = null;
|
||||
// 迭代所有子对象(包括非激活的)
|
||||
// GetChild() 只获取激活状态的子对象,甚至不会获取非激活的 Transform。
|
||||
// 更好的方法是迭代所有 Transform,检查 parent 是否是 currentTransform。
|
||||
// 然而,Unity的API并没有直接提供一个GetChildren(includeInactive: true)的方法,
|
||||
// 所以我们需要遍历所有场景中的Transform或用递归查找。
|
||||
// 对于层级较深或子对象较多的情况,这种遍历可能会比较慢。
|
||||
//
|
||||
// 另一种常见但更低效的方法是使用 FindObjectsOfTypeAll<Transform>() 然后过滤,
|
||||
// 但那会遍历整个场景。
|
||||
//
|
||||
// 最直接且相对高效的方法是递归查找子Transform。
|
||||
// 迭代当前 Transform 的所有直接子 Transform
|
||||
for (var i = 0; i < currentTransform.childCount; i++)
|
||||
{
|
||||
var child = currentTransform.GetChild(i);
|
||||
if (child.name == part)
|
||||
{
|
||||
foundChild = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundChild != null)
|
||||
{
|
||||
currentTransform = foundChild;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 未找到当前路径部分对应的子对象
|
||||
Debug.LogWarning(
|
||||
$"FindObjectByPath: Could not find '{part}' under '{currentTransform.name}' at path '{path}'.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return currentTransform.gameObject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// (推荐使用) 更通用的方法:在任意指定Transform下根据相对路径查找子Transform,包括隐藏对象。
|
||||
/// 路径示例:"Parent/Child/Grandchild" 或 "MyObject"
|
||||
/// </summary>
|
||||
/// <param name="rootTransform">起始查找的根Transform。</param>
|
||||
/// <param name="relativePath">要查找的子Transform的相对路径。</param>
|
||||
/// <returns>找到的Transform,如果未找到则返回null。</returns>
|
||||
public static Transform FindTransformByRelativePath(Transform rootTransform, string relativePath)
|
||||
{
|
||||
if (rootTransform == null)
|
||||
{
|
||||
Debug.LogError("FindTransformByRelativePath: rootTransform cannot be null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(relativePath))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
"FindTransformByRelativePath: relativePath is empty or whitespace. Returning the rootTransform itself.");
|
||||
return rootTransform;
|
||||
}
|
||||
|
||||
var pathParts = relativePath.Split(new char[] { '/' }, System.StringSplitOptions.RemoveEmptyEntries);
|
||||
var currentTransform = rootTransform;
|
||||
foreach (var partName in pathParts)
|
||||
{
|
||||
Transform foundChild = null;
|
||||
// GetChild(i) 方法返回的 Transform 即使其 GameObject.activeSelf 为 false,它仍然是存在的。
|
||||
// 所以这种遍历是包括非激活对象的。
|
||||
for (var i = 0; i < currentTransform.childCount; i++)
|
||||
{
|
||||
var child = currentTransform.GetChild(i);
|
||||
if (child.name == partName)
|
||||
{
|
||||
foundChild = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundChild != null)
|
||||
{
|
||||
currentTransform = foundChild;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"FindTransformByRelativePath: Could not find '{partName}' under '{currentTransform.name}' (Full Path Attempted: {relativePath}).");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return currentTransform;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在整个场景中根据完整路径查找GameObject,包括隐藏对象。
|
||||
/// 注意:如果场景中有多个同名根对象,此方法可能只返回第一个找到的。
|
||||
/// 如果路径以'/'开头,它会被视为相对于场景根目录的路径。
|
||||
/// 例如: "MyRootObject/Child/Grandchild"
|
||||
/// </summary>
|
||||
/// <param name="fullPath">要查找的对象的完整路径。</param>
|
||||
/// <returns>找到的GameObject,如果未找到则返回null。</returns>
|
||||
public static GameObject FindObjectByFullPath(string fullPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fullPath))
|
||||
{
|
||||
Debug.LogWarning("FindObjectByFullPath: Path is empty or whitespace.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 规范化路径
|
||||
var pathParts = fullPath.Split(new char[] { '/' }, System.StringSplitOptions.RemoveEmptyEntries);
|
||||
if (pathParts.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var rootObjectName = pathParts[0];
|
||||
// 查找所有根对象 (即没有父Transform的对象)
|
||||
// Unity没有直接提供获取所有root GameObjects的API
|
||||
// 我们可以通过迭代所有活跃的GameObject,并检查其parent是否为null来找到根对象。
|
||||
// 但为了包含隐藏对象,我们需要更复杂的方法。
|
||||
//
|
||||
// 更好的方法是使用 FindObjectsOfTypeAll<Transform>() 找到所有Transform,
|
||||
// 然后过滤出parent为null的作为根对象。
|
||||
// 注意:FindObjectsOfTypeAll 即使是静态方法,其效率也可能低于遍历已经知道的子对象。
|
||||
// 在编辑器模式下,Resources.FindObjectsOfTypeAll<GameObject>() 可以获取所有对象。
|
||||
// 在运行时,我们通常需要先有一个起点。
|
||||
// 为了在运行时找到根对象(包括非激活的),我们可以先尝试 FindObjectOfType<GameObject>()
|
||||
// 或者遍历当前场景中的所有Transform,然后向上追溯到根。
|
||||
// 最直接的做法是假设我们知道路径的第一个部分是根对象,然后从那里开始。
|
||||
// 尝试通过 GameObject.Find() 或类似的编辑器工具去查找根对象
|
||||
// GameObject.Find() 只查找激活的根对象,且只通过名称查找。
|
||||
// GameObject.Find(rootObjectName) 即使找到了,也可能不会是整个路径的起点。
|
||||
// 为了包含隐藏对象,我们需要遍历。
|
||||
// 一个更可靠的方法是,先找到所有根Transform,然后对每个根Transform调用 FindTransformByRelativePath。
|
||||
// 获取所有根Transform的方法:
|
||||
var rootTransforms = new List<Transform>();
|
||||
var allTransformsInScene =
|
||||
Resources.FindObjectsOfTypeAll<Transform>(); // 注意:此方法在Editor中有效,在Build中会有些限制
|
||||
// 在编辑器环境中,Resources.FindObjectsOfTypeAll 很好用。
|
||||
// 在运行时,这个方法通常只返回在Resources文件夹中的对象,或者在场景中已有的对象,而不是所有已加载的对象。
|
||||
// 对于运行时,更可靠的是遍历所有的场景:
|
||||
for (var i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++)
|
||||
{
|
||||
var scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i);
|
||||
if (scene.isLoaded)
|
||||
{
|
||||
foreach (var go in scene.GetRootGameObjects())
|
||||
{
|
||||
// go 是根 GameObject,它可能是激活的或非激活的。
|
||||
// GetRootGameObjects() 已经包含了非激活的根对象。
|
||||
rootTransforms.Add(go.transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var rootT in rootTransforms)
|
||||
{
|
||||
if (rootT.name == rootObjectName)
|
||||
{
|
||||
// 如果路径只有一个部分,就是根对象本身
|
||||
if (pathParts.Length == 1)
|
||||
{
|
||||
return rootT.gameObject;
|
||||
}
|
||||
|
||||
// 构建子路径(移除根对象的名称)
|
||||
var relativeSubPath = string.Join("/", pathParts, 1, pathParts.Length - 1);
|
||||
var result = FindTransformByRelativePath(rootT, relativeSubPath);
|
||||
if (result != null)
|
||||
{
|
||||
return result.gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogWarning(
|
||||
$"FindObjectByFullPath: Could not find any root object named '{rootObjectName}' or the full path '{fullPath}'.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
64
CharacterPreview/HideSelfOnLeisure.cs
Normal file
64
CharacterPreview/HideSelfOnLeisure.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace CharacterPreview
|
||||
{
|
||||
public class HideSelfOnLeisure:MonoBehaviour,IPointerEnterHandler,IPointerExitHandler
|
||||
{
|
||||
private Image image;
|
||||
private Color CurrentColor => ModBehaviour.config.data.canEdit ? Color.green : Color.red;
|
||||
private Color HideColor => ModBehaviour.config.data.canEdit
|
||||
? new Color(Color.green.r, Color.green.g, Color.green.b, 0.1f)
|
||||
: new Color(Color.red.r, Color.red.g, Color.red.b, 0.1f);
|
||||
|
||||
public float hideTime = 3f;
|
||||
public float animationTime = 1f;
|
||||
private float timer = 0;
|
||||
private float animationTimer = 0;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
image = GetComponent<Image>();
|
||||
// if (!image)
|
||||
// gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (timer < hideTime)
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
if (timer >= hideTime)
|
||||
{
|
||||
animationTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (animationTimer < animationTime)
|
||||
{
|
||||
animationTimer += Time.deltaTime;
|
||||
// if(animationTimer >= animationTime)
|
||||
// animationTimer=animationTime;
|
||||
var t = animationTimer / animationTime;
|
||||
t = Mathf.Clamp01(t);
|
||||
image.color = Color.Lerp(CurrentColor, HideColor, t);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
if(!image)return;
|
||||
timer = hideTime;
|
||||
animationTimer = animationTime;
|
||||
image.color = CurrentColor;
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
if (!image) return;
|
||||
timer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
175
CharacterPreview/Load_DuckovCustomModel.cs
Normal file
175
CharacterPreview/Load_DuckovCustomModel.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CharacterPreview
|
||||
{
|
||||
public static class Load_DuckovCustomModel
|
||||
{
|
||||
public static ModelMove? CreateModel()
|
||||
{
|
||||
Action startMethod = null;
|
||||
Action midMethod = null;
|
||||
Action endMethod = null;
|
||||
// 尝试获取 LevelManager_OnLevelBeginInitializing
|
||||
try
|
||||
{
|
||||
Debug.Log("\n[Main Runner] 尝试获取 LevelManager_OnLevelBeginInitializing...");
|
||||
startMethod =
|
||||
GetPrivateStaticVoidMethod(
|
||||
"DuckovCustomModel.ModEntry.LevelManager_OnLevelBeginInitializing");
|
||||
Debug.Log("[Main Runner] 成功获取 LevelManager_OnLevelBeginInitializing 委托。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[Main Runner] <color=red>获取 LevelManager_OnLevelBeginInitializing 时发生错误:</color> {ex.GetType().Name} - {ex.Message}");
|
||||
}
|
||||
|
||||
// 尝试获取 LevelManager_OnLevelInitialized
|
||||
try
|
||||
{
|
||||
Debug.Log("\n[Main Runner] 尝试获取 LevelManager_OnLevelInitialized...");
|
||||
midMethod = GetPrivateStaticVoidMethod(
|
||||
"DuckovCustomModel.ModEntry.LevelManager_OnLevelInitialized");
|
||||
Debug.Log("[Main Runner] 成功获取 LevelManager_OnLevelInitialized 委托。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[Main Runner] <color=red>获取 LevelManager_OnLevelInitialized 时发生错误:</color> {ex.GetType().Name} - {ex.Message}");
|
||||
}
|
||||
|
||||
// 尝试获取 LevelManager_OnAfterLevelInitialized
|
||||
try
|
||||
{
|
||||
Debug.Log("\n[Main Runner] 尝试获取 LevelManager_OnAfterLevelInitialized...");
|
||||
endMethod = GetPrivateStaticVoidMethod(
|
||||
"DuckovCustomModel.ModEntry.LevelManager_OnAfterLevelInitialized");
|
||||
Debug.Log("[Main Runner] 成功获取 LevelManager_OnAfterLevelInitialized 委托。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[Main Runner] <color=red>获取 LevelManager_OnAfterLevelInitialized 时发生错误:</color> {ex.GetType().Name} - {ex.Message}");
|
||||
}
|
||||
|
||||
Debug.Log("\n[Main Runner] --- 调用获取到的方法 ---");
|
||||
if (startMethod != null)
|
||||
{
|
||||
Debug.Log("[Main Runner] 正在调用 LevelManager_OnLevelBeginInitializing...");
|
||||
startMethod.Invoke();
|
||||
Debug.Log("[Main Runner] LevelManager_OnLevelBeginInitializing 调用完成。");
|
||||
}
|
||||
else
|
||||
Debug.LogWarning("[Main Runner] LevelManager_OnLevelBeginInitializing 委托为 null,跳过调用。");
|
||||
|
||||
if (midMethod != null)
|
||||
{
|
||||
Debug.Log("[Main Runner] 正在调用 LevelManager_OnLevelInitialized...");
|
||||
midMethod.Invoke();
|
||||
Debug.Log("[Main Runner] LevelManager_OnLevelInitialized 调用完成。");
|
||||
}
|
||||
else
|
||||
Debug.LogWarning("[Main Runner] LevelManager_OnLevelInitialized 委托为 null,跳过调用。");
|
||||
|
||||
if (endMethod != null)
|
||||
{
|
||||
Debug.Log("[Main Runner] 正在调用 LevelManager_OnAfterLevelInitialized...");
|
||||
endMethod.Invoke();
|
||||
Debug.Log("[Main Runner] LevelManager_OnAfterLevelInitialized 调用完成。");
|
||||
}
|
||||
else
|
||||
Debug.LogWarning("[Main Runner] LevelManager_OnAfterLevelInitialized 委托为 null,跳过调用。");
|
||||
// var target = DuckovCustomModel.Core.Data.ModelTarget.Character;
|
||||
//
|
||||
// Debug.Log("运行替换");
|
||||
// var currentModelID = DuckovCustomModel.ModEntry.UsingModel?.GetModelID(target) ?? string.Empty;
|
||||
// if (string.IsNullOrEmpty(currentModelID))
|
||||
// {
|
||||
// // 错误日志:未能获取当前模型ID
|
||||
// Debug.LogError($"[DuckovCustomModel] Failed to get current model ID for target '{target}'. Current model reference might be null or GetModelID returned null/empty. Returning null.");
|
||||
// return null;
|
||||
// }
|
||||
// // 注意:out _ 用于忽略 ModelManager 返回的第一个 out 参数
|
||||
// if (!DuckovCustomModel.Managers.ModelManager.FindModelByID(currentModelID, out _, out var modelInfo))
|
||||
// {
|
||||
// // 错误日志:未能找到指定的模型
|
||||
// Debug.LogError($"[DuckovCustomModel] Model with ID '{currentModelID}' not found by ModelManager for target '{target}'. Returning null.");
|
||||
// return null;
|
||||
// }
|
||||
// if (!modelInfo.CompatibleWithType(target))
|
||||
// {
|
||||
// // 错误日志:模型与目标不兼容
|
||||
// Debug.LogError($"[DuckovCustomModel] Model '{currentModelID}' is not compatible with target '{target}'. Returning null.");
|
||||
// return null;
|
||||
// }
|
||||
// Debug.Log($"inf{currentModelID}");
|
||||
// // 如果前面的检查都通过,则应用模型
|
||||
// DuckovCustomModel.Managers.ModelListManager.ApplyModelToTarget(target, currentModelID, true);
|
||||
//
|
||||
// var handlers=DuckovCustomModel.Managers.ModelManager.GetAllModelHandlers(target);
|
||||
// Debug.Log($"handlers{handlers.Count}");
|
||||
// Debug.Log("运行完成");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Action GetPrivateStaticVoidMethod(string fullMethodName)
|
||||
{
|
||||
var lastDotIndex = fullMethodName.LastIndexOf('.');
|
||||
if (lastDotIndex == -1 || lastDotIndex == fullMethodName.Length - 1)
|
||||
{
|
||||
throw new ArgumentException($"无效的方法名格式: {fullMethodName}。应为 'Namespace.ClassName.MethodName'。");
|
||||
}
|
||||
|
||||
var fullTypeName = fullMethodName.Substring(0, lastDotIndex);
|
||||
var methodName = fullMethodName.Substring(lastDotIndex + 1);
|
||||
Type targetType = null;
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
targetType = assembly.GetType(fullTypeName);
|
||||
if (targetType != null)
|
||||
{
|
||||
break; // 找到类型后停止搜索
|
||||
}
|
||||
}
|
||||
|
||||
if (targetType == null)
|
||||
{
|
||||
throw new TypeLoadException($"在任何已加载的程序集中都未找到类型: {fullTypeName}。");
|
||||
}
|
||||
|
||||
var methodInfo = targetType.GetMethod(
|
||||
methodName,
|
||||
BindingFlags.NonPublic | BindingFlags.Static
|
||||
);
|
||||
if (methodInfo == null)
|
||||
{
|
||||
throw new MissingMethodException(
|
||||
$"在类型 '{fullTypeName}' 中未找到名为 '{methodName}' 的私有静态方法," +
|
||||
$"或者该方法不满足私有、静态、无参的条件。"
|
||||
);
|
||||
}
|
||||
|
||||
if (methodInfo.GetParameters().Length != 0)
|
||||
{
|
||||
throw new MissingMethodException(
|
||||
$"方法 '{fullMethodName}' 存在参数,不符合无参 Action 的要求。"
|
||||
);
|
||||
}
|
||||
|
||||
if (methodInfo.ReturnType != typeof(void))
|
||||
{
|
||||
throw new MissingMethodException(
|
||||
$"方法 '{fullMethodName}' 返回值不是 void,不符合 Action 的要求。"
|
||||
);
|
||||
}
|
||||
|
||||
return (Action)methodInfo.CreateDelegate(typeof(Action));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Duckov.Utilities;
|
||||
using ItemStatsSystem;
|
||||
using Saves;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
@@ -11,98 +13,211 @@ namespace CharacterPreview
|
||||
{
|
||||
public class ModBehaviour : Duckov.Modding.ModBehaviour
|
||||
{
|
||||
private static CharacterModel characterModel;
|
||||
private static CharacterMainControl characterControl;
|
||||
public static ModelMove modelMove;
|
||||
private const string characterFaceSaveKey = "CustomFace_MainCharacter";
|
||||
private OnPointerClick? instance;
|
||||
private const string characterItemSaveKey = "MainCharacterItemData";
|
||||
|
||||
private const string ModelName = "CharacterPreviewModel";
|
||||
|
||||
public static Config config;
|
||||
|
||||
private static GameObject cameraModelObject;
|
||||
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
config.Save();
|
||||
}
|
||||
|
||||
protected override void OnAfterSetup()
|
||||
{
|
||||
MainMenu.OnMainMenuAwake += CreateCharacterModel;
|
||||
//
|
||||
// if (!instance)
|
||||
// {
|
||||
// instance = GetPointerClickEventReceiver();
|
||||
// if (instance == null)
|
||||
// {
|
||||
// Debug.LogError("未能找到 SceneLoader.Instance.pointerClickEventRecevier!");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// instance.onPointerClick.AddListener((t) => CreateCharacterModel());
|
||||
// }
|
||||
//
|
||||
// }
|
||||
var path=Path.Combine(info.path,"config.json");
|
||||
config = Config.Load(path);
|
||||
SceneManager.sceneLoaded+=OnSceneLoaded;
|
||||
AddListen();
|
||||
|
||||
}
|
||||
|
||||
protected override void OnBeforeDeactivate()
|
||||
{
|
||||
MainMenu.OnMainMenuAwake -= CreateCharacterModel;
|
||||
|
||||
if (characterModel)
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
RemoveModel();
|
||||
}
|
||||
|
||||
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
{
|
||||
if (scene.name == "MainMenu")
|
||||
{
|
||||
Destroy(characterModel.gameObject);
|
||||
AddListen();
|
||||
// _ = CreateCharacterModel();
|
||||
}
|
||||
}
|
||||
|
||||
public static void CreateCharacterModel()
|
||||
private void AddListen()
|
||||
{
|
||||
if (characterModel)
|
||||
var canvasObj = GameObject.Find("Canvas");
|
||||
if (canvasObj == null)
|
||||
{
|
||||
Debug.LogWarning("CharacterModel is already created");
|
||||
Debug.Log("Canvas not found");
|
||||
return;
|
||||
}
|
||||
var mainMenu=GameObjectUtils.FindObjectByPath(canvasObj, "MainMenuContainer");
|
||||
if (!mainMenu)
|
||||
{
|
||||
Debug.Log("MainMenuContainer not found");
|
||||
return;
|
||||
}
|
||||
|
||||
var listen = mainMenu.GetComponent<ShowListen>();
|
||||
if (!listen)
|
||||
{
|
||||
mainMenu.AddComponent<ShowListen>();
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveModel()
|
||||
{
|
||||
if (characterControl)
|
||||
{
|
||||
Destroy(characterControl.gameObject);
|
||||
characterControl = null;
|
||||
modelMove = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async UniTask CreateCharacterModel()
|
||||
{
|
||||
if (characterControl)
|
||||
{
|
||||
Debug.LogWarning("[CreateCharacterModel] CharacterModel 已经被创建,跳过重复创建。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (SceneManager.GetActiveScene().name != "MainMenu")
|
||||
{
|
||||
Debug.LogWarning("非主菜单");
|
||||
Debug.LogWarning(
|
||||
$"[CreateCharacterModel] 当前场景为 \"{SceneManager.GetActiveScene().name}\",非主菜单界面 \"MainMenu\",无法创建角色模型。");
|
||||
return;
|
||||
}
|
||||
if (!characterModel)
|
||||
|
||||
Item item = await ItemSavesUtilities.LoadItem(characterItemSaveKey);
|
||||
if (item == null)
|
||||
{
|
||||
var prefab = GetCharacterModelPrefab_Reflection();
|
||||
if (prefab == null)
|
||||
Debug.Log($"[CreateCharacterModel] 未找到已保存的 {characterItemSaveKey},将使用默认角色模板创建新角色。");
|
||||
|
||||
var defaultTypeID = GameplayDataSettings.ItemAssets.DefaultCharacterItemTypeID;
|
||||
item = await ItemAssetsCollection.InstantiateAsync(defaultTypeID);
|
||||
if (item == null)
|
||||
{
|
||||
Debug.LogError("未能获取 CharacterModel 预制体!");
|
||||
Debug.LogError("[CreateCharacterModel] 无法通过默认角色类型 ID 实例化角色物品,请确认资源是否存在且配置正确。");
|
||||
return;
|
||||
}
|
||||
characterModel = Instantiate(prefab);
|
||||
}
|
||||
|
||||
if (characterModel)
|
||||
|
||||
// 获取角色模型预制体
|
||||
var model = GetCharacterModelPrefab_Reflection();
|
||||
if (model == null)
|
||||
{
|
||||
characterModel.name = ModelName;
|
||||
var customFaceSettingData = SavesSystem.Load<CustomFaceSettingData>(characterFaceSaveKey);
|
||||
if (!customFaceSettingData.savedSetting)
|
||||
Debug.LogError("[CreateCharacterModel] 通过反射获取角色模型预制体失败,请检查 GetCharacterModelPrefab_Reflection 方法实现。");
|
||||
return;
|
||||
}
|
||||
|
||||
characterControl = CreateCharacter(
|
||||
item, model, Vector3.zero, Quaternion.identity);
|
||||
|
||||
|
||||
if (characterControl == null)
|
||||
{
|
||||
Debug.LogError("[CreateCharacterModel] 角色创建失败,返回的 characterControl 为 null。");
|
||||
return;
|
||||
}
|
||||
|
||||
characterControl.enabled = false;
|
||||
characterControl.gameObject.name = ModelName;
|
||||
|
||||
// 加载自定义面部设置
|
||||
var customFaceSettingData = SavesSystem.Load<CustomFaceSettingData>(characterFaceSaveKey);
|
||||
if (!customFaceSettingData.savedSetting)
|
||||
{
|
||||
Debug.LogWarning("[CreateCharacterModel] 未能加载有效的 CustomFaceSettingData,将使用默认预设。");
|
||||
|
||||
if (GameplayDataSettings.CustomFaceData?.DefaultPreset?.settings == null)
|
||||
{
|
||||
Debug.LogError("[CreateCharacterModel] 默认面部预设也为空!无法应用面部设置。");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("未能找到或加载 CustomFaceSettingData !");
|
||||
customFaceSettingData = GameplayDataSettings.CustomFaceData.DefaultPreset.settings;
|
||||
}
|
||||
characterModel.CustomFace.LoadFromData(customFaceSettingData);
|
||||
characterModel.gameObject.AddComponent<ModelMove>();
|
||||
}
|
||||
|
||||
if (characterControl.characterModel?.CustomFace != null)
|
||||
{
|
||||
characterControl.characterModel.CustomFace.LoadFromData(customFaceSettingData);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[CreateCharacterModel] 跳过面部数据加载:CustomFace 组件或数据为空。");
|
||||
}
|
||||
|
||||
// 添加移动组件
|
||||
modelMove = characterControl.gameObject.GetComponent<ModelMove>();
|
||||
if (modelMove == null)
|
||||
{
|
||||
modelMove = characterControl.gameObject.AddComponent<ModelMove>();
|
||||
}
|
||||
|
||||
Debug.LogWarning("这个是模型的动画组件报错,由于这个是部分初始化,导致内容不完全,不影响(其实是因为没影响就懒得一个个分析了,欸嘿(*^▽^*))");
|
||||
HideCamera();
|
||||
// SetModelShow(false);
|
||||
}
|
||||
|
||||
private static void SetModelShow(bool show)
|
||||
{
|
||||
if (characterControl)
|
||||
{
|
||||
characterControl.gameObject.SetActive(show);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void HideCamera()
|
||||
{
|
||||
if (!cameraModelObject)
|
||||
{
|
||||
var camera = FindObjectOfType<Camera>();
|
||||
if (camera == null)
|
||||
{
|
||||
Debug.LogWarning("[CreateCharacterModel] 场景中未找到 Camera 对象。");
|
||||
}
|
||||
else
|
||||
{
|
||||
cameraModelObject = GameObjectUtils.FindObjectByPath(camera.gameObject, "Camera01_prefab");
|
||||
if (cameraModelObject == null)
|
||||
{
|
||||
Debug.LogWarning("[CreateCharacterModel] 在 Camera 下未找到路径为 \"Camera01_prefab\" 的子对象。");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cameraModelObject)
|
||||
{
|
||||
if (config.data.hideCamera)
|
||||
{
|
||||
cameraModelObject.SetActive(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
cameraModelObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[CreateCharacterModel] 未找到摄像机模型对象,无法控制其显示状态。");
|
||||
}
|
||||
}
|
||||
|
||||
OnPointerClick GetPointerClickEventReceiver()
|
||||
{
|
||||
var sl = SceneLoader.Instance;
|
||||
// 使用反射获取 SceneLoader 中的 pointerClickEventReceiver 字段
|
||||
var field = typeof(SceneLoader).GetField("pointerClickEventRecevier", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (field == null)
|
||||
{
|
||||
Debug.LogError("pointerClickEventRecevier 字段在 SceneLoader 中未找到!");
|
||||
return null;
|
||||
}
|
||||
// 获取字段值并转换为 OnPointerClick
|
||||
var eventReceiver = field.GetValue(sl) as OnPointerClick;
|
||||
if (eventReceiver == null)
|
||||
{
|
||||
Debug.LogError("pointerClickEventRecevier 字段的值为空!");
|
||||
return null;
|
||||
}
|
||||
return eventReceiver;
|
||||
}
|
||||
private static CharacterModel GetCharacterModelPrefab_Reflection()
|
||||
{
|
||||
// 获取 LevelManager 实例
|
||||
@@ -120,7 +235,7 @@ namespace CharacterPreview
|
||||
Debug.LogError("characterModel 字段在 LevelManager 中未找到!");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// 获取字段值并转换为 CharacterModel
|
||||
var modelPrefab = field.GetValue(lm) as CharacterModel;
|
||||
if (modelPrefab == null)
|
||||
@@ -130,5 +245,58 @@ namespace CharacterPreview
|
||||
}
|
||||
return modelPrefab;
|
||||
}
|
||||
/// <summary>
|
||||
/// 检查指定全限定类名的类型是否存在于已加载的程序集中。
|
||||
/// </summary>
|
||||
/// <param name="fullTypeName">完整类名,如 "DuckovCustomModel.ModBehaviour"</param>
|
||||
/// <returns>如果存在返回 true,否则 false</returns>
|
||||
public static bool IsTypeLoaded(string fullTypeName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fullTypeName))
|
||||
return false;
|
||||
|
||||
// 先尝试用 Type.GetType(适用于 mscorlib 和当前程序集)
|
||||
var type = Type.GetType(fullTypeName, throwOnError: false, ignoreCase: false);
|
||||
if (type != null)
|
||||
return true;
|
||||
|
||||
// 遍历所有已加载的程序集
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
try
|
||||
{
|
||||
type = assembly.GetType(fullTypeName, throwOnError: false, ignoreCase: false);
|
||||
if (type != null)
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 某些程序集可能无法读取(如动态生成、权限问题等),跳过
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static CharacterMainControl CreateCharacter(
|
||||
Item itemInstance,
|
||||
CharacterModel modelPrefab,
|
||||
Vector3 pos,
|
||||
Quaternion rotation)
|
||||
{
|
||||
var character = Instantiate(GameplayDataSettings.Prefabs.CharacterPrefab, pos, rotation);
|
||||
var _characterModel = Instantiate(modelPrefab);
|
||||
character.SetCharacterModel(_characterModel);
|
||||
if (itemInstance == null)
|
||||
{
|
||||
if ((bool) (Object) character)
|
||||
Destroy(character.gameObject);
|
||||
return null;
|
||||
}
|
||||
character.SetItem(itemInstance);
|
||||
return character;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ using UnityEngine;
|
||||
|
||||
namespace CharacterPreview
|
||||
{
|
||||
public class ModelMove:MonoBehaviour
|
||||
public class ModelMove : MonoBehaviour
|
||||
{
|
||||
// 摄像机属性
|
||||
private Camera _camera;
|
||||
public Camera currentCamera
|
||||
|
||||
public Camera CurrentCamera
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -18,36 +20,280 @@ namespace CharacterPreview
|
||||
_camera = FindObjectOfType<Camera>();
|
||||
}
|
||||
}
|
||||
|
||||
return _camera;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 horizontalMoveRange = new Vector2(-1f, 2f); // X轴范围
|
||||
public Vector2 verticalMoveRange = new Vector2(-2f, 2f); // Y轴范围
|
||||
public Vector2 depthMoveRange = new Vector2(-1, 10f); // Z轴范围 (深度)
|
||||
|
||||
public Vector2 scaleRange = new Vector2(0.5f, 3f); // 缩放范围 (x=min, y=max)
|
||||
|
||||
// 初始状态变量
|
||||
private Vector3 _initialScale = Vector3.one;
|
||||
|
||||
private Quaternion _initialRotation = Quaternion.identity;
|
||||
|
||||
// 模型相对于摄像机的初始局部位置偏移量
|
||||
private Vector3 _initialCameraLocalOffset;
|
||||
|
||||
// 用于Input输入的控制器对象
|
||||
private GameObject _controlModelMoveGameObject;
|
||||
|
||||
public bool CanFix => ModBehaviour.config.data.canEdit;
|
||||
|
||||
// 模型在摄像机局部空间中的默认位置偏移量 (用于Reset)
|
||||
public static readonly Vector3 DefaultCameraLocalOffset = new Vector3(0.38f, -0.92f, 2.00f);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
EnsureControlModelMoveGameObject();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (currentCamera)
|
||||
if (ModBehaviour.config.data.use)
|
||||
{
|
||||
var worldPos = CameraLocalToWorld(currentCamera, new Vector3(1, -1, 1));
|
||||
transform.position = worldPos;
|
||||
ApplyConfig();
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.position = new Vector3(8, 8, -16);
|
||||
RefreshPosition();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// // 调试功能,按下 "-" 打印模型相对于摄像机的局部位置
|
||||
// if (Input.GetKeyDown(KeyCode.Minus))
|
||||
// {
|
||||
// if (CurrentCamera)
|
||||
// {
|
||||
// var localPositionRelativeToCamera =
|
||||
// CurrentCamera.transform.InverseTransformPoint(transform.position);
|
||||
// Debug.Log($"模型相对于摄像机的局部位置: {localPositionRelativeToCamera}");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Debug.LogWarning("找不到摄像机,无法计算局部位置。模型的全局位置为: " + transform.position);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// 销毁时清理生成的控制器GameObject
|
||||
if (_controlModelMoveGameObject)
|
||||
{
|
||||
Destroy(_controlModelMoveGameObject);
|
||||
_controlModelMoveGameObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 将摄像机局部坐标系中的点转换为世界坐标系中的点。
|
||||
/// 假设摄像机局部坐标系:前向为 +Z,右为 +X,上为 +Y。
|
||||
/// 确保ControlModelMove GameObject存在并绑定脚本。
|
||||
/// </summary>
|
||||
/// <param name="camera">目标摄像机</param>
|
||||
/// <param name="localPoint">在摄像机局部坐标系中的点</param>
|
||||
/// <returns>对应的世界坐标</returns>
|
||||
private void EnsureControlModelMoveGameObject()
|
||||
{
|
||||
if (_controlModelMoveGameObject == null)
|
||||
{
|
||||
// 尝试查找场景中已有的ControlModelMove
|
||||
_controlModelMoveGameObject = GameObject.Find("ControlModelMove");
|
||||
if (_controlModelMoveGameObject == null)
|
||||
{
|
||||
// 如果没有,则创建新的
|
||||
_controlModelMoveGameObject = new GameObject("ControlModelMove");
|
||||
}
|
||||
|
||||
// 确保ControlModelMove组件已添加
|
||||
if (_controlModelMoveGameObject.GetComponent<ControlModelMove>() == null)
|
||||
{
|
||||
_controlModelMoveGameObject.AddComponent<ControlModelMove>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将模型朝向当前摄像机。
|
||||
/// </summary>
|
||||
public void LookAtCamera()
|
||||
{
|
||||
if(!CanFix)
|
||||
return;
|
||||
if (CurrentCamera)
|
||||
{
|
||||
transform.LookAt(CurrentCamera.transform);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("ModelMove.LookAtCamera: 找不到摄像机,无法使模型朝向摄像机。");
|
||||
}
|
||||
SaveDataToConfig();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置模型的位置、旋转和缩放
|
||||
/// </summary>
|
||||
public void RefreshPosition()
|
||||
{
|
||||
if(!CanFix)
|
||||
return;
|
||||
// 重置缩放和旋转到初始状态
|
||||
transform.localScale = _initialScale;
|
||||
transform.rotation = _initialRotation;
|
||||
|
||||
if (CurrentCamera)
|
||||
{
|
||||
// 计算模型在世界空间中相对于摄像机的默认位置
|
||||
var worldPos = CameraLocalToWorld(CurrentCamera, DefaultCameraLocalOffset);
|
||||
transform.position = worldPos;
|
||||
|
||||
// 使模型朝向摄像机
|
||||
LookAtCamera();
|
||||
|
||||
// 记录模型重置后的相对于摄像机的局部偏移量,作为后续移动的基准
|
||||
_initialCameraLocalOffset = CurrentCamera.transform.InverseTransformPoint(transform.position);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("ModelMove.RefreshPosition: 找不到摄像机。模型将保持其当前世界位置,且相机相对移动功能将无法正常工作。");
|
||||
_initialCameraLocalOffset = transform.position;
|
||||
transform.position = new Vector3(8, 8, -16);
|
||||
}
|
||||
SaveDataToConfig();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移动模型
|
||||
/// </summary>
|
||||
/// <param name="shift">相对于当前总偏移量的位移增量(在摄像机局部坐标系下)</param>
|
||||
public void Move(Vector3 shift)
|
||||
{
|
||||
if(!CanFix)
|
||||
return;
|
||||
if (!CurrentCamera)
|
||||
{
|
||||
Debug.LogWarning("ModelMove.Move: 找不到摄像机,无法执行相机相对移动。");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var currentLocalPos = CurrentCamera.transform.InverseTransformPoint(transform.position);
|
||||
var currentOffsetFromInitial = currentLocalPos - _initialCameraLocalOffset;
|
||||
var targetOffsetFromInitial = currentOffsetFromInitial + shift;
|
||||
targetOffsetFromInitial.x = Mathf.Clamp(targetOffsetFromInitial.x, horizontalMoveRange.x, horizontalMoveRange.y);
|
||||
targetOffsetFromInitial.y = Mathf.Clamp(targetOffsetFromInitial.y, verticalMoveRange.x, verticalMoveRange.y);
|
||||
targetOffsetFromInitial.z = Mathf.Clamp(targetOffsetFromInitial.z, depthMoveRange.x, depthMoveRange.y);
|
||||
|
||||
var newLocalPos = _initialCameraLocalOffset + targetOffsetFromInitial;
|
||||
|
||||
transform.position = CurrentCamera.transform.TransformPoint(newLocalPos);
|
||||
|
||||
SaveDataToConfig();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 等比例缩放模型
|
||||
/// </summary>
|
||||
/// <param name="increment">缩放增量,通常来自鼠标滚轮或UI滑块</param>
|
||||
public void Scale(float increment)
|
||||
{
|
||||
if(!CanFix)
|
||||
return;
|
||||
// 假设是等比例缩放,所以只取x轴的值来计算
|
||||
var currentScale = transform.localScale.x;
|
||||
var newScale = currentScale + increment;
|
||||
|
||||
// 将新的缩放值限制在预设范围内
|
||||
newScale = Mathf.Clamp(newScale, scaleRange.x, scaleRange.y);
|
||||
|
||||
// 应用新的等比例缩放值
|
||||
SaveDataToConfig();
|
||||
transform.localScale = new Vector3(newScale, newScale, newScale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 旋转模型
|
||||
/// </summary>
|
||||
/// <param name="rotationAmount">旋转量,通常来自鼠标拖拽的偏移量(delta)</param>
|
||||
public void Rotate(Vector2 rotationAmount)
|
||||
{
|
||||
if(!CanFix)
|
||||
return;
|
||||
// 左右拖拽(rotationAmount.x)时,围绕世界坐标的Y轴旋转,使模型保持直立
|
||||
transform.Rotate(Vector3.up, -rotationAmount.x * Time.deltaTime, Space.World);
|
||||
|
||||
if (CurrentCamera)
|
||||
{
|
||||
// 上下拖拽(rotationAmount.y)时,围绕摄像机的右方向轴旋转,感觉更自然
|
||||
transform.Rotate(CurrentCamera.transform.right, rotationAmount.y * Time.deltaTime, Space.World);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果找不到相机,则退而求其次,围绕世界坐标的X轴旋转
|
||||
Debug.LogWarning("ModelMove.Rotate: 找不到摄像机,上下旋转将使用世界X轴。");
|
||||
transform.Rotate(Vector3.right, rotationAmount.y * Time.deltaTime, Space.World);
|
||||
}
|
||||
SaveDataToConfig();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 沿摄像机的Z轴(朝向)旋转模型,实现“滚动”或“倾斜”效果。
|
||||
/// </summary>
|
||||
/// <param name="angle">要旋转的角度。正值通常为顺时针,负值为逆时针。</param>
|
||||
public void RotateAroundCameraZ(float angle)
|
||||
{
|
||||
if(!CanFix)
|
||||
return;
|
||||
if (CurrentCamera == null)
|
||||
{
|
||||
Debug.LogWarning("ModelMove.RotateAroundCameraZ: 无法执行围绕相机Z轴的旋转,因为找不到相机。");
|
||||
return;
|
||||
}
|
||||
|
||||
var rotationAxis = CurrentCamera.transform.forward;
|
||||
transform.Rotate(rotationAxis, angle, Space.World);
|
||||
SaveDataToConfig();
|
||||
}
|
||||
|
||||
public void SaveDataToConfig()
|
||||
{
|
||||
var configData = ModBehaviour.config.data;
|
||||
configData.modelPosition = CurrentCamera.transform.InverseTransformPoint(transform.position);
|
||||
configData.modelRotation = transform.eulerAngles;
|
||||
configData.modelScale = CurrentCamera.transform.localScale.x;
|
||||
}
|
||||
|
||||
public void ApplyConfig()
|
||||
{
|
||||
var configData = ModBehaviour.config.data;
|
||||
transform.position = CameraLocalToWorld(CurrentCamera, configData.modelPosition);
|
||||
transform.eulerAngles = configData.modelRotation;
|
||||
transform.localScale = new Vector3(configData.modelScale, configData.modelScale, configData.modelScale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将摄像机局部空间中的点转换为世界空间中的点。
|
||||
/// </summary>
|
||||
/// <param name="camera">参考摄像机。</param>
|
||||
/// <param name="localPoint">摄像机局部空间中的点。</param>
|
||||
/// <returns>世界空间中的点。</returns>
|
||||
public static Vector3 CameraLocalToWorld(Camera camera, Vector3 localPoint)
|
||||
{
|
||||
if (camera == null)
|
||||
throw new System.ArgumentNullException(nameof(camera));
|
||||
throw new ArgumentNullException(nameof(camera),
|
||||
"Camera cannot be null for CameraLocalToWorld conversion.");
|
||||
|
||||
Transform camTransform = camera.transform;
|
||||
// 旋转局部点到世界方向,然后加上摄像机位置
|
||||
var camTransform = camera.transform;
|
||||
return camTransform.position + camTransform.rotation * localPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
21
CharacterPreview/ShowListen.cs
Normal file
21
CharacterPreview/ShowListen.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CharacterPreview
|
||||
{
|
||||
public class ShowListen:MonoBehaviour
|
||||
{
|
||||
private void OnEnable()
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = ModBehaviour.CreateCharacterModel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"创建角色模型失败: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("CharacterPreview")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+9b9121897387fa7e2a57d76690a2a9ae848b1705")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+8fcbdc5649e0b93fd1b771001f53cdbb81da2c78")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("CharacterPreview")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("CharacterPreview")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
3bcd831ea4f29882c4ad0e74553f21cd67857c5ac8ecf673bf0a5c0e6b28519c
|
||||
a35a52a5782dcfa53768ea1eb0acd5bd17c1e3d2dfc5d618e23c884a1a098d9e
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
c29902ba5d072858998bd0700a0022b61d2b982553e1d41c36961a930ba99211
|
||||
9ceb1c845a1b18d2def0bf1eea585e3dc69eb9cff9872b77a69643b5e6fc7903
|
||||
|
||||
@@ -5,4 +5,3 @@ D:\vs_project\DuckovMods\CharacterPreview\obj\Debug\CharacterPreview.AssemblyInf
|
||||
D:\vs_project\DuckovMods\CharacterPreview\obj\Debug\CharacterPreview.AssemblyInfo.cs
|
||||
D:\vs_project\DuckovMods\CharacterPreview\obj\Debug\CharacterPreview.csproj.CoreCompileInputs.cache
|
||||
D:\vs_project\DuckovMods\CharacterPreview\obj\Debug\CharacterPreview.dll
|
||||
D:\vs_project\DuckovMods\CharacterPreview\obj\Debug\Characte.69A9E5B8.Up2Date
|
||||
|
||||
Binary file not shown.
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("CharacterPreview")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+9b9121897387fa7e2a57d76690a2a9ae848b1705")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+8fcbdc5649e0b93fd1b771001f53cdbb81da2c78")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("CharacterPreview")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("CharacterPreview")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
cdbee0654e5bd2c9896d77407e3d7ce5ae6183e3f37c36106e534970cbdd063c
|
||||
2dcee58ea3d10e33f69ccfa06e1fe3a4fe22d6ce0e9a2c07a40e5dff5a0c39dd
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
356db5ad935a1120367a47fbf381e8f0de76f293776515bb91fa0b0cadad25c7
|
||||
c0134ee1afbfeb7d9ea2fc05c8c4e57fa94629a377c1dde9e9b9bbb913bac608
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user