feat: 角色展示v1.2

This commit is contained in:
m0_75251201
2025-11-18 18:45:14 +08:00
parent 8fcbdc5649
commit 6cb89ba439
51 changed files with 1558 additions and 250 deletions

View File

@@ -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>

View 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;
}
}
}

View 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();
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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));
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}
}

View 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}");
}
}
}
}

View File

@@ -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")]

View File

@@ -1 +1 @@
3bcd831ea4f29882c4ad0e74553f21cd67857c5ac8ecf673bf0a5c0e6b28519c
a35a52a5782dcfa53768ea1eb0acd5bd17c1e3d2dfc5d618e23c884a1a098d9e

View File

@@ -1 +1 @@
c29902ba5d072858998bd0700a0022b61d2b982553e1d41c36961a930ba99211
9ceb1c845a1b18d2def0bf1eea585e3dc69eb9cff9872b77a69643b5e6fc7903

View File

@@ -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

View File

@@ -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")]

View File

@@ -1 +1 @@
cdbee0654e5bd2c9896d77407e3d7ce5ae6183e3f37c36106e534970cbdd063c
2dcee58ea3d10e33f69ccfa06e1fe3a4fe22d6ce0e9a2c07a40e5dff5a0c39dd

View File

@@ -1 +1 @@
356db5ad935a1120367a47fbf381e8f0de76f293776515bb91fa0b0cadad25c7
c0134ee1afbfeb7d9ea2fc05c8c4e57fa94629a377c1dde9e9b9bbb913bac608